diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index da4d405f02..67930b8030 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -1,5 +1,6 @@ - - {#if icon} - - - + + (showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} + > + {#if icon} + + + + {/if} + {#if $$slots} + + {/if} + {#if !disabled && tooltip} + + + + + + {/if} + + {#if showTooltip && tooltip} + + + + + {/if} - {#if $$slots} - - {/if} - + diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 8edb68a38e..69bfdda72a 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -14,16 +14,20 @@ export let value = null export let placeholder = null export let appendTo = undefined + export let timeOnly = false const dispatch = createEventDispatcher() const flatpickrId = `${generateID()}-wrapper` let open = false - let flatpickr + let flatpickr, flatpickrOptions, isTimeOnly + + $: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly $: flatpickrOptions = { element: `#${flatpickrId}`, - enableTime: enableTime || false, + enableTime: isTimeOnly || enableTime || false, + noCalendar: isTimeOnly || false, 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, appendTo, disableMobile: "true", @@ -35,6 +39,11 @@ if (newValue) { 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) } @@ -67,7 +76,11 @@ return null } 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 date = val } else if (isNaN(val)) { @@ -77,7 +90,7 @@ // Treat as numerical timestamp date = new Date(parseInt(val)) } - const time = date.getTime() + time = date.getTime() if (isNaN(time)) { return null } @@ -88,69 +101,71 @@ } - - - {#if !!error} + + {#if !!error} + + + + {/if} + + + - + - {/if} - + - - - - - - - + +{/key} {#if open} {/if} diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index 7d5656a22d..9298c49177 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -9,6 +9,7 @@ export let disabled = false export let error = null export let enableTime = true + export let timeOnly = false export let placeholder = null export let appendTo = undefined @@ -27,6 +28,7 @@ {value} {placeholder} {enableTime} + {timeOnly} {appendTo} on:change={onChange} /> diff --git a/packages/bbui/src/Table/DateTimeRenderer.svelte b/packages/bbui/src/Table/DateTimeRenderer.svelte index 8a06082d58..ff750cecd8 100644 --- a/packages/bbui/src/Table/DateTimeRenderer.svelte +++ b/packages/bbui/src/Table/DateTimeRenderer.svelte @@ -2,9 +2,18 @@ import dayjs from "dayjs" 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) -{dayjs(value).format("MMMM D YYYY, HH:mm")} + + {dayjs(isTime ? time : value).format( + isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm" + )} + diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 808c3a49ec..6025758e71 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -59,6 +59,9 @@ $: schemaReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) + $: hasSchema = + Object.keys(schema || {}).length !== 0 || + Object.keys(query?.schema || {}).length !== 0 function getSelectedQuery() { return cloneDeep( @@ -294,6 +297,7 @@ bind:value={query.name} defaultValue="Untitled" on:change={() => (query.flags.urlName = false)} + on:save={saveQuery} /> Access level @@ -313,7 +317,15 @@ - Send + Send + Save @@ -527,9 +539,6 @@ >{response?.info.size} - Save query {/if} diff --git a/packages/client/manifest.json b/packages/client/manifest.json index ab48142ad5..29cbc0536c 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2453,6 +2453,12 @@ "key": "enableTime", "defaultValue": true }, + { + "type": "boolean", + "label": "Time Only", + "key": "timeOnly", + "defaultValue": false + }, { "type": "text", "label": "Default value", diff --git a/packages/client/src/components/app/forms/DateTimeField.svelte b/packages/client/src/components/app/forms/DateTimeField.svelte index 515b6adc77..022a634bc5 100644 --- a/packages/client/src/components/app/forms/DateTimeField.svelte +++ b/packages/client/src/components/app/forms/DateTimeField.svelte @@ -7,6 +7,7 @@ export let placeholder export let disabled = false export let enableTime = false + export let timeOnly = false export let validation export let defaultValue @@ -33,6 +34,7 @@ id={fieldState.fieldId} appendTo={document.getElementById("flatpickr-root")} {enableTime} + {timeOnly} {placeholder} /> {/if} diff --git a/packages/server/scripts/integrations/mssql/data/setup.sql b/packages/server/scripts/integrations/mssql/data/setup.sql index d0b4cfc39c..b6ab4f5274 100644 --- a/packages/server/scripts/integrations/mssql/data/setup.sql +++ b/packages/server/scripts/integrations/mssql/data/setup.sql @@ -36,13 +36,9 @@ CREATE TABLE people INSERT products (name, description) VALUES - ('Bananas', 'Fruit thing'); - -INSERT products - (name, description) -VALUES + ('Bananas', 'Fruit thing'), ('Meat', 'Animal thing'); - + INSERT tasks (taskname, productid) VALUES diff --git a/packages/server/scripts/integrations/mysql/init.sql b/packages/server/scripts/integrations/mysql/init.sql index 4dd75c36d3..f37ef0d532 100644 --- a/packages/server/scripts/integrations/mysql/init.sql +++ b/packages/server/scripts/integrations/mysql/init.sql @@ -19,6 +19,12 @@ CREATE TABLE Tasks ( FOREIGN KEY(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 Tasks (PersonID, TaskName) VALUES (1, 'assembling'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing'); +INSERT INTO Products (name, updated) VALUES ('Meat', '11:00:22'), ('Fruit', '10:00:00'); diff --git a/packages/server/src/api/controllers/view/exporters.js b/packages/server/src/api/controllers/view/exporters.js index 0cca3b5f89..1232640d0a 100644 --- a/packages/server/src/api/controllers/view/exporters.js +++ b/packages/server/src/api/controllers/view/exporters.js @@ -5,8 +5,11 @@ exports.csv = function (headers, rows) { csv = `${csv}\n${headers .map(header => { let val = row[header] - val = typeof val === "object" ? JSON.stringify(val) : val - return `"${val}"`.trim() + val = + typeof val === "object" + ? `"${JSON.stringify(val).replace(/"/g, "'")}"` + : `"${val}"` + return val.trim() }) .join(",")}` } diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index 9357d53cde..dac576836e 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -230,7 +230,6 @@ describe("/queries", () => { }) describe("variables", () => { - async function preview(datasource, fields) { return config.previewQuery(request, config, datasource, fields) } diff --git a/packages/server/src/threads/query.js b/packages/server/src/threads/query.js index 23b4dc1ef8..5b1a30b57d 100644 --- a/packages/server/src/threads/query.js +++ b/packages/server/src/threads/query.js @@ -161,10 +161,16 @@ class QueryRunner { const responses = await Promise.all(dynamics) for (let i = 0; i < foundVars.length; i++) { const variable = foundVars[i] - parameters[variable.name] = processStringSync(variable.value, { - data: responses[i].rows, - info: responses[i].extra, - }) + parameters[variable.name] = processStringSync( + variable.value, + { + data: responses[i].rows, + info: responses[i].extra, + }, + { + escapeNewlines: true, + } + ) // make sure its known that this uses dynamic variables in case it fails this.hasDynamicVariables = true } @@ -188,6 +194,7 @@ class QueryRunner { enrichedQuery[key] = processStringSync(fields[key], parameters, { noEscaping: true, noHelpers: true, + escapeNewlines: true, }) } else { enrichedQuery[key] = fields[key] diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index fe17d7d317..ec1052b0b5 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1163,19 +1163,7 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@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": +"@budibase/string-templates@^1.0.50", "@budibase/string-templates@^1.0.50-alpha.1": version "1.0.50" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50.tgz#53386f3c09891ef21bd47870d25cf7e12f6fac86" integrity sha512-jmqmikU3Xt0I0hY1S1QI8x5770Z+Tu6zGxb9iQsk3PUCkKCpHz70Rl3Q7x5nYsDfOlhT4YHaHKgYYQYJ6hRafw== diff --git a/packages/string-templates/src/helpers/index.js b/packages/string-templates/src/helpers/index.js index 6b9195047e..ad4082e3a4 100644 --- a/packages/string-templates/src/helpers/index.js +++ b/packages/string-templates/src/helpers/index.js @@ -21,7 +21,7 @@ const HELPERS = [ // javascript helper new Helper(HelperFunctionNames.JS, processJS, false), // this help is applied to all statements - new Helper(HelperFunctionNames.ALL, value => { + new Helper(HelperFunctionNames.ALL, (value, { __opts }) => { if ( value != null && typeof value === "object" && @@ -36,7 +36,11 @@ const HELPERS = [ if (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") { return text } @@ -62,10 +66,14 @@ module.exports.HelperNames = () => { ) } -module.exports.registerAll = handlebars => { +module.exports.registerMinimum = handlebars => { for (let helper of HELPERS) { helper.register(handlebars) } +} + +module.exports.registerAll = handlebars => { + module.exports.registerMinimum(handlebars) // register imported helpers externalHandlebars.registerAll(handlebars) } diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index 5ffd1bf1cc..7996bb9f1f 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -1,5 +1,5 @@ const handlebars = require("handlebars") -const { registerAll } = require("./helpers/index") +const { registerAll, registerMinimum } = require("./helpers/index") const processors = require("./processors") const { atob, btoa } = require("./utilities") const manifest = require("../manifest.json") @@ -8,6 +8,7 @@ const { FIND_HBS_REGEX, FIND_DOUBLE_HBS_REGEX } = require("./utilities") const hbsInstance = handlebars.create() registerAll(hbsInstance) const hbsInstanceNoHelpers = handlebars.create() +registerMinimum(hbsInstanceNoHelpers) const defaultOpts = { noHelpers: false, noEscaping: false } /** @@ -105,9 +106,7 @@ module.exports.processStringSync = (string, context, opts) => { throw "Cannot process non-string types." } try { - // finalising adds a helper, can't do this with no helpers - const shouldFinalise = !opts.noHelpers - string = processors.preprocess(string, shouldFinalise) + string = processors.preprocess(string, opts) // this does not throw an error when template can't be fulfilled, have to try correct beforehand const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const templateString = @@ -119,8 +118,10 @@ module.exports.processStringSync = (string, context, opts) => { return processors.postprocess( template({ now: new Date(now).toISOString(), + __opts: opts, ...context, - }) + }), + { escapeNewlines: opts ? opts.escapeNewlines : false } ) } catch (err) { return input @@ -176,7 +177,9 @@ module.exports.isValid = (string, opts) => { const context = {} try { const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance - instance.compile(processors.preprocess(string, false))(context) + instance.compile(processors.preprocess(string, { noFinalise: true }))( + context + ) return true } catch (err) { const msg = err && err.message ? err.message : err diff --git a/packages/string-templates/src/processors/index.js b/packages/string-templates/src/processors/index.js index 174041133a..aae18aed8b 100644 --- a/packages/string-templates/src/processors/index.js +++ b/packages/string-templates/src/processors/index.js @@ -2,7 +2,7 @@ const { FIND_HBS_REGEX } = require("../utilities") const preprocessor = require("./preprocessor") const postprocessor = require("./postprocessor") -function process(output, processors) { +function process(output, processors, opts) { for (let processor of processors) { // if a literal statement has occurred stop if (typeof output !== "string") { @@ -15,24 +15,22 @@ function process(output, processors) { continue } for (let match of matches) { - output = processor.process(output, match) + output = processor.process(output, match, opts) } } return output } -module.exports.preprocess = (string, finalise = true) => { +module.exports.preprocess = (string, opts) => { let processors = preprocessor.processors - // the pre-processor finalisation stops handlebars from ever throwing an error - // might want to pre-process for other benefits but still want to see errors - if (!finalise) { + if (opts.noFinalise) { processors = processors.filter( processor => processor.name !== preprocessor.PreprocessorNames.FINALISE ) } + return process(string, processors, opts) +} +module.exports.postprocess = string => { + let processors = postprocessor.processors return process(string, processors) } - -module.exports.postprocess = string => { - return process(string, postprocessor.processors) -} diff --git a/packages/string-templates/src/processors/postprocessor.js b/packages/string-templates/src/processors/postprocessor.js index 7fc3f663fe..f78a572d07 100644 --- a/packages/string-templates/src/processors/postprocessor.js +++ b/packages/string-templates/src/processors/postprocessor.js @@ -16,6 +16,8 @@ class Postprocessor { } } +module.exports.PostProcessorNames = PostProcessorNames + module.exports.processors = [ new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => { if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) { diff --git a/packages/string-templates/src/processors/preprocessor.js b/packages/string-templates/src/processors/preprocessor.js index 6f6537674a..4b296d0fc7 100644 --- a/packages/string-templates/src/processors/preprocessor.js +++ b/packages/string-templates/src/processors/preprocessor.js @@ -16,8 +16,8 @@ class Preprocessor { this.fn = fn } - process(fullString, statement) { - const output = this.fn(statement) + process(fullString, statement, opts) { + const output = this.fn(statement, opts) const idx = fullString.indexOf(statement) return swapStrings(fullString, idx, statement.length, output) } @@ -48,7 +48,8 @@ module.exports.processors = [ 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) if (insideStatement.charAt(0) === " ") { insideStatement = insideStatement.slice(1) @@ -63,7 +64,10 @@ module.exports.processors = [ return statement } } - if (HelperNames().some(option => option.includes(possibleHelper))) { + if ( + !noHelpers && + HelperNames().some(option => option.includes(possibleHelper)) + ) { insideStatement = `(${insideStatement})` } return `{{ all ${insideStatement} }}` diff --git a/packages/string-templates/test/escapes.spec.js b/packages/string-templates/test/escapes.spec.js index 7e55b66b88..b845fddec9 100644 --- a/packages/string-templates/test/escapes.spec.js +++ b/packages/string-templates/test/escapes.spec.js @@ -59,3 +59,33 @@ describe("attempt some complex problems", () => { 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) + }) +}) diff --git a/packages/string-templates/test/helpers.spec.js b/packages/string-templates/test/helpers.spec.js index b4179475fb..0d39660d59 100644 --- a/packages/string-templates/test/helpers.spec.js +++ b/packages/string-templates/test/helpers.spec.js @@ -20,7 +20,7 @@ describe("test that it can run without helpers", () => { ) const valid = await processString("{{ avg 1 1 1 }}", {}) expect(valid).toBe("1") - expect(output).toBe("{{ avg 1 1 1 }}") + expect(output).toBe("") }) })