Merge pull request #74 from shogunpurple/state-manage

State management
This commit is contained in:
Martin McKeaveney 2020-01-31 10:30:32 +00:00 committed by GitHub
commit d5e3fd583c
17 changed files with 701 additions and 228 deletions

View File

@ -0,0 +1,53 @@
<script>
export let disabled = false;
export let hidden = false;
export let primary = true;
export let alert = false;
export let warning = false;
</script>
<style>
.primary {
color: #0055ff;
background: rgb(54, 133, 249, 0.1);
}
.alert {
color: rgba(255, 0, 31, 1);
background: rgba(255, 0, 31, 0.1);;
}
.button {
font-size: 18px;
font-weight: bold;
border-radius: 5px;
border: none;
width: 167px;
height: 64px;
}
.button:hover {
cursor: pointer;
}
.button:disabled {
color: rgba(22, 48, 87, 0.2);
cursor: default;
background: transparent;
}
.hidden {
visibility: hidden;
}
</style>
<button
on:click
class="button"
class:hidden
class:primary
class:alert
class:warning
{disabled}>
<slot />
</button>

View File

@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M13 9h8L11 24v-9H4l9-15v9zm-2 2V7.22L7.532 13H13v4.394L17.263 11H11z" />
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,19 @@
<svg
on:click
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M15.728 9.686l-1.414-1.414L5
17.586V19h1.414l9.314-9.314zm1.414-1.414l1.414-1.414-1.414-1.414-1.414 1.414
1.414 1.414zM7.242 21H3v-4.243L16.435 3.322a1 1 0 0 1 1.414 0l2.829 2.829a1
1 0 0 1 0 1.414L7.243 21z" />
</svg>
<style>
svg:hover {
cursor: pointer;
}
</style>

After

Width:  |  Height:  |  Size: 446 B

View File

@ -4,3 +4,5 @@ export { default as TerminalIcon } from './Terminal.svelte';
export { default as InputIcon } from './Input.svelte'; export { default as InputIcon } from './Input.svelte';
export { default as ImageIcon } from './Image.svelte'; export { default as ImageIcon } from './Image.svelte';
export { default as ArrowDownIcon } from './ArrowDown.svelte'; export { default as ArrowDownIcon } from './ArrowDown.svelte';
export { default as EventsIcon } from './Events.svelte';
export { default as PencilIcon } from './Pencil.svelte';

View File

@ -0,0 +1,27 @@
<script>
export let value = "";
</script>
<style>
input {
display: block;
font-size: 14px;
font-family: sans-serif;
font-weight: 500;
color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid #ccc;
height: 50px;
}
</style>
<input type="text" on:change bind:value />

View File

@ -1,41 +1,44 @@
<script> <script>
import UIkit from "uikit"; import UIkit from "uikit";
export let isOpen = false; export let isOpen = false;
export let onClosed = () => {}; export let onClosed = () => {};
export let id = ""; export let id = "";
let ukModal; let ukModal;
let listenerAdded = false; let listenerAdded = false;
$: { $: {
if(ukModal && !listenerAdded) { if (ukModal && !listenerAdded) {
listenerAdded = true; listenerAdded = true;
ukModal.addEventListener("hidden", onClosed); ukModal.addEventListener("hidden", onClosed);
} }
if(ukModal) { if (ukModal) {
if(isOpen) { if (isOpen) {
UIkit.modal(ukModal).show(); UIkit.modal(ukModal).show();
} else { } else {
UIkit.modal(ukModal).hide(); UIkit.modal(ukModal).hide();
} }
} }
} }
</script> </script>
<div bind:this={ukModal} uk-modal {id}>
<div class="uk-modal-dialog uk-modal-body" uk-overflow-auto>
<slot />
</div>
</div>
<style> <style>
.uk-modal-dialog {
border-radius: 0.3rem;
width: 60%;
height: 80vh;
display: flex;
flex-direction: column;
}
</style>
.uk-modal-dialog { <div bind:this={ukModal} uk-modal {id}>
border-radius: .3rem; <div class="uk-modal-dialog uk-modal-body" uk-overflow-auto>
} {#if onClosed}
<button class="uk-modal-close-default" type="button" uk-close />
</style> {/if}
<slot />
</div>
</div>

View File

@ -0,0 +1,23 @@
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
background: rgba(249, 249, 249, 1);
width: 1.8rem;
height: 1.8rem;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: 700;
color: rgba(22, 48, 87, 1);
}
</style>
<button on:click>+</button>

View File

@ -0,0 +1,55 @@
<script>
import getIcon from "./icon";
export let value;
</script>
<style>
.select-container {
padding-bottom: 10px;
font-size: 0.9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
}
select {
display: block;
font-size: 14px;
font-family: sans-serif;
font-weight: 500;
color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid #ccc;
height: 50px;
}
.arrow {
position: absolute;
right: 10px;
top: 0;
bottom: 0;
margin: auto;
width: 30px;
height: 30px;
pointer-events: none;
color: var(--primary100);
}
</style>
<div class="select-container">
<select on:change {value}>
<slot />
</select>
<span class="arrow">
{@html getIcon('chevron-down', '24')}
</span>
</div>

View File

@ -2,9 +2,10 @@
import PropsView from "./PropsView.svelte"; import PropsView from "./PropsView.svelte";
import { store } from "../builderStore"; import { store } from "../builderStore";
import IconButton from "../common/IconButton.svelte"; import IconButton from "../common/IconButton.svelte";
import { LayoutIcon, PaintIcon, TerminalIcon } from '../common/Icons/'; import { LayoutIcon, PaintIcon, TerminalIcon, EventsIcon } from '../common/Icons/';
import CodeEditor from './CodeEditor.svelte'; import CodeEditor from './CodeEditor.svelte';
import LayoutEditor from './LayoutEditor.svelte'; import LayoutEditor from './LayoutEditor.svelte';
import EventsEditor from "./EventsEditor";
let current_view = 'props'; let current_view = 'props';
@ -17,6 +18,7 @@
const onPropChanged = store.setComponentProp; const onPropChanged = store.setComponentProp;
const onStyleChanged = store.setComponentStyle; const onStyleChanged = store.setComponentStyle;
</script> </script>
<div class="root"> <div class="root">
@ -36,6 +38,11 @@
<TerminalIcon /> <TerminalIcon />
</button> </button>
</li> </li>
<li>
<button class:selected={current_view === 'events'} on:click={() => current_view = 'events'}>
<EventsIcon />
</button>
</li>
</ul> </ul>
{#if !componentInfo.component} {#if !componentInfo.component}
@ -45,6 +52,8 @@
<PropsView {componentInfo} {components} {onPropChanged} /> <PropsView {componentInfo} {components} {onPropChanged} />
{:else if current_view === 'layout'} {:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {componentInfo}/> <LayoutEditor {onStyleChanged} {componentInfo}/>
{:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} />
{:else} {:else}
<CodeEditor /> <CodeEditor />
{/if} {/if}

View File

@ -1,88 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import EventSelector from "./EventSelector.svelte";
import {
filter
} from "lodash/fp";
import {EVENT_TYPE_MEMBER_NAME} from "../common/eventHandlers";
export let parentProps;
export let propDef;
export let onValueChanged;
$: events = parentProps[propDef.____name];
const addHandler = () => {
const newHandler = {parameters:{}};
newHandler[EVENT_TYPE_MEMBER_NAME] = "";
events = [...events, newHandler];
onValueChanged(events);
}
const onEventHandlerChanged = (oldEvent) => (newEvent) => {
const indexOfOldEvent = events.indexOf(oldEvent);
const newEvents = [...events];
newEvents.splice(
events.indexOf(oldEvent),
1,
newEvent);
events = newEvents;
onValueChanged(events);
}
const removeHandler = (index) => () => {
events = filter(e => e !== events[index])(events);
onValueChanged(events);
}
</script>
<div class="root">
<div class="control-container">
{#each events as ev, index}
<div class="handler-container">
<EventSelector onChanged={onEventHandlerChanged(ev)}
onRemoved={removeHandler(index)}
event={ev} />
</div>
<div class="separator"></div>
{/each}
<div class="addelement-container"
on:click={addHandler}>
<IconButton icon="plus"
size="12"/>
</div>
</div>
</div>
<style>
.addelement-container {
cursor: pointer;
padding: 3px 0px;
text-align: center;
}
.addelement-container:hover {
background-color: var(--primary25);
margin-top: 5px;
}
.control-container {
padding-left: 3px;
background: var(--secondary10);
}
.separator {
width: 60%;
margin: 10px auto;
border-style:solid;
border-width: 1px 0 0 0;
border-color: var(--primary25);
}
</style>

View File

@ -1,105 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import StateBindingControl from "./StateBindingControl.svelte";
import {
find, map, keys, reduce, keyBy
} from "lodash/fp";
import { pipe, userWithFullAccess } from "../common/core";
import { EVENT_TYPE_MEMBER_NAME, allHandlers } from "../common/eventHandlers";
import { store } from "../builderStore";
export let event;
export let onChanged;
export let onRemoved;
let eventType;
let parameters = [];
$: events = allHandlers(
{hierarchy: $store.hierarchy},
userWithFullAccess({
hierarchy: s.hierarchy,
actions: keyBy("name")($store.actions)
})
);
$: if(event) {
eventType = event[EVENT_TYPE_MEMBER_NAME];
parameters = pipe(event.parameters, [
keys,
map(k => ({name:k, value:event.parameters[k]}))
]);
} else {
eventType = "";
parameters = [];
}
const eventChanged = (type, parameters) => {
const paramsAsObject = reduce(
(obj, p) => {
obj[p.name] = p.value;
return obj;
}
, {}
)(parameters)
const ev = {};
ev[EVENT_TYPE_MEMBER_NAME]=type;
ev.parameters = paramsAsObject;
onChanged(ev);
}
const eventTypeChanged = (ev) => {
const eType = find(e => e.name === ev.target.value)(events);
const emptyParameters = map(p => ({name:p, value:""}))(eType.parameters);
eventChanged(eType.name, emptyParameters);
}
const onParameterChanged = index => val => {
const newparameters = [...parameters];
newparameters[index].value = val;
eventChanged(eventType, newparameters);
}
</script>
<div class="type-selector-container">
<select class="type-selector uk-select uk-form-small " value={eventType} on:change={eventTypeChanged}>
<option></option>
{#each events as ev}
<option value={ev.name}>{ev.name}</option>
{/each}
</select>
<IconButton icon="trash"
size="12"
on:click={onRemoved}/>
</div>
{#if parameters}
{#each parameters as p, index}
<div>
{p.name}
</div>
<StateBindingControl onChanged={onParameterChanged(index)}
value={p.value} />
{/each}
{/if}
<style>
.type-selector-container {
display: flex;
}
.type-selector {
border-color: var(--primary50);
border-radius: 2px;
width: 50px;
flex: 1 0 auto;
}
</style>

View File

@ -0,0 +1,164 @@
<script>
import Modal from "../../common/Modal.svelte";
import HandlerSelector from "./HandlerSelector.svelte";
import IconButton from "../../common/IconButton.svelte";
import ActionButton from "../../common/ActionButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte";
import Input from "../../common/Input.svelte";
import getIcon from "../../common/icon";
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers";
export let event;
export let eventOptions;
export let open;
export let onClose;
export let onPropChanged;
let eventType = "onClick";
let draftEventHandler = { parameters: [] };
$: eventData = event || { handlers: [] };
const closeModal = () => {
onClose();
draftEventHandler = { parameters: [] };
eventData = { handlers: [] };
};
const updateEventHandler = (updatedHandler, index) => {
eventData.handlers[index] = updatedHandler;
};
const updateDraftEventHandler = updatedHandler => {
draftEventHandler = updatedHandler;
};
const deleteEventHandler = index => {
eventData.handlers.splice(index, 1);
eventData = eventData;
};
const createNewEventHandler = handler => {
const newHandler = handler || {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: ""
};
eventData.handlers.push(newHandler);
eventData = eventData;
};
const deleteEvent = () => {
onPropChanged(eventType, []);
closeModal();
};
const saveEventData = () => {
onPropChanged(eventType, eventData.handlers);
closeModal();
};
</script>
<style>
h2 {
color: var(--primary100);
font-size: 20px;
font-weight: bold;
margin-bottom: 0;
}
h5 {
color: rgba(22, 48, 87, 0.6);
font-size: 15px;
margin: 0;
}
.event-options {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.actions,
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
margin-top: auto;
}
header {
margin-top: 30px;
margin-bottom: 10px;
}
a {
color: rgba(22, 48, 87, 0.6);
font-size: 12px;
margin-top: 0;
}
</style>
<Modal bind:isOpen={open} onClosed={closeModal}>
<h2>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
</h2>
<a href="https://docs.budibase.com/" target="_blank">
Click here to learn more about component events
</a>
<div class="event-options">
<div>
<header>
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select :value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
</div>
<header>
<h5>Event Action(s)</h5>
{@html getIcon('info', 20)}
</header>
<HandlerSelector
newHandler
onChanged={updateDraftEventHandler}
onCreate={() => {
createNewEventHandler(draftEventHandler);
draftEventHandler = { parameters: [] };
}}
handler={draftEventHandler} />
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector
{index}
onChanged={updateEventHandler}
onRemoved={() => deleteEventHandler(index)}
{handler} />
{/each}
{/if}
<div class="actions">
<ActionButton
alert
disabled={eventData.handlers.length === 0}
hidden={!eventData.name}
on:click={deleteEvent}>
Delete
</ActionButton>
<ActionButton
disabled={eventData.handlers.length === 0}
on:click={saveEventData}>
Save
</ActionButton>
</div>
</Modal>

View File

@ -0,0 +1,152 @@
<script>
import {
keys,
map,
some,
includes,
cloneDeep,
isEqual,
sortBy,
filter,
difference
} from "lodash/fp";
import { pipe } from "../../common/core";
import Checkbox from "../../common/Checkbox.svelte";
import Textbox from "../../common/Textbox.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import IconButton from "../../common/IconButton.svelte";
import Modal from "../../common/Modal.svelte";
import EventEditorModal from "./EventEditorModal.svelte";
import HandlerSelector from "./HandlerSelector.svelte";
import { PencilIcon } from "../../common/Icons";
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers";
export const EVENT_TYPE = "event";
export let componentInfo;
export let onPropChanged = () => {};
export let components;
let modalOpen = false;
let events = [];
let selectedEvent = null;
$: {
events = Object.keys(componentInfo)
.filter(key => findType(key) === EVENT_TYPE)
.map(key => ({ name: key, handlers: componentInfo[key] }));
}
function findType(propName) {
if (!componentInfo._component) return;
return components.find(({ name }) => name === componentInfo._component)
.props[propName];
}
const openModal = event => {
selectedEvent = event;
modalOpen = true;
};
const closeModal = () => {
selectedEvent = null;
modalOpen = false;
};
</script>
<style>
h3 {
text-transform: uppercase;
font-size: 12px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
.root {
font-size: 10pt;
width: 100%;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
.handler-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: 2px solid #f9f9f9;
height: 80px;
}
.hierarchy-item {
cursor: pointer;
padding: 11px 7px;
margin: 5px 0;
border-radius: 5px;
font-size: 1.5em;
width: 100%;
}
.hierarchy-item:hover {
background: #f9f9f9;
}
.event-name {
margin-top: 5px;
font-weight: bold;
font-size: 16px;
color: rgba(22, 48, 87, 0.6);
align-self: end;
}
.edit-text {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
align-self: end;
justify-self: end;
font-size: 10px;
color: rgba(35, 65, 105, 0.4);
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;
}
</style>
<header>
<h3>Events</h3>
<PlusButton on:click={() => openModal()} />
</header>
<div class="root">
<form class="uk-form-stacked form-root">
{#each events as event, index}
{#if event.handlers.length > 0}
<div
class="handler-container hierarchy-item {selectedEvent && selectedEvent.index === index ? 'selected' : ''}"
on:click={() => openModal({ ...event, index })}>
<span class="event-name">{event.name}</span>
<span class="edit-text">EDIT</span>
</div>
{/if}
{/each}
</form>
</div>
<EventEditorModal
{onPropChanged}
open={modalOpen}
onClose={closeModal}
eventOptions={events}
event={selectedEvent}
/>

View File

@ -0,0 +1,148 @@
<script>
import IconButton from "../../common/IconButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte";
import StateBindingControl from "../StateBindingControl.svelte";
import { find, map, keys, reduce, keyBy } from "lodash/fp";
import { pipe, userWithFullAccess } from "../../common/core";
import {
EVENT_TYPE_MEMBER_NAME,
allHandlers
} from "../../common/eventHandlers";
import { store } from "../../builderStore";
export let handler;
export let onCreate;
export let onChanged;
export let onRemoved;
export let index;
export let newHandler;
let eventOptions;
let handlerType;
let parameters = [];
$: eventOptions = allHandlers(
{ hierarchy: $store.hierarchy },
userWithFullAccess({
hierarchy: $store.hierarchy,
actions: keyBy("name")($store.actions)
})
);
$: {
if (handler) {
handlerType = handler[EVENT_TYPE_MEMBER_NAME];
parameters = Object.entries(handler.parameters).map(([name, value]) => ({
name,
value
}));
} else {
// Empty Handler
handlerType = "";
parameters = [];
}
}
const handlerChanged = (type, params) => {
const handlerParams = {};
for (let param of params) {
handlerParams[param.name] = param.value;
}
const updatedHandler = {
[EVENT_TYPE_MEMBER_NAME]: type,
parameters: handlerParams
};
onChanged(updatedHandler, index);
};
const handlerTypeChanged = e => {
const handlerType = eventOptions.find(
handler => handler.name === e.target.value
);
const defaultParams = handlerType.parameters.map(param => ({
name: param,
value: ""
}));
handlerChanged(handlerType.name, defaultParams);
};
const onParameterChanged = index => value => {
const newParams = [...parameters];
newParams[index].value = value;
handlerChanged(handlerType, newParams);
};
</script>
<style>
.type-selector-container {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(223, 223, 223, 0.5);
border: 1px solid #dfdfdf;
margin-bottom: 18px;
}
.handler-option {
display: flex;
flex-direction: column;
}
.new-handler {
background: #fff;
}
.handler-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding: 22px;
}
.event-action-button {
margin-right: 20px;
}
span {
font-size: 12px;
margin-bottom: 5px;
}
</style>
<div class="type-selector-container {newHandler && 'new-handler'}">
<div class="handler-controls">
<div class="handler-option">
<span>Action</span>
<Select value={handlerType} on:change={handlerTypeChanged}>
<option />
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
{#if parameters}
{#each parameters as param, idx}
<div class="handler-option">
<span>{param.name}</span>
<StateBindingControl
onChanged={onParameterChanged(idx)}
value={param.value} />
</div>
{/each}
{/if}
</div>
<div class="event-action-button">
{#if parameters.length > 0}
{#if newHandler}
<PlusButton on:click={onCreate} />
{:else}
<IconButton icon="x" on:click={onRemoved} />
{/if}
{/if}
</div>
</div>

View File

@ -0,0 +1 @@
export { default } from "./EventsEditor.svelte";

View File

@ -1,5 +1,6 @@
<script> <script>
import IconButton from "../common/IconButton.svelte"; import IconButton from "../common/IconButton.svelte";
import Input from "../common/Input.svelte";
import { import {
isBinding, getBinding, setBinding isBinding, getBinding, setBinding
} from "../common/binding"; } from "../common/binding";
@ -119,9 +120,9 @@
{/each} {/each}
</select> </select>
{:else} {:else}
<input on:change={ev => onChanged(ev.target.value)} <Input
bind:value={value} on:change={ev => onChanged(ev.target.value)}
style="flex: 1 0 auto;" /> bind:value={value} />
{/if} {/if}
</div> </div>

View File

@ -14,7 +14,7 @@ module.exports = async (budibaseContext) => {
app.context.master, app.context.master,
config.latestPackagesFolder config.latestPackagesFolder
); );
app.use(koaBody({ multipart : true })); app.use(koaBody({ multipart: true }));
app.use(router(config, app).routes()); app.use(router(config, app).routes());
return app.listen(config.port); return app.listen(config.port);
}; };