Merge pull request #4393 from Budibase/fix/feb-fixes
Various February fixes and features
This commit is contained in:
commit
d7337e18ec
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
@ -11,36 +12,67 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let active = false
|
export let active = false
|
||||||
|
export let tooltip = undefined
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<div class:container={!!tooltip}>
|
||||||
class:spectrum-Button--cta={cta}
|
<button
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--warning={warning}
|
class:spectrum-Button--secondary={secondary}
|
||||||
class:spectrum-Button--overBackground={overBackground}
|
class:spectrum-Button--warning={warning}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--overBackground={overBackground}
|
||||||
class:active
|
class:spectrum-Button--quiet={quiet}
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class:active
|
||||||
{disabled}
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
on:click|preventDefault
|
{disabled}
|
||||||
>
|
on:click|preventDefault
|
||||||
{#if icon}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
<svg
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
>
|
||||||
focusable="false"
|
{#if icon}
|
||||||
aria-hidden="true"
|
<svg
|
||||||
aria-label={icon}
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
>
|
focusable="false"
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
aria-hidden="true"
|
||||||
</svg>
|
aria-label={icon}
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{#if $$slots}
|
||||||
|
<span class="spectrum-Button-label"><slot /></span>
|
||||||
|
{/if}
|
||||||
|
{#if !disabled && tooltip}
|
||||||
|
<div class="tooltip-icon">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Info"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if showTooltip && tooltip}
|
||||||
|
<div class="position">
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $$slots}
|
</div>
|
||||||
<span class="spectrum-Button-label"><slot /></span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -49,4 +81,23 @@
|
||||||
.active {
|
.active {
|
||||||
color: var(--spectrum-global-color-blue-600) !important;
|
color: var(--spectrum-global-color-blue-600) !important;
|
||||||
}
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
width: 160px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
.position {
|
||||||
|
position: relative;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.tooltip-icon {
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,16 +14,20 @@
|
||||||
export let value = null
|
export let value = null
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
|
export let timeOnly = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${generateID()}-wrapper`
|
const flatpickrId = `${generateID()}-wrapper`
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr
|
let flatpickr, flatpickrOptions, isTimeOnly
|
||||||
|
|
||||||
|
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
|
||||||
$: flatpickrOptions = {
|
$: flatpickrOptions = {
|
||||||
element: `#${flatpickrId}`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: enableTime || false,
|
enableTime: isTimeOnly || enableTime || false,
|
||||||
|
noCalendar: isTimeOnly || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
|
@ -35,6 +39,11 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
|
// if time only set date component to today
|
||||||
|
if (timeOnly) {
|
||||||
|
const todayDate = new Date().toISOString().split("T")[0]
|
||||||
|
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
||||||
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +76,11 @@
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let date
|
let date
|
||||||
if (val instanceof Date) {
|
let time = new Date(`0-${val}`)
|
||||||
|
// it is a string like 00:00:00, just time
|
||||||
|
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
||||||
|
date = time
|
||||||
|
} else if (val instanceof Date) {
|
||||||
// Use real date obj if already parsed
|
// Use real date obj if already parsed
|
||||||
date = val
|
date = val
|
||||||
} else if (isNaN(val)) {
|
} else if (isNaN(val)) {
|
||||||
|
@ -77,7 +90,7 @@
|
||||||
// Treat as numerical timestamp
|
// Treat as numerical timestamp
|
||||||
date = new Date(parseInt(val))
|
date = new Date(parseInt(val))
|
||||||
}
|
}
|
||||||
const time = date.getTime()
|
time = date.getTime()
|
||||||
if (isNaN(time)) {
|
if (isNaN(time)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -88,69 +101,71 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Flatpickr
|
{#key isTimeOnly}
|
||||||
bind:flatpickr
|
<Flatpickr
|
||||||
value={parseDate(value)}
|
bind:flatpickr
|
||||||
on:open={onOpen}
|
value={parseDate(value)}
|
||||||
on:close={onClose}
|
on:open={onOpen}
|
||||||
options={flatpickrOptions}
|
on:close={onClose}
|
||||||
on:change={handleChange}
|
options={flatpickrOptions}
|
||||||
element={`#${flatpickrId}`}
|
on:change={handleChange}
|
||||||
>
|
element={`#${flatpickrId}`}
|
||||||
<div
|
|
||||||
id={flatpickrId}
|
|
||||||
class:is-disabled={disabled}
|
|
||||||
class:is-invalid={!!error}
|
|
||||||
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
|
|
||||||
class:is-focused={open}
|
|
||||||
aria-readonly="false"
|
|
||||||
aria-required="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
on:click={flatpickr?.open}
|
id={flatpickrId}
|
||||||
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
|
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
|
||||||
|
class:is-focused={open}
|
||||||
|
aria-readonly="false"
|
||||||
|
aria-required="false"
|
||||||
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
{#if !!error}
|
<div
|
||||||
|
on:click={flatpickr?.open}
|
||||||
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
>
|
||||||
|
{#if !!error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
data-input
|
||||||
|
type="text"
|
||||||
|
{disabled}
|
||||||
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
{placeholder}
|
||||||
|
{id}
|
||||||
|
{value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
||||||
|
tabindex="-1"
|
||||||
|
{disabled}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
on:click={flatpickr?.open}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
aria-label="Calendar"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
<use xlink:href="#spectrum-icon-18-Calendar" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
</button>
|
||||||
<input
|
|
||||||
data-input
|
|
||||||
type="text"
|
|
||||||
{disabled}
|
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
|
||||||
{placeholder}
|
|
||||||
{id}
|
|
||||||
{value}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
</Flatpickr>
|
||||||
type="button"
|
{/key}
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
|
||||||
tabindex="-1"
|
|
||||||
{disabled}
|
|
||||||
class:is-invalid={!!error}
|
|
||||||
on:click={flatpickr?.open}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-label="Calendar"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-Calendar" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Flatpickr>
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="overlay" on:mousedown|self={flatpickr?.close} />
|
<div class="overlay" on:mousedown|self={flatpickr?.close} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let enableTime = true
|
export let enableTime = true
|
||||||
|
export let timeOnly = false
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
|
{timeOnly}
|
||||||
{appendTo}
|
{appendTo}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,9 +2,18 @@
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||||
|
// date, but will make actual ISO dates invalid
|
||||||
|
$: time = new Date(`0-${value}`)
|
||||||
|
$: isTime = !isNaN(time)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>{dayjs(value).format("MMMM D YYYY, HH:mm")}</div>
|
<div>
|
||||||
|
{dayjs(isTime ? time : value).format(
|
||||||
|
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
class:icon-small={size === "M" || size === "S"}
|
class:icon-small={size === "M" || size === "S"}
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:focus
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +48,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
dispatch("change")
|
dispatch("change")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
dispatch("save", value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="parent">
|
<div class="parent">
|
||||||
|
@ -39,7 +43,10 @@
|
||||||
name="SaveFloppy"
|
name="SaveFloppy"
|
||||||
hoverable
|
hoverable
|
||||||
size="S"
|
size="S"
|
||||||
on:click={() => setEditing(false)}
|
on:click={() => {
|
||||||
|
setEditing(false)
|
||||||
|
save()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if query?.parameters?.length > 0}
|
{#if query?.parameters?.length > 0}
|
||||||
<div>
|
<div class="params">
|
||||||
<BindingBuilder
|
<BindingBuilder
|
||||||
bind:customParams={parameters.queryParams}
|
bind:customParams={parameters.queryParams}
|
||||||
queryBindings={query.parameters}
|
queryBindings={query.parameters}
|
||||||
|
@ -70,3 +70,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.params {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -59,6 +59,9 @@
|
||||||
$: schemaReadOnly = !responseSuccess
|
$: schemaReadOnly = !responseSuccess
|
||||||
$: variablesReadOnly = !responseSuccess
|
$: variablesReadOnly = !responseSuccess
|
||||||
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
|
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
|
||||||
|
$: hasSchema =
|
||||||
|
Object.keys(schema || {}).length !== 0 ||
|
||||||
|
Object.keys(query?.schema || {}).length !== 0
|
||||||
|
|
||||||
function getSelectedQuery() {
|
function getSelectedQuery() {
|
||||||
return cloneDeep(
|
return cloneDeep(
|
||||||
|
@ -294,6 +297,7 @@
|
||||||
bind:value={query.name}
|
bind:value={query.name}
|
||||||
defaultValue="Untitled"
|
defaultValue="Untitled"
|
||||||
on:change={() => (query.flags.urlName = false)}
|
on:change={() => (query.flags.urlName = false)}
|
||||||
|
on:save={saveQuery}
|
||||||
/>
|
/>
|
||||||
<div class="access">
|
<div class="access">
|
||||||
<Label>Access level</Label>
|
<Label>Access level</Label>
|
||||||
|
@ -313,7 +317,15 @@
|
||||||
<div class="url">
|
<div class="url">
|
||||||
<Input bind:value={url} placeholder="http://www.api.com/endpoint" />
|
<Input bind:value={url} placeholder="http://www.api.com/endpoint" />
|
||||||
</div>
|
</div>
|
||||||
<Button cta disabled={!url} on:click={runQuery}>Send</Button>
|
<Button primary disabled={!url} on:click={runQuery}>Send</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!query.name}
|
||||||
|
cta
|
||||||
|
on:click={saveQuery}
|
||||||
|
tooltip={!hasSchema
|
||||||
|
? "Saving a query before sending will mean no schema is generated"
|
||||||
|
: null}>Save</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
||||||
<Tab title="Bindings">
|
<Tab title="Bindings">
|
||||||
|
@ -527,9 +539,6 @@
|
||||||
>{response?.info.size}</span
|
>{response?.info.size}</span
|
||||||
>
|
>
|
||||||
</Label>
|
</Label>
|
||||||
<Button disabled={!responseSuccess} cta on:click={saveQuery}
|
|
||||||
>Save query</Button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -2453,6 +2453,12 @@
|
||||||
"key": "enableTime",
|
"key": "enableTime",
|
||||||
"defaultValue": true
|
"defaultValue": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Time Only",
|
||||||
|
"key": "timeOnly",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let enableTime = false
|
export let enableTime = false
|
||||||
|
export let timeOnly = false
|
||||||
export let validation
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
appendTo={document.getElementById("flatpickr-root")}
|
appendTo={document.getElementById("flatpickr-root")}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
|
{timeOnly}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -36,13 +36,9 @@ CREATE TABLE people
|
||||||
INSERT products
|
INSERT products
|
||||||
(name, description)
|
(name, description)
|
||||||
VALUES
|
VALUES
|
||||||
('Bananas', 'Fruit thing');
|
('Bananas', 'Fruit thing'),
|
||||||
|
|
||||||
INSERT products
|
|
||||||
(name, description)
|
|
||||||
VALUES
|
|
||||||
('Meat', 'Animal thing');
|
('Meat', 'Animal thing');
|
||||||
|
|
||||||
INSERT tasks
|
INSERT tasks
|
||||||
(taskname, productid)
|
(taskname, productid)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
|
@ -19,6 +19,12 @@ CREATE TABLE Tasks (
|
||||||
FOREIGN KEY(PersonID)
|
FOREIGN KEY(PersonID)
|
||||||
REFERENCES Persons(PersonID)
|
REFERENCES Persons(PersonID)
|
||||||
);
|
);
|
||||||
|
CREATE TABLE Products (
|
||||||
|
id serial primary key,
|
||||||
|
name text,
|
||||||
|
updated time
|
||||||
|
);
|
||||||
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07');
|
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07');
|
||||||
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
|
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
|
||||||
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
|
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
|
||||||
|
INSERT INTO Products (name, updated) VALUES ('Meat', '11:00:22'), ('Fruit', '10:00:00');
|
||||||
|
|
|
@ -5,8 +5,11 @@ exports.csv = function (headers, rows) {
|
||||||
csv = `${csv}\n${headers
|
csv = `${csv}\n${headers
|
||||||
.map(header => {
|
.map(header => {
|
||||||
let val = row[header]
|
let val = row[header]
|
||||||
val = typeof val === "object" ? JSON.stringify(val) : val
|
val =
|
||||||
return `"${val}"`.trim()
|
typeof val === "object"
|
||||||
|
? `"${JSON.stringify(val).replace(/"/g, "'")}"`
|
||||||
|
: `"${val}"`
|
||||||
|
return val.trim()
|
||||||
})
|
})
|
||||||
.join(",")}`
|
.join(",")}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,6 @@ describe("/queries", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("variables", () => {
|
describe("variables", () => {
|
||||||
|
|
||||||
async function preview(datasource, fields) {
|
async function preview(datasource, fields) {
|
||||||
return config.previewQuery(request, config, datasource, fields)
|
return config.previewQuery(request, config, datasource, fields)
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,10 +161,16 @@ class QueryRunner {
|
||||||
const responses = await Promise.all(dynamics)
|
const responses = await Promise.all(dynamics)
|
||||||
for (let i = 0; i < foundVars.length; i++) {
|
for (let i = 0; i < foundVars.length; i++) {
|
||||||
const variable = foundVars[i]
|
const variable = foundVars[i]
|
||||||
parameters[variable.name] = processStringSync(variable.value, {
|
parameters[variable.name] = processStringSync(
|
||||||
data: responses[i].rows,
|
variable.value,
|
||||||
info: responses[i].extra,
|
{
|
||||||
})
|
data: responses[i].rows,
|
||||||
|
info: responses[i].extra,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
escapeNewlines: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
// make sure its known that this uses dynamic variables in case it fails
|
// make sure its known that this uses dynamic variables in case it fails
|
||||||
this.hasDynamicVariables = true
|
this.hasDynamicVariables = true
|
||||||
}
|
}
|
||||||
|
@ -188,6 +194,7 @@ class QueryRunner {
|
||||||
enrichedQuery[key] = processStringSync(fields[key], parameters, {
|
enrichedQuery[key] = processStringSync(fields[key], parameters, {
|
||||||
noEscaping: true,
|
noEscaping: true,
|
||||||
noHelpers: true,
|
noHelpers: true,
|
||||||
|
escapeNewlines: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
enrichedQuery[key] = fields[key]
|
enrichedQuery[key] = fields[key]
|
||||||
|
|
|
@ -1163,19 +1163,7 @@
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/string-templates@1.0.50-alpha.1":
|
"@budibase/string-templates@^1.0.50", "@budibase/string-templates@^1.0.50-alpha.1":
|
||||||
version "1.0.50-alpha.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50-alpha.1.tgz#ea05a74a3031850652812be21f44d40249e21d43"
|
|
||||||
integrity sha512-TM6WNzGBlwV0FhR8luejg8CQGa9IaMrMLrW+gfkl4fqEafoWIeKueVhT/lVuPtUK2NzGxmKfNRoasZu6wM6grA==
|
|
||||||
dependencies:
|
|
||||||
"@budibase/handlebars-helpers" "^0.11.7"
|
|
||||||
dayjs "^1.10.4"
|
|
||||||
handlebars "^4.7.6"
|
|
||||||
handlebars-utils "^1.0.6"
|
|
||||||
lodash "^4.17.20"
|
|
||||||
vm2 "^3.9.4"
|
|
||||||
|
|
||||||
"@budibase/string-templates@^1.0.50":
|
|
||||||
version "1.0.50"
|
version "1.0.50"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50.tgz#53386f3c09891ef21bd47870d25cf7e12f6fac86"
|
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50.tgz#53386f3c09891ef21bd47870d25cf7e12f6fac86"
|
||||||
integrity sha512-jmqmikU3Xt0I0hY1S1QI8x5770Z+Tu6zGxb9iQsk3PUCkKCpHz70Rl3Q7x5nYsDfOlhT4YHaHKgYYQYJ6hRafw==
|
integrity sha512-jmqmikU3Xt0I0hY1S1QI8x5770Z+Tu6zGxb9iQsk3PUCkKCpHz70Rl3Q7x5nYsDfOlhT4YHaHKgYYQYJ6hRafw==
|
||||||
|
|
|
@ -21,7 +21,7 @@ const HELPERS = [
|
||||||
// javascript helper
|
// javascript helper
|
||||||
new Helper(HelperFunctionNames.JS, processJS, false),
|
new Helper(HelperFunctionNames.JS, processJS, false),
|
||||||
// this help is applied to all statements
|
// this help is applied to all statements
|
||||||
new Helper(HelperFunctionNames.ALL, value => {
|
new Helper(HelperFunctionNames.ALL, (value, { __opts }) => {
|
||||||
if (
|
if (
|
||||||
value != null &&
|
value != null &&
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
|
@ -36,7 +36,11 @@ const HELPERS = [
|
||||||
if (value && value.string) {
|
if (value && value.string) {
|
||||||
value = value.string
|
value = value.string
|
||||||
}
|
}
|
||||||
let text = new SafeString(value.replace(/&/g, "&"))
|
let text = value
|
||||||
|
if (__opts && __opts.escapeNewlines) {
|
||||||
|
text = value.replace(/\n/g, "\\n")
|
||||||
|
}
|
||||||
|
text = new SafeString(text.replace(/&/g, "&"))
|
||||||
if (text == null || typeof text !== "string") {
|
if (text == null || typeof text !== "string") {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
@ -62,10 +66,14 @@ module.exports.HelperNames = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.registerAll = handlebars => {
|
module.exports.registerMinimum = handlebars => {
|
||||||
for (let helper of HELPERS) {
|
for (let helper of HELPERS) {
|
||||||
helper.register(handlebars)
|
helper.register(handlebars)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.registerAll = handlebars => {
|
||||||
|
module.exports.registerMinimum(handlebars)
|
||||||
// register imported helpers
|
// register imported helpers
|
||||||
externalHandlebars.registerAll(handlebars)
|
externalHandlebars.registerAll(handlebars)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const handlebars = require("handlebars")
|
const handlebars = require("handlebars")
|
||||||
const { registerAll } = require("./helpers/index")
|
const { registerAll, registerMinimum } = require("./helpers/index")
|
||||||
const processors = require("./processors")
|
const processors = require("./processors")
|
||||||
const { atob, btoa } = require("./utilities")
|
const { atob, btoa } = require("./utilities")
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
|
@ -8,6 +8,7 @@ const { FIND_HBS_REGEX, FIND_DOUBLE_HBS_REGEX } = require("./utilities")
|
||||||
const hbsInstance = handlebars.create()
|
const hbsInstance = handlebars.create()
|
||||||
registerAll(hbsInstance)
|
registerAll(hbsInstance)
|
||||||
const hbsInstanceNoHelpers = handlebars.create()
|
const hbsInstanceNoHelpers = handlebars.create()
|
||||||
|
registerMinimum(hbsInstanceNoHelpers)
|
||||||
const defaultOpts = { noHelpers: false, noEscaping: false }
|
const defaultOpts = { noHelpers: false, noEscaping: false }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,9 +106,7 @@ module.exports.processStringSync = (string, context, opts) => {
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// finalising adds a helper, can't do this with no helpers
|
string = processors.preprocess(string, opts)
|
||||||
const shouldFinalise = !opts.noHelpers
|
|
||||||
string = processors.preprocess(string, shouldFinalise)
|
|
||||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||||
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||||
const templateString =
|
const templateString =
|
||||||
|
@ -119,8 +118,10 @@ module.exports.processStringSync = (string, context, opts) => {
|
||||||
return processors.postprocess(
|
return processors.postprocess(
|
||||||
template({
|
template({
|
||||||
now: new Date(now).toISOString(),
|
now: new Date(now).toISOString(),
|
||||||
|
__opts: opts,
|
||||||
...context,
|
...context,
|
||||||
})
|
}),
|
||||||
|
{ escapeNewlines: opts ? opts.escapeNewlines : false }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return input
|
return input
|
||||||
|
@ -176,7 +177,9 @@ module.exports.isValid = (string, opts) => {
|
||||||
const context = {}
|
const context = {}
|
||||||
try {
|
try {
|
||||||
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
|
||||||
instance.compile(processors.preprocess(string, false))(context)
|
instance.compile(processors.preprocess(string, { noFinalise: true }))(
|
||||||
|
context
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err && err.message ? err.message : err
|
const msg = err && err.message ? err.message : err
|
||||||
|
|
|
@ -2,7 +2,7 @@ const { FIND_HBS_REGEX } = require("../utilities")
|
||||||
const preprocessor = require("./preprocessor")
|
const preprocessor = require("./preprocessor")
|
||||||
const postprocessor = require("./postprocessor")
|
const postprocessor = require("./postprocessor")
|
||||||
|
|
||||||
function process(output, processors) {
|
function process(output, processors, opts) {
|
||||||
for (let processor of processors) {
|
for (let processor of processors) {
|
||||||
// if a literal statement has occurred stop
|
// if a literal statement has occurred stop
|
||||||
if (typeof output !== "string") {
|
if (typeof output !== "string") {
|
||||||
|
@ -15,24 +15,22 @@ function process(output, processors) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (let match of matches) {
|
for (let match of matches) {
|
||||||
output = processor.process(output, match)
|
output = processor.process(output, match, opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.preprocess = (string, finalise = true) => {
|
module.exports.preprocess = (string, opts) => {
|
||||||
let processors = preprocessor.processors
|
let processors = preprocessor.processors
|
||||||
// the pre-processor finalisation stops handlebars from ever throwing an error
|
if (opts.noFinalise) {
|
||||||
// might want to pre-process for other benefits but still want to see errors
|
|
||||||
if (!finalise) {
|
|
||||||
processors = processors.filter(
|
processors = processors.filter(
|
||||||
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
|
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return process(string, processors, opts)
|
||||||
|
}
|
||||||
|
module.exports.postprocess = string => {
|
||||||
|
let processors = postprocessor.processors
|
||||||
return process(string, processors)
|
return process(string, processors)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.postprocess = string => {
|
|
||||||
return process(string, postprocessor.processors)
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ class Postprocessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.PostProcessorNames = PostProcessorNames
|
||||||
|
|
||||||
module.exports.processors = [
|
module.exports.processors = [
|
||||||
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
|
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
|
||||||
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {
|
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {
|
||||||
|
|
|
@ -16,8 +16,8 @@ class Preprocessor {
|
||||||
this.fn = fn
|
this.fn = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
process(fullString, statement) {
|
process(fullString, statement, opts) {
|
||||||
const output = this.fn(statement)
|
const output = this.fn(statement, opts)
|
||||||
const idx = fullString.indexOf(statement)
|
const idx = fullString.indexOf(statement)
|
||||||
return swapStrings(fullString, idx, statement.length, output)
|
return swapStrings(fullString, idx, statement.length, output)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,8 @@ module.exports.processors = [
|
||||||
return statement
|
return statement
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new Preprocessor(PreprocessorNames.FINALISE, statement => {
|
new Preprocessor(PreprocessorNames.FINALISE, (statement, opts) => {
|
||||||
|
const noHelpers = opts && opts.noHelpers
|
||||||
let insideStatement = statement.slice(2, statement.length - 2)
|
let insideStatement = statement.slice(2, statement.length - 2)
|
||||||
if (insideStatement.charAt(0) === " ") {
|
if (insideStatement.charAt(0) === " ") {
|
||||||
insideStatement = insideStatement.slice(1)
|
insideStatement = insideStatement.slice(1)
|
||||||
|
@ -63,7 +64,10 @@ module.exports.processors = [
|
||||||
return statement
|
return statement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (HelperNames().some(option => option.includes(possibleHelper))) {
|
if (
|
||||||
|
!noHelpers &&
|
||||||
|
HelperNames().some(option => option.includes(possibleHelper))
|
||||||
|
) {
|
||||||
insideStatement = `(${insideStatement})`
|
insideStatement = `(${insideStatement})`
|
||||||
}
|
}
|
||||||
return `{{ all ${insideStatement} }}`
|
return `{{ all ${insideStatement} }}`
|
||||||
|
|
|
@ -59,3 +59,33 @@ describe("attempt some complex problems", () => {
|
||||||
expect(output).toBe("nulltest")
|
expect(output).toBe("nulltest")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("check behaviour with newlines", () => {
|
||||||
|
const context = {
|
||||||
|
binding: `Hello
|
||||||
|
there`
|
||||||
|
}
|
||||||
|
it("should escape new line to \\n with double brace", async () => {
|
||||||
|
const hbs = JSON.stringify({
|
||||||
|
body: "{{ binding }}"
|
||||||
|
})
|
||||||
|
const output = await processString(hbs, context, { escapeNewlines: true })
|
||||||
|
expect(JSON.parse(output).body).toBe(context.binding)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should work the same with triple brace", async () => {
|
||||||
|
const hbs = JSON.stringify({
|
||||||
|
body: "{{{ binding }}}"
|
||||||
|
})
|
||||||
|
const output = await processString(hbs, context, { escapeNewlines: true })
|
||||||
|
expect(JSON.parse(output).body).toBe(context.binding)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should still work with helpers disabled", async () => {
|
||||||
|
const hbs = JSON.stringify({
|
||||||
|
body: "{{ binding }}"
|
||||||
|
})
|
||||||
|
const output = await processString(hbs, context, { escapeNewlines: true, noHelpers: true })
|
||||||
|
expect(JSON.parse(output).body).toBe(context.binding)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe("test that it can run without helpers", () => {
|
||||||
)
|
)
|
||||||
const valid = await processString("{{ avg 1 1 1 }}", {})
|
const valid = await processString("{{ avg 1 1 1 }}", {})
|
||||||
expect(valid).toBe("1")
|
expect(valid).toBe("1")
|
||||||
expect(output).toBe("{{ avg 1 1 1 }}")
|
expect(output).toBe("")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue