|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@ -1,142 +1,54 @@ |
|||
import { pipe } from "components/common/core" |
|||
import { filter, map, reduce, toPairs } from "lodash/fp" |
|||
|
|||
const self = n => n |
|||
const join_with = delimiter => a => a.join(delimiter) |
|||
const empty_string_to_unset = s => (s.length ? s : "0") |
|||
const add_suffix_if_number = suffix => s => { |
|||
try { |
|||
if (isNaN(s) || isNaN(parseFloat(s))) return s |
|||
} catch (_) { |
|||
return s |
|||
export const generate_screen_css = component_arr => { |
|||
let styles = "" |
|||
for (const { _styles, _id, _children, _component } of component_arr) { |
|||
let [componentName] = _component.match(/[a-z]*$/) |
|||
Object.keys(_styles).forEach(selector => { |
|||
const cssString = generate_css(_styles[selector]) |
|||
if (cssString) { |
|||
styles += apply_class(_id, componentName, cssString, selector) |
|||
} |
|||
}) |
|||
if (_children && _children.length) { |
|||
styles += generate_screen_css(_children) + "\n" |
|||
} |
|||
} |
|||
return s + suffix |
|||
} |
|||
|
|||
export const make_margin = values => |
|||
pipe(values, [ |
|||
map(empty_string_to_unset), |
|||
map(add_suffix_if_number("px")), |
|||
join_with(" "), |
|||
]) |
|||
|
|||
const css_map = { |
|||
templaterows: { |
|||
name: "grid-template-rows", |
|||
generate: self, |
|||
}, |
|||
templatecolumns: { |
|||
name: "grid-template-columns", |
|||
generate: self, |
|||
}, |
|||
align: { |
|||
name: "align-items", |
|||
generate: self, |
|||
}, |
|||
justify: { |
|||
name: "justify-content", |
|||
generate: self, |
|||
}, |
|||
direction: { |
|||
name: "flex-direction", |
|||
generate: self, |
|||
}, |
|||
gridarea: { |
|||
name: "grid-area", |
|||
generate: make_margin, |
|||
}, |
|||
gap: { |
|||
name: "grid-gap", |
|||
generate: n => `${n}px`, |
|||
}, |
|||
columnstart: { |
|||
name: "grid-column-start", |
|||
generate: self, |
|||
}, |
|||
columnend: { |
|||
name: "grid-column-end", |
|||
generate: self, |
|||
}, |
|||
rowstart: { |
|||
name: "grid-row-start", |
|||
generate: self, |
|||
}, |
|||
rowend: { |
|||
name: "grid-row-end", |
|||
generate: self, |
|||
}, |
|||
padding: { |
|||
name: "padding", |
|||
generate: make_margin, |
|||
}, |
|||
margin: { |
|||
name: "margin", |
|||
generate: make_margin, |
|||
}, |
|||
zindex: { |
|||
name: "z-index", |
|||
generate: self, |
|||
}, |
|||
height: { |
|||
name: "height", |
|||
generate: self, |
|||
}, |
|||
width: { |
|||
name: "width", |
|||
generate: self, |
|||
}, |
|||
return styles.trim() |
|||
} |
|||
|
|||
export const generate_rule = ([name, values]) => |
|||
`${css_map[name].name}: ${css_map[name].generate(values)};` |
|||
|
|||
const handle_grid = (acc, [name, value]) => { |
|||
let tmp = [] |
|||
|
|||
if (name === "row" || name === "column") { |
|||
if (value[0]) tmp.push([`${name}start`, value[0]]) |
|||
if (value[1]) tmp.push([`${name}end`, value[1]]) |
|||
return acc.concat(tmp) |
|||
} |
|||
export const generate_css = style => { |
|||
let cssString = Object.entries(style).reduce((str, [key, value]) => { |
|||
//TODO Handle arrays and objects here also
|
|||
if (typeof value === "string") { |
|||
if (value) { |
|||
return (str += `${key}: ${value};\n`) |
|||
} |
|||
} else if (Array.isArray(value)) { |
|||
if (value.length > 0 && !value.every(v => v === "")) { |
|||
return (str += `${key}: ${value |
|||
.map(generate_array_styles) |
|||
.join(" ")};\n`)
|
|||
} |
|||
} |
|||
}, "") |
|||
|
|||
return acc.concat([[name, value]]) |
|||
return (cssString || "").trim() |
|||
} |
|||
|
|||
const object_to_css_string = [ |
|||
toPairs, |
|||
reduce(handle_grid, []), |
|||
filter(v => (Array.isArray(v[1]) ? v[1].some(s => s.length) : v[1].length)), |
|||
map(generate_rule), |
|||
join_with("\n"), |
|||
] |
|||
|
|||
export const generate_css = ({ layout, position }) => { |
|||
let _layout = pipe(layout, object_to_css_string) |
|||
if (_layout.length) { |
|||
_layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};` |
|||
} |
|||
|
|||
return { |
|||
layout: _layout, |
|||
position: pipe(position, object_to_css_string), |
|||
export const generate_array_styles = item => { |
|||
let safeItem = item === "" ? 0 : item |
|||
let hasPx = new RegExp("px$") |
|||
if (!hasPx.test(safeItem)) { |
|||
return `${safeItem}px` |
|||
} else { |
|||
return safeItem |
|||
} |
|||
} |
|||
|
|||
const apply_class = (id, name, styles) => `.${name}-${id} {\n${styles}\n}` |
|||
|
|||
export const generate_screen_css = component_array => { |
|||
let styles = "" |
|||
let emptyStyles = { layout: {}, position: {} } |
|||
|
|||
for (let i = 0; i < component_array.length; i += 1) { |
|||
const { _styles, _id, _children } = component_array[i] |
|||
const { layout, position } = generate_css(_styles || emptyStyles) |
|||
|
|||
styles += apply_class(_id, "pos", position) + "\n" |
|||
styles += apply_class(_id, "lay", layout) + "\n" |
|||
if (_children && _children.length) { |
|||
styles += generate_screen_css(_children) + "\n" |
|||
} |
|||
export const apply_class = (id, name = "element", styles, selector) => { |
|||
if (selector === "normal") { |
|||
return `.${name}-${id} {\n${styles}\n}` |
|||
} else { |
|||
let sel = selector === "selected" ? "::selection" : `:${selector}` |
|||
return `.${name}-${id}${sel} {\n${styles}\n}` |
|||
} |
|||
return styles.trim() |
|||
} |
|||
|
|||
|
After Width: | Height: | Size: 370 B |
|
After Width: | Height: | Size: 561 B |
|
After Width: | Height: | Size: 529 B |
|
After Width: | Height: | Size: 493 B |
|
After Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 689 B |
|
After Width: | Height: | Size: 428 B |
@ -0,0 +1,35 @@ |
|||
<script> |
|||
export let categories = [] |
|||
export let selectedCategory = {} |
|||
export let onClick = category => {} |
|||
</script> |
|||
|
|||
<div class="tabs"> |
|||
{#each categories as category} |
|||
<li |
|||
on:click={() => onClick(category)} |
|||
class:active={selectedCategory === category}> |
|||
{category.name} |
|||
</li> |
|||
{/each} |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
.tabs { |
|||
display: flex; |
|||
list-style: none; |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
} |
|||
|
|||
li { |
|||
color: var(--ink-lighter); |
|||
cursor: pointer; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.active { |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,71 @@ |
|||
<script> |
|||
import PropertyGroup from "./PropertyGroup.svelte" |
|||
import FlatButtonGroup from "./FlatButtonGroup.svelte" |
|||
|
|||
export let panelDefinition = {} |
|||
export let componentInstance = {} |
|||
export let componentDefinition = {} |
|||
export let onStyleChanged = () => {} |
|||
|
|||
let selectedCategory = "normal" |
|||
|
|||
const getProperties = name => panelDefinition[name] |
|||
|
|||
function onChange(category) { |
|||
selectedCategory = category |
|||
} |
|||
|
|||
const buttonProps = [ |
|||
{ value: "normal", text: "Normal" }, |
|||
{ value: "hover", text: "Hover" }, |
|||
{ value: "active", text: "Active" }, |
|||
{ value: "selected", text: "Selected" }, |
|||
] |
|||
|
|||
$: propertyGroupNames = Object.keys(panelDefinition) |
|||
</script> |
|||
|
|||
<div class="design-view-container"> |
|||
|
|||
<div class="design-view-state-categories"> |
|||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> |
|||
</div> |
|||
|
|||
<div class="design-view-property-groups"> |
|||
{#if propertyGroupNames.length > 0} |
|||
{#each propertyGroupNames as groupName} |
|||
<PropertyGroup |
|||
name={groupName} |
|||
properties={getProperties(groupName)} |
|||
styleCategory={selectedCategory} |
|||
{onStyleChanged} |
|||
{componentDefinition} |
|||
{componentInstance} /> |
|||
{/each} |
|||
{:else} |
|||
<div class="no-design"> |
|||
<span>This component does not have any design properties</span> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.design-view-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
|
|||
.design-view-state-categories { |
|||
flex: 0 0 50px; |
|||
} |
|||
|
|||
.design-view-property-groups { |
|||
flex: 1; |
|||
} |
|||
|
|||
.no-design { |
|||
text-align: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
export let value = "" |
|||
export let text = "" |
|||
export let icon = "" |
|||
export let onClick = value => {} |
|||
export let selected = false |
|||
|
|||
$: useIcon = !!icon |
|||
</script> |
|||
|
|||
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}> |
|||
{#if useIcon} |
|||
<i class={icon} /> |
|||
{:else} |
|||
<span>{text}</span> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.flatbutton { |
|||
cursor: pointer; |
|||
padding: 8px 4px; |
|||
text-align: center; |
|||
background: #ffffff; |
|||
color: var(--ink-light); |
|||
border-radius: 5px; |
|||
font-family: Roboto; |
|||
font-size: 13px; |
|||
font-weight: 500; |
|||
transition: background 0.5s, color 0.5s ease; |
|||
text-rendering: optimizeLegibility; |
|||
} |
|||
|
|||
.selected { |
|||
background: #808192; |
|||
color: #ffffff; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,52 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
import FlatButton from "./FlatButton.svelte" |
|||
export let buttonProps = [] |
|||
export let isMultiSelect = false |
|||
export let value = [] |
|||
export let initialValue = "" |
|||
export let onChange = selected => {} |
|||
|
|||
onMount(() => { |
|||
if (!value && !!initialValue) { |
|||
value = initialValue |
|||
} |
|||
}) |
|||
|
|||
function onButtonClicked(v) { |
|||
let val |
|||
if (isMultiSelect) { |
|||
if (value.includes(v)) { |
|||
let idx = value.findIndex(i => i === v) |
|||
val = [...value].splice(idx, 1) |
|||
} else { |
|||
val = [...value, v] |
|||
} |
|||
} else { |
|||
val = v |
|||
} |
|||
onChange(val) |
|||
} |
|||
</script> |
|||
|
|||
<div class="flatbutton-group"> |
|||
{#each buttonProps as props} |
|||
<div class="button-container"> |
|||
<FlatButton |
|||
selected={value.includes(props.value)} |
|||
onClick={onButtonClicked} |
|||
{...props} /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.flatbutton-group { |
|||
display: flex; |
|||
} |
|||
|
|||
.button-container { |
|||
flex: 1; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,36 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
export let value = "" |
|||
export let onChange = value => {} |
|||
export let options = [] |
|||
export let initialValue = "" |
|||
export let styleBindingProperty = "" |
|||
|
|||
const handleStyleBind = value => |
|||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
|||
|
|||
$: isOptionsObject = options.every(o => typeof o === "object") |
|||
|
|||
onMount(() => { |
|||
if (!value && !!initialValue) { |
|||
value = initialValue |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<select |
|||
class="uk-select uk-form-small" |
|||
{value} |
|||
on:change={ev => onChange(ev.target.value)}> |
|||
{#if isOptionsObject} |
|||
{#each options as { value, label }} |
|||
<option {...handleStyleBind(value || label)} value={value || label}> |
|||
{label} |
|||
</option> |
|||
{/each} |
|||
{:else} |
|||
{#each options as value} |
|||
<option {...handleStyleBind(value)} {value}>{value}</option> |
|||
{/each} |
|||
{/if} |
|||
</select> |
|||
@ -0,0 +1,66 @@ |
|||
<script> |
|||
import { onMount, getContext } from "svelte" |
|||
|
|||
export let label = "" |
|||
export let control = null |
|||
export let key = "" |
|||
export let value |
|||
export let props = {} |
|||
export let onChange = () => {} |
|||
|
|||
function handleChange(key, v) { |
|||
if (v.target) { |
|||
let val = props.valueKey ? v.target[props.valueKey] : v.target.value |
|||
onChange(key, val) |
|||
} else { |
|||
onChange(key, v) |
|||
} |
|||
} |
|||
|
|||
const safeValue = () => { |
|||
return value === undefined && props.defaultValue !== undefined |
|||
? props.defaultValue |
|||
: value |
|||
} |
|||
|
|||
//Incase the component has a different value key name |
|||
const handlevalueKey = value => |
|||
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() } |
|||
</script> |
|||
|
|||
<div class="property-control"> |
|||
<div class="label">{label}</div> |
|||
<div class="control"> |
|||
<svelte:component |
|||
this={control} |
|||
{...handlevalueKey(value)} |
|||
on:change={val => handleChange(key, val)} |
|||
onChange={val => handleChange(key, val)} |
|||
{...props} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.property-control { |
|||
display: flex; |
|||
flex-flow: row; |
|||
margin: 8px 0px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.label { |
|||
flex: 0 0 50px; |
|||
font-size: 12px; |
|||
font-weight: 400; |
|||
text-align: left; |
|||
color: var(--ink); |
|||
margin-right: auto; |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
.control { |
|||
flex: 1; |
|||
padding-left: 2px; |
|||
max-width: 164px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,82 @@ |
|||
<script> |
|||
import { excludeProps } from "./propertyCategories.js" |
|||
import PropertyControl from "./PropertyControl.svelte" |
|||
|
|||
export let name = "" |
|||
export let styleCategory = "normal" |
|||
export let properties = [] |
|||
export let componentInstance = {} |
|||
export let onStyleChanged = () => {} |
|||
|
|||
export let show = false |
|||
|
|||
const capitalize = name => name[0].toUpperCase() + name.slice(1) |
|||
|
|||
$: icon = show ? "ri-arrow-down-s-fill" : "ri-arrow-right-s-fill" |
|||
$: style = componentInstance["_styles"][styleCategory] || {} |
|||
</script> |
|||
|
|||
<div class="property-group-container"> |
|||
<div class="property-group-name" on:click={() => (show = !show)}> |
|||
<div class="icon"> |
|||
<i class={icon} /> |
|||
</div> |
|||
<div class="name">{capitalize(name)}</div> |
|||
</div> |
|||
<div class="property-panel" class:show> |
|||
|
|||
{#each properties as props} |
|||
<PropertyControl |
|||
label={props.label} |
|||
control={props.control} |
|||
key={props.key} |
|||
value={style[props.key]} |
|||
onChange={(key, value) => onStyleChanged(styleCategory, key, value)} |
|||
props={{ ...excludeProps(props, ['control', 'label']) }} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.property-group-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: auto; |
|||
background: var(--grey-light); |
|||
margin: 0px 0px 4px 0px; |
|||
padding: 8px 12px; |
|||
justify-content: center; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.property-group-name { |
|||
cursor: pointer; |
|||
display: flex; |
|||
flex-flow: row nowrap; |
|||
} |
|||
|
|||
.name { |
|||
flex: 1; |
|||
text-align: left; |
|||
padding-top: 2px; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
letter-spacing: 0.14px; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.icon { |
|||
flex: 0 0 20px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.property-panel { |
|||
height: 0px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.show { |
|||
overflow: auto; |
|||
height: auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,41 @@ |
|||
<script> |
|||
import PropertyControl from "./PropertyControl.svelte" |
|||
import InputGroup from "../common/Inputs/InputGroup.svelte" |
|||
import Colorpicker from "../common/Colorpicker.svelte" |
|||
import { excludeProps } from "./propertyCategories.js" |
|||
|
|||
export let panelDefinition = [] |
|||
export let componentDefinition = {} |
|||
export let componentInstance = {} |
|||
export let onChange = () => {} |
|||
|
|||
const propExistsOnComponentDef = prop => prop in componentDefinition.props |
|||
|
|||
function handleChange(key, data) { |
|||
data.target ? onChange(key, data.target.value) : onChange(key, data) |
|||
} |
|||
</script> |
|||
|
|||
{#if panelDefinition.length > 0} |
|||
{#each panelDefinition as definition} |
|||
{#if propExistsOnComponentDef(definition.key)} |
|||
<PropertyControl |
|||
control={definition.control} |
|||
label={definition.label} |
|||
key={definition.key} |
|||
value={componentInstance[definition.key]} |
|||
{onChange} |
|||
props={{ ...excludeProps(definition, ['control', 'label']) }} /> |
|||
{/if} |
|||
{/each} |
|||
{:else} |
|||
<div> |
|||
<span>This component does not have any settings.</span> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
div { |
|||
text-align: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,240 @@ |
|||
<script> |
|||
import ComponentsHierarchy from "./ComponentsHierarchy.svelte" |
|||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" |
|||
import PageLayout from "./PageLayout.svelte" |
|||
import PagesList from "./PagesList.svelte" |
|||
import { store } from "builderStore" |
|||
import IconButton from "components/common/IconButton.svelte" |
|||
import NewScreen from "./NewScreen.svelte" |
|||
import CurrentItemPreview from "./CurrentItemPreview.svelte" |
|||
import SettingsView from "./SettingsView.svelte" |
|||
import PageView from "./PageView.svelte" |
|||
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
import { last } from "lodash/fp" |
|||
import { AddIcon } from "components/common/Icons" |
|||
|
|||
let newScreenPicker |
|||
let confirmDeleteDialog |
|||
let componentToDelete = "" |
|||
|
|||
const newScreen = () => { |
|||
newScreenPicker.show() |
|||
} |
|||
|
|||
let settingsView |
|||
const settings = () => { |
|||
settingsView.show() |
|||
} |
|||
|
|||
const confirmDeleteComponent = component => { |
|||
componentToDelete = component |
|||
confirmDeleteDialog.show() |
|||
} |
|||
|
|||
const lastPartOfName = c => (c ? last(c.split("/")) : "") |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<div class="ui-nav"> |
|||
|
|||
<div class="pages-list-container"> |
|||
<div class="nav-header"> |
|||
<span class="navigator-title">Navigator</span> |
|||
<div class="border-line" /> |
|||
|
|||
<span class="components-nav-page">Pages</span> |
|||
</div> |
|||
|
|||
<div class="nav-items-container"> |
|||
<PagesList /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="border-line" /> |
|||
|
|||
<PageLayout layout={$store.pages[$store.currentPageName]} /> |
|||
|
|||
<div class="border-line" /> |
|||
|
|||
<div class="components-list-container"> |
|||
<div class="nav-group-header"> |
|||
<span class="components-nav-header" style="margin-top: 0;"> |
|||
Screens |
|||
</span> |
|||
<div> |
|||
<button on:click={newScreen}> |
|||
<AddIcon /> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="nav-items-container"> |
|||
<ComponentsHierarchy screens={$store.screens} /> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<div class="preview-pane"> |
|||
<CurrentItemPreview /> |
|||
</div> |
|||
|
|||
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'} |
|||
<div class="components-pane"> |
|||
<ComponentsPaneSwitcher /> |
|||
</div> |
|||
{/if} |
|||
|
|||
</div> |
|||
|
|||
<NewScreen bind:this={newScreenPicker} /> |
|||
<SettingsView bind:this={settingsView} /> |
|||
|
|||
<ConfirmDialog |
|||
bind:this={confirmDeleteDialog} |
|||
title="Confirm Delete" |
|||
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`} |
|||
okText="Delete Component" |
|||
onOk={() => store.deleteComponent(componentToDelete)} /> |
|||
|
|||
<style> |
|||
button { |
|||
cursor: pointer; |
|||
outline: none; |
|||
border: none; |
|||
border-radius: 5px; |
|||
width: 20px; |
|||
padding-bottom: 10px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0; |
|||
} |
|||
|
|||
.root { |
|||
display: grid; |
|||
grid-template-columns: 275px 1fr 300px; |
|||
height: 100%; |
|||
width: 100%; |
|||
background: #fafafa; |
|||
} |
|||
|
|||
@media only screen and (min-width: 1800px) { |
|||
.root { |
|||
display: grid; |
|||
grid-template-columns: 300px 1fr 300px; |
|||
height: 100%; |
|||
width: 100%; |
|||
background: #fafafa; |
|||
} |
|||
} |
|||
|
|||
.ui-nav { |
|||
grid-column: 1; |
|||
background-color: var(--white); |
|||
height: calc(100vh - 49px); |
|||
padding: 0; |
|||
overflow: scroll; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.preview-pane { |
|||
grid-column: 2; |
|||
margin: 40px; |
|||
background: #fff; |
|||
border-radius: 5px; |
|||
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.components-pane { |
|||
grid-column: 3; |
|||
background-color: var(--white); |
|||
height: 100vh; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.components-nav-page { |
|||
font-size: 13px; |
|||
color: #000333; |
|||
text-transform: uppercase; |
|||
padding-left: 20px; |
|||
margin-top: 20px; |
|||
font-weight: 600; |
|||
opacity: 0.4; |
|||
letter-spacing: 1px; |
|||
} |
|||
|
|||
.components-nav-header { |
|||
font-size: 13px; |
|||
color: #000333; |
|||
text-transform: uppercase; |
|||
margin-top: 20px; |
|||
font-weight: 600; |
|||
opacity: 0.4; |
|||
letter-spacing: 1px; |
|||
} |
|||
|
|||
.nav-header { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.nav-items-container { |
|||
padding: 1rem 0rem 0rem 0rem; |
|||
} |
|||
|
|||
.nav-group-header { |
|||
display: flex; |
|||
padding: 0px 20px 0px 20px; |
|||
font-size: 0.9rem; |
|||
font-weight: bold; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.nav-group-header > div:nth-child(1) { |
|||
padding: 0rem 0.5rem 0rem 0rem; |
|||
vertical-align: bottom; |
|||
grid-column-start: icon; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
.nav-group-header > span:nth-child(3) { |
|||
margin-left: 5px; |
|||
vertical-align: bottom; |
|||
grid-column-start: title; |
|||
margin-top: auto; |
|||
} |
|||
|
|||
.nav-group-header > div:nth-child(3) { |
|||
vertical-align: bottom; |
|||
grid-column-start: button; |
|||
cursor: pointer; |
|||
color: var(--primary75); |
|||
} |
|||
|
|||
.nav-group-header > div:nth-child(3):hover { |
|||
color: var(--primary75); |
|||
} |
|||
|
|||
.navigator-title { |
|||
font-size: 14px; |
|||
color: var(--secondary100); |
|||
font-weight: 600; |
|||
text-transform: uppercase; |
|||
padding: 0 20px 20px 20px; |
|||
line-height: 1rem !important; |
|||
letter-spacing: 1px; |
|||
} |
|||
|
|||
.border-line { |
|||
border-bottom: 1px solid #d8d8d8; |
|||
} |
|||
|
|||
.components-list-container { |
|||
padding: 20px 0px 0 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,173 @@ |
|||
import Input from "../common/Input.svelte" |
|||
import OptionSelect from "./OptionSelect.svelte" |
|||
import InputGroup from "../common/Inputs/InputGroup.svelte" |
|||
// import Colorpicker from "../common/Colorpicker.svelte"
|
|||
/* |
|||
TODO: Allow for default values for all properties |
|||
*/ |
|||
|
|||
export const layout = [ |
|||
{ |
|||
label: "Direction", |
|||
key: "flex-direction", |
|||
control: OptionSelect, |
|||
initialValue: "columnReverse", |
|||
options: [ |
|||
{ label: "row" }, |
|||
{ label: "row-reverse", value: "rowReverse" }, |
|||
{ label: "column" }, |
|||
{ label: "column-reverse", value: "columnReverse" }, |
|||
], |
|||
}, |
|||
{ label: "Justify", key: "justify-content", control: Input }, |
|||
{ label: "Align", key: "align-items", control: Input }, |
|||
{ |
|||
label: "Wrap", |
|||
key: "flex-wrap", |
|||
control: OptionSelect, |
|||
options: [{ label: "wrap" }, { label: "no wrap", value: "noWrap" }], |
|||
}, |
|||
] |
|||
|
|||
const spacingMeta = [ |
|||
{ placeholder: "T" }, |
|||
{ placeholder: "R" }, |
|||
{ placeholder: "B" }, |
|||
{ placeholder: "L" }, |
|||
] |
|||
export const spacing = [ |
|||
{ |
|||
label: "Padding", |
|||
key: "padding", |
|||
control: InputGroup, |
|||
meta: spacingMeta, |
|||
}, |
|||
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta }, |
|||
] |
|||
|
|||
export const size = [ |
|||
{ label: "Width", key: "width", control: Input }, |
|||
{ label: "Height", key: "height", control: Input }, |
|||
{ label: "Min W", key: "min-width", control: Input }, |
|||
{ label: "Min H", key: "min-height", control: Input }, |
|||
{ label: "Max W", key: "max-width", control: Input }, |
|||
{ label: "Max H", key: "max-height", control: Input }, |
|||
] |
|||
|
|||
export const position = [ |
|||
{ |
|||
label: "Position", |
|||
key: "position", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "static" }, |
|||
{ label: "relative" }, |
|||
{ label: "fixed" }, |
|||
{ label: "absolute" }, |
|||
{ label: "sticky" }, |
|||
], |
|||
}, |
|||
] |
|||
|
|||
export const typography = [ |
|||
{ |
|||
label: "Font", |
|||
key: "font-family", |
|||
control: OptionSelect, |
|||
defaultValue: "initial", |
|||
options: [ |
|||
"initial", |
|||
"Times New Roman", |
|||
"Georgia", |
|||
"Arial", |
|||
"Arial Black", |
|||
"Comic Sans MS", |
|||
"Impact", |
|||
"Lucida Sans Unicode", |
|||
], |
|||
styleBindingProperty: "font-family", |
|||
}, |
|||
{ |
|||
label: "Weight", |
|||
key: "font-weight", |
|||
control: OptionSelect, |
|||
options: [ |
|||
{ label: "normal" }, |
|||
{ label: "bold" }, |
|||
{ label: "bolder" }, |
|||
{ label: "lighter" }, |
|||
], |
|||
}, |
|||
{ label: "size", key: "font-size", defaultValue: "", control: Input }, |
|||
{ label: "Line H", key: "line-height", control: Input }, |
|||
{ |
|||
label: "Color", |
|||
key: "color", |
|||
control: OptionSelect, |
|||
options: ["black", "white", "red", "blue", "green"], |
|||
}, |
|||
{ |
|||
label: "align", |
|||
key: "text-align", |
|||
control: OptionSelect, |
|||
options: ["initial", "left", "right", "center", "justify"], |
|||
}, //custom
|
|||
{ label: "transform", key: "text-transform", control: Input }, //custom
|
|||
{ label: "style", key: "font-style", control: Input }, //custom
|
|||
] |
|||
|
|||
export const background = [ |
|||
{ |
|||
label: "Background", |
|||
key: "background", |
|||
control: OptionSelect, |
|||
options: ["black", "white", "red", "blue", "green"], |
|||
}, |
|||
{ label: "Image", key: "image", control: Input }, //custom
|
|||
] |
|||
|
|||
export const border = [ |
|||
{ label: "Radius", key: "border-radius", control: Input }, |
|||
{ label: "Width", key: "border-width", control: Input }, //custom
|
|||
{ |
|||
label: "Color", |
|||
key: "border-color", |
|||
control: OptionSelect, |
|||
options: ["black", "white", "red", "blue", "green"], |
|||
}, |
|||
{ label: "Style", key: "border-style", control: Input }, |
|||
] |
|||
|
|||
export const effects = [ |
|||
{ label: "Opacity", key: "opacity", control: Input }, |
|||
{ label: "Rotate", key: "transform", control: Input }, //needs special control
|
|||
{ label: "Shadow", key: "box-shadow", control: Input }, |
|||
] |
|||
|
|||
export const transitions = [ |
|||
{ label: "Property", key: "transition-property", control: Input }, |
|||
{ label: "Duration", key: "transition-timing-function", control: Input }, |
|||
{ label: "Ease", key: "transition-ease", control: Input }, |
|||
] |
|||
|
|||
export const all = { |
|||
layout, |
|||
spacing, |
|||
size, |
|||
position, |
|||
typography, |
|||
background, |
|||
border, |
|||
effects, |
|||
transitions, |
|||
} |
|||
|
|||
export function excludeProps(props, propsToExclude) { |
|||
const modifiedProps = {} |
|||
for (const prop in props) { |
|||
if (!propsToExclude.includes(prop)) { |
|||
modifiedProps[prop] = props[prop] |
|||
} |
|||
} |
|||
return modifiedProps |
|||
} |
|||
@ -1 +0,0 @@ |
|||
export { default as BlockPanel } from "./BlockPanel.svelte"; |
|||
@ -0,0 +1,88 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { backendUiStore, workflowStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"; |
|||
|
|||
$: workflow = $workflowStore.workflows.find( |
|||
wf => wf._id === $workflowStore.selectedWorkflowId |
|||
) |
|||
$: workflowBlock = $workflowStore.selectedWorkflowBlock |
|||
|
|||
function deleteWorkflow() { |
|||
workflowStore.actions.deleteWorkflow(workflow) |
|||
} |
|||
|
|||
function deleteWorkflowBlock() { |
|||
// TODO: implement |
|||
workflowStore.actions.deleteWorkflowBlock(workflowBlock) |
|||
} |
|||
</script> |
|||
|
|||
<section> |
|||
<header> |
|||
<span>Setup</span> |
|||
</header> |
|||
<div class="panel-body"> |
|||
{#if workflowBlock} |
|||
<WorkflowBlockSetup {workflowBlock} /> |
|||
<button class="delete-workflow-button hoverable" on:click={deleteWorkflowBlock}> |
|||
Delete {workflowBlock.type} |
|||
</button> |
|||
{:else if $workflowStore.selectedWorkflowId} |
|||
<label class="uk-form-label">Workflow: {workflow.name}</label> |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label">Name</label> |
|||
<div class="uk-form-controls"> |
|||
<input |
|||
type="text" |
|||
class="budibase__input" |
|||
bind:value={workflow.name} /> |
|||
</div> |
|||
</div> |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label">User Access</label> |
|||
Some User Access Stuff Here |
|||
</div> |
|||
<button class="delete-workflow-button hoverable" on:click={deleteWorkflow}> |
|||
Delete Workflow |
|||
</button> |
|||
{/if} |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
header { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
span:not(.selected) { |
|||
color: var(--dark-grey); |
|||
} |
|||
|
|||
label { |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
color: var(--font); |
|||
} |
|||
|
|||
.delete-workflow-button { |
|||
font-family: Roboto; |
|||
width: 100%; |
|||
border: solid 1px #f2f2f2; |
|||
border-radius: 2px; |
|||
background: var(--white); |
|||
height: 32px; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
export let workflowBlock |
|||
$: workflowArgs = Object.keys(workflowBlock.args) |
|||
</script> |
|||
|
|||
<label class="uk-form-label">{workflowBlock.heading}: {workflowBlock.heading}</label> |
|||
{#each workflowArgs as workflowArg} |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label">Name</label> |
|||
<div class="uk-form-controls"> |
|||
<input |
|||
type="text" |
|||
class="budibase__input" |
|||
bind:value={workflowBlock.args[workflowArg]} /> |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
@ -0,0 +1 @@ |
|||
export { default as SetupPanel } from "./SetupPanel.svelte"; |
|||
@ -0,0 +1,51 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { workflowStore, backendUiStore } from "builderStore" |
|||
import Flowchart from "./svelte-flows/Flowchart.svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
let canvas |
|||
let workflow |
|||
let uiTree |
|||
let instanceId = $backendUiStore.selectedDatabase._id |
|||
|
|||
$: workflow = $workflowStore.workflows.find( |
|||
wf => wf._id === $workflowStore.selectedWorkflowId |
|||
) |
|||
|
|||
// Build a renderable UI Tree for the flowchart generator |
|||
function buildUiTree(block, tree = []) { |
|||
if (!block) return tree |
|||
|
|||
tree.push({ |
|||
type: block.type, |
|||
heading: block.actionId, |
|||
args: block.args, |
|||
body: JSON.stringify(block.args), |
|||
}) |
|||
|
|||
return buildUiTree(block.next, tree) |
|||
} |
|||
|
|||
$: if (workflow) uiTree = workflow.definition ? buildUiTree(workflow.definition.next) : [] |
|||
|
|||
function onDelete(block) { |
|||
// TODO finish |
|||
workflowStore.actions.deleteWorkflowBlock(block); |
|||
} |
|||
|
|||
function onSelect(block) { |
|||
workflowStore.update(state => { |
|||
state.selectedWorkflowBlock = block |
|||
return state |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<section> |
|||
<Flowchart |
|||
blocks={uiTree} |
|||
onSelect={onSelect} |
|||
on:delete={onDelete} |
|||
/> |
|||
</section> |
|||
@ -0,0 +1,23 @@ |
|||
<script> |
|||
import FlowItem from "./FlowItem.svelte" |
|||
|
|||
export let blocks = [] |
|||
export let onSelect |
|||
</script> |
|||
|
|||
<section class="canvas"> |
|||
{#each blocks as block, idx} |
|||
<FlowItem onSelect={onSelect} {block} /> |
|||
{#if idx !== blocks.length - 1} |
|||
<i class="ri-arrow-down-line"></i> |
|||
{/if} |
|||
{/each} |
|||
</section> |
|||
|
|||
<style> |
|||
.canvas { |
|||
display: flex; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,33 @@ |
|||
<script> |
|||
export let onSelect |
|||
export let block |
|||
|
|||
function selectBlock() { |
|||
onSelect(block); |
|||
} |
|||
</script> |
|||
|
|||
<div class="hoverable" on:click={selectBlock}> |
|||
<header>{block.heading}</header> |
|||
<hr /> |
|||
<p>{block.body}</p> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
border: 1px solid black; |
|||
width: 320px; |
|||
padding: 20px; |
|||
margin-bottom: 60px; |
|||
border-radius: 5px; |
|||
transition: 0.3s all; |
|||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08); |
|||
background-color: var(--font); |
|||
font-size: 16px; |
|||
color: var(--white); |
|||
} |
|||
|
|||
div:hover { |
|||
transform: scale(1.05); |
|||
} |
|||
</style> |
|||
@ -1 +0,0 @@ |
|||
export { default as WorkflowList } from "./WorkflowList.svelte"; |
|||
@ -0,0 +1,51 @@ |
|||
<script> |
|||
import { workflowStore } from "builderStore"; |
|||
|
|||
export let blockType |
|||
export let blockDefinition |
|||
|
|||
function addBlockToWorkflow() { |
|||
// TODO: store the block type in the DB as well |
|||
workflowStore.actions.addBlockToWorkflow({ blockDefinition, blockType }); |
|||
} |
|||
</script> |
|||
|
|||
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}> |
|||
<div> |
|||
<i class={blockDefinition.icon} /> |
|||
</div> |
|||
<div class="workflow-text"> |
|||
<h4>{blockDefinition.name}</h4> |
|||
<p>{blockDefinition.description}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.workflow-block { |
|||
display: flex; |
|||
padding: 20px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.workflow-text { |
|||
margin-left: 10px; |
|||
} |
|||
|
|||
i { |
|||
background: var(--secondary); |
|||
color: var(--dark-grey); |
|||
padding: 10px; |
|||
} |
|||
|
|||
h4 { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
p { |
|||
font-size: 12px; |
|||
color: var(--dark-grey); |
|||
margin: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { backendUiStore, workflowStore } from "builderStore" |
|||
import { WorkflowList, BlockList } from "./" |
|||
import api from "builderStore/api" |
|||
import blockDefinitions from "./blockDefinitions" |
|||
|
|||
const WORKFLOW_TABS = [ |
|||
{ |
|||
name: "Workflows", |
|||
key: "WORKFLOWS", |
|||
}, |
|||
{ |
|||
name: "Add", |
|||
key: "ADD", |
|||
}, |
|||
] |
|||
|
|||
let selectedTab = "WORKFLOWS" |
|||
let definitions = [] |
|||
</script> |
|||
|
|||
<section> |
|||
<header> |
|||
<span |
|||
class="hoverable" |
|||
class:selected={selectedTab === 'WORKFLOWS'} |
|||
on:click={() => (selectedTab = 'WORKFLOWS')}> |
|||
Workflows |
|||
</span> |
|||
{#if $workflowStore.selectedWorkflowId} |
|||
<span |
|||
class="hoverable" |
|||
class:selected={selectedTab === 'ADD'} |
|||
on:click={() => (selectedTab = 'ADD')}> |
|||
Add |
|||
</span> |
|||
{/if} |
|||
</header> |
|||
{#if selectedTab === 'WORKFLOWS'} |
|||
<WorkflowList /> |
|||
{:else if selectedTab === 'ADD'} |
|||
<BlockList /> |
|||
{/if} |
|||
</section> |
|||
|
|||
<style> |
|||
header { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
span:not(.selected) { |
|||
color: var(--dark-grey); |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,3 @@ |
|||
export { default as WorkflowPanel } from "./WorkflowPanel.svelte"; |
|||
export { default as BlockList } from "./BlockList/BlockList.svelte"; |
|||
export { default as WorkflowList } from "./WorkflowList/WorkflowList.svelte"; |
|||
@ -1,49 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { workflowStore, backendUiStore } from "builderStore"; |
|||
import api from "builderStore/api"; |
|||
|
|||
let canvas |
|||
let workflow |
|||
let instanceId = $backendUiStore.selectedDatabase._id; |
|||
|
|||
$: workflow = $workflowStore.workflows.find(wf => wf._id === $workflowStore.selectedWorkflowId) |
|||
|
|||
// $: if (workflow && workflow.uiTree) flowy.import(workflow.uiTree); |
|||
|
|||
onMount(() => { |
|||
flowy(canvas, onGrab, onRelease, onSnap); |
|||
}); |
|||
|
|||
function onGrab(block) { |
|||
console.log(block); |
|||
} |
|||
|
|||
function onSnap(block, first, parent){ |
|||
workflow.uiTree = flowy.output(); |
|||
workflowStore.actions.update({ instanceId, workflow }) |
|||
return true; |
|||
} |
|||
|
|||
function onRelease() { |
|||
} |
|||
|
|||
// function onGrab(block) { |
|||
// // When the user grabs a block |
|||
// } |
|||
// function onRelease() { |
|||
// // When the user releases a block |
|||
// console.log(flowy.output()) |
|||
// } |
|||
// function onSnap(block, first, parent) { |
|||
// console.log(flowy.output()) |
|||
// console.log(block, first, parent) |
|||
// // When a block snaps with another one |
|||
// } |
|||
// function onRearrange(block, parent) { |
|||
// console.log(block, parent) |
|||
// // When a block is rearranged |
|||
// } |
|||
</script> |
|||
|
|||
<section bind:this={canvas} class="canvas" /> |
|||
@ -1,5 +1,5 @@ |
|||
<script> |
|||
import WorkflowBuilder from "./flowy/WorkflowBuilder.svelte"; |
|||
import WorkflowBuilder from "./WorkflowBuilder/WorkflowBuilder.svelte"; |
|||
</script> |
|||
|
|||
<WorkflowBuilder /> |
|||
@ -1,38 +0,0 @@ |
|||
import api from "builderStore/api"; |
|||
|
|||
class Orchestrator { |
|||
set strategy(strategy) { |
|||
this._stategy = strategy |
|||
} |
|||
|
|||
execute(workflow) { |
|||
this._strategy.execute(workflow); |
|||
} |
|||
} |
|||
|
|||
const ClientStrategy = { |
|||
execute: function(workflow) { |
|||
const block = workflow.next; |
|||
const EXECUTE_WORKFLOW_URL = `api/${workflow.instanceId}/workflows/${workflow._id}`; |
|||
|
|||
switch (block.type) { |
|||
case "CLIENT": |
|||
// fetch the workflow code from the server, then execute it here in the client
|
|||
// catch any errors
|
|||
// check against the conditions in the workflow
|
|||
// if everything is fine, recurse
|
|||
this.execute(workflow.next); |
|||
break; |
|||
case "SERVER": |
|||
// hit the server endpoint and wait for the response
|
|||
// catch any errors
|
|||
// check against the conditions in the workflow
|
|||
// if everything is fine, recurse
|
|||
await api.post() |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -1,320 +1,55 @@ |
|||
import { |
|||
generate_css, |
|||
make_margin, |
|||
generate_screen_css, |
|||
generate_array_styles |
|||
} from "../src/builderStore/generate_css.js" |
|||
|
|||
describe("make_margin", () => { |
|||
test("it should generate a valid rule", () => { |
|||
expect(make_margin(["1", "1", "1", "1"])).toEqual("1px 1px 1px 1px") |
|||
}) |
|||
|
|||
test("empty values should output 0", () => { |
|||
expect(make_margin(["1", "1", "", ""])).toEqual("1px 1px 0px 0px") |
|||
expect(make_margin(["1", "", "", "1"])).toEqual("1px 0px 0px 1px") |
|||
expect(make_margin(["", "", "", ""])).toEqual("0px 0px 0px 0px") |
|||
}) |
|||
}) |
|||
|
|||
describe("generate_css", () => { |
|||
test("it should generate a valid css rule: grid-area", () => { |
|||
expect(generate_css({ layout: { gridarea: ["", "", "", ""] } })).toEqual({ |
|||
layout: "", |
|||
position: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: grid-gap", () => { |
|||
expect(generate_css({ layout: { gap: "10" } })).toEqual({ |
|||
layout: "grid-gap: 10px;\ndisplay: grid;", |
|||
position: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: column 1", () => { |
|||
expect(generate_css({ position: { column: ["", ""] } })).toEqual({ |
|||
layout: "", |
|||
position: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: column 2", () => { |
|||
expect(generate_css({ position: { column: ["1", ""] } })).toEqual({ |
|||
position: "grid-column-start: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: column 3", () => { |
|||
expect(generate_css({ position: { column: ["", "1"] } })).toEqual({ |
|||
position: "grid-column-end: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: column 4", () => { |
|||
expect(generate_css({ position: { column: ["1", "1"] } })).toEqual({ |
|||
position: "grid-column-start: 1;\ngrid-column-end: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: row 1", () => { |
|||
expect(generate_css({ position: { row: ["", ""] } })).toEqual({ |
|||
layout: "", |
|||
position: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: row 2", () => { |
|||
expect(generate_css({ position: { row: ["1", ""] } })).toEqual({ |
|||
position: "grid-row-start: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: row 3", () => { |
|||
expect(generate_css({ position: { row: ["", "1"] } })).toEqual({ |
|||
position: "grid-row-end: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: row 4", () => { |
|||
expect(generate_css({ position: { row: ["1", "1"] } })).toEqual({ |
|||
position: "grid-row-start: 1;\ngrid-row-end: 1;", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: padding 1", () => { |
|||
expect( |
|||
generate_css({ position: { padding: ["1", "1", "1", "1"] } }) |
|||
).toEqual({ |
|||
position: "padding: 1px 1px 1px 1px;", |
|||
layout: "", |
|||
}) |
|||
test("Check how partially empty arrays are handled", () => { |
|||
expect(["", "5", "", ""].map(generate_array_styles)).toEqual(["0px", "5px", "0px", "0px"]) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: padding 2", () => { |
|||
expect(generate_css({ position: { padding: ["1", "", "", "1"] } })).toEqual( |
|||
{ |
|||
position: "padding: 1px 0px 0px 1px;", |
|||
layout: "", |
|||
} |
|||
) |
|||
test("Check how array styles are output", () => { |
|||
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;") |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: margin 1", () => { |
|||
expect( |
|||
generate_css({ position: { margin: ["1", "1", "1", "1"] } }) |
|||
).toEqual({ |
|||
position: "margin: 1px 1px 1px 1px;", |
|||
layout: "", |
|||
}) |
|||
test("Check handling of an array with empty string values", () => { |
|||
expect(generate_css({ padding: ["", "", "", ""] })).toBe("") |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: margin 2", () => { |
|||
expect(generate_css({ position: { margin: ["1", "", "", "1"] } })).toEqual({ |
|||
position: "margin: 1px 0px 0px 1px;", |
|||
layout: "", |
|||
}) |
|||
test("Check handling of an empty array", () => { |
|||
expect(generate_css({ margin: [] })).toBe("") |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: z-index 1", () => { |
|||
expect(generate_css({ position: { zindex: "" } })).toEqual({ |
|||
position: "", |
|||
layout: "", |
|||
}) |
|||
}) |
|||
|
|||
test("it should generate a valid css rule: z-index 2", () => { |
|||
expect(generate_css({ position: { zindex: "1" } })).toEqual({ |
|||
position: "z-index: 1;", |
|||
layout: "", |
|||
}) |
|||
test("Check handling of valid font property", () => { |
|||
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;") |
|||
}) |
|||
}) |
|||
|
|||
describe("generate_screen_css", () => { |
|||
test("it should compile the css for a list of components", () => { |
|||
const components = [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 1, |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 2, |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 3, |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 4, |
|||
}, |
|||
] |
|||
|
|||
const compiled = `.pos-1 {
|
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-1 { |
|||
|
|||
} |
|||
.pos-2 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-2 { |
|||
|
|||
} |
|||
.pos-3 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-3 { |
|||
|
|||
} |
|||
.pos-4 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-4 { |
|||
|
|||
}` |
|||
|
|||
expect(generate_screen_css(components)).toEqual(compiled) |
|||
describe("generate_screen_css", () => { |
|||
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } } |
|||
|
|||
test("Test generation of normal css styles", () => { |
|||
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
test("it should compile the css for a list of components", () => { |
|||
const components = [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 1, |
|||
_children: [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 2, |
|||
_children: [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 3, |
|||
_children: [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 4, |
|||
_children: [ |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 5, |
|||
_children: [], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 6, |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 7, |
|||
}, |
|||
{ |
|||
_styles: { |
|||
layout: { gridarea: ["", "", "", ""] }, |
|||
position: { margin: ["1", "1", "1", "1"] }, |
|||
}, |
|||
_id: 8, |
|||
}, |
|||
] |
|||
|
|||
const compiled = `.pos-1 {
|
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-1 { |
|||
|
|||
} |
|||
.pos-2 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-2 { |
|||
|
|||
} |
|||
.pos-3 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-3 { |
|||
|
|||
} |
|||
.pos-4 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-4 { |
|||
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } } |
|||
|
|||
} |
|||
.pos-5 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-5 { |
|||
|
|||
} |
|||
.pos-6 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-6 { |
|||
|
|||
} |
|||
.pos-7 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-7 { |
|||
test("Test generation of hover css styles", () => { |
|||
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
} |
|||
.pos-8 { |
|||
margin: 1px 1px 1px 1px; |
|||
} |
|||
.lay-8 { |
|||
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } } |
|||
|
|||
test("Test generation of selection css styles", () => { |
|||
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
}` |
|||
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } } |
|||
|
|||
expect(generate_screen_css(components)).toEqual(compiled) |
|||
}) |
|||
}) |
|||
test.only("Testing handling of empty component styles", () => { |
|||
expect(generate_screen_css([emptyComponent])).toBe("") |
|||
}) |
|||
}) |
|||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 381 KiB |
@ -0,0 +1,27 @@ |
|||
const sgMail = require('@sendgrid/mail'); |
|||
|
|||
sgMail.setApiKey(process.env.SENDGRID_API_KEY); |
|||
|
|||
module.exports = async function sendEmail(args) { |
|||
|
|||
const msg = { |
|||
to: args.to, |
|||
from: args.from, |
|||
subject: args.subject, |
|||
text: args.text |
|||
}; |
|||
|
|||
try { |
|||
await sgMail.send(msg); |
|||
return { |
|||
success: true, |
|||
err |
|||
} |
|||
} catch (err) { |
|||
return { |
|||
success: false, |
|||
err |
|||
} |
|||
} |
|||
|
|||
} |
|||