designer for nested components

This commit is contained in:
michael shanks 2019-08-19 08:51:01 +01:00
parent 0d7d9f471e
commit 53c3a54230
9 changed files with 216 additions and 35 deletions

View File

@ -1,17 +1,19 @@
const apiCall = (method) => (url, body, returnResponse=false) => const apiCall = (method, returnResponse) => (url, body) =>
fetch(url, { fetch(url, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: body && JSON.stringify(body), body: body && JSON.stringify(body),
}).then(r => returnResponse ? r : r.json()); }).then(r =>
returnResponse ? r.json() : r
);
const post = apiCall("POST"); const post = apiCall("POST", true);
const get = apiCall("GET"); const get = apiCall("GET", true);
const patch = apiCall("PATCH"); const patch = apiCall("PATCH", true);
const del = apiCall("DELETE"); const del = apiCall("DELETE", false);
export default { export default {
post, get, patch, delete:del post, get, patch, delete:del

View File

@ -384,6 +384,10 @@ const saveDerivedComponent = store => (derivedComponent) => {
]); ]);
s.allComponents = components; s.allComponents = components;
s.currentFrontEndItem = derivedComponent;
s.currentComponentInfo = getNewComponentInfo(
s.allComponents, componentName);
s.currentComponentIsNew = false;
api.post(`/_builder/api/${s.appname}/derivedcomponent`, derivedComponent); api.post(`/_builder/api/${s.appname}/derivedcomponent`, derivedComponent);

View File

@ -0,0 +1,94 @@
<script>
import PropsView from "./PropsView.svelte";
import IconButton from "../common/IconButton.svelte";
import { getComponentInfo } from "./pagesParsing/createProps";
import { store } from "../builderStore";
import { cloneDeep } from "lodash/fp";
import { fade, slide } from 'svelte/transition';
export let propertyName = "";
export let onGoBack = () => {};
export let instanceProps = {};
export let onPropsChanged = () => {};
let editingSubComponentName;
let editingSubComponentProps;
let allComponents;
store.subscribe(s => {
allComponents = s.allComponents;
})
$: componentInfo = getComponentInfo(
allComponents, instanceProps._component);
const onSubComponentGoBack = () => {
editingSubComponentName = null;
editingSubComponentProps = null;
}
const onEditComponentProp = (propName) => {
editingSubComponentName = propName;
editingSubComponentProps = instanceProps[propName];
};
const onSubComponentPropsChanged = (subProps) => {
const newProps = cloneDeep(instanceProps);
newProps[editingSubComponentName] = subProps;
instanceProps = newProps;
onPropsChanged(newProps);
}
const propsChanged = newProps => {
instanceProps = newProps;
onPropsChanged(newProps);
}
</script>
<div>
<div class="title">
<IconButton icon="chevron-left"
on:click={onGoBack}/>
<span>{propertyName}</span>
</div>
{#if editingSubComponentName}
<div in:slide={{delay: 250, duration: 300}}
out:fade>
<svelte:self onPropsChanged={onSubComponentPropsChanged}
onGoBack={onSubComponentGoBack}
instanceProps={editingSubComponentProps}
propertyName={editingSubComponentName} />
</div>
{:else}
<PropsView {instanceProps}
{componentInfo}
onPropsChanged={propsChanged}
{onEditComponentProp} />
{/if}
</div>
<style>
.title {
padding:3px;
background-color: white;
color: var(--secondary100);
border-style:solid;
border-width: 1px 0 0 0;
border-color: var(--lightslate);
}
.title > span {
margin-left: 10px;
}
</style>

View File

@ -16,6 +16,8 @@ const emptyProps = () => ({_component:""})
export let props = emptyProps(); export let props = emptyProps();
export let onValueChanged = () => {}; export let onValueChanged = () => {};
export let onComponentChosen = () => {};
export let onEdit = () => {};
export let label = "" export let label = ""
export let disabled = false; export let disabled = false;
@ -43,10 +45,11 @@ const clearComponent = () => {
showDialog(); showDialog();
} }
const onComponentChosen = (component) => { const componentChosen = (component) => {
const componentInfo = getComponentInfo(allComponents, component.name); const componentInfo = getComponentInfo(allComponents, component.name);
props = componentInfo.fullProps; props = componentInfo.fullProps;
onValueChanged(props); onValueChanged(props);
onComponentChosen();
hideDialog(); hideDialog();
} }
@ -74,7 +77,8 @@ const confirmClearComponent = () => {
</div> </div>
<div> <div>
{#if !disabled && componentSelected} {#if !disabled && componentSelected}
<IconButton icon="edit" /> <IconButton icon="edit"
on:click={onEdit}/>
<IconButton icon="trash" <IconButton icon="trash"
on:click={clearComponent} /> on:click={clearComponent} />
@ -91,7 +95,7 @@ const confirmClearComponent = () => {
{#if modalAction === CHOOSE_COMPONENT} {#if modalAction === CHOOSE_COMPONENT}
<div class="uk-modal-body"> <div class="uk-modal-body">
<ComponentSearch {onComponentChosen} /> <ComponentSearch onComponentChosen={componentChosen} />
</div> </div>
{:else if modalAction === CLEAR_COMPONENT} {:else if modalAction === CLEAR_COMPONENT}
<div class="uk-modal-body"> <div class="uk-modal-body">

View File

@ -12,6 +12,7 @@ import {
} from "./pagesParsing/createProps"; } from "./pagesParsing/createProps";
import Button from "../common/Button.svelte"; import Button from "../common/Button.svelte";
import ButtonGroup from "../common/ButtonGroup.svelte"; import ButtonGroup from "../common/ButtonGroup.svelte";
import ComponentInstanceEditor from "./ComponentInstanceEditor.svelte";
import { import {
cloneDeep, cloneDeep,
@ -33,6 +34,10 @@ let componentDetailsExpanded = false;
let componentInfo; let componentInfo;
let modalElement let modalElement
let propsValidationErrors = []; let propsValidationErrors = [];
let editingComponentInstance;
let editingComponentInstancePropName="";
let allComponents;
$: shortName = last(name.split("/")); $: shortName = last(name.split("/"));
store.subscribe(s => { store.subscribe(s => {
@ -43,6 +48,7 @@ store.subscribe(s => {
tagsString = join(", ")(component.tags); tagsString = join(", ")(component.tags);
componentInfo = s.currentComponentInfo; componentInfo = s.currentComponentInfo;
componentDetailsExpanded = s.currentComponentIsNew; componentDetailsExpanded = s.currentComponentIsNew;
allComponents = s.allComponents;
}); });
const save = () => { const save = () => {
@ -72,8 +78,17 @@ const onPropsValidate = result => {
propsValidationErrors = result; propsValidationErrors = result;
} }
const onPropsChanged = props => { const updateComponent = doChange => {
assign(component.props, props); const newComponent = cloneDeep(component);
doChange(newComponent);
component = newComponent;
componentInfo = getComponentInfo(allComponents, newComponent);
}
const onPropsChanged = newProps => {
updateComponent(newComponent =>
assign(newComponent.props, newProps));
} }
const validate = () => { const validate = () => {
@ -96,6 +111,21 @@ const showDialog = () => {
UIkit.modal(modalElement).show(); UIkit.modal(modalElement).show();
} }
const onEditComponentProp = (propName) => {
editingComponentInstance = component.props[propName];
editingComponentInstancePropName = propName;
}
const componentInstanceCancelEdit = () => {
editingComponentInstance = null;
editingComponentInstancePropName = "";
}
const componentInstancePropsChanged = (instanceProps) => {
updateComponent(newComponent =>
newComponent.props[editingComponentInstancePropName] = instanceProps);
}
</script> </script>
<div class="root"> <div class="root">
@ -114,15 +144,21 @@ const showDialog = () => {
</div> </div>
</div> </div>
<div class="body"> {#if editingComponentInstance}
<ComponentInstanceEditor onGoBack={componentInstanceCancelEdit}
propertyName={editingComponentInstancePropName}
instanceProps={editingComponentInstance}
onPropsChanged={componentInstancePropsChanged}/>
{:else}
<div>
<div class="section-header" on:click={() => componentDetailsExpanded = !componentDetailsExpanded}> <div class="section-header padding" on:click={() => componentDetailsExpanded = !componentDetailsExpanded}>
<span style="margin-right: 7px">Component Details</span> <span style="margin-right: 7px">Component Details</span>
<IconButton icon={componentDetailsExpanded ? "chevron-down" : "chevron-right"}/> <IconButton icon={componentDetailsExpanded ? "chevron-down" : "chevron-right"}/>
</div> </div>
{#if componentDetailsExpanded} {#if componentDetailsExpanded}
<div> <div class="padding">
<Textbox label="Name" <Textbox label="Name"
infoText="use forward slash to store in subfolders" infoText="use forward slash to store in subfolders"
bind:text={name} bind:text={name}
@ -137,15 +173,19 @@ const showDialog = () => {
</div> </div>
{/if} {/if}
<p class="section-header"><span>Properties</span></p> <div class="section-header padding">
<span>Properties</span>
</div>
<PropsView onValidate={onPropsValidate} <PropsView onValidate={onPropsValidate}
{componentInfo} {componentInfo}
{onPropsChanged}/> {onPropsChanged}
{onEditComponentProp}/>
</div> </div>
{/if}
</div> </div>
@ -188,8 +228,8 @@ const showDialog = () => {
border-width: 0px 0px 0px 1px; border-width: 0px 0px 0px 1px;
} }
.body { .padding {
padding: 10px; padding: 0px 5px 0px 10px;
} }
.title { .title {
@ -218,6 +258,7 @@ const showDialog = () => {
.section-header { .section-header {
vertical-align: middle; vertical-align: middle;
margin-top: 20px;
} }
</style> </style>

View File

@ -11,6 +11,14 @@ export let fieldHasError =() => {};
export let propDef = {}; export let propDef = {};
export let props = {}; export let props = {};
export let disabled; export let disabled;
export let index;
export let onEditComponent = () => {};
$: isOdd = (index % 2 !== 0);
const setComponentProp = (props) => {
setProp(propDef.____name, props);
}
</script> </script>
@ -33,7 +41,9 @@ export let disabled;
<ComponentPropSelector label={propDef.____name} <ComponentPropSelector label={propDef.____name}
props={props[propDef.____name]} props={props[propDef.____name]}
{disabled} {disabled}
onValueChanged={props => setProp(propDef.____name, props)}/> onEdit={onEditComponent}
onComponentChosen={onEditComponent}
onValueChanged={setComponentProp}/>
{:else} {:else}
<Textbox label={propDef.____name} <Textbox label={propDef.____name}
text={props[propDef.____name]} text={props[propDef.____name]}
@ -48,7 +58,10 @@ export let disabled;
<style> <style>
.root { .root {
margin-top: 7px; padding: 3px 5px 7px 10px;
border-style: dotted;
border-width: 0 0 1px 0;
border-color: var(--primary25);
} }
</style> </style>

View File

@ -8,7 +8,8 @@ import {
cloneDeep, cloneDeep,
isEqual, isEqual,
sortBy, sortBy,
filter filter,
difference
} from "lodash/fp"; } from "lodash/fp";
import { pipe } from "../common/core"; import { pipe } from "../common/core";
import { import {
@ -29,12 +30,14 @@ export let onValidate = () => {};
export let componentInfo; export let componentInfo;
export let instanceProps = null; export let instanceProps = null;
export let onPropsChanged = () => {}; export let onPropsChanged = () => {};
export let onEditComponentProp = () => {};
let errors = []; let errors = [];
let props = {}; let props = {};
let propsDefinitions = []; let propsDefinitions = [];
let inheritedPropsDefinitions = []; let inheritedPropsDefinitions = [];
let inheritedExpanded = false; let inheritedExpanded = false;
let isInstance = false;
const isPropInherited = name => const isPropInherited = name =>
includes(name)(componentInfo.inheritedProps); includes(name)(componentInfo.inheritedProps);
@ -42,7 +45,8 @@ const isPropInherited = name =>
$: { $: {
if(componentInfo) if(componentInfo)
{ {
props = instanceProps isInstance = !!instanceProps;
props = isInstance
? getInstanceProps(componentInfo, instanceProps) ? getInstanceProps(componentInfo, instanceProps)
: cloneDeep(componentInfo.fullProps); : cloneDeep(componentInfo.fullProps);
@ -65,16 +69,22 @@ $: {
let setProp = (name, value) => { let setProp = (name, value) => {
const newProps = cloneDeep(props); const newProps = cloneDeep(props);
newProps[name] = value;
const finalProps = {}; let finalProps = isInstance ? newProps : cloneDeep(componentInfo.component.props);
if(!isInstance) {
const nowSet = [];
for(let p of componentInfo.unsetProps) { for(let p of componentInfo.unsetProps) {
if(!isEqual(newProps[p])(componentInfo.rootDefaultProps[p])) { if(!isEqual(newProps[p])(componentInfo.rootDefaultProps[p])) {
finalProps[p] = newProps[p]; finalProps[p] = newProps[p];
nowSet.push(p);
} }
} }
componentInfo.unsetProps = difference(nowSet)(componentInfo.unsetProps);
}
newProps[name] = value;
finalProps[name] = value;
props = newProps; props = newProps;
if(validate(finalProps)) if(validate(finalProps))
onPropsChanged(finalProps); onPropsChanged(finalProps);
@ -90,25 +100,30 @@ const validate = (finalProps) => {
const fieldHasError = (propName) => const fieldHasError = (propName) =>
some(e => e.propName === propName)(errors); some(e => e.propName === propName)(errors);
const onEditComponent = (propName) => () => {
onEditComponentProp(propName);
}
</script> </script>
<div class="root"> <div class="root">
<form class="uk-form-stacked"> <form class="uk-form-stacked">
{#each propsDefinitions as propDef} {#each propsDefinitions as propDef, index}
<PropControl {errors} <PropControl {errors}
{setProp} {setProp}
{fieldHasError} {fieldHasError}
{propDef} {propDef}
{props} {props}
{index}
onEditComponent={onEditComponent(propDef.____name)}
disabled={false} /> disabled={false} />
{/each} {/each}
{#if inheritedPropsDefinitions.length > 0} {#if inheritedPropsDefinitions.length > 0}
<div class="inherited-title"> <div class="inherited-title padding">
<div>Inherited</div> <div>Inherited</div>
<div> <div>
<IconButton icon={inheritedExpanded ? "chevron-down" : "chevron-right"} <IconButton icon={inheritedExpanded ? "chevron-down" : "chevron-right"}
@ -118,13 +133,14 @@ const fieldHasError = (propName) =>
{/if} {/if}
{#if inheritedExpanded} {#if inheritedExpanded}
{#each inheritedPropsDefinitions as propDef} {#each inheritedPropsDefinitions as propDef, index}
<PropControl {errors} <PropControl {errors}
{setProp} {setProp}
{fieldHasError} {fieldHasError}
{propDef} {propDef}
{props} {props}
{index}
disabled={true} /> disabled={true} />
{/each} {/each}
@ -143,6 +159,10 @@ const fieldHasError = (propName) =>
font-size:10pt; font-size:10pt;
} }
.padding {
padding: 0 10px;
}
.inherited-title { .inherited-title {
margin-top: 40px; margin-top: 40px;
display: grid; display: grid;

View File

@ -71,7 +71,7 @@ const newComponent = () => {
.root { .root {
display: grid; display: grid;
grid-template-columns: [uiNav] 250px [preview] auto [properties] 250px; grid-template-columns: [uiNav] 250px [preview] auto [properties] 300px;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

View File

@ -66,8 +66,11 @@ export const getNewComponentInfo = (allComponents, inherits) => {
} }
export const getComponentInfo = (allComponents, cname, stack=[], subComponentProps=null) => { export const getComponentInfo = (allComponents, comp, stack=[], subComponentProps=null) => {
const component = find(c => c.name === cname)(allComponents); const component = isString(comp)
? find(c => c.name === comp)(allComponents)
: comp;
const cname = isString(comp) ? comp : comp.name;
if(isRootComponent(component)) { if(isRootComponent(component)) {
subComponentProps = subComponentProps||{}; subComponentProps = subComponentProps||{};
const p = createProps(cname, component.props, subComponentProps); const p = createProps(cname, component.props, subComponentProps);