Merge branch 'master' of github.com:Budibase/budibase into bugfixes

This commit is contained in:
Michael Shanks 2020-06-30 14:13:27 +01:00
commit 4df27df9af
9 changed files with 443 additions and 151 deletions

View File

@ -13,16 +13,18 @@
import Palette from "./Palette.svelte" import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte" import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte" import Input from "./Input.svelte"
import Portal from "./Portal.svelte"
export let value = "#3ec1d3ff" export let value = "#3ec1d3ff"
export let open = false;
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false export let disableSwatches = false
export let format = "hexa" export let format = "hexa"
export let open = false export let style = ""
export let pickerHeight = 0 export let pickerHeight = 0
export let pickerWidth = 0 export let pickerWidth = 0
let colorPicker = null
let adder = null let adder = null
let h = null let h = null
@ -38,6 +40,10 @@
getRecentColors() getRecentColors()
} }
if(colorPicker) {
colorPicker.focus()
}
if (format) { if (format) {
convertAndSetHSVA() convertAndSetHSVA()
} }
@ -50,6 +56,12 @@
} }
} }
function handleEscape(e) {
if(open && e.key === "Escape") {
open = false
}
}
function setRecentColor(color) { function setRecentColor(color) {
if (swatches.length === 12) { if (swatches.length === 12) {
swatches.splice(0, 1) swatches.splice(0, 1)
@ -145,85 +157,97 @@
} }
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : "" $: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: style = buildStyle({ background: value, border }) $: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0 $: shrink = swatches.length > 0
</script> </script>
<div
class="colorpicker-container"
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="palette-panel"> <Portal>
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} /> <div
</div> class="colorpicker-container"
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
on:keydown={handleEscape}
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="control-panel"> <div class="palette-panel">
<div class="alpha-hue-panel"> <Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
<div> </div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px"> <div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" style={selectedColorStyle} />
</CheckedBackground>
</div>
<div>
<Slider <Slider
type="alpha" type="hue"
value={a} value={h}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)} on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} /> on:dragend={dispatchValue} />
</CheckedBackground>
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue} />
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div> </div>
</div> </div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div> </div>
</Portal>
</div>
<style> <style>
.colorpicker-container { .colorpicker-container {
position: absolute;
outline: none;
z-index: 3;
display: flex; display: flex;
font-size: 11px; font-size: 11px;
font-weight: 400; font-weight: 400;
flex-direction: column; flex-direction: column;
/* height: 265px; */ margin: 5px 0px;
height: auto; height: auto;
width: 220px; width: 220px;
background: #ffffff; background: #ffffff;

View File

@ -1,7 +1,7 @@
<script> <script>
import Colorpicker from "./Colorpicker.svelte" import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte" import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, afterUpdate, beforeUpdate } from "svelte" import { createEventDispatcher, beforeUpdate } from "svelte"
import { buildStyle } from "./helpers.js" import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -15,7 +15,8 @@
export let height = "25px" export let height = "25px"
let format = "hexa" let format = "hexa"
let dimensions = { top: 0, left: 0 } let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null let colorPreview = null
let previewHeight = null let previewHeight = null
@ -23,17 +24,8 @@
let pickerWidth = 0 let pickerWidth = 0
let pickerHeight = 0 let pickerHeight = 0
let anchorEl = null
let parentNodes = []
let errorMsg = null let errorMsg = null
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
top: `${dimensions.top}px`,
left: `${dimensions.left}px`,
})
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
beforeUpdate(() => { beforeUpdate(() => {
@ -46,25 +38,6 @@
} }
}) })
afterUpdate(() => {
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while (!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window
.getComputedStyle(curEl)
.getPropertyValue("overflow")
if (/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
parentNodes = els
}
})
function openColorpicker(event) { function openColorpicker(event) {
if (colorPreview) { if (colorPreview) {
@ -72,43 +45,45 @@
} }
} }
$: if (open && colorPreview) { function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: if(open && colorPreview) {
const { const {
top: spaceAbove, top: spaceAbove,
width, width,
bottom, bottom,
right, right,
left: spaceLeft, left,
} = colorPreview.getBoundingClientRect() } = colorPreview.getBoundingClientRect()
const { innerHeight, innerWidth } = window
const { offsetLeft, offsetTop } = colorPreview const spaceBelow = window.innerHeight - bottom
//get the scrollTop value for all scrollable parent elements const previewCenter = previewWidth / 2
let scrollTop = parentNodes.reduce(
(scrollAcc, el) => (scrollAcc += el.scrollTop),
0
)
const spaceBelow = innerHeight - spaceAbove - previewHeight let y, x;
const top =
spaceAbove > spaceBelow
? offsetTop - pickerHeight - scrollTop
: offsetTop + previewHeight - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll if(spaceAbove > spaceBelow) {
const spaceRight = innerWidth - spaceLeft + previewWidth positionSide = "bottom"
const left = y = (window.innerHeight - spaceAbove)
spaceRight > spaceLeft }else{
? offsetLeft + previewWidth positionSide = "top"
: offsetLeft - pickerWidth y = bottom
}
dimensions = { top, left } x = (left + previewCenter) - (pickerWidth / 2)
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
} }
function onColorChange(color) { $: previewStyle = buildStyle({ width, height, background: value })
value = color.detail $: errorPreviewStyle = buildStyle({ width, height })
dispatch("change", color.detail) $: pickerStyle = buildStyle({
} [positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</script> </script>
<div class="color-preview-container"> <div class="color-preview-container">
@ -124,19 +99,19 @@
</CheckedBackground> </CheckedBackground>
{#if open} {#if open}
<div transition:fade class="picker-container" style={pickerStyle}> <Colorpicker
<Colorpicker style={pickerStyle}
on:change={onColorChange} on:change={onColorChange}
on:addswatch on:addswatch
on:removeswatch on:removeswatch
bind:format bind:format
bind:value bind:value
bind:pickerHeight bind:pickerHeight
bind:pickerWidth bind:pickerWidth
{swatches} bind:open
{disableSwatches} {swatches}
{open} /> {disableSwatches}
</div> />
<div on:click|self={() => (open = false)} class="overlay" /> <div on:click|self={() => (open = false)} class="overlay" />
{/if} {/if}
{:else} {:else}
@ -154,6 +129,7 @@
} }
.color-preview { .color-preview {
cursor: pointer;
border-radius: 3px; border-radius: 3px;
border: 1px solid #dedada; border: 1px solid #dedada;
} }
@ -166,12 +142,12 @@
cursor: not-allowed; cursor: not-allowed;
} }
.picker-container { /* .picker-container {
position: absolute; position: absolute;
z-index: 3; z-index: 3;
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
} } */
.overlay { .overlay {
position: fixed; position: fixed;

View File

@ -0,0 +1,37 @@
<script>
import { onMount } from "svelte";
export let target = document.body;
let targetEl;
let portal;
let componentInstance;
onMount(() => {
if (typeof target === "string") {
targetEl = document.querySelector(target);
// Force exit
if (targetEl === null) {
return () => {};
}
} else if (target instanceof HTMLElement) {
targetEl = target;
} else {
throw new TypeError(
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.`
);
}
portal = document.createElement("div");
targetEl.appendChild(portal);
portal.appendChild(componentInstance);
return () => {
targetEl.removeChild(portal);
};
});
</script>
<div bind:this={componentInstance}>
<slot />
</div>

View File

@ -321,17 +321,44 @@ export default {
name: "Form", name: "Form",
description: "A component that generates a form from your data.", description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill", icon: "ri-file-edit-fill",
properties: { commonProps: {},
design: { ...all }, children: [
settings: [{ label: "Model", key: "model", control: ModelSelect }], {
}, _component: "@budibase/standard-components/dataform",
_component: "@budibase/standard-components/dataform", name: "Form Basic",
template: { icon: "ri-file-edit-fill",
component: "@budibase/materialdesign-components/Form", properties: {
description: "Form for saving a record", design: { ...all },
name: "@budibase/materialdesign-components/recordForm", settings: [
}, {
children: [], label: "Model",
key: "model",
control: ModelSelect,
},
],
},
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
name: "@budibase/materialdesign-components/recordForm",
},
},
{
_component: "@budibase/standard-components/dataformwide",
name: "Form Wide",
icon: "ri-file-edit-fill",
properties: {
design: { ...all },
settings: [
{
label: "Model",
key: "model",
control: ModelSelect,
},
],
},
},
],
}, },
{ {
name: "Chart", name: "Chart",

View File

@ -88,7 +88,6 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 5;
} }
.preview-pane { .preview-pane {

View File

@ -209,6 +209,13 @@
"model": "models" "model": "models"
} }
}, },
"dataformwide": {
"description": "an HTML table that fetches data from a model or view and displays it.",
"data": true,
"props": {
"model": "models"
}
},
"datalist": { "datalist": {
"description": "A configurable data list that attaches to your backend models.", "description": "A configurable data list that attaches to your backend models.",
"data": true, "data": true,

View File

@ -4,6 +4,12 @@
export let _bb export let _bb
export let model export let model
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
let username let username
let password let password
let newModel = { let newModel = {
@ -59,15 +65,23 @@
<form class="form" on:submit|preventDefault> <form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1> <h1>{modelDef.name} Form</h1>
<hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label> <label class="form-label" for="form-stacked-text">{field}</label>
<input {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
class="input" <select on:blur={handleInput(field)}>
placeholder={field} {#each schema[field].constraints.inclusion as opt}
type={schema[field].type === 'string' ? 'text' : schema[field].type} <option>{opt}</option>
on:change={handleInput(field)} /> {/each}
</select>
{:else}
<input
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
{/if}
</div> </div>
<hr /> <hr />
{/each} {/each}
@ -143,8 +157,42 @@
} }
button:hover { button:hover {
background-color: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
border-color: #393c44; 0 4px 6px -2px rgba(0, 0, 0, 0.05);
color: #393c44; }
input[type="checkbox"] {
width: 32px;
height: 32px;
padding: 0;
margin: 0;
vertical-align: bottom;
position: relative;
top: -1px;
*overflow: hidden;
}
select::-ms-expand {
display: none;
}
select {
display: inline-block;
cursor: pointer;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
} }
</style> </style>

View File

@ -0,0 +1,173 @@
<script>
import { onMount } from "svelte"
export let _bb
export let model
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
let username
let password
let newModel = {
modelId: model,
}
let store = _bb.store
let schema = {}
let modelDef = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = Object.keys(schema)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json()
schema = modelDef.schema
}
async function save() {
const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, newModel)
const json = await response.json()
store.update(state => {
state[model] = state[model] ? [...state[model], json] : [json]
return state
})
}
const handleInput = field => event => {
let value
if (event.target.type === "checkbox") {
value = event.target.checked
newModel[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
newModel[field] = value
return
}
value = event.target.value
newModel[field] = value
}
</script>
<form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1>
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else}
<input
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
{/if}
</div>
<hr />
{/each}
<div class="button-block">
<button on:click={save}>Submit Form</button>
</div>
</div>
</form>
<style>
.form {
display: flex;
flex-direction: column;
justify-content: center;
margin: auto;
padding: 40px;
}
.form-content {
margin-bottom: 20px;
}
.input {
border-radius: 5px;
border: 1px solid #e6e6e6;
padding: 1em;
font-size: 16px;
}
.form-item {
display: grid;
grid-template-columns: 30% 1fr;
margin-bottom: 16px;
align-items: center;
gap: 1em;
}
hr {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
hr:nth-last-child(2) {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
.button-block {
display: flex;
justify-content: flex-end;
}
button {
font-size: 16px;
padding: 8px 16px;
box-sizing: border-box;
border-radius: 4px;
color: white;
background-color: black;
outline: none;
height: 40px;
cursor: pointer;
transition: all 0.2s ease 0s;
overflow: hidden;
outline: none;
user-select: none;
white-space: nowrap;
text-align: center;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
transform: scale(2);
cursor: pointer;
}
select::-ms-expand {
display: none;
}
select {
cursor: pointer;
display: inline-block;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
</style>

View File

@ -16,6 +16,7 @@ export { default as icon } from "./Icon.svelte"
export { default as Navigation } from "./Navigation.svelte" export { default as Navigation } from "./Navigation.svelte"
export { default as datatable } from "./DataTable.svelte" export { default as datatable } from "./DataTable.svelte"
export { default as dataform } from "./DataForm.svelte" export { default as dataform } from "./DataForm.svelte"
export { default as dataformwide } from "./DataFormWide.svelte"
export { default as datachart } from "./DataChart.svelte" export { default as datachart } from "./DataChart.svelte"
export { default as datalist } from "./DataList.svelte" export { default as datalist } from "./DataList.svelte"
export { default as list } from "./List.svelte" export { default as list } from "./List.svelte"