diff --git a/packages/bbui/src/Form/Core/Switch.svelte b/packages/bbui/src/Form/Core/Switch.svelte
index 667b2ab871..9e45683382 100644
--- a/packages/bbui/src/Form/Core/Switch.svelte
+++ b/packages/bbui/src/Form/Core/Switch.svelte
@@ -25,7 +25,9 @@
class="spectrum-Switch-input"
/>
- {text}
+ {#if text}
+ {text}
+ {/if}
diff --git a/packages/bbui/src/Layout/Layout.svelte b/packages/bbui/src/Layout/Layout.svelte
index bd564ff40e..c66a409242 100644
--- a/packages/bbui/src/Layout/Layout.svelte
+++ b/packages/bbui/src/Layout/Layout.svelte
@@ -71,6 +71,9 @@
.gap-L {
grid-gap: var(--spectrum-alias-grid-gutter-medium);
}
+ .gap-XL {
+ grid-gap: var(--spectrum-alias-grid-gutter-large);
+ }
.horizontal.gap-S :global(*) + :global(*) {
margin-left: var(--spectrum-alias-grid-gutter-xsmall);
}
diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte
new file mode 100644
index 0000000000..7fb6414ad8
--- /dev/null
+++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte
@@ -0,0 +1,60 @@
+
+
+{#key height}
+
+{/key}
diff --git a/packages/bbui/src/Markdown/MarkdownViewer.svelte b/packages/bbui/src/Markdown/MarkdownViewer.svelte
new file mode 100644
index 0000000000..5705020f45
--- /dev/null
+++ b/packages/bbui/src/Markdown/MarkdownViewer.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
diff --git a/packages/bbui/src/Markdown/SpectrumMDE.svelte b/packages/bbui/src/Markdown/SpectrumMDE.svelte
new file mode 100644
index 0000000000..9b0832c91f
--- /dev/null
+++ b/packages/bbui/src/Markdown/SpectrumMDE.svelte
@@ -0,0 +1,184 @@
+
+
+
+
+
diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte
index 09cc4f6c52..c18be1e4e1 100644
--- a/packages/bbui/src/Modal/ModalContent.svelte
+++ b/packages/bbui/src/Modal/ModalContent.svelte
@@ -18,10 +18,23 @@
export let disabled = false
export let showDivider = true
+ export let showSecondaryButton = false
+ export let secondaryButtonText = undefined
+ export let secondaryAction = undefined
+ export let secondaryButtonWarning = false
+
const { hide, cancel } = getContext(Context.Modal)
let loading = false
$: confirmDisabled = disabled || loading
+ async function secondary() {
+ loading = true
+ if (!secondaryAction || (await secondaryAction()) !== false) {
+ hide()
+ }
+ loading = false
+ }
+
async function confirm() {
loading = true
if (!onConfirm || (await onConfirm()) !== false) {
@@ -73,6 +86,18 @@
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
>
+
+ {#if showSecondaryButton && secondaryButtonText && secondaryAction}
+
{/if}
@@ -136,4 +161,8 @@
display: flex;
justify-content: space-between;
}
+
+ .secondary-action {
+ margin-right: auto;
+ }
diff --git a/packages/bbui/src/Notification/Notification.svelte b/packages/bbui/src/Notification/Notification.svelte
index cebc859bda..1d21131553 100644
--- a/packages/bbui/src/Notification/Notification.svelte
+++ b/packages/bbui/src/Notification/Notification.svelte
@@ -1,7 +1,12 @@
+
+
diff --git a/packages/bbui/src/Notification/NotificationDisplay.svelte b/packages/bbui/src/Notification/NotificationDisplay.svelte
index 9d96bf7e70..eb778f3aa0 100644
--- a/packages/bbui/src/Notification/NotificationDisplay.svelte
+++ b/packages/bbui/src/Notification/NotificationDisplay.svelte
@@ -1,7 +1,6 @@
+
+
+
+
diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte
index 9a53fd0169..4dda31240a 100644
--- a/packages/bbui/src/Table/CellRenderer.svelte
+++ b/packages/bbui/src/Table/CellRenderer.svelte
@@ -6,6 +6,7 @@
import AttachmentRenderer from "./AttachmentRenderer.svelte"
import ArrayRenderer from "./ArrayRenderer.svelte"
import InternalRenderer from "./InternalRenderer.svelte"
+ import { processStringSync } from "@budibase/string-templates"
export let row
export let schema
@@ -28,10 +29,33 @@
$: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
+ $: width = schema?.width || "150px"
+ $: cellValue = getCellValue(value, schema.template)
+
+ const getCellValue = (value, template) => {
+ if (!template) {
+ return value
+ }
+ return processStringSync(template, { value })
+ }
-{#if renderer && (customRenderer || (value != null && value !== ""))}
-
+{#if renderer && (customRenderer || (cellValue != null && cellValue !== ""))}
+
{/if}
+
+
diff --git a/packages/bbui/src/Table/CodeRenderer.svelte b/packages/bbui/src/Table/CodeRenderer.svelte
new file mode 100644
index 0000000000..5658851197
--- /dev/null
+++ b/packages/bbui/src/Table/CodeRenderer.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/packages/bbui/src/Table/DateTimeRenderer.svelte b/packages/bbui/src/Table/DateTimeRenderer.svelte
index 8a06082d58..5d856968e7 100644
--- a/packages/bbui/src/Table/DateTimeRenderer.svelte
+++ b/packages/bbui/src/Table/DateTimeRenderer.svelte
@@ -2,12 +2,23 @@
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(isTime ? time : value).format(
+ isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
+ )}
+
diff --git a/packages/bbui/src/Table/InternalRenderer.svelte b/packages/bbui/src/Table/InternalRenderer.svelte
index 0f894ac853..7e2dd0b2aa 100644
--- a/packages/bbui/src/Table/InternalRenderer.svelte
+++ b/packages/bbui/src/Table/InternalRenderer.svelte
@@ -8,21 +8,38 @@
copyToClipboard(value)
}
- function copyToClipboard(value) {
- navigator.clipboard.writeText(value).then(() => {
- notifications.success("Copied")
+ const copyToClipboard = value => {
+ return new Promise(res => {
+ if (navigator.clipboard && window.isSecureContext) {
+ // Try using the clipboard API first
+ navigator.clipboard.writeText(value).then(res)
+ } else {
+ // Fall back to the textarea hack
+ let textArea = document.createElement("textarea")
+ textArea.value = value
+ textArea.style.position = "fixed"
+ textArea.style.left = "-9999px"
+ textArea.style.top = "-9999px"
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+ document.execCommand("copy")
+ textArea.remove()
+ res()
+ }
})
+ .then(() => {
+ notifications.success("Copied to clipboard")
+ })
+ .catch(() => {
+ notifications.error(
+ "Failed to copy to clipboard. Check the dev console for the value."
+ )
+ console.warn("Failed to copy the value", value)
+ })
}
-
-
diff --git a/packages/bbui/src/Table/StringRenderer.svelte b/packages/bbui/src/Table/StringRenderer.svelte
index 6fd731f5ec..e881551bc8 100644
--- a/packages/bbui/src/Table/StringRenderer.svelte
+++ b/packages/bbui/src/Table/StringRenderer.svelte
@@ -8,6 +8,7 @@
div {
overflow: hidden;
text-overflow: ellipsis;
- width: 150px;
+ white-space: nowrap;
+ max-width: var(--max-cell-width);
}
diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte
index bcd84e7112..e89b4e849a 100644
--- a/packages/bbui/src/Table/Table.svelte
+++ b/packages/bbui/src/Table/Table.svelte
@@ -3,7 +3,8 @@
import "@spectrum-css/table/dist/index-vars.css"
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
- import { cloneDeep } from "lodash"
+ import { cloneDeep, deepGet } from "../helpers"
+ import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
/**
* The expected schema is our normal couch schemas for our tables.
@@ -14,6 +15,11 @@
* sortable: Set to false to disable sorting data by a certain column
* editable: Set to false to disable editing a certain column if the
* allowEditColumns prop is true
+ * width: the width of the column
+ * align: the alignment of the column
+ * template: a HBS or JS binding to use as the value
+ * background: the background color
+ * color: the text color
*/
export let data = []
export let schema = {}
@@ -28,13 +34,14 @@
export let editColumnTitle = "Edit"
export let customRenderers = []
export let disableSorting = false
+ export let autoSortColumns = true
+ export let compact = false
const dispatch = createEventDispatcher()
// Config
- const rowHeight = 55
const headerHeight = 36
- const rowPreload = 5
+ $: rowHeight = compact ? 46 : 55
// Sorting state
let sortColumn
@@ -45,32 +52,20 @@
let loaded = false
$: schema = fixSchema(schema)
$: if (!loading) loaded = true
- $: rows = data ?? []
- $: visibleRowCount = getVisibleRowCount(loaded, height, rows.length, rowCount)
- $: contentStyle = getContentStyle(visibleRowCount, rowCount)
- $: sortedRows = sortRows(rows, sortColumn, sortOrder)
- $: fields = getFields(schema, showAutoColumns)
- $: showEditColumn = allowEditRows || allowSelectRows
-
- // Scrolling state
- let timeout
- let nextScrollTop = 0
- let scrollTop = 0
- $: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
- $: lastVisibleRow = calculateLastVisibleRow(
- firstVisibleRow,
- visibleRowCount,
- rows.length
+ $: fields = getFields(schema, showAutoColumns, autoSortColumns)
+ $: rows = fields?.length ? data || [] : []
+ $: visibleRowCount = getVisibleRowCount(
+ loaded,
+ height,
+ rows.length,
+ rowCount,
+ rowHeight
)
-
- // Reset state when data changes
- $: rows.length, reset()
- const reset = () => {
- nextScrollTop = 0
- scrollTop = 0
- clearTimeout(timeout)
- timeout = null
- }
+ $: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
+ $: sortedRows = sortRows(rows, sortColumn, sortOrder)
+ $: gridStyle = getGridStyle(fields, schema, showEditColumn)
+ $: showEditColumn = allowEditRows || allowSelectRows
+ $: cellStyles = computeCellStyles(schema)
const fixSchema = schema => {
let fixedSchema = {}
@@ -90,7 +85,7 @@
return fixedSchema
}
- const getVisibleRowCount = (loaded, height, allRows, rowCount) => {
+ const getVisibleRowCount = (loaded, height, allRows, rowCount, rowHeight) => {
if (!loaded) {
return rowCount || 0
}
@@ -100,11 +95,28 @@
return Math.min(allRows, Math.ceil(height / rowHeight))
}
- const getContentStyle = (visibleRows, rowCount) => {
+ const getContentStyle = (visibleRows, rowCount, rowHeight) => {
if (!rowCount || !visibleRows) {
return ""
}
- return `height: ${headerHeight + visibleRows * (rowHeight + 1)}px;`
+ return `height: ${headerHeight + visibleRows * rowHeight}px;`
+ }
+
+ const getGridStyle = (fields, schema, showEditColumn) => {
+ let style = "grid-template-columns:"
+ if (showEditColumn) {
+ style += " auto"
+ }
+ fields?.forEach(field => {
+ const fieldSchema = schema[field]
+ if (fieldSchema.width) {
+ style += ` ${fieldSchema.width}`
+ } else {
+ style += " minmax(auto, 1fr)"
+ }
+ })
+ style += ";"
+ return style
}
const sortRows = (rows, sortColumn, sortOrder) => {
@@ -143,14 +155,14 @@
return name || ""
}
- const getFields = (schema, showAutoColumns) => {
+ const getFields = (schema, showAutoColumns, autoSortColumns) => {
let columns = []
let autoColumns = []
Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
if (!field || !fieldSchema) {
return
}
- if (!fieldSchema?.autocolumn) {
+ if (!autoSortColumns || !fieldSchema?.autocolumn) {
columns.push(fieldSchema)
} else if (showAutoColumns) {
autoColumns.push(fieldSchema)
@@ -171,28 +183,6 @@
.map(column => column.name)
}
- const onScroll = event => {
- nextScrollTop = event.target.scrollTop
- if (timeout) {
- return
- }
- timeout = setTimeout(() => {
- scrollTop = nextScrollTop
- timeout = null
- }, 50)
- }
-
- const calculateFirstVisibleRow = scrollTop => {
- return Math.max(Math.floor(scrollTop / (rowHeight + 1)) - rowPreload, 0)
- }
-
- const calculateLastVisibleRow = (firstRow, visibleRowCount, allRowCount) => {
- if (visibleRowCount === 0) {
- return -1
- }
- return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount)
- }
-
const editColumn = (e, field) => {
e.stopPropagation()
dispatch("editcolumn", field)
@@ -213,170 +203,233 @@
selectedRows = [...selectedRows, row]
}
}
+
+ const computeCellStyles = schema => {
+ let styles = {}
+ Object.keys(schema || {}).forEach(field => {
+ styles[field] = ""
+ if (schema[field].color) {
+ styles[field] += `color: ${schema[field].color};`
+ }
+ if (schema[field].background) {
+ styles[field] += `background-color: ${schema[field].background};`
+ }
+ if (schema[field].align === "Center") {
+ styles[field] += "justify-content: center; text-align: center;"
+ }
+ if (schema[field].align === "Right") {
+ styles[field] += "justify-content: flex-end; text-align: right;"
+ }
+ })
+ return styles
+ }
-
diff --git a/packages/bbui/src/Tabs/Tab.svelte b/packages/bbui/src/Tabs/Tab.svelte
index 86f2c0ee52..0aa59f7f8a 100644
--- a/packages/bbui/src/Tabs/Tab.svelte
+++ b/packages/bbui/src/Tabs/Tab.svelte
@@ -5,7 +5,7 @@
export let icon = ""
const dispatch = createEventDispatcher()
- const selected = getContext("tab")
+ let selected = getContext("tab")
let tab
let tabInfo
@@ -16,8 +16,8 @@
// We just need to get this off the main thread to fix this, by using
// a 0ms timeout.
setTimeout(() => {
- tabInfo = tab.getBoundingClientRect()
- if ($selected.title === title) {
+ tabInfo = tab?.getBoundingClientRect()
+ if (tabInfo && $selected.title === title) {
$selected.info = tabInfo
}
}, 0)
diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte
index 5fdaf0e434..71dbceac3c 100644
--- a/packages/bbui/src/Tabs/Tabs.svelte
+++ b/packages/bbui/src/Tabs/Tabs.svelte
@@ -6,8 +6,14 @@
export let selected
export let vertical = false
export let noPadding = false
+ // added as a separate option as noPadding is used for vertical padding
+ export let noHorizPadding = false
export let quiet = false
export let emphasized = false
+ // overlay content from the tab bar onto tabs e.g. for a dropdown
+ export let onTop = false
+
+ let thisSelected = undefined
let _id = id()
const tab = writable({ title: selected, id: _id, emphasized })
@@ -18,9 +24,19 @@
const dispatch = createEventDispatcher()
$: {
- if ($tab.title !== selected) {
+ if (thisSelected !== selected) {
+ thisSelected = selected
+ dispatch("select", thisSelected)
+ } else if ($tab.title !== thisSelected) {
+ thisSelected = $tab.title
selected = $tab.title
- dispatch("select", selected)
+ dispatch("select", thisSelected)
+ }
+ if ($tab.title !== thisSelected) {
+ tab.update(state => {
+ state.title = thisSelected
+ return state
+ })
}
}
@@ -59,10 +75,12 @@
{#if $tab.info}
@@ -83,7 +101,9 @@
.quiet {
border-bottom: none !important;
}
-
+ .onTop {
+ z-index: 20;
+ }
.spectrum-Tabs {
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
@@ -99,6 +119,9 @@
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
bottom: 0 !important;
}
+ .noHorizPadding {
+ padding: 0;
+ }
.noPadding {
margin: 0;
}
diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte
new file mode 100644
index 0000000000..78c69942e5
--- /dev/null
+++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte
@@ -0,0 +1,61 @@
+
+
+
+
+ {#if tooltip}
+
+
(showTooltip = true)}
+ on:mouseleave={() => (showTooltip = false)}
+ on:focus
+ >
+
+
+ {#if showTooltip}
+
+
+
+ {/if}
+
+ {/if}
+
+
+
diff --git a/packages/bbui/src/Typography/Detail.svelte b/packages/bbui/src/Typography/Detail.svelte
index 7cbafdadef..bb5c78c11e 100644
--- a/packages/bbui/src/Typography/Detail.svelte
+++ b/packages/bbui/src/Typography/Detail.svelte
@@ -13,3 +13,9 @@
>
+
+
diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js
new file mode 100644
index 0000000000..cf40e12d74
--- /dev/null
+++ b/packages/bbui/src/helpers.js
@@ -0,0 +1,108 @@
+/**
+ * Generates a DOM safe UUID.
+ * Starting with a letter is important to make it DOM safe.
+ * @return {string} a random DOM safe UUID
+ */
+export function uuid() {
+ return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
+ const r = (Math.random() * 16) | 0
+ const v = c === "x" ? r : (r & 0x3) | 0x8
+ return v.toString(16)
+ })
+}
+
+/**
+ * Capitalises a string
+ * @param string the string to capitalise
+ * @return {string} the capitalised string
+ */
+export const capitalise = string => {
+ if (!string) {
+ return string
+ }
+ return string.substring(0, 1).toUpperCase() + string.substring(1)
+}
+
+/**
+ * Computes a short hash of a string
+ * @param string the string to compute a hash of
+ * @return {string} the hash string
+ */
+export const hashString = string => {
+ if (!string) {
+ return "0"
+ }
+ let hash = 0
+ for (let i = 0; i < string.length; i++) {
+ let char = string.charCodeAt(i)
+ hash = (hash << 5) - hash + char
+ hash = hash & hash // Convert to 32bit integer
+ }
+ return hash.toString()
+}
+
+/**
+ * Gets a key within an object. The key supports dot syntax for retrieving deep
+ * fields - e.g. "a.b.c".
+ * Exact matches of keys with dots in them take precedence over nested keys of
+ * the same path - e.g. getting "a.b" from { "a.b": "foo", a: { b: "bar" } }
+ * will return "foo" over "bar".
+ * @param obj the object
+ * @param key the key
+ * @return {*|null} the value or null if a value was not found for this key
+ */
+export const deepGet = (obj, key) => {
+ if (!obj || !key) {
+ return null
+ }
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ return obj[key]
+ }
+ const split = key.split(".")
+ for (let i = 0; i < split.length; i++) {
+ obj = obj?.[split[i]]
+ }
+ return obj
+}
+
+/**
+ * Sets a key within an object. The key supports dot syntax for retrieving deep
+ * fields - e.g. "a.b.c".
+ * Exact matches of keys with dots in them take precedence over nested keys of
+ * the same path - e.g. setting "a.b" of { "a.b": "foo", a: { b: "bar" } }
+ * will override the value "foo" rather than "bar".
+ * If a deep path is specified and the parent keys don't exist then these will
+ * be created.
+ * @param obj the object
+ * @param key the key
+ * @param value the value
+ */
+export const deepSet = (obj, key, value) => {
+ if (!obj || !key) {
+ return
+ }
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ obj[key] = value
+ return
+ }
+ const split = key.split(".")
+ for (let i = 0; i < split.length - 1; i++) {
+ const nextKey = split[i]
+ if (obj && obj[nextKey] == null) {
+ obj[nextKey] = {}
+ }
+ obj = obj?.[nextKey]
+ }
+ if (!obj) {
+ return
+ }
+ obj[split[split.length - 1]] = value
+}
+
+/**
+ * Deeply clones an object. Functions are not supported.
+ * @param obj the object to clone
+ */
+export const cloneDeep = obj => {
+ return JSON.parse(JSON.stringify(obj))
+}
diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js
index 377d451604..d3bc11cf9d 100644
--- a/packages/bbui/src/index.js
+++ b/packages/bbui/src/index.js
@@ -59,6 +59,14 @@ export { default as Badge } from "./Badge/Badge.svelte"
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
+export { default as Banner } from "./Banner/Banner.svelte"
+export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
+export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
+export { default as RichTextField } from "./Form/RichTextField.svelte"
+
+// Renderers
+export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
+export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
// Typography
export { default as Body } from "./Typography/Body.svelte"
@@ -76,3 +84,6 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
+
+// Helpers
+export * as Helpers from "./helpers"
diff --git a/packages/bbui/src/utils/helpers.js b/packages/bbui/src/utils/helpers.js
deleted file mode 100644
index 83d305d573..0000000000
--- a/packages/bbui/src/utils/helpers.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const generateID = () => {
- const rand = Math.random().toString(32).substring(2)
-
- // Starts with a letter so that its a valid DOM ID
- return `A${rand}`
-}
-
-export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
diff --git a/packages/bbui/yarn.lock b/packages/bbui/yarn.lock
index a102e6c148..28c009b331 100644
--- a/packages/bbui/yarn.lock
+++ b/packages/bbui/yarn.lock
@@ -28,6 +28,43 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@budibase/handlebars-helpers@^0.11.7":
+ version "0.11.8"
+ resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
+ integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
+ dependencies:
+ array-sort "^1.0.0"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ for-in "^1.0.2"
+ get-object "^0.2.0"
+ get-value "^3.0.1"
+ handlebars "^4.7.7"
+ handlebars-utils "^1.0.6"
+ has-value "^2.0.2"
+ helper-md "^0.2.2"
+ html-tag "^2.0.0"
+ is-even "^1.0.0"
+ is-glob "^4.0.1"
+ kind-of "^6.0.3"
+ micromatch "^3.1.5"
+ relative "^3.0.2"
+ striptags "^3.1.1"
+ to-gfm-code-block "^0.1.1"
+ year "^0.2.1"
+
+"@budibase/string-templates@^1.0.66-alpha.0":
+ version "1.0.72"
+ resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.72.tgz#acc154e402cce98ea30eedde9c6124183ee9b37c"
+ integrity sha512-w715TjgO6NUHkZNqoOEo8lAKJ/PQ4b00ATWSX5VB523SAu7y/uOiqKqV1E3fgwxq1o8L+Ff7rn9FTkiYtjkV/g==
+ 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"
+
"@rollup/plugin-commonjs@^16.0.0":
version "16.0.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz#169004d56cd0f0a1d0f35915d31a036b0efe281f"
@@ -271,6 +308,13 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
integrity sha512-vzS9KqYXot4J3AEER/u618MXWAS+IoMvYMNrOoscKiLLKYQWenaueakUWulFonToPd/9vIpqtdbwxznqrK5qDw==
+"@types/codemirror@^5.60.4":
+ version "5.60.5"
+ resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.5.tgz#5b989a3b4bbe657458cf372c92b6bfda6061a2b7"
+ integrity sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==
+ dependencies:
+ "@types/tern" "*"
+
"@types/estree@*":
version "0.0.47"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
@@ -281,6 +325,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+"@types/marked@^4.0.1":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.2.tgz#cb2dbf10da2f41cf20bd91fb5f89b67540c282f7"
+ integrity sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==
+
"@types/node@*":
version "14.14.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
@@ -303,6 +352,13 @@
dependencies:
"@types/node" "*"
+"@types/tern@*":
+ version "0.23.4"
+ resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb"
+ integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==
+ dependencies:
+ "@types/estree" "*"
+
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -311,11 +367,21 @@ accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
+acorn-walk@^8.2.0:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+ integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
acorn@^7.3.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+acorn@^8.7.0:
+ version "8.7.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
+ integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
+
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -343,28 +409,87 @@ anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
-argparse@^1.0.7:
+argparse@^1.0.10, argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+array-sort@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a"
+ integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==
+ dependencies:
+ default-compare "^1.0.0"
+ get-value "^2.0.6"
+ kind-of "^5.0.2"
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+atob@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+autolinker@~0.28.0:
+ version "0.28.1"
+ resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47"
+ integrity sha1-BlK0kYgYefB3XazgzcoyM5QqTkc=
+ dependencies:
+ gulp-header "^1.7.1"
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -404,6 +529,22 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -437,6 +578,21 @@ bytes@3.1.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -516,6 +672,16 @@ chokidar@^3.0.0:
optionalDependencies:
fsevents "~2.3.1"
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
coa@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
@@ -525,6 +691,26 @@ coa@^2.0.2:
chalk "^2.4.1"
q "^1.1.2"
+codemirror-spell-checker@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
+ integrity sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=
+ dependencies:
+ typo-js "*"
+
+codemirror@^5.63.1:
+ version "5.65.1"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.1.tgz#5988a812c974c467f964bcc1a00c944e373de502"
+ integrity sha512-s6aac+DD+4O2u1aBmdxhB7yz2XU7tG3snOyQ05Kxifahz7hoxnfxIRHxiCSEv3TUC38dIVH8G+lZH9UWSfGQxA==
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
color-convert@^1.9.0, color-convert@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -580,12 +766,17 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-with-sourcemaps@^1.1.0:
+concat-with-sourcemaps@*, concat-with-sourcemaps@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e"
integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==
@@ -621,6 +812,16 @@ cookie@0.4.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
cosmiconfig@^5.0.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
@@ -792,7 +993,7 @@ dayjs@^1.10.4:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
-debug@2.6.9:
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -806,11 +1007,23 @@ debug@^3.0.1:
dependencies:
ms "^2.1.1"
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+default-compare@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f"
+ integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==
+ dependencies:
+ kind-of "^5.0.2"
+
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -818,6 +1031,28 @@ define-properties@^1.1.3:
dependencies:
object-keys "^1.0.12"
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@@ -861,6 +1096,17 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
+easymde@^2.16.1:
+ version "2.16.1"
+ resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.16.1.tgz#f4c2380312615cb33826f1a1fecfaa4022ff551a"
+ integrity sha512-FihYgjRsKfhGNk89SHSqxKLC4aJ1kfybPWW6iAmtb5GnXu+tnFPSzSaGBmk1RRlCuhFSjhF0SnIMGVPjEzkr6g==
+ dependencies:
+ "@types/codemirror" "^5.60.4"
+ "@types/marked" "^4.0.1"
+ codemirror "^5.63.1"
+ codemirror-spell-checker "1.1.2"
+ marked "^4.0.10"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -881,6 +1127,11 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+ent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+ integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -974,6 +1225,19 @@ eventemitter3@^4.0.4:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
express-history-api-fallback@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/express-history-api-fallback/-/express-history-api-fallback-2.2.1.tgz#3a2ad27f7bebc90fc533d110d7c6d83097bcd057"
@@ -1031,6 +1295,45 @@ express@^4.16.3:
utils-merge "1.0.1"
vary "~1.1.2"
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1056,16 +1359,33 @@ flatpickr@^4.5.2:
resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.9.tgz#9a13383e8a6814bda5d232eae3fcdccb97dc1499"
integrity sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+fs-exists-sync@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
+ integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1097,6 +1417,26 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
+get-object@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c"
+ integrity sha1-2S/31RkMZFMM2gVD2sY6PUf+jAw=
+ dependencies:
+ is-number "^2.0.2"
+ isobject "^0.2.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+get-value@^3.0.0, get-value@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8"
+ integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==
+ dependencies:
+ isobject "^3.0.1"
+
glob-parent@~5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -1116,6 +1456,35 @@ glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
+gulp-header@^1.7.1:
+ version "1.8.12"
+ resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84"
+ integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==
+ dependencies:
+ concat-with-sourcemaps "*"
+ lodash.template "^4.4.0"
+ through2 "^2.0.0"
+
+handlebars-utils@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9"
+ integrity sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==
+ dependencies:
+ kind-of "^6.0.0"
+ typeof-article "^0.1.1"
+
+handlebars@^4.7.6, handlebars@^4.7.7:
+ version "4.7.7"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+ integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
+ dependencies:
+ minimist "^1.2.5"
+ neo-async "^2.6.0"
+ source-map "^0.6.1"
+ wordwrap "^1.0.0"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@@ -1136,6 +1505,52 @@ has-symbols@^1.0.1, has-symbols@^1.0.2:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-value@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-2.0.2.tgz#d0f12e8780ba8e90e66ad1a21c707fdb67c25658"
+ integrity sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA==
+ dependencies:
+ get-value "^3.0.0"
+ has-values "^2.0.1"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has-values@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d"
+ integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w==
+ dependencies:
+ kind-of "^6.0.2"
+
has@^1.0.0, has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@@ -1143,6 +1558,16 @@ has@^1.0.0, has@^1.0.3:
dependencies:
function-bind "^1.1.1"
+helper-md@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/helper-md/-/helper-md-0.2.2.tgz#c1f59d7e55bbae23362fd8a0e971607aec69d41f"
+ integrity sha1-wfWdflW7riM2L9ig6XFgeuxp1B8=
+ dependencies:
+ ent "^2.2.0"
+ extend-shallow "^2.0.1"
+ fs-exists-sync "^0.1.0"
+ remarkable "^1.6.2"
+
hex-color-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@@ -1158,6 +1583,14 @@ hsla-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+html-tag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/html-tag/-/html-tag-2.0.0.tgz#36c3bc8d816fd30b570d5764a497a641640c2fed"
+ integrity sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==
+ dependencies:
+ is-self-closing "^1.0.1"
+ kind-of "^6.0.0"
+
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@@ -1240,7 +1673,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4:
+inherits@2, inherits@2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -1260,6 +1693,20 @@ is-absolute-url@^2.0.0:
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -1289,6 +1736,11 @@ is-boolean-object@^1.1.0:
dependencies:
call-bind "^1.0.0"
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
is-callable@^1.1.4, is-callable@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
@@ -1313,16 +1765,67 @@ is-core-module@^2.2.0:
dependencies:
has "^1.0.3"
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
is-date-object@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
is-directory@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+is-even@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-even/-/is-even-1.0.0.tgz#76b5055fbad8d294a86b6a949015e1c97b717c06"
+ integrity sha1-drUFX7rY0pSoa2qUkBXhyXtxfAY=
+ dependencies:
+ is-odd "^0.1.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1350,6 +1853,20 @@ is-number-object@^1.0.4:
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
+is-number@^2.0.2:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -1360,6 +1877,20 @@ is-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+is-odd@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-0.1.2.tgz#bc573b5ce371ef2aad6e6f49799b72bef13978a7"
+ integrity sha1-vFc7XONx7yqtbm9JeZtyvvE5eKc=
+ dependencies:
+ is-number "^3.0.0"
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
is-reference@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
@@ -1380,6 +1911,13 @@ is-resolvable@^1.0.0:
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+is-self-closing@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-self-closing/-/is-self-closing-1.0.1.tgz#5f406b527c7b12610176320338af0fa3896416e4"
+ integrity sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg==
+ dependencies:
+ self-closing-tags "^1.0.1"
+
is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
@@ -1392,11 +1930,38 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
dependencies:
has-symbols "^1.0.1"
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+isarray@1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+isobject@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-0.2.0.tgz#a3432192f39b910b5f02cc989487836ec70aa85e"
+ integrity sha1-o0MhkvObkQtfAsyYlIeDbscKqF4=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
jest-worker@^26.2.1:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
@@ -1436,6 +2001,30 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0, kind-of@^5.0.2:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -1450,6 +2039,11 @@ loader-utils@^1.1.0:
emojis-list "^3.0.0"
json5 "^1.0.1"
+lodash._reinterpolate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+ integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -1460,11 +2054,31 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+lodash.template@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
+ integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
+ dependencies:
+ lodash._reinterpolate "^3.0.0"
+ lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
+ integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
+ dependencies:
+ lodash._reinterpolate "^3.0.0"
+
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+lodash@^4.17.20:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
@@ -1472,6 +2086,23 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.4"
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+marked@^4.0.10:
+ version "4.0.12"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
+ integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
+
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
@@ -1502,6 +2133,25 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+micromatch@^3.1.5:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
mime-db@1.47.0:
version "1.47.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
@@ -1531,6 +2181,14 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+mixin-deep@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -1554,15 +2212,37 @@ ms@^2.1.1:
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.1.22:
- version "3.1.22"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
- integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.0.tgz#5906f776fd886c66c24f3653e0c46fcb1d4ad6b0"
+ integrity sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+neo-async@^2.6.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
node-releases@^1.1.71:
version "1.1.73"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
@@ -1603,6 +2283,15 @@ nth-check@^1.0.2:
dependencies:
boolbase "~1.0.0"
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
object-inspect@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
@@ -1613,6 +2302,13 @@ object-keys@^1.0.12, object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
@@ -1632,6 +2328,13 @@ object.getownpropertydescriptors@^2.1.0:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
object.values@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee"
@@ -1706,6 +2409,11 @@ parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -1741,6 +2449,11 @@ pify@^5.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
postcss-calc@^7.0.1:
version "7.0.5"
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e"
@@ -2076,14 +2789,19 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
supports-color "^6.1.0"
postcss@^8.2.9:
- version "8.2.10"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b"
- integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==
+ version "8.2.13"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.13.tgz#dbe043e26e3c068e45113b1ed6375d2d37e2129f"
+ integrity sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.22"
source-map "^0.6.1"
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
promise.series@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd"
@@ -2139,6 +2857,19 @@ raw-body@^2.3.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
+readable-stream@~2.3.6:
+ version "2.3.7"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
readdirp@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
@@ -2146,6 +2877,39 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+relative@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f"
+ integrity sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=
+ dependencies:
+ isobject "^2.0.0"
+
+remarkable@^1.6.2:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00"
+ integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==
+ dependencies:
+ argparse "^1.0.10"
+ autolinker "~0.28.0"
+
+repeat-element@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+ integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
require-relative@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
@@ -2166,6 +2930,11 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
resolve@^1.17.0, resolve@^1.19.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
@@ -2174,6 +2943,11 @@ resolve@^1.17.0, resolve@^1.19.0:
is-core-module "^2.2.0"
path-parse "^1.0.6"
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@@ -2235,7 +3009,7 @@ rollup@^2.45.2:
optionalDependencies:
fsevents "~2.3.1"
-safe-buffer@5.1.2, safe-buffer@~5.1.1:
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
@@ -2250,6 +3024,13 @@ safe-identifier@^0.4.2:
resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb"
integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -2260,6 +3041,11 @@ sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+self-closing-tags@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/self-closing-tags/-/self-closing-tags-1.0.1.tgz#6c5fa497994bb826b484216916371accee490a5d"
+ integrity sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA==
+
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -2296,6 +3082,16 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
+set-value@^2.0.0, set-value@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
@@ -2320,12 +3116,53 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
"source-map-fast@npm:source-map@0.7.3", source-map@~0.7.2:
name source-map-fast
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+source-map-resolve@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+ integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ dependencies:
+ atob "^2.1.2"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
source-map-support@~0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
@@ -2334,6 +3171,11 @@ source-map-support@~0.5.19:
buffer-from "^1.0.0"
source-map "^0.6.0"
+source-map-url@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
+ integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
+
source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -2349,6 +3191,13 @@ sourcemap-codec@^1.4.4:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -2359,6 +3208,14 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@@ -2385,6 +3242,18 @@ string.prototype.trimstart@^1.0.4:
call-bind "^1.0.2"
define-properties "^1.1.3"
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+striptags@^3.1.1:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
+ integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
+
style-inject@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3"
@@ -2465,11 +3334,39 @@ terser@^5.0.0:
source-map "~0.7.2"
source-map-support "~0.5.19"
+through2@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+ integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+ dependencies:
+ readable-stream "~2.3.6"
+ xtend "~4.0.1"
+
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+to-gfm-code-block@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz#25d045a5fae553189e9637b590900da732d8aa82"
+ integrity sha1-JdBFpfrlUxielje1kJANpzLYqoI=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -2477,6 +3374,16 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
@@ -2490,6 +3397,23 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
+typeof-article@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/typeof-article/-/typeof-article-0.1.1.tgz#9f07e733c3fbb646ffa9e61c08debacd460e06af"
+ integrity sha1-nwfnM8P7tkb/qeYcCN66zUYOBq8=
+ dependencies:
+ kind-of "^3.1.0"
+
+typo-js@*:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.1.tgz#334a0d8c3f6c56f2f1e15fdf6c31677793cbbe9b"
+ integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
+
+uglify-js@^3.1.4:
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d"
+ integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==
+
unbox-primitive@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@@ -2500,6 +3424,16 @@ unbox-primitive@^1.0.0:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
+union-value@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+ integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^2.0.1"
+
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
@@ -2520,7 +3454,25 @@ unquote@~1.1.1:
resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
-util-deprecate@^1.0.2:
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -2550,6 +3502,14 @@ vendors@^1.0.0:
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+vm2@^3.9.4:
+ version "3.9.8"
+ resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.8.tgz#e99c000db042735cd2f94d8db6c42163a17be04e"
+ integrity sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==
+ dependencies:
+ acorn "^8.7.0"
+ acorn-walk "^8.2.0"
+
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -2568,6 +3528,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
+wordwrap@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -2580,7 +3545,17 @@ ws@^5.2.0:
dependencies:
async-limiter "~1.0.0"
+xtend@~4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+year@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/year/-/year-0.2.1.tgz#4083ae520a318b23ec86037f3000cb892bdf9bb0"
+ integrity sha1-QIOuUgoxiyPshgN/MADLiSvfm7A=
diff --git a/packages/builder/assets/bb-spaceship.svg b/packages/builder/assets/bb-spaceship.svg
new file mode 100755
index 0000000000..a0bc5a49cd
--- /dev/null
+++ b/packages/builder/assets/bb-spaceship.svg
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json
index 0908f2c839..fb9953ae6c 100644
--- a/packages/builder/cypress.json
+++ b/packages/builder/cypress.json
@@ -1,9 +1,10 @@
{
- "baseUrl": "http://localhost:10001/builder/",
- "video": true,
+ "baseUrl": "http://localhost:10001",
+ "video": false,
"projectId": "bmbemn",
"env": {
"PORT": "10001",
- "JWT_SECRET": "test"
+ "JWT_SECRET": "test",
+ "HOST_IP": ""
}
}
diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
index b3e0f413f8..e67f1eb2a2 100644
--- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
+++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
@@ -1,14 +1,17 @@
-context("Add Multi-Option Datatype", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- })
+import filterTests from "../support/filterTests"
- it("should create a new table, with data", () => {
- cy.createTable("Multi Data")
- cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
- cy.addRowMultiValue(["1", "2", "3", "4", "5"])
- })
+filterTests(['all'], () => {
+ context("Add Multi-Option Datatype", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ it("should create a new table, with data", () => {
+ cy.createTable("Multi Data")
+ cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
+ cy.addRowMultiValue(["1", "2", "3", "4", "5"])
+ })
it("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
@@ -39,6 +42,7 @@ context("Add Multi-Option Datatype", () => {
cy.getComponent(componentId)
.find(".spectrum-Picker-label")
.contains("(5)")
+ })
})
})
})
diff --git a/packages/builder/cypress/integration/addRadioButtons.spec.js b/packages/builder/cypress/integration/addRadioButtons.spec.js
index 68d0a74d55..9888b56086 100644
--- a/packages/builder/cypress/integration/addRadioButtons.spec.js
+++ b/packages/builder/cypress/integration/addRadioButtons.spec.js
@@ -1,35 +1,39 @@
-context("Add Radio Buttons", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- })
+import filterTests from "../support/filterTests"
-it("should add Radio Buttons options picker on form, add data, and confirm", () => {
- cy.navigateToFrontend()
- cy.addComponent("Form", "Form")
- cy.addComponent("Form", "Options Picker").then((componentId) => {
- // Provide field setting
- cy.get(`[data-cy="field-prop-control"]`).type("1")
- // Open dropdown and select Radio buttons
- cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
- cy.get('.spectrum-Popover').contains('Radio buttons')
- .wait(500)
- .click()
+filterTests(['all'], () => {
+ context("Add Radio Buttons", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
})
- const radioButtonsTotal = 3
- // Add values and confirm total
- addRadioButtonData(radioButtonsTotal)
- cy.getComponent(componentId).find('[type="radio"]')
- .should('have.length', radioButtonsTotal)
- })
- })
-
- const addRadioButtonData = (totalRadioButtons) => {
- cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
- cy.get('.spectrum-Popover').contains('Custom')
+
+ it("should add Radio Buttons options picker on form, add data, and confirm", () => {
+ cy.navigateToFrontend()
+ cy.addComponent("Form", "Form")
+ cy.addComponent("Form", "Options Picker").then((componentId) => {
+ // Provide field setting
+ cy.get(`[data-cy="field-prop-control"]`).type("1")
+ // Open dropdown and select Radio buttons
+ cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
+ cy.get('.spectrum-Popover').contains('Radio buttons')
.wait(500)
.click()
+ })
+ const radioButtonsTotal = 3
+ // Add values and confirm total
+ addRadioButtonData(radioButtonsTotal)
+ cy.getComponent(componentId).find('[type="radio"]')
+ .should('have.length', radioButtonsTotal)
+ })
})
- cy.addCustomSourceOptions(totalRadioButtons)
- }
+
+ const addRadioButtonData = (totalRadioButtons) => {
+ cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
+ cy.get('.spectrum-Popover').contains('Custom')
+ .wait(500)
+ .click()
+ })
+ cy.addCustomSourceOptions(totalRadioButtons)
+ }
+ })
})
diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js
new file mode 100644
index 0000000000..ab65e6561f
--- /dev/null
+++ b/packages/builder/cypress/integration/autoScreensUI.spec.js
@@ -0,0 +1,51 @@
+import filterTests from "../support/filterTests"
+
+filterTests(['smoke', 'all'], () => {
+ context("Auto Screens UI", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ it("should generate internal table screens", () => {
+ // Create autogenerated screens from the internal table
+ cy.createAutogeneratedScreens(["Cypress Tests"])
+ // Confirm screens have been auto generated
+ cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
+ cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
+ .and('contain', 'cypress-tests/new/row')
+ })
+
+ it("should generate multiple internal table screens at once", () => {
+ // Create a second internal table
+ const initialTable = "Cypress Tests"
+ const secondTable = "Table Two"
+ cy.createTable(secondTable)
+ // Create autogenerated screens from the internal tables
+ cy.createAutogeneratedScreens([initialTable, secondTable])
+ // Confirm screens have been auto generated
+ cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
+ // Previously generated tables are suffixed with numbers - as expected
+ cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id')
+ .and('contain', 'cypress-tests-2/new/row')
+ cy.get(".nav-items-container").contains("table-two").click()
+ cy.get(".nav-items-container").should('contain', 'table-two/:id')
+ .and('contain', 'table-two/new/row')
+ })
+
+ if (Cypress.env("TEST_ENV")) {
+ it("should generate data source screens", () => {
+ // Using MySQL data source for testing this
+ const datasource = "MySQL"
+ // Select & configure MySQL data source
+ cy.selectExternalDatasource(datasource)
+ cy.addDatasourceConfig(datasource)
+ // Create autogenerated screens from a MySQL table - MySQL contains books table
+ cy.createAutogeneratedScreens(["books"])
+ cy.get(".nav-items-container").contains("books").click()
+ cy.get(".nav-items-container").should('contain', 'books/:id')
+ .and('contain', 'books/new/row')
+ })
+ }
+ })
+})
diff --git a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js
new file mode 100644
index 0000000000..92d03a102d
--- /dev/null
+++ b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js
@@ -0,0 +1,43 @@
+import filterTests from "../support/filterTests"
+
+filterTests(['all'], () => {
+ context("Change Application Icon and Colour", () => {
+ before(() => {
+ cy.login()
+ })
+
+ it("should change the icon and colour for an application", () => {
+ // Search for test application
+ cy.searchForApplication("Cypress Tests")
+ cy.get(".appTable")
+ .within(() => {
+ cy.get(".spectrum-Icon").eq(1).click()
+ })
+ cy.get(".spectrum-Menu").contains("Edit icon").click()
+ // Select random icon
+ cy.get(".grid").within(() => {
+ cy.get(".icon-item").eq(Math.floor(Math.random() * 23) + 1).click()
+ })
+ // Select random colour
+ cy.get(".fill").click()
+ cy.get(".colors").within(() => {
+ cy.get(".color").eq(Math.floor(Math.random() * 33) + 1).click()
+ })
+ cy.intercept('**/applications/**').as('iconChange')
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.wait("@iconChange")
+ cy.get("@iconChange").its('response.statusCode')
+ .should('eq', 200)
+ cy.wait(1000)
+ // Confirm icon has changed from default
+ // Confirm colour has been applied - There is no default colour
+ cy.get(".appTable")
+ .within(() => {
+ cy.get('[aria-label]').eq(0).children()
+ .should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps')
+ cy.get(".title").children().children()
+ .should('have.attr', 'style').and('contains', 'color')
+ })
+ })
+ })
+})
diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js
index 34f152b540..273683aec8 100644
--- a/packages/builder/cypress/integration/createApp.spec.js
+++ b/packages/builder/cypress/integration/createApp.spec.js
@@ -1,8 +1,12 @@
-context("Create an Application", () => {
- it("should create a new application", () => {
- cy.login()
- cy.createTestApp()
- cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
- cy.contains("Cypress Tests").should("exist")
- })
+import filterTests from '../support/filterTests'
+
+filterTests(['smoke', 'all'], () => {
+ context("Create an Application", () => {
+ it("should create a new application", () => {
+ cy.login()
+ cy.createTestApp()
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
+ cy.contains("Cypress Tests").should("exist")
+ })
+ })
})
diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js
index afd405d5ab..e8892d16e2 100644
--- a/packages/builder/cypress/integration/createAutomation.spec.js
+++ b/packages/builder/cypress/integration/createAutomation.spec.js
@@ -1,62 +1,69 @@
-context("Create a automation", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- })
+import filterTests from "../support/filterTests"
- // https://on.cypress.io/interacting-with-elements
- it("should create a automation", () => {
- cy.createTestTableWithData()
- cy.wait(2000)
- cy.contains("Automate").click()
- cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get("input").type("Add Row")
- cy.contains("Row Created").click()
+filterTests(['smoke', 'all'], () => {
+ context("Create a automation", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ it("should create a automation", () => {
+ cy.createTestTableWithData()
+ cy.wait(2000)
+ cy.contains("Automate").click()
+ cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get("input").type("Add Row")
+ cy.contains("Row Created").click({ force: true })
+ cy.wait(500)
+ cy.get(".spectrum-Button--cta").click()
+ })
+
+ // Setup trigger
+ cy.contains("Setup").click()
+ cy.get(".spectrum-Picker-label").click()
cy.wait(500)
- cy.get(".spectrum-Button--cta").click()
- })
-
- // Setup trigger
- cy.contains("Setup").click()
- cy.get(".spectrum-Picker-label").click()
- cy.wait(500)
- cy.contains("dog").click()
- cy.wait(2000)
- // Create action
- cy.get(".block > .spectrum-Icon").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.wait(1000)
- cy.contains("Create Row").trigger('mouseover').click().click()
- cy.get(".spectrum-Button--cta").click()
- })
- cy.contains("Setup").click()
- cy.get(".spectrum-Picker-label").click()
- cy.contains("dog").click()
- cy.get(".spectrum-Textfield-input")
- .first()
- .type("goodboy")
- cy.get(".spectrum-Textfield-input")
- .eq(1)
- .type("11")
-
- cy.contains("Run test").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.wait(1000)
+ cy.contains("dog").click()
+ cy.wait(2000)
+ // Create action
+ cy.get('[aria-label="AddCircle"]').eq(1).click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.wait(1000)
+ cy.contains("Create Row").trigger('mouseover').click().click()
+ cy.get(".spectrum-Button--cta").click()
+ })
+ cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click()
cy.contains("dog").click()
cy.get(".spectrum-Textfield-input")
- .first()
- .type("goodboy")
+ .first()
+ .type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
cy.get(".spectrum-Textfield-input")
.eq(1)
.type("11")
- cy.get(".spectrum-Textfield-input")
- .eq(2)
- .type("123456")
- cy.get(".spectrum-Textfield-input")
- .eq(3)
- .type("123456")
+ cy.contains("Finish and test automation").click()
+
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.wait(1000)
+ cy.get(".spectrum-Picker-label").click()
+ cy.contains("dog").click()
+ cy.wait(1000)
+ cy.get(".spectrum-Textfield-input")
+ .first()
+ .type("automationGoodboy")
+ cy.get(".spectrum-Textfield-input")
+ .eq(1)
+ .type("11")
+ cy.get(".spectrum-Textfield-input")
+ .eq(2)
+ .type("123456")
+ cy.get(".spectrum-Textfield-input")
+ .eq(3)
+ .type("123456")
+ cy.contains("Test").click()
+ })
+ cy.contains("Data").click()
+ cy.contains("automationGoodboy")
})
})
})
diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js
index 3f85526b11..8bf1ec8ea4 100644
--- a/packages/builder/cypress/integration/createBinding.spec.js
+++ b/packages/builder/cypress/integration/createBinding.spec.js
@@ -1,58 +1,62 @@
-context("Create Bindings", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- cy.navigateToFrontend()
- })
+import filterTests from "../support/filterTests"
- it("should add a current user binding", () => {
- cy.addComponent("Elements", "Paragraph").then(() => {
- addSettingBinding("text", "Current User._id")
+filterTests(['smoke', 'all'], () => {
+ context("Create Bindings", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ cy.navigateToFrontend()
+ })
+
+ it("should add a current user binding", () => {
+ cy.addComponent("Elements", "Paragraph").then(() => {
+ addSettingBinding("text", "Current User._id")
+ })
+ })
+
+ it("should handle an invalid binding", () => {
+ cy.addComponent("Elements", "Paragraph").then(componentId => {
+ // Cypress needs to escape curly brackets
+ cy.get("[data-cy=setting-text] input")
+ .type("{{}{{}{{} Current User._id {}}{}}")
+ .blur()
+ cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
+ })
+ })
+
+ it("should add a URL param binding", () => {
+ const paramName = "foo"
+ cy.createScreen("Test Param", `/test/:${paramName}`)
+ cy.addComponent("Elements", "Paragraph").then(componentId => {
+ addSettingBinding("text", `URL.${paramName}`)
+ // The builder preview pages don't have a real URL, so all we can do
+ // is check that we were able to bind to the property, and that the
+ // component exists on the page
+ cy.getComponent(componentId).should("have.text", "New Paragraph")
+ })
+ })
+
+ it("should add a binding with a handlebars helper", () => {
+ cy.addComponent("Elements", "Paragraph").then(componentId => {
+ // Cypress needs to escape curly brackets
+ cy.get("[data-cy=setting-text] input")
+ .type("{{}{{} add 1 2 {}}{}}")
+ .blur()
+ cy.getComponent(componentId).should("have.text", "3")
+ })
})
})
- it("should handle an invalid binding", () => {
- cy.addComponent("Elements", "Paragraph").then(componentId => {
- // Cypress needs to escape curly brackets
- cy.get("[data-cy=setting-text] input")
- .type("{{}{{}{{} Current User._id {}}{}}")
- .blur()
- cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
+ const addSettingBinding = (setting, bindingText, clickOption = true) => {
+ cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click()
+ cy.get(".drawer").within(() => {
+ if (clickOption) {
+ cy.contains(bindingText).click()
+ cy.get("textarea").should("have.value", `{{ ${bindingText} }}`)
+ } else {
+ cy.get("textarea").type(bindingText)
+ }
+ cy.contains("Save").click()
})
- })
-
- it("should add a URL param binding", () => {
- const paramName = "foo"
- cy.createScreen("Test Param", `/test/:${paramName}`)
- cy.addComponent("Elements", "Paragraph").then(componentId => {
- addSettingBinding("text", `URL.${paramName}`)
- // The builder preview pages don't have a real URL, so all we can do
- // is check that we were able to bind to the property, and that the
- // component exists on the page
- cy.getComponent(componentId).should("have.text", "New Paragraph")
- })
- })
-
- it("should add a binding with a handlebars helper", () => {
- cy.addComponent("Elements", "Paragraph").then(componentId => {
- // Cypress needs to escape curly brackets
- cy.get("[data-cy=setting-text] input")
- .type("{{}{{} add 1 2 {}}{}}")
- .blur()
- cy.getComponent(componentId).should("have.text", "3")
- })
- })
+ }
})
-
-const addSettingBinding = (setting, bindingText, clickOption = true) => {
- cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click()
- cy.get(".drawer").within(() => {
- if (clickOption) {
- cy.contains(bindingText).click()
- cy.get("textarea").should("have.value", `{{ ${bindingText} }}`)
- } else {
- cy.get("textarea").type(bindingText)
- }
- cy.contains("Save").click()
- })
-}
diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js
index 93867e0f1f..e13439d9c6 100644
--- a/packages/builder/cypress/integration/createComponents.spec.js
+++ b/packages/builder/cypress/integration/createComponents.spec.js
@@ -1,92 +1,97 @@
// TODO for now components are skipped, might not be good to keep doing this
-xcontext("Create Components", () => {
- let headlineId
- before(() => {
- cy.login()
- cy.createTestApp()
- cy.createTable("dog")
- cy.addColumn("dog", "name", "Text")
- cy.addColumn("dog", "age", "Number")
- cy.addColumn("dog", "type", "Options")
- cy.navigateToFrontend()
- })
+import filterTests from "../support/filterTests"
- it("should add a container", () => {
- cy.addComponent(null, "Container").then(componentId => {
- cy.getComponent(componentId).should("exist")
+filterTests(['all'], () => {
+ xcontext("Create Components", () => {
+ let headlineId
+
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ cy.createTable("dog")
+ cy.addColumn("dog", "name", "Text")
+ cy.addColumn("dog", "age", "Number")
+ cy.addColumn("dog", "type", "Options")
+ cy.navigateToFrontend()
})
- })
- it("should add a headline", () => {
- cy.addComponent("Elements", "Headline").then(componentId => {
- headlineId = componentId
- cy.getComponent(headlineId).should("exist")
+ it("should add a container", () => {
+ cy.addComponent(null, "Container").then(componentId => {
+ cy.getComponent(componentId).should("exist")
+ })
})
- })
- it("should change the text of the headline", () => {
- const text = "Lorem ipsum dolor sit amet."
- cy.get("[data-cy=Settings]").click()
- cy.get("[data-cy=setting-text] input")
- .type(text)
- .blur()
- cy.getComponent(headlineId).should("have.text", text)
- })
+ it("should add a headline", () => {
+ cy.addComponent("Elements", "Headline").then(componentId => {
+ headlineId = componentId
+ cy.getComponent(headlineId).should("exist")
+ })
+ })
- it("should change the size of the headline", () => {
- cy.get("[data-cy=Design]").click()
- cy.contains("Typography").click()
- cy.get("[data-cy=font-size-prop-control]").click()
- cy.contains("60px").click()
- cy.getComponent(headlineId).should("have.css", "font-size", "60px")
- })
-
- it("should create a form and reset to match schema", () => {
- cy.addComponent("Form", "Form").then(() => {
+ it("should change the text of the headline", () => {
+ const text = "Lorem ipsum dolor sit amet."
cy.get("[data-cy=Settings]").click()
- cy.get("[data-cy=setting-dataSource]")
- .contains("Choose option")
- .click()
- cy.get(".dropdown")
- .contains("dog")
- .click()
- cy.addComponent("Form", "Field Group").then(fieldGroupId => {
+ cy.get("[data-cy=setting-text] input")
+ .type(text)
+ .blur()
+ cy.getComponent(headlineId).should("have.text", text)
+ })
+
+ it("should change the size of the headline", () => {
+ cy.get("[data-cy=Design]").click()
+ cy.contains("Typography").click()
+ cy.get("[data-cy=font-size-prop-control]").click()
+ cy.contains("60px").click()
+ cy.getComponent(headlineId).should("have.css", "font-size", "60px")
+ })
+
+ it("should create a form and reset to match schema", () => {
+ cy.addComponent("Form", "Form").then(() => {
cy.get("[data-cy=Settings]").click()
- cy.contains("Update Form Fields").click()
- cy.get(".modal")
- .get("button.primary")
+ cy.get("[data-cy=setting-dataSource]")
+ .contains("Choose option")
.click()
- cy.getComponent(fieldGroupId).within(() => {
- cy.contains("name").should("exist")
- cy.contains("age").should("exist")
- cy.contains("type").should("exist")
+ cy.get(".dropdown")
+ .contains("dog")
+ .click()
+ cy.addComponent("Form", "Field Group").then(fieldGroupId => {
+ cy.get("[data-cy=Settings]").click()
+ cy.contains("Update Form Fields").click()
+ cy.get(".modal")
+ .get("button.primary")
+ .click()
+ cy.getComponent(fieldGroupId).within(() => {
+ cy.contains("name").should("exist")
+ cy.contains("age").should("exist")
+ cy.contains("type").should("exist")
+ })
+ cy.getComponent(fieldGroupId)
+ .find("input")
+ .should("have.length", 2)
+ cy.getComponent(fieldGroupId)
+ .find(".spectrum-Picker")
+ .should("have.length", 1)
})
- cy.getComponent(fieldGroupId)
- .find("input")
- .should("have.length", 2)
- cy.getComponent(fieldGroupId)
- .find(".spectrum-Picker")
- .should("have.length", 1)
})
})
- })
- it("deletes a component", () => {
- cy.addComponent("Elements", "Paragraph").then(componentId => {
- cy.get("[data-cy=setting-_instanceName] input")
- .type(componentId)
- .blur()
- cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
- force: true,
+ it("deletes a component", () => {
+ cy.addComponent("Elements", "Paragraph").then(componentId => {
+ cy.get("[data-cy=setting-_instanceName] input")
+ .type(componentId)
+ .blur()
+ cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
+ force: true,
+ })
+ cy.get(".dropdown-container")
+ .contains("Delete")
+ .click()
+ cy.get(".modal")
+ .contains("Delete Component")
+ .click()
+ cy.getComponent(componentId).should("not.exist")
})
- cy.get(".dropdown-container")
- .contains("Delete")
- .click()
- cy.get(".modal")
- .contains("Delete Component")
- .click()
- cy.getComponent(componentId).should("not.exist")
})
})
})
diff --git a/packages/builder/cypress/integration/createScreen.js b/packages/builder/cypress/integration/createScreen.js
index 26c7cdd846..ada68d82dc 100644
--- a/packages/builder/cypress/integration/createScreen.js
+++ b/packages/builder/cypress/integration/createScreen.js
@@ -1,21 +1,25 @@
-context("Screen Tests", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- cy.navigateToFrontend()
- })
+import filterTests from "../support/filterTests"
- it("Should successfully create a screen", () => {
- cy.createScreen("Test Screen", "/test")
- cy.get(".nav-items-container").within(() => {
- cy.contains("/test").should("exist")
+filterTests(["smoke", "all"], () => {
+ context("Screen Tests", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ cy.navigateToFrontend()
})
- })
- it("Should update the url", () => {
- cy.createScreen("Test Screen", "test with spaces")
- cy.get(".nav-items-container").within(() => {
- cy.contains("/test-with-spaces").should("exist")
+ it("Should successfully create a screen", () => {
+ cy.createScreen("Test Screen", "/test")
+ cy.get(".nav-items-container").within(() => {
+ cy.contains("/test").should("exist")
+ })
+ })
+
+ it("Should update the url", () => {
+ cy.createScreen("Test Screen", "test with spaces")
+ cy.get(".nav-items-container").within(() => {
+ cy.contains("/test-with-spaces").should("exist")
+ })
})
})
})
diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js
index 20ec919b57..bac6806bcd 100644
--- a/packages/builder/cypress/integration/createTable.spec.js
+++ b/packages/builder/cypress/integration/createTable.spec.js
@@ -1,74 +1,110 @@
-context("Create a Table", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- })
+import filterTests from "../support/filterTests"
- it("should create a new Table", () => {
- cy.createTable("dog")
- cy.wait(1000)
- // Check if Table exists
- cy.get(".table-title h1").should("have.text", "dog")
- })
+filterTests(["smoke", "all"], () => {
+ context("Create a Table", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
- it("adds a new column to the table", () => {
- cy.addColumn("dog", "name", "Text")
- cy.contains("name").should("be.visible")
- })
+ it("should create a new Table", () => {
+ cy.createTable("dog")
+ cy.wait(1000)
+ // Check if Table exists
+ cy.get(".table-title h1").should("have.text", "dog")
+ })
- it("creates a row in the table", () => {
- cy.addRow(["Rover"])
- cy.contains("Rover").should("be.visible")
- })
+ it("adds a new column to the table", () => {
+ cy.addColumn("dog", "name", "Text")
+ cy.contains("name").should("be.visible")
+ })
- it("updates a column on the table", () => {
- cy.get(".title").click()
- cy.get(".spectrum-Table-editIcon > use").click()
- cy.get("input").eq(1).type("updated", { force: true })
- // Unset table display column
- cy.get(".spectrum-Switch-input").eq(1).click()
- cy.contains("Save Column").click()
- cy.contains("nameupdated ").should("contain", "nameupdated")
- })
+ it("creates a row in the table", () => {
+ cy.addRow(["Rover"])
+ cy.contains("Rover").should("be.visible")
+ })
-
- it("edits a row", () => {
- cy.contains("button", "Edit").click({ force: true })
- cy.wait(1000)
- cy.get(".spectrum-Modal input").clear()
- cy.get(".spectrum-Modal input").type("Updated")
- cy.contains("Save").click()
- cy.contains("Updated").should("have.text", "Updated")
- })
-
- it("deletes a row", () => {
- cy.get(".spectrum-Checkbox-input").check({ force: true })
- cy.contains("Delete 1 row(s)").click()
- cy.get(".spectrum-Modal").contains("Delete").click()
- cy.contains("RoverUpdated").should("not.exist")
- })
+ it("updates a column on the table", () => {
+ cy.get(".title").click()
+ cy.get(".spectrum-Table-editIcon > use").click()
+ cy.get("input").eq(1).type("updated", { force: true })
+ // Unset table display column
+ cy.get(".spectrum-Switch-input").eq(1).click()
+ cy.contains("Save Column").click()
+ cy.contains("nameupdated ").should("contain", "nameupdated")
+ })
- it("deletes a column", () => {
- cy.get(".title").click()
- cy.get(".spectrum-Table-editIcon > use").click()
- cy.contains("Delete").click()
- cy.wait(50)
- cy.get(`[data-cy="delete-column-confirm"]`).type("nameupdated")
- cy.contains("Delete Column").click()
- cy.contains("nameupdated").should("not.exist")
- })
+ it("edits a row", () => {
+ cy.contains("button", "Edit").click({ force: true })
+ cy.wait(1000)
+ cy.get(".spectrum-Modal input").clear()
+ cy.get(".spectrum-Modal input").type("Updated")
+ cy.contains("Save").click()
+ cy.contains("Updated").should("have.text", "Updated")
+ })
- it("deletes a table", () => {
- cy.get(".nav-item")
- .contains("dog")
- .parents(".nav-item")
- .first()
- .within(() => {
- cy.get(".actions .spectrum-Icon").click({ force: true })
+ it("deletes a row", () => {
+ cy.get(".spectrum-Checkbox-input").check({ force: true })
+ cy.contains("Delete 1 row(s)").click()
+ cy.get(".spectrum-Modal").contains("Delete").click()
+ cy.contains("RoverUpdated").should("not.exist")
+ })
+
+ if (Cypress.env("TEST_ENV")) {
+ // No Pagination in CI - Test env only for the next two tests
+ it("Adds 15 rows and checks pagination", () => {
+ // 10 rows per page, 15 rows should create 2 pages within table
+ const totalRows = 16
+ for (let i = 1; i < totalRows; i++) {
+ cy.addRow([i])
+ }
+ cy.wait(1000)
+ cy.get(".spectrum-Pagination").within(() => {
+ cy.get(".spectrum-ActionButton").eq(1).click()
+ })
+ cy.get(".spectrum-Pagination").within(() => {
+ cy.get(".spectrum-Body--secondary").contains("Page 2")
+ })
})
- cy.get(".spectrum-Menu > :nth-child(2)").click()
- cy.get(`[data-cy="delete-table-confirm"]`).type("dog")
- cy.contains("Delete Table").click()
- cy.contains("dog").should("not.exist")
+
+ it("Deletes rows and checks pagination", () => {
+ // Delete rows, removing second page of rows from table
+ const deleteRows = 5
+ cy.get(".spectrum-Checkbox-input").check({ force: true })
+ cy.get(".spectrum-Table")
+ cy.contains("Delete 5 row(s)").click()
+ cy.get(".spectrum-Modal").contains("Delete").click()
+ cy.wait(1000)
+
+ // Confirm table only has one page
+ cy.get(".spectrum-Pagination").within(() => {
+ cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled")
+ })
+ })
+ }
+
+ it("deletes a column", () => {
+ const columnName = "nameupdated"
+ cy.get(".title").click()
+ cy.get(".spectrum-Table-editIcon > use").click()
+ cy.contains("Delete").click()
+ cy.get('[data-cy="delete-column-confirm"]').type(columnName)
+ cy.contains("Delete Column").click()
+ cy.contains("nameupdated").should("not.exist")
+ })
+
+ it("deletes a table", () => {
+ cy.get(".nav-item")
+ .contains("dog")
+ .parents(".nav-item")
+ .first()
+ .within(() => {
+ cy.get(".actions .spectrum-Icon").click({ force: true })
+ })
+ cy.get(".spectrum-Menu > :nth-child(2)").click()
+ cy.get('[data-cy="delete-table-confirm"]').type("dog")
+ cy.contains("Delete Table").click()
+ cy.contains("dog").should("not.exist")
+ })
})
})
diff --git a/packages/builder/cypress/integration/createUser.spec.js b/packages/builder/cypress/integration/createUser.spec.js
deleted file mode 100644
index 18ae8a16a0..0000000000
--- a/packages/builder/cypress/integration/createUser.spec.js
+++ /dev/null
@@ -1,10 +0,0 @@
-context("Create a User", () => {
- before(() => {
- cy.login()
- })
-
- it("should create a user", () => {
- cy.createUser("bbuser@test.com")
- cy.contains("bbuser").should("be.visible")
- })
-})
diff --git a/packages/builder/cypress/integration/createUserAndRoles.spec.js b/packages/builder/cypress/integration/createUserAndRoles.spec.js
new file mode 100644
index 0000000000..ce6d370187
--- /dev/null
+++ b/packages/builder/cypress/integration/createUserAndRoles.spec.js
@@ -0,0 +1,180 @@
+import filterTests from "../support/filterTests"
+
+filterTests(["smoke", "all"], () => {
+ context("Create a User and Assign Roles", () => {
+ before(() => {
+ cy.login()
+ })
+
+ it("should create a user", () => {
+ cy.createUser("bbuser@test.com")
+ cy.get(".spectrum-Table").should("contain", "bbuser")
+ })
+
+ it("should confirm there is No Access for a New User", () => {
+ // Click into the user
+ cy.contains("bbuser").click()
+ cy.wait(500)
+ // Get No Access table - Confirm it has apps in it
+ cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found")
+ // Get Configure Roles table - Confirm it has no apps
+ cy.get(".spectrum-Table").eq(0).contains("No rows found")
+ })
+
+ it("should assign role types", () => {
+ // 3 apps minimum required - to assign an app to each role type
+ cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
+ .its("body")
+ .then(val => {
+ if (val.length < 3) {
+ for (let i = 1; i < 3; i++) {
+ const uuid = () => Cypress._.random(0, 1e6)
+ const name = uuid()
+ cy.createApp(name)
+ }
+ }
+ })
+ // Navigate back to the user
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
+ cy.wait(500)
+ cy.get(".spectrum-SideNav").contains("Users").click()
+ cy.wait(500)
+ cy.get(".spectrum-Table").contains("bbuser").click()
+ cy.wait(1000)
+ for (let i = 0; i < 3; i++) {
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .eq(0)
+ .find(".spectrum-Table-cell")
+ .eq(0)
+ .click()
+ cy.wait(500)
+ cy.get(".spectrum-Dialog-grid")
+ .contains("Choose an option")
+ .click()
+ .then(() => {
+ cy.wait(1000)
+ if (i == 0) {
+ cy.get(".spectrum-Popover").contains("Admin").click()
+ }
+ if (i == 1) {
+ cy.get(".spectrum-Popover").contains("Power").click()
+ }
+ if (i == 2) {
+ cy.get(".spectrum-Popover").contains("Basic").click()
+ }
+ cy.wait(1000)
+ cy.get(".spectrum-Button")
+ .contains("Update role")
+ .click({ force: true })
+ })
+ }
+ // Confirm roles exist within Configure roles table
+ cy.wait(2000)
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .within(assginedRoles => {
+ expect(assginedRoles).to.contain("Admin")
+ expect(assginedRoles).to.contain("Power")
+ expect(assginedRoles).to.contain("Basic")
+ })
+ })
+
+ it("should unassign role types", () => {
+ // Set each app within Configure roles table to 'No Access'
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .then(len => {
+ for (let i = 0; i < len; i++) {
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .eq(0)
+ .find(".spectrum-Table-cell")
+ .eq(0)
+ .click()
+ .then(() => {
+ cy.get(".spectrum-Picker").eq(1).click({ force: true })
+ cy.wait(500)
+ cy.get(".spectrum-Popover").contains("No Access").click()
+ })
+ cy.get(".spectrum-Button")
+ .contains("Update role")
+ .click({ force: true })
+ cy.wait(1000)
+ }
+ })
+ // Confirm Configure roles table no longer has any apps in it
+ cy.get(".spectrum-Table").eq(0).contains("No rows found")
+ })
+
+ it("should enable Developer access", () => {
+ // Enable Developer access
+ cy.get(".field")
+ .eq(4)
+ .within(() => {
+ cy.get(".spectrum-Switch-input").click({ force: true })
+ })
+ // No Access table should now be empty
+ cy.get(".container")
+ .contains("No Access")
+ .parent()
+ .within(() => {
+ cy.get(".spectrum-Table").contains("No rows found")
+ })
+
+ // Each app within Configure roles should have Admin access
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .then(len => {
+ for (let i = 0; i < len; i++) {
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .eq(i)
+ .contains("Admin")
+ cy.wait(500)
+ }
+ })
+ })
+
+ it("should disable Developer access", () => {
+ // Disable Developer access
+ cy.get(".field")
+ .eq(4)
+ .within(() => {
+ cy.get(".spectrum-Switch-input").click({ force: true })
+ })
+ // Configure roles table should now be empty
+ cy.get(".container")
+ .contains("Configure roles")
+ .parent()
+ .within(() => {
+ cy.get(".spectrum-Table").contains("No rows found")
+ })
+ })
+
+ it("should delete a user", () => {
+ // Click Delete user button
+ cy.get(".spectrum-Button")
+ .contains("Delete user")
+ .click({ force: true })
+ .then(() => {
+ // Confirm deletion within modal
+ cy.wait(500)
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Delete user")
+ .click({ force: true })
+ cy.wait(4000)
+ })
+ })
+ cy.get(".spectrum-Table").should("not.have.text", "bbuser")
+ })
+ })
+})
diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js
index e82ab67c0d..6b06cde0bc 100644
--- a/packages/builder/cypress/integration/createView.spec.js
+++ b/packages/builder/cypress/integration/createView.spec.js
@@ -1,152 +1,156 @@
-context("Create a View", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- cy.createTable("data")
- cy.addColumn("data", "group", "Text")
- cy.addColumn("data", "age", "Number")
- cy.addColumn("data", "rating", "Number")
+import filterTests from "../support/filterTests"
- // 6 Rows
- cy.addRow(["Students", 25, 1])
- cy.addRow(["Students", 20, 3])
- cy.addRow(["Students", 18, 6])
- cy.addRow(["Students", 25, 2])
- cy.addRow(["Teachers", 49, 5])
- cy.addRow(["Teachers", 36, 3])
- })
+filterTests(['smoke', 'all'], () => {
+ context("Create a View", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ cy.createTable("data")
+ cy.addColumn("data", "group", "Text")
+ cy.addColumn("data", "age", "Number")
+ cy.addColumn("data", "rating", "Number")
- it("creates a view", () => {
- cy.contains("Create view").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get("input").type("Test View")
- cy.get("button").contains("Create View").click({ force: true })
+ // 6 Rows
+ cy.addRow(["Students", 25, 1])
+ cy.addRow(["Students", 20, 3])
+ cy.addRow(["Students", 18, 6])
+ cy.addRow(["Students", 25, 2])
+ cy.addRow(["Teachers", 49, 5])
+ cy.addRow(["Teachers", 36, 3])
})
- cy.get(".table-title h1").contains("Test View")
- cy.get(".title").then($headers => {
- expect($headers).to.have.length(3)
- const headers = Array.from($headers).map(header =>
- header.textContent.trim()
- )
- expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
+
+ it("creates a view", () => {
+ cy.contains("Create view").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get("input").type("Test View")
+ cy.get("button").contains("Create View").click({ force: true })
+ })
+ cy.get(".table-title h1").contains("Test View")
+ cy.get(".title").then($headers => {
+ expect($headers).to.have.length(3)
+ const headers = Array.from($headers).map(header =>
+ header.textContent.trim()
+ )
+ expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
+ })
+ })
+
+ it("filters the view by age over 10", () => {
+ cy.contains("Filter").click()
+ cy.contains("Add Filter").click()
+
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get(".spectrum-Picker-label").eq(0).click()
+ cy.contains("age").click({ force: true })
+
+ cy.get(".spectrum-Picker-label").eq(1).click()
+ cy.contains("More Than").click({ force: true })
+
+ cy.get("input").type(18)
+ cy.contains("Save").click()
+ })
+
+ cy.get(".spectrum-Table-row").get($values => {
+ expect($values).to.have.length(5)
+ })
+ })
+
+ it("creates a stats calculation view based on age", () => {
+ cy.wait(1000)
+ cy.contains("Calculate").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get(".spectrum-Picker-label").eq(0).click()
+ cy.contains("Statistics").click()
+
+ cy.get(".spectrum-Picker-label").eq(1).click()
+ cy.contains("age").click({ force: true })
+
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ })
+ cy.wait(1000)
+
+ cy.get(".title").then($headers => {
+ expect($headers).to.have.length(7)
+ const headers = Array.from($headers).map(header =>
+ header.textContent.trim()
+ )
+ expect(removeSpacing(headers)).to.deep.eq([
+ "field",
+ "sum",
+ "min",
+ "max",
+ "count",
+ "sumsqr",
+ "avg",
+ ])
+ })
+ cy.get(".spectrum-Table-cell").then($values => {
+ let values = Array.from($values).map(header => header.textContent.trim())
+ expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
+ })
+ })
+
+ it("groups the view by group", () => {
+ cy.contains("Group by").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get(".spectrum-Picker-label").eq(0).click()
+ cy.contains("group").click()
+ cy.contains("Save").click()
+ })
+ cy.wait(1000)
+ cy.contains("Students").should("be.visible")
+ cy.contains("Teachers").should("be.visible")
+
+ cy.get(".spectrum-Table-cell").then($values => {
+ let values = Array.from($values).map(header => header.textContent.trim())
+ expect(values).to.deep.eq([
+ "Students",
+ "70",
+ "20",
+ "25",
+ "3",
+ "1650",
+ "23.333333333333332",
+ "Teachers",
+ "85",
+ "36",
+ "49",
+ "2",
+ "3697",
+ "42.5",
+ ])
+ })
+ })
+
+ it("renames a view", () => {
+ cy.contains(".nav-item", "Test View")
+ .find(".actions .icon")
+ .click({ force: true })
+ cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.get("input").type(" Updated")
+ cy.contains("Save").click()
+ })
+ cy.wait(1000)
+ cy.contains("Test View Updated").should("be.visible")
+ })
+
+ it("deletes a view", () => {
+ cy.contains(".nav-item", "Test View Updated")
+ .find(".actions .icon")
+ .click({ force: true })
+ cy.contains("Delete").click()
+ cy.contains("Delete View").click()
+ cy.wait(500)
+ cy.contains("TestView Updated").should("not.exist")
})
})
- it("filters the view by age over 10", () => {
- cy.contains("Filter").click()
- cy.contains("Add Filter").click()
-
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get(".spectrum-Picker-label").eq(0).click()
- cy.contains("age").click({ force: true })
-
- cy.get(".spectrum-Picker-label").eq(1).click()
- cy.contains("More Than").click({ force: true })
-
- cy.get("input").type(18)
- cy.contains("Save").click()
- })
-
- cy.get(".spectrum-Table-row").get($values => {
- expect($values).to.have.length(5)
- })
- })
-
- it("creates a stats calculation view based on age", () => {
- cy.wait(1000)
- cy.contains("Calculate").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get(".spectrum-Picker-label").eq(0).click()
- cy.contains("Statistics").click()
-
- cy.get(".spectrum-Picker-label").eq(1).click()
- cy.contains("age").click({ force: true })
-
- cy.get(".spectrum-Button").contains("Save").click({ force: true })
- })
- cy.wait(1000)
-
- cy.get(".title").then($headers => {
- expect($headers).to.have.length(7)
- const headers = Array.from($headers).map(header =>
- header.textContent.trim()
- )
- expect(removeSpacing(headers)).to.deep.eq([
- "field",
- "sum",
- "min",
- "max",
- "count",
- "sumsqr",
- "avg",
- ])
- })
- cy.get(".spectrum-Table-cell").then($values => {
- let values = Array.from($values).map(header => header.textContent.trim())
- expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
- })
- })
-
- it("groups the view by group", () => {
- cy.contains("Group by").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get(".spectrum-Picker-label").eq(0).click()
- cy.contains("group").click()
- cy.contains("Save").click()
- })
- cy.wait(1000)
- cy.contains("Students").should("be.visible")
- cy.contains("Teachers").should("be.visible")
-
- cy.get(".spectrum-Table-cell").then($values => {
- let values = Array.from($values).map(header => header.textContent.trim())
- expect(values).to.deep.eq([
- "Students",
- "70",
- "20",
- "25",
- "3",
- "1650",
- "23.333333333333332",
- "Teachers",
- "85",
- "36",
- "49",
- "2",
- "3697",
- "42.5",
- ])
- })
- })
-
- it("renames a view", () => {
- cy.contains(".nav-item", "Test View")
- .find(".actions .icon")
- .click({ force: true })
- cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
- cy.get(".modal-inner-wrapper").within(() => {
- cy.get("input").type(" Updated")
- cy.contains("Save").click()
- })
- cy.wait(1000)
- cy.contains("Test View Updated").should("be.visible")
- })
-
- it("deletes a view", () => {
- cy.contains(".nav-item", "Test View Updated")
- .find(".actions .icon")
- .click({ force: true })
- cy.contains("Delete").click()
- cy.contains("Delete View").click()
- cy.wait(500)
- cy.contains("TestView Updated").should("not.exist")
- })
-})
-
-function removeSpacing(headers) {
- let newHeaders = []
- for (let header of headers) {
- newHeaders.push(header.replace(/\s\s+/g, " "))
+ function removeSpacing(headers) {
+ let newHeaders = []
+ for (let header of headers) {
+ newHeaders.push(header.replace(/\s\s+/g, " "))
+ }
+ return newHeaders
}
- return newHeaders
-}
+})
diff --git a/packages/builder/cypress/integration/customThemingProperties.spec.js b/packages/builder/cypress/integration/customThemingProperties.spec.js
index 5b7922bde7..ed3478ca67 100644
--- a/packages/builder/cypress/integration/customThemingProperties.spec.js
+++ b/packages/builder/cypress/integration/customThemingProperties.spec.js
@@ -1,84 +1,87 @@
-xcontext("Custom Theming Properties", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- cy.navigateToFrontend()
- })
+import filterTests from "../support/filterTests"
- /* Default Values:
- Button roundness = Large
- Accent colour = Blue 600
- Accent colour (hover) = Blue 500
- Navigation bar background colour = Gray 100
- Navigation bar text colour = Gray 800 */
- it("should reset the color property values", () => {
- // Open Theme modal and change colours
- cy.get(".spectrum-ActionButton-label").contains("Theme").click()
- cy.get(".spectrum-Picker").contains("Large").click()
- .parents()
- .get(".spectrum-Menu-itemLabel").contains("None").click()
- changeThemeColors()
- // Reset colours
- cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
- // Check values have reset
- checkThemeColorDefaults()
- })
-
- /* Button Roundness Values:
- None = 0
- Small = 4px
- Medium = 8px
- Large = 16px */
- it("should test button roundness", () => {
- const buttonRoundnessValues = ["0", "4px", "8px", "16px"]
- cy.wait(1000)
- // Add button, change roundness and confirm value
- cy.addComponent("Button", null).then((componentId) => {
- buttonRoundnessValues.forEach(function (item, index){
- cy.get(".spectrum-ActionButton-label").contains("Theme").click()
- cy.get(".setting").contains("Button roundness").parent()
- .get(".select-wrapper").click()
- cy.get(".spectrum-Popover").find('li').eq(index).click()
- cy.get(".spectrum-Button").contains("View changes").click({force: true})
- cy.reload()
- cy.getComponent(componentId)
- .parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`)
+filterTests(['all'], () => {
+ xcontext("Custom Theming Properties", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ cy.navigateToFrontend()
+ })
+
+ /* Default Values:
+ Button roundness = Large
+ Accent colour = Blue 600
+ Accent colour (hover) = Blue 500
+ Navigation bar background colour = Gray 100
+ Navigation bar text colour = Gray 800 */
+ it("should reset the color property values", () => {
+ // Open Theme modal and change colours
+ cy.get(".spectrum-ActionButton-label").contains("Theme").click()
+ cy.get(".spectrum-Picker").contains("Large").click()
+ .parents()
+ .get(".spectrum-Menu-itemLabel").contains("None").click()
+ changeThemeColors()
+ // Reset colours
+ cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
+ // Check values have reset
+ checkThemeColorDefaults()
+ })
+
+ /* Button Roundness Values:
+ None = 0
+ Small = 4px
+ Medium = 8px
+ Large = 16px */
+ it("should test button roundness", () => {
+ const buttonRoundnessValues = ["0", "4px", "8px", "16px"]
+ cy.wait(1000)
+ // Add button, change roundness and confirm value
+ cy.addComponent("Button", null).then((componentId) => {
+ buttonRoundnessValues.forEach(function (item, index){
+ cy.get(".spectrum-ActionButton-label").contains("Theme").click()
+ cy.get(".setting").contains("Button roundness").parent()
+ .get(".select-wrapper").click()
+ cy.get(".spectrum-Popover").find('li').eq(index).click()
+ cy.get(".spectrum-Button").contains("View changes").click({force: true})
+ cy.reload()
+ cy.getComponent(componentId)
+ .parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`)
+ })
})
})
+
+ const changeThemeColors = () => {
+ // Changes the theme colours
+ cy.get(".spectrum-FieldLabel").contains("Accent color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .find('[title="Red 400"]').click()
+ cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .find('[title="Orange 400"]').click()
+ cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .find('[title="Yellow 400"]').click()
+ cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .find('[title="Green 400"]').click()
+ }
+
+ const checkThemeColorDefaults = () => {
+ cy.get(".spectrum-FieldLabel").contains("Accent color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .get('[title="Blue 600"]').children().find('[aria-label="Checkmark"]')
+ cy.get(".spectrum-Dialog-grid").click()
+ cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .get('[title="Blue 500"]').children().find('[aria-label="Checkmark"]')
+ cy.get(".spectrum-Dialog-grid").click()
+ cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .get('[title="Gray 100"]').children().find('[aria-label="Checkmark"]')
+ cy.get(".spectrum-Dialog-grid").click()
+ cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
+ .parent().find(".container.svelte-z3cm5a").click()
+ .get('[title="Gray 800"]').children().find('[aria-label="Checkmark"]')
+ }
})
-
- const changeThemeColors = () => {
- // Changes the theme colours
- cy.get(".spectrum-FieldLabel").contains("Accent color")
- .parent().find(".container.svelte-z3cm5a").click()
- .find('[title="Red 400"]').click()
- cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
- .parent().find(".container.svelte-z3cm5a").click()
- .find('[title="Orange 400"]').click()
- cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
- .parent().find(".container.svelte-z3cm5a").click()
- .find('[title="Yellow 400"]').click()
- cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
- .parent().find(".container.svelte-z3cm5a").click()
- .find('[title="Green 400"]').click()
- }
-
- const checkThemeColorDefaults = () => {
- cy.get(".spectrum-FieldLabel").contains("Accent color")
- .parent().find(".container.svelte-z3cm5a").click()
- .get('[title="Blue 600"]').children().find('[aria-label="Checkmark"]')
- cy.get(".spectrum-Dialog-grid").click()
- cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
- .parent().find(".container.svelte-z3cm5a").click()
- .get('[title="Blue 500"]').children().find('[aria-label="Checkmark"]')
- cy.get(".spectrum-Dialog-grid").click()
- cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
- .parent().find(".container.svelte-z3cm5a").click()
- .get('[title="Gray 100"]').children().find('[aria-label="Checkmark"]')
- cy.get(".spectrum-Dialog-grid").click()
- cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
- .parent().find(".container.svelte-z3cm5a").click()
- .get('[title="Gray 800"]').children().find('[aria-label="Checkmark"]')
- }
-
})
diff --git a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js
new file mode 100644
index 0000000000..1bee7b5ec1
--- /dev/null
+++ b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js
@@ -0,0 +1,43 @@
+import filterTests from "../../support/filterTests"
+
+filterTests(['all'], () => {
+ context("Datasource Wizard", () => {
+ if (Cypress.env("TEST_ENV")) {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ it("should navigate in and out of a datasource via wizard", () => {
+ // Select PostgreSQL and add config (without fetch)
+ const datasource = "Oracle"
+ cy.selectExternalDatasource(datasource)
+ cy.addDatasourceConfig(datasource, true)
+
+ // Navigate back within datasource wizard
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button").contains("Back").click({ force: true })
+ cy.wait(1000)
+ })
+
+ // Select PostgreSQL datasource again
+ cy.get(".item-list").contains(datasource).click()
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button").contains("Continue").click({ force: true })
+ })
+
+ // Fetch tables after selection
+ // Previously entered config should not have been saved
+ // Config is back to default values
+ // Modal will close and provide 500 error
+ cy.intercept('**/datasources').as('datasourceConnection')
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button").contains("Save and fetch tables").click({ force: true })
+ })
+ cy.wait("@datasourceConnection")
+ cy.get("@datasourceConnection").its('response.body')
+ .should('have.property', 'status', 500)
+ })
+ }
+ })
+})
diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js
new file mode 100644
index 0000000000..03f59a6004
--- /dev/null
+++ b/packages/builder/cypress/integration/datasources/mySql.spec.js
@@ -0,0 +1,222 @@
+import filterTests from "../../support/filterTests"
+
+filterTests(["all"], () => {
+ context("MySQL Datasource Testing", () => {
+ if (Cypress.env("TEST_ENV")) {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+ const datasource = "MySQL"
+ const queryName = "Cypress Test Query"
+ const queryRename = "CT Query Rename"
+
+ it("Should add MySQL data source without configuration", () => {
+ // Select MySQL data source
+ cy.selectExternalDatasource(datasource)
+ // Attempt to fetch tables without applying configuration
+ cy.intercept("**/datasources").as("datasource")
+ cy.get(".spectrum-Button")
+ .contains("Save and fetch tables")
+ .click({ force: true })
+ // Intercept Request after button click & apply assertions
+ cy.wait("@datasource")
+ cy.get("@datasource")
+ .its("response.body")
+ .should(
+ "have.property",
+ "message",
+ "connect ECONNREFUSED 127.0.0.1:3306"
+ )
+ cy.get("@datasource")
+ .its("response.body")
+ .should("have.property", "status", 500)
+ })
+
+ it("should add MySQL data source and fetch tables", () => {
+ // Add & configure MySQL data source
+ cy.selectExternalDatasource(datasource)
+ cy.intercept("**/datasources").as("datasource")
+ cy.addDatasourceConfig(datasource)
+ // Check response from datasource after adding configuration
+ cy.wait("@datasource")
+ cy.get("@datasource").its("response.statusCode").should("eq", 200)
+ // Confirm fetch tables was successful
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("be.gt", 0)
+ })
+
+ it("should check table fetching error", () => {
+ // MySQL test data source contains tables without primary keys
+ cy.get(".spectrum-InLineAlert")
+ .should("contain", "Error fetching tables")
+ .and("contain", "No primary key constraint found")
+ })
+
+ it("should define a One relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("One").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ })
+ // Confirm table length & column name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 1)
+ cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
+ })
+
+ it("should define a Many relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("Many").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("LOCATIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
+ cy.get(".spectrum-Picker").eq(5).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ cy.wait(1000)
+ })
+ // Confirm table length & relationship name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 2)
+ cy.get(".spectrum-Table-cell").should(
+ "contain",
+ "LOCATIONS through COUNTRIES → REGIONS"
+ )
+ })
+
+ it("should delete relationships", () => {
+ // Delete both relationships
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .then(len => {
+ for (let i = 0; i < len; i++) {
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .within(() => {
+ cy.get(".spectrum-Table-row").eq(0).click()
+ cy.wait(500)
+ })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Delete")
+ .click({ force: true })
+ })
+ cy.reload()
+ }
+ // Confirm relationships no longer exist
+ cy.get(".spectrum-Body").should(
+ "contain",
+ "No relationships configured"
+ )
+ })
+ })
+
+ it("should add a query", () => {
+ // Add query
+ cy.get(".spectrum-Button").contains("Add query").click({ force: true })
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").type(queryName)
+ })
+ // Insert Query within Fields section
+ cy.get(".CodeMirror textarea")
+ .eq(0)
+ .type("SELECT * FROM books", { force: true })
+ // Intercept query execution
+ cy.intercept("**/queries/preview").as("query")
+ cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
+ cy.wait(500)
+ cy.wait("@query")
+ // Assert against Status Code & Body
+ cy.get("@query").its("response.statusCode").should("eq", 200)
+ cy.get("@query").its("response.body").should("not.be.empty")
+ // Save query
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".nav-item").should("contain", queryName)
+ })
+
+ it("should duplicate a query", () => {
+ // Get last nav item - The query
+ cy.get(".nav-item")
+ .last()
+ .within(() => {
+ cy.get(".icon").eq(1).click({ force: true })
+ })
+ // Select and confirm duplication
+ cy.get(".spectrum-Menu").contains("Duplicate").click()
+ cy.get(".nav-item").should("contain", queryName + " (1)")
+ })
+
+ it("should edit a query name", () => {
+ // Rename query
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").clear().type(queryRename)
+ })
+ // Save query
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".nav-item").should("contain", queryRename)
+ })
+
+ it("should delete a query", () => {
+ // Get last nav item - The query
+ for (let i = 0; i < 2; i++) {
+ cy.get(".nav-item")
+ .last()
+ .within(() => {
+ cy.get(".icon").eq(1).click({ force: true })
+ })
+ // Select Delete
+ cy.get(".spectrum-Menu").contains("Delete").click()
+ cy.get(".spectrum-Button")
+ .contains("Delete Query")
+ .click({ force: true })
+ cy.wait(1000)
+ }
+ // Confirm deletion
+ cy.get(".nav-item").should("not.contain", queryName)
+ cy.get(".nav-item").should("not.contain", queryRename)
+ })
+ }
+ })
+})
diff --git a/packages/builder/cypress/integration/datasources/oracle.spec.js b/packages/builder/cypress/integration/datasources/oracle.spec.js
new file mode 100644
index 0000000000..73c25001c9
--- /dev/null
+++ b/packages/builder/cypress/integration/datasources/oracle.spec.js
@@ -0,0 +1,230 @@
+import filterTests from "../../support/filterTests"
+
+filterTests(["all"], () => {
+ context("Oracle Datasource Testing", () => {
+ if (Cypress.env("TEST_ENV")) {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+ const datasource = "Oracle"
+ const queryName = "Cypress Test Query"
+ const queryRename = "CT Query Rename"
+
+ it("Should add Oracle data source and skip table fetch", () => {
+ // Select Oracle data source
+ cy.selectExternalDatasource(datasource)
+ // Skip table fetch - no config added
+ cy.get(".spectrum-Button")
+ .contains("Skip table fetch")
+ .click({ force: true })
+ cy.wait(500)
+ // Confirm config contains localhost
+ cy.get(".spectrum-Textfield-input")
+ .eq(1)
+ .should("have.value", "localhost")
+ // Add another Oracle data source, configure & skip table fetch
+ cy.selectExternalDatasource(datasource)
+ cy.addDatasourceConfig(datasource, true)
+ // Confirm config and no tables
+ cy.get(".spectrum-Textfield-input")
+ .eq(1)
+ .should("have.value", Cypress.env("oracle").HOST)
+ cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
+ })
+
+ it("Should add Oracle data source and fetch tables without configuration", () => {
+ // Select Oracle data source
+ cy.selectExternalDatasource(datasource)
+ // Attempt to fetch tables without applying configuration
+ cy.intercept("**/datasources").as("datasource")
+ cy.get(".spectrum-Button")
+ .contains("Save and fetch tables")
+ .click({ force: true })
+ // Intercept Request after button click & apply assertions
+ cy.wait("@datasource")
+ cy.get("@datasource")
+ .its("response.body")
+ .should("have.property", "status", 500)
+ })
+
+ it("should add Oracle data source and fetch tables", () => {
+ // Add & configure Oracle data source
+ cy.selectExternalDatasource(datasource)
+ cy.intercept("**/datasources").as("datasource")
+ cy.addDatasourceConfig(datasource)
+ // Check response from datasource after adding configuration
+ cy.wait("@datasource")
+ cy.get("@datasource").its("response.statusCode").should("eq", 200)
+ // Confirm fetch tables was successful
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("be.gt", 0)
+ })
+
+ it("should define a One relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("One").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ })
+ // Confirm table length & column name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 1)
+ cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
+ })
+
+ it("should define a Many relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("Many").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("LOCATIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
+ cy.get(".spectrum-Picker").eq(5).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ })
+ // Confirm table length & relationship name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 2)
+ cy.get(".spectrum-Table-cell").should(
+ "contain",
+ "LOCATIONS through COUNTRIES → REGIONS"
+ )
+ })
+
+ it("should delete relationships", () => {
+ // Delete both relationships
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .then(len => {
+ for (let i = 0; i < len; i++) {
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .within(() => {
+ cy.get(".spectrum-Table-row").eq(0).click()
+ cy.wait(500)
+ })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Delete")
+ .click({ force: true })
+ })
+ cy.reload()
+ }
+ // Confirm relationships no longer exist
+ cy.get(".spectrum-Body").should(
+ "contain",
+ "No relationships configured"
+ )
+ })
+ })
+
+ it("should add a query", () => {
+ // Add query
+ cy.get(".spectrum-Button").contains("Add query").click({ force: true })
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").type(queryName)
+ })
+ // Insert Query within Fields section
+ cy.get(".CodeMirror textarea")
+ .eq(0)
+ .type("SELECT * FROM JOBS", { force: true })
+ // Intercept query execution
+ cy.intercept("**/queries/preview").as("query")
+ cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
+ cy.wait(500)
+ cy.wait("@query")
+ // Assert against Status Code & Body
+ cy.get("@query").its("response.statusCode").should("eq", 200)
+ cy.get("@query").its("response.body").should("not.be.empty")
+ // Save query
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".nav-item").should("contain", queryName)
+ })
+
+ it("should duplicate a query", () => {
+ // Get query nav item
+ cy.get(".nav-item")
+ .contains(queryName)
+ .parent()
+ .within(() => {
+ cy.get(".spectrum-Icon").eq(1).click({ force: true })
+ })
+ // Select and confirm duplication
+ cy.get(".spectrum-Menu").contains("Duplicate").click()
+ cy.get(".nav-item").should("contain", queryName + " (1)")
+ })
+
+ it("should edit a query name", () => {
+ // Rename query
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").clear().type(queryRename)
+ })
+ // Save query
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".nav-item").should("contain", queryRename)
+ })
+
+ it("should delete a query", () => {
+ // Get query nav item - QueryName
+ cy.get(".nav-item")
+ .contains(queryName)
+ .parent()
+ .within(() => {
+ cy.get(".spectrum-Icon").eq(1).click({ force: true })
+ })
+
+ // Select Delete
+ cy.get(".spectrum-Menu").contains("Delete").click()
+ cy.get(".spectrum-Button")
+ .contains("Delete Query")
+ .click({ force: true })
+ cy.wait(1000)
+
+ // Confirm deletion
+ cy.get(".nav-item").should("not.contain", queryName)
+ })
+ }
+ })
+})
diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js
new file mode 100644
index 0000000000..3f55636623
--- /dev/null
+++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js
@@ -0,0 +1,285 @@
+import filterTests from "../../support/filterTests"
+
+filterTests(["all"], () => {
+ context("PostgreSQL Datasource Testing", () => {
+ if (Cypress.env("TEST_ENV")) {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+ const datasource = "PostgreSQL"
+ const queryName = "Cypress Test Query"
+ const queryRename = "CT Query Rename"
+
+ it("Should add PostgreSQL data source without configuration", () => {
+ // Select PostgreSQL data source
+ cy.selectExternalDatasource(datasource)
+ // Attempt to fetch tables without applying configuration
+ cy.intercept("**/datasources").as("datasource")
+ cy.get(".spectrum-Button")
+ .contains("Save and fetch tables")
+ .click({ force: true })
+ // Intercept Request after button click & apply assertions
+ cy.wait("@datasource")
+ cy.get("@datasource")
+ .its("response.body")
+ .should(
+ "have.property",
+ "message",
+ "connect ECONNREFUSED 127.0.0.1:5432"
+ )
+ cy.get("@datasource")
+ .its("response.body")
+ .should("have.property", "status", 500)
+ })
+
+ it("should add PostgreSQL data source and fetch tables", () => {
+ // Add & configure PostgreSQL data source
+ cy.selectExternalDatasource(datasource)
+ cy.intercept("**/datasources").as("datasource")
+ cy.addDatasourceConfig(datasource)
+ // Check response from datasource after adding configuration
+ cy.wait("@datasource")
+ cy.get("@datasource").its("response.statusCode").should("eq", 200)
+ // Confirm fetch tables was successful
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("be.gt", 0)
+ })
+
+ it("should define a One relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("One").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ })
+ // Confirm table length & column name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 1)
+ cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
+ })
+
+ it("should define a Many relationship type", () => {
+ // Select relationship type & configure
+ cy.get(".spectrum-Button")
+ .contains("Define relationship")
+ .click({ force: true })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Picker").eq(0).click()
+ cy.get(".spectrum-Popover").contains("Many").click()
+ cy.get(".spectrum-Picker").eq(1).click()
+ cy.get(".spectrum-Popover").contains("LOCATIONS").click()
+ cy.get(".spectrum-Picker").eq(2).click()
+ cy.get(".spectrum-Popover").contains("REGIONS").click()
+ cy.get(".spectrum-Picker").eq(3).click()
+ cy.get(".spectrum-Popover").contains("COUNTRIES").click()
+ cy.get(".spectrum-Picker").eq(4).click()
+ cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
+ cy.get(".spectrum-Picker").eq(5).click()
+ cy.get(".spectrum-Popover").contains("REGION_ID").click()
+ // Save relationship & reload page
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.reload()
+ })
+ // Confirm table length & relationship name
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 2)
+ cy.get(".spectrum-Table-cell").should(
+ "contain",
+ "LOCATIONS through COUNTRIES → REGIONS"
+ )
+ })
+
+ it("should delete a relationship", () => {
+ cy.get(".hierarchy-items-container").contains(datasource).click()
+ cy.reload()
+ // Delete one relationship
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .within(() => {
+ cy.get(".spectrum-Table-row").eq(0).click()
+ cy.wait(500)
+ })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button").contains("Delete").click({ force: true })
+ })
+ cy.reload()
+ // Confirm relationship was deleted
+ cy.get(".spectrum-Table")
+ .eq(1)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 1)
+ })
+
+ it("should add a query", () => {
+ // Add query
+ cy.get(".spectrum-Button").contains("Add query").click({ force: true })
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").type(queryName)
+ })
+ // Insert Query within Fields section
+ cy.get(".CodeMirror textarea")
+ .eq(0)
+ .type("SELECT * FROM books", { force: true })
+ // Intercept query execution
+ cy.intercept("**/queries/preview").as("query")
+ cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
+ cy.wait(500)
+ cy.wait("@query")
+ // Assert against Status Code & Body
+ cy.get("@query").its("response.statusCode").should("eq", 200)
+ cy.get("@query").its("response.body").should("not.be.empty")
+ // Save query
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".hierarchy-items-container").should("contain", queryName)
+ })
+
+ it("should switch to schema with no tables", () => {
+ // Switch Schema - To one without any tables
+ cy.get(".hierarchy-items-container").contains(datasource).click()
+ switchSchema("randomText")
+
+ // No tables displayed
+ cy.get(".spectrum-Body").eq(2).should("contain", "No tables found")
+
+ // Previously created query should be visible
+ cy.get(".spectrum-Table").should("contain", queryName)
+ })
+
+ it("should switch schemas", () => {
+ // Switch schema - To one with tables
+ switchSchema("1")
+
+ // Confirm tables exist - Check for specific one
+ cy.get(".spectrum-Table").eq(0).should("contain", "test")
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("eq", 1)
+
+ // Confirm specific table visible within left nav bar
+ cy.get(".hierarchy-items-container").should("contain", "test")
+
+ // Switch back to public schema
+ switchSchema("public")
+
+ // Confirm tables exist - again
+ cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS")
+ cy.get(".spectrum-Table")
+ .eq(0)
+ .find(".spectrum-Table-row")
+ .its("length")
+ .should("be.gt", 1)
+
+ // Confirm specific table visible within left nav bar
+ cy.get(".hierarchy-items-container").should("contain", "REGIONS")
+
+ // No relationships and one query
+ cy.get(".spectrum-Body")
+ .eq(3)
+ .should("contain", "No relationships configured.")
+ cy.get(".spectrum-Table").eq(1).should("contain", queryName)
+ })
+
+ it("should duplicate a query", () => {
+ // Get last nav item - The query
+ cy.get(".nav-item")
+ .last()
+ .within(() => {
+ cy.get(".icon").eq(1).click({ force: true })
+ })
+ // Select and confirm duplication
+ cy.get(".spectrum-Menu").contains("Duplicate").click()
+ cy.get(".nav-item").should("contain", queryName + " (1)")
+ })
+
+ it("should edit a query name", () => {
+ // Access query
+ cy.get(".hierarchy-items-container")
+ .contains(queryName + " (1)")
+ .click()
+
+ // Rename query
+ cy.get(".spectrum-Form-item")
+ .eq(0)
+ .within(() => {
+ cy.get("input").clear().type(queryRename)
+ })
+
+ // Run and Save query
+ cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
+ cy.wait(500)
+ cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
+ cy.get(".nav-item").should("contain", queryRename)
+ })
+
+ it("should delete a query", () => {
+ // Get last nav item - The query
+ for (let i = 0; i < 2; i++) {
+ cy.get(".nav-item")
+ .last()
+ .within(() => {
+ cy.get(".icon").eq(1).click({ force: true })
+ })
+ // Select Delete
+ cy.get(".spectrum-Menu").contains("Delete").click()
+ cy.get(".spectrum-Button")
+ .contains("Delete Query")
+ .click({ force: true })
+ cy.wait(1000)
+ }
+ // Confirm deletion
+ cy.get(".nav-item").should("not.contain", queryName)
+ cy.get(".nav-item").should("not.contain", queryRename)
+ })
+
+ const switchSchema = schema => {
+ // Edit configuration - Change Schema
+ cy.get(".spectrum-Textfield")
+ .eq(6)
+ .within(() => {
+ cy.get("input").clear().type(schema)
+ })
+ // Save configuration & fetch
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.get(".spectrum-Button")
+ .contains("Fetch tables")
+ .click({ force: true })
+ // Click fetch tables again within modal
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Fetch tables")
+ .click({ force: true })
+ })
+ cy.reload()
+ cy.wait(5000)
+ }
+ }
+ })
+})
diff --git a/packages/builder/cypress/integration/datasources/rest.spec.js b/packages/builder/cypress/integration/datasources/rest.spec.js
new file mode 100644
index 0000000000..f39d174831
--- /dev/null
+++ b/packages/builder/cypress/integration/datasources/rest.spec.js
@@ -0,0 +1,43 @@
+import filterTests from "../../support/filterTests"
+
+filterTests(['smoke', 'all'], () => {
+ context("REST Datasource Testing", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ const datasource = "REST"
+ const restUrl = "https://api.openbrewerydb.org/breweries"
+
+ it("Should add REST data source with incorrect API", () => {
+ // Select REST data source
+ cy.selectExternalDatasource(datasource)
+ // Enter incorrect api & attempt to send query
+ cy.wait(500)
+ cy.get(".spectrum-Button").contains("Add query").click({ force: true })
+ cy.intercept('**/preview').as('queryError')
+ cy.get("input").clear().type("random text")
+ cy.get(".spectrum-Button").contains("Send").click({ force: true })
+ // Intercept Request after button click & apply assertions
+ cy.wait("@queryError")
+ cy.get("@queryError").its('response.body')
+ .should('have.property', 'message', 'Invalid URL: http://random text?')
+ cy.get("@queryError").its('response.body')
+ .should('have.property', 'status', 400)
+ })
+
+ it("should add and configure a REST datasource", () => {
+ // Select REST datasource and create query
+ cy.selectExternalDatasource(datasource)
+ cy.wait(500)
+ // createRestQuery confirms query creation
+ cy.createRestQuery("GET", restUrl)
+ // Confirm status code response within REST datasource
+ cy.get(".spectrum-FieldLabel")
+ .contains("Status")
+ .children()
+ .should('contain', 200)
+ })
+ })
+})
diff --git a/packages/builder/cypress/integration/queryLevelTransformers.spec.js b/packages/builder/cypress/integration/queryLevelTransformers.spec.js
new file mode 100644
index 0000000000..d6d4278eb4
--- /dev/null
+++ b/packages/builder/cypress/integration/queryLevelTransformers.spec.js
@@ -0,0 +1,116 @@
+import filterTests from "../support/filterTests"
+
+filterTests(['smoke', 'all'], () => {
+ context("Query Level Transformers", () => {
+ before(() => {
+ cy.login()
+ cy.deleteApp("Cypress Tests")
+ cy.createApp("Cypress Tests")
+ })
+
+ it("should write a transformer function", () => {
+ // Add REST datasource - contains API for breweries
+ const datasource = "REST"
+ const restUrl = "https://api.openbrewerydb.org/breweries"
+ cy.selectExternalDatasource(datasource)
+ cy.createRestQuery("GET", restUrl)
+ cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
+ // Get Transformer Function from file
+ cy.readFile("cypress/support/queryLevelTransformerFunction.js").then((transformerFunction) => {
+ cy.get(".CodeMirror textarea")
+ // Highlight current text and overwrite with file contents
+ .type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
+ .type(transformerFunction, { parseSpecialCharSequences: false })
+ })
+ // Send Query
+ cy.intercept('**/queries/preview').as('query')
+ cy.get(".spectrum-Button").contains("Send").click({ force: true })
+ cy.wait("@query")
+ // Assert against Status Code, body, & body rows
+ cy.get("@query").its('response.statusCode')
+ .should('eq', 200)
+ cy.get("@query").its('response.body').should('not.be.empty')
+ cy.get("@query").its('response.body.rows').should('not.be.empty')
+ })
+
+ it("should add data to the previous query", () => {
+ // Add REST datasource - contains API for breweries
+ const datasource = "REST"
+ const restUrl = "https://api.openbrewerydb.org/breweries"
+ cy.selectExternalDatasource(datasource)
+ cy.createRestQuery("GET", restUrl)
+ cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
+ // Get Transformer Function with Data from file
+ cy.readFile("cypress/support/queryLevelTransformerFunctionWithData.js").then((transformerFunction) => {
+ //console.log(transformerFunction[1])
+ cy.get(".CodeMirror textarea")
+ // Highlight current text and overwrite with file contents
+ .type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
+ .type(transformerFunction, { parseSpecialCharSequences: false })
+ })
+ // Send Query
+ cy.intercept('**/queries/preview').as('query')
+ cy.get(".spectrum-Button").contains("Send").click({ force: true })
+ cy.wait("@query")
+ // Assert against Status Code, body, & body rows
+ cy.get("@query").its('response.statusCode')
+ .should('eq', 200)
+ cy.get("@query").its('response.body').should('not.be.empty')
+ cy.get("@query").its('response.body.rows').should('not.be.empty')
+ })
+
+ it("should run an invalid query within the transformer section", () => {
+ // Add REST datasource - contains API for breweries
+ const datasource = "REST"
+ const restUrl = "https://api.openbrewerydb.org/breweries"
+ cy.selectExternalDatasource(datasource)
+ cy.createRestQuery("GET", restUrl)
+ cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
+ // Clear the code box and add "test"
+ cy.get(".CodeMirror textarea")
+ .type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
+ .type("test")
+ // Run Query and intercept
+ cy.intercept('**/preview').as('queryError')
+ cy.get(".spectrum-Button").contains("Send").click({ force: true })
+ cy.wait("@queryError")
+ cy.wait(500)
+ // Assert against message and status for the query error
+ cy.get("@queryError").its('response.body').should('have.property', 'message', "test is not defined")
+ cy.get("@queryError").its('response.body').should('have.property', 'status', 400)
+ })
+
+ xit("should run an invalid query via POST request", () => {
+ // POST request with transformer as null
+ cy.request({method: 'POST',
+ url: `${Cypress.config().baseUrl}/api/queries/`,
+ body: {fields : {"headers":{},"queryString":null,"path":null},
+ parameters : [],
+ schema : {},
+ name : "test",
+ queryVerb : "read",
+ transformer : null,
+ datasourceId: "test"},
+ // Expected 400 error - Transformer must be a string
+ failOnStatusCode: false}).then((response) => {
+ expect(response.status).to.equal(400)
+ expect(response.body.message).to.include('Invalid body - "transformer" must be a string')
+ })
+ })
+
+ xit("should run an empty query", () => {
+ // POST request with Transformer as an empty string
+ cy.request({method: 'POST',
+ url: `${Cypress.config().baseUrl}/api/queries/preview`,
+ body: {fields : {"headers":{},"queryString":null,"path":null},
+ queryVerb : "read",
+ transformer : "",
+ datasourceId: "test"},
+ // Expected 400 error - Transformer is not allowed to be empty
+ failOnStatusCode: false}).then((response) => {
+ expect(response.status).to.equal(400)
+ expect(response.body.message).to.include('Invalid body - "transformer" is not allowed to be empty')
+ })
+ })
+ })
+})
diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js
index 95a152c017..48e829fa03 100644
--- a/packages/builder/cypress/integration/renameAnApplication.spec.js
+++ b/packages/builder/cypress/integration/renameAnApplication.spec.js
@@ -1,103 +1,133 @@
-context("Rename an App", () => {
- beforeEach(() => {
- cy.login()
- cy.createTestApp()
- })
+import filterTests from "../support/filterTests"
-it("should rename an unpublished application", () => {
- const appRename = "Cypress Renamed"
- // Rename app, Search for app, Confirm name was changed
- cy.get(".home-logo").click()
- renameApp(appRename)
- cy.searchForApplication(appRename)
- cy.get(".appGrid").find(".wrapper").should("have.length", 1)
- cy.deleteApp(appRename)
-})
-
-xit("Should rename a published application", () => {
- // It is not possible to rename a published application
- const appRename = "Cypress Renamed"
- // Publish the app
- cy.get(".toprightnav")
- cy.get(".spectrum-Button").contains("Publish").click({force: true})
- cy.get(".spectrum-Dialog-grid")
- .within(() => {
- // Click publish again within the modal
- cy.get(".spectrum-Button").contains("Publish").click({force: true})
+filterTests(['all'], () => {
+ context("Rename an App", () => {
+ beforeEach(() => {
+ cy.login()
+ cy.createTestApp()
})
- // Rename app, Search for app, Confirm name was changed
- cy.get(".home-logo").click()
- renameApp(appRename, true)
- cy.searchForApplication(appRename)
- cy.get(".appGrid").find(".wrapper").should("have.length", 1)
-})
-it("Should try to rename an application to have no name", () => {
- cy.get(".home-logo").click()
- renameApp(" ", false, true)
- // Close modal and confirm name has not been changed
- cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
- cy.searchForApplication("Cypress Tests")
- cy.get(".appGrid").find(".wrapper").should("have.length", 1)
-})
-
-xit("Should create two applications with the same name", () => {
- // It is not possible to have applications with the same name
- const appName = "Cypress Tests"
- cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
- cy.wait(500)
- cy.get(".spectrum-Button").contains("Create app").click({force: true})
- cy.contains(/Start from scratch/).click()
- cy.get(".spectrum-Modal")
- .within(() => {
- cy.get("input").eq(0).type(appName)
- cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
- cy.get(".error").should("have.text", "Another app with the same name already exists")
+ it("should rename an unpublished application", () => {
+ const appName = "Cypress Tests"
+ const appRename = "Cypress Renamed"
+ // Rename app, Search for app, Confirm name was changed
+ cy.get(".home-logo").click()
+ renameApp(appName, appRename)
+ cy.reload()
+ cy.wait(1000)
+ cy.searchForApplication(appRename)
+ cy.get(".appTable").find(".title").should("have.length", 1)
+ // Set app name back to Cypress Tests
+ cy.reload()
+ cy.wait(1000)
+ renameApp(appRename, appName)
})
-})
-
-it("should validate application names", () => {
- // App name must be letters, numbers and spaces only
- // This test checks numbers and special characters specifically
- const numberName = 12345
- const specialCharName = "£$%^"
- cy.get(".home-logo").click()
- renameApp(numberName)
- cy.searchForApplication(numberName)
- cy.get(".appGrid").find(".wrapper").should("have.length", 1)
- renameApp(specialCharName)
- cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
-})
-
- const renameApp = (appName, published, noName) => {
- cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
- .its("body")
- .then(val => {
- if (val.length > 0) {
- cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
- // Check for when an app is published
- if (published == true){
- // Should not have Edit as option, will unpublish app
- cy.should("not.have.value", "Edit")
- cy.get(".spectrum-Menu").contains("Unpublish").click()
- cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
- cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
- }
- cy.contains("Edit").click()
- cy.get(".spectrum-Modal")
+
+ xit("Should rename a published application", () => {
+ // It is not possible to rename a published application
+ const appName = "Cypress Tests"
+ const appRename = "Cypress Renamed"
+ // Publish the app
+ cy.get(".toprightnav")
+ cy.get(".spectrum-Button").contains("Publish").click({force: true})
+ cy.get(".spectrum-Dialog-grid")
.within(() => {
- if (noName == true){
- cy.get("input").clear()
- cy.get(".spectrum-Dialog-grid").click()
- .contains("App name must be letters, numbers and spaces only")
- return cy
+ // Click publish again within the modal
+ cy.get(".spectrum-Button").contains("Publish").click({force: true})
+ })
+ // Rename app, Search for app, Confirm name was changed
+ cy.get(".home-logo").click()
+ renameApp(appName, appRename, true)
+ cy.searchForApplication(appRename)
+ cy.get(".appTable").find(".wrapper").should("have.length", 1)
+ })
+
+ it("Should try to rename an application to have no name", () => {
+ const appName = "Cypress Tests"
+ cy.get(".home-logo").click()
+ renameApp(appName, " ", false, true)
+ cy.wait(500)
+ // Close modal and confirm name has not been changed
+ cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
+ cy.reload()
+ cy.wait(1000)
+ cy.searchForApplication(appName)
+ cy.get(".appTable").find(".title").should("have.length", 1)
+
+ })
+
+ xit("Should create two applications with the same name", () => {
+ // It is not possible to have applications with the same name
+ const appName = "Cypress Tests"
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
+ cy.wait(500)
+ cy.get(".spectrum-Button").contains("Create app").click({force: true})
+ cy.contains(/Start from scratch/).click()
+ cy.get(".spectrum-Modal")
+ .within(() => {
+ cy.get("input").eq(0).type(appName)
+ cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
+ cy.get(".error").should("have.text", "Another app with the same name already exists")
+ })
+ })
+
+ it("should validate application names", () => {
+ // App name must be letters, numbers and spaces only
+ // This test checks numbers and special characters specifically
+ const appName = "Cypress Tests"
+ const numberName = 12345
+ const specialCharName = "£$%^"
+ cy.get(".home-logo").click()
+ renameApp(appName, numberName)
+ cy.reload()
+ cy.wait(1000)
+ cy.searchForApplication(numberName)
+ cy.get(".appTable").find(".title").should("have.length", 1)
+ cy.reload()
+ cy.wait(1000)
+ renameApp(numberName, specialCharName)
+ cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
+ // Set app name back to Cypress Tests
+ cy.reload()
+ cy.wait(1000)
+ renameApp(numberName, appName)
+ })
+
+ const renameApp = (originalName, changedName, published, noName) => {
+ cy.searchForApplication(originalName)
+ cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
+ .its("body")
+ .then(val => {
+ if (val.length > 0) {
+ cy.get(".appTable")
+ .within(() => {
+ cy.get(".spectrum-Icon").eq(1).click()
+ })
+ // Check for when an app is published
+ if (published == true){
+ // Should not have Edit as option, will unpublish app
+ cy.should("not.have.value", "Edit")
+ cy.get(".spectrum-Menu").contains("Unpublish").click()
+ cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
+ cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
}
- cy.get("input").clear()
- cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
- cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
- cy.wait(500)
- })
+ cy.contains("Edit").click()
+ cy.get(".spectrum-Modal")
+ .within(() => {
+ if (noName == true){
+ cy.get("input").clear()
+ cy.get(".spectrum-Dialog-grid").click()
+ .contains("App name must be letters, numbers and spaces only")
+ return cy
+ }
+ cy.get("input").clear()
+ cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
+ cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
+ cy.wait(500)
+ })
+ }
+ })
+
}
})
-}
})
diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js
new file mode 100644
index 0000000000..c64d19f230
--- /dev/null
+++ b/packages/builder/cypress/integration/revertApp.spec.js
@@ -0,0 +1,67 @@
+import filterTests from "../support/filterTests"
+
+filterTests(['smoke', 'all'], () => {
+ context("Revert apps", () => {
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
+
+ it("should try to revert an unpublished app", () => {
+ // Click revert icon
+ cy.get(".toprightnav").within(() => {
+ cy.get(".spectrum-Icon").eq(1).click()
+ })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ // Enter app name before revert
+ cy.get("input").type("Cypress Tests")
+ cy.intercept('**/revert').as('revertApp')
+ // Click Revert
+ cy.get(".spectrum-Button").contains("Revert").click({ force: true })
+ // Intercept Request after button click & apply assertions
+ cy.wait("@revertApp")
+ cy.get("@revertApp").its('response.body').should('have.property', 'message', "App has not yet been deployed")
+ cy.get("@revertApp").its('response.body').should('have.property', 'status', 400)
+ })
+ })
+
+ it("should revert a published app", () => {
+ // Add initial component - Paragraph
+ cy.addComponent("Elements", "Paragraph")
+ // Publish app
+ cy.get(".spectrum-Button").contains("Publish").click({ force: true })
+ cy.get(".spectrum-ButtonGroup").within(() => {
+ cy.get(".spectrum-Button").contains("Publish").click({ force: true })
+ })
+ // Add second component - Button
+ cy.addComponent("Elements", "Button")
+ // Click Revert
+ cy.get(".toprightnav").within(() => {
+ cy.get(".spectrum-Icon").eq(1).click()
+ })
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ // Click Revert
+ cy.get(".spectrum-Button").contains("Revert").click({ force: true })
+ cy.wait(1000)
+ })
+ // Confirm Paragraph component is still visible
+ cy.get(".root").contains("New Paragraph")
+ // Confirm Button component is not visible
+ cy.get(".root").should("not.have.text", "New Button")
+ cy.wait(500)
+ })
+
+ it("should enter incorrect app name when reverting", () => {
+ // Click Revert
+ cy.get(".toprightnav").within(() => {
+ cy.get(".spectrum-Icon").eq(1).click({ force: true })
+ })
+ // Enter incorrect app name
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get("input").type("Cypress Tests")
+ // Revert button within modal should be disabled
+ cy.get(".spectrum-Button").eq(1).should('be.disabled')
+ })
+ })
+ })
+})
diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js
index 7657303853..ca5a65c7f5 100644
--- a/packages/builder/cypress/setup.js
+++ b/packages/builder/cypress/setup.js
@@ -3,9 +3,6 @@ const path = require("path")
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
-// these run on ports we don't normally use so that they can run alongside the
-const fs = require("fs")
-
// normal development system
const WORKER_PORT = "10002"
const MAIN_PORT = cypressConfig.env.PORT
@@ -18,7 +15,7 @@ process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = 1
process.env.WORKER_URL = "http://localhost:10002/"
process.env.APPS_URL = `http://localhost:${MAIN_PORT}/`
-process.env.MINIO_URL = `http://localhost:${MAIN_PORT}/`
+process.env.MINIO_URL = `http://localhost:4004`
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"
process.env.COUCH_DB_USER = "budibase"
@@ -29,22 +26,20 @@ process.env.ALLOW_DEV_AUTOMATIONS = 1
// Stop info logs polluting test outputs
process.env.LOG_LEVEL = "error"
-async function run() {
+exports.run = (
+ serverLoc = "../../server/dist",
+ workerLoc = "../../worker/dist"
+) => {
// require("dotenv").config({ path: resolve(dir, ".env") })
- if (!fs.existsSync("../server/dist")) {
- console.error("Unable to run cypress, need to build server first")
- process.exit(-1)
- }
-
// don't make this a variable or top level require
// it will cause environment module to be loaded prematurely
- const server = require("../../server/dist/app")
+ require(serverLoc)
process.env.PORT = WORKER_PORT
- const worker = require("../../worker/src/index")
+ require(workerLoc)
// reload main port for rest of system
process.env.PORT = MAIN_PORT
- server.on("close", () => console.log("Server Closed"))
- worker.on("close", () => console.log("Worker Closed"))
}
-run()
+if (require.main === module) {
+ exports.run()
+}
diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js
index 1b1cc0c2b0..40fe6706c9 100644
--- a/packages/builder/cypress/support/commands.js
+++ b/packages/builder/cypress/support/commands.js
@@ -10,7 +10,7 @@ Cypress.on("uncaught:exception", () => {
})
Cypress.Commands.add("login", () => {
- cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(2000)
cy.url().then(url => {
if (url.includes("builder/admin")) {
@@ -33,29 +33,69 @@ Cypress.Commands.add("login", () => {
})
Cypress.Commands.add("createApp", name => {
- cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
- cy.contains(/Start from scratch/).dblclick()
+ cy.get(".spectrum-Button").contains("Create app").click({ force: true })
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
- cy.wait(7000)
+ cy.wait(5000)
})
+ cy.createTable("Cypress Tests", true)
})
-Cypress.Commands.add("deleteApp", appName => {
- cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
- cy.wait(1000)
- cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
+Cypress.Commands.add("deleteApp", name => {
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
+ cy.wait(2000)
+ cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
- cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
- cy.contains("Delete").click()
- cy.get(".spectrum-Modal").within(() => {
- cy.get("input").type(appName)
- cy.get(".spectrum-Button--warning").click()
+ cy.searchForApplication(name)
+ cy.get(".appTable").within(() => {
+ cy.get(".spectrum-Icon").eq(1).click()
})
+ cy.get(".spectrum-Menu").then($menu => {
+ if ($menu.text().includes("Unpublish")) {
+ cy.get(".spectrum-Menu").contains("Unpublish").click()
+ cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
+ } else {
+ cy.get(".spectrum-Menu").contains("Delete").click()
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get("input").type(name)
+ })
+ cy.get(".spectrum-Button--warning").click()
+ }
+ })
+ } else {
+ return
+ }
+ })
+})
+
+Cypress.Commands.add("deleteAllApps", () => {
+ cy.visit(`${Cypress.config().baseUrl}/builder`)
+ cy.wait(500)
+ cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
+ .its("body")
+ .then(val => {
+ for (let i = 0; i < val.length; i++) {
+ cy.get(".spectrum-Heading")
+ .eq(1)
+ .then(app => {
+ const name = app.text()
+ cy.get(".title")
+ .children()
+ .within(() => {
+ cy.get(".spectrum-Icon").eq(0).click()
+ })
+ cy.get(".spectrum-Menu").contains("Delete").click()
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get("input").type(name)
+ cy.get(".spectrum-Button--warning").click()
+ })
+ cy.reload()
+ })
}
})
})
@@ -64,6 +104,7 @@ Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests"
cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.")
+ cy.createScreen("home", "home")
})
Cypress.Commands.add("createTestTableWithData", () => {
@@ -72,10 +113,18 @@ Cypress.Commands.add("createTestTableWithData", () => {
cy.addColumn("dog", "age", "Number")
})
-Cypress.Commands.add("createTable", tableName => {
- cy.contains("Budibase DB").click()
- cy.contains("Create new table").click()
-
+Cypress.Commands.add("createTable", (tableName, initialTable) => {
+ if (!initialTable) {
+ cy.navigateToDataSection()
+ cy.get(".add-button").click()
+ }
+ cy.wait(7000)
+ cy.get(".spectrum-Modal")
+ .contains("Budibase DB")
+ .click({ force: true })
+ .then(() => {
+ cy.get(".spectrum-Button").contains("Continue").click({ force: true })
+ })
cy.get(".spectrum-Modal").within(() => {
cy.wait(1000)
cy.get("input").first().type(tableName).blur()
@@ -182,20 +231,49 @@ Cypress.Commands.add("navigateToFrontend", () => {
cy.wait(1000)
cy.contains("Design").click()
cy.get(".spectrum-Search").type("/")
- cy.get(".nav-item").contains("Home").click()
+ cy.get(".nav-item").contains("home").click()
+})
+
+Cypress.Commands.add("navigateToDataSection", () => {
+ // Clicks on the Data tab
+ cy.wait(500)
+ cy.contains("Data").click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {
+ cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => {
- cy.get(".item").first().click()
- cy.get(".spectrum-Button--cta").click()
+ cy.get(".item").contains("Blank").click()
+ cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
+ cy.wait(500)
})
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Form-itemField").eq(0).type(screenName)
+ cy.get(".spectrum-Form-itemField").eq(1).type(route)
+ cy.get(".spectrum-Button").contains("Continue").click({ force: true })
+ cy.wait(1000)
+ })
+})
+
+Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
+ // Screen name must already exist within data source
+ cy.contains("Design").click()
+ cy.get("[aria-label=AddCircle]").click()
+ for (let i = 0; i < screenNames.length; i++) {
+ cy.get(".item").contains(screenNames[i]).click()
+ }
+ cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
+ cy.wait(4000)
+})
+
+Cypress.Commands.add("addRow", values => {
+ cy.contains("Create row").click()
cy.get(".spectrum-Modal").within(() => {
- cy.get("input").first().clear().type(screenName)
- cy.get("input").eq(1).clear().type(route)
- cy.get(".spectrum-Button--cta").click()
- cy.wait(2000)
+ for (let i = 0; i < values.length; i++) {
+ cy.get("input").eq(i).type(values[i]).blur()
+ }
+ cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
})
@@ -233,7 +311,144 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
})
Cypress.Commands.add("searchForApplication", appName => {
- cy.get(".spectrum-Textfield").within(() => {
- cy.get("input").eq(0).type(appName)
+ cy.wait(1000)
+ // Searches for the app
+ cy.get(".filter").then(() => {
+ cy.get(".spectrum-Textfield").within(() => {
+ cy.get("input").eq(0).type(appName)
+ })
+ })
+ // Confirms app exists after search
+ cy.get(".appTable").contains(appName)
+})
+
+Cypress.Commands.add("selectExternalDatasource", datasourceName => {
+ // Navigates to Data Section
+ cy.navigateToDataSection()
+ // Open Data Source modal
+ cy.get(".nav").within(() => {
+ cy.get(".add-button").click()
+ })
+ // Clicks specified datasource & continue
+ cy.get(".item-list").contains(datasourceName).click()
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button").contains("Continue").click({ force: true })
})
})
+
+Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
+ // selectExternalDatasource should be called prior to this
+ // Adds the config for specified datasource & fetches tables
+ // Currently supports MySQL, PostgreSQL, Oracle
+ // Host IP Address
+ cy.wait(500)
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".form-row")
+ .eq(0)
+ .within(() => {
+ cy.get(".spectrum-Textfield").within(() => {
+ if (datasource == "Oracle") {
+ cy.get("input").clear().type(Cypress.env("oracle").HOST)
+ } else {
+ cy.get("input").clear().type(Cypress.env("HOST_IP"))
+ }
+ })
+ })
+ })
+ // Database Name
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ if (datasource == "MySQL") {
+ cy.get(".form-row")
+ .eq(4)
+ .within(() => {
+ cy.get("input").clear().type(Cypress.env("mysql").DATABASE)
+ })
+ } else {
+ cy.get(".form-row")
+ .eq(2)
+ .within(() => {
+ if (datasource == "PostgreSQL") {
+ cy.get("input").clear().type(Cypress.env("postgresql").DATABASE)
+ }
+ if (datasource == "Oracle") {
+ cy.get("input").clear().type(Cypress.env("oracle").DATABASE)
+ }
+ })
+ }
+ })
+ // User
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ if (datasource == "MySQL") {
+ cy.get(".form-row")
+ .eq(2)
+ .within(() => {
+ cy.get("input").clear().type(Cypress.env("mysql").USER)
+ })
+ } else {
+ cy.get(".form-row")
+ .eq(3)
+ .within(() => {
+ if (datasource == "PostgreSQL") {
+ cy.get("input").clear().type(Cypress.env("postgresql").USER)
+ }
+ if (datasource == "Oracle") {
+ cy.get("input").clear().type(Cypress.env("oracle").USER)
+ }
+ })
+ }
+ })
+ // Password
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ if (datasource == "MySQL") {
+ cy.get(".form-row")
+ .eq(3)
+ .within(() => {
+ cy.get("input").clear().type(Cypress.env("mysql").PASSWORD)
+ })
+ } else {
+ cy.get(".form-row")
+ .eq(4)
+ .within(() => {
+ if (datasource == "PostgreSQL") {
+ cy.get("input").clear().type(Cypress.env("postgresql").PASSWORD)
+ }
+ if (datasource == "Oracle") {
+ cy.get("input").clear().type(Cypress.env("oracle").PASSWORD)
+ }
+ })
+ }
+ })
+ // Click to fetch tables
+ if (skipFetch) {
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Skip table fetch")
+ .click({ force: true })
+ })
+ } else {
+ cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".spectrum-Button")
+ .contains("Save and fetch tables")
+ .click({ force: true })
+ cy.wait(1000)
+ })
+ }
+})
+
+Cypress.Commands.add("createRestQuery", (method, restUrl) => {
+ // addExternalDatasource should be called prior to this
+ // Configures REST datasource & sends query
+ cy.wait(1000)
+ cy.get(".spectrum-Button").contains("Add query").click({ force: true })
+ // Select Method & add Rest URL
+ cy.get(".spectrum-Picker-label").eq(1).click()
+ cy.get(".spectrum-Menu").contains(method).click()
+ cy.get("input").clear().type(restUrl)
+ // Send query
+ cy.get(".spectrum-Button").contains("Send").click({ force: true })
+ cy.wait(500)
+ cy.get(".spectrum-Button").contains("Save").click({ force: true })
+ cy.get(".hierarchy-items-container")
+ .should("contain", method)
+ .and("contain", restUrl)
+})
diff --git a/packages/builder/cypress/support/filterTests.js b/packages/builder/cypress/support/filterTests.js
new file mode 100644
index 0000000000..074fd05d33
--- /dev/null
+++ b/packages/builder/cypress/support/filterTests.js
@@ -0,0 +1,16 @@
+const filterTests = (testTags, runTest) => {
+ // testTags is an array of tags
+ // runTest is all tests
+ if (Cypress.env("TEST_TAGS")) {
+ const tags = Cypress.env("TEST_TAGS").split("/")
+ const found = testTags.some($testTags => tags.includes($testTags))
+
+ if (found) {
+ runTest()
+ }
+ } else {
+ runTest()
+ }
+}
+
+export default filterTests
diff --git a/packages/builder/cypress/support/queryLevelTransformerFunction.js b/packages/builder/cypress/support/queryLevelTransformerFunction.js
new file mode 100644
index 0000000000..7dc05018f8
--- /dev/null
+++ b/packages/builder/cypress/support/queryLevelTransformerFunction.js
@@ -0,0 +1,14 @@
+/* eslint-disable */
+const breweries = data
+const totals = {}
+
+for (let brewery of breweries)
+ {const state = brewery.state
+ if (totals[state] == null)
+ {totals[state] = 1
+ } else
+ {totals[state]++
+ }
+}
+const entries = Object.entries(totals)
+return entries.map(([state, count]) => ({ state, count }))
diff --git a/packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js b/packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
new file mode 100644
index 0000000000..fcf50b4412
--- /dev/null
+++ b/packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
@@ -0,0 +1,31 @@
+/* eslint-disable */
+const breweries = data
+const totals = {}
+for (let brewery of breweries)
+ {const state = brewery.state
+ if (totals[state] == null)
+ {totals[state] = 1
+ } else
+ {totals[state]++
+ }
+}
+const stateCodes =
+ {texas: "tx",
+ colorado: "co",
+ florida: "fl",
+ iwoa: "ia",
+ louisiana: "la",
+ california: "ca",
+ pennsylvania: "pa",
+ georgia: "ga",
+ "new hampshire": "nh",
+ virginia: "va",
+ michigan: "mi",
+ maryland: "md",
+ ohio: "oh",
+}
+const entries = Object.entries(totals)
+return entries.map(([state, count]) =>
+ {stateCodes[state.toLowerCase()]
+ return { state, count, flag: "http://flags.ox3.in/svg/us/${stateCode}.svg" }
+})
diff --git a/packages/builder/cypress/ts/setup.ts b/packages/builder/cypress/ts/setup.ts
new file mode 100644
index 0000000000..b6b12bf730
--- /dev/null
+++ b/packages/builder/cypress/ts/setup.ts
@@ -0,0 +1,4 @@
+// @ts-ignore
+import { run } from "../setup"
+
+run("../../server/src/index", "../../worker/src/index")
diff --git a/packages/builder/nuxt.config.js b/packages/builder/nuxt.config.js
new file mode 100644
index 0000000000..72798a820c
--- /dev/null
+++ b/packages/builder/nuxt.config.js
@@ -0,0 +1,4 @@
+export default {
+ ssr: false,
+ target: "static",
+}
diff --git a/packages/builder/package.json b/packages/builder/package.json
index e8ca02c430..3ca2a167b8 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,22 +1,21 @@
{
"name": "@budibase/builder",
- "version": "0.9.190-alpha.3",
+ "version": "1.0.76-alpha.3",
"license": "GPL-3.0",
"private": true,
"scripts": {
"build": "routify -b && vite build --emptyOutDir",
"start": "routify -c rollup",
- "test": "jest",
- "test:watch": "jest --watchAll",
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
- "cy:setup": "node ./cypress/setup.js",
- "cy:run": "cypress run",
+ "cy:setup": "ts-node ./cypress/ts/setup.ts",
+ "cy:setup:ci": "node ./cypress/setup.js",
+ "cy:run": "xvfb-run cypress run --headed --browser chrome",
"cy:open": "cypress open",
- "cy:run:ci": "cypress run --record --key f308590b-6070-41af-b970-794a3823d451",
+ "cy:run:ci": "cypress run --record",
"cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
- "cy:ci": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
+ "cy:ci": "start-server-and-test cy:setup:ci http://localhost:10001/builder cy:run",
"cy:debug": "start-server-and-test cy:setup http://localhost:10001/builder cy:open"
},
"jest": {
@@ -65,10 +64,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "^0.9.190-alpha.3",
- "@budibase/client": "^0.9.190-alpha.3",
- "@budibase/colorpicker": "1.1.2",
- "@budibase/string-templates": "^0.9.190-alpha.3",
+ "@budibase/bbui": "^1.0.76-alpha.3",
+ "@budibase/client": "^1.0.76-alpha.3",
+ "@budibase/frontend-core": "^1.0.76-alpha.3",
+ "@budibase/string-templates": "^1.0.76-alpha.3",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@@ -95,7 +94,7 @@
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.6.3",
- "cypress": "^5.1.0",
+ "cypress": "^9.3.1",
"cypress-terminal-report": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
@@ -106,6 +105,8 @@
"start-server-and-test": "^1.12.1",
"svelte": "^3.38.2",
"svelte-jester": "^1.3.2",
+ "ts-node": "^10.4.0",
+ "typescript": "^4.5.5",
"vite": "^2.1.5"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js
index d38b7bba4f..177d5320a5 100644
--- a/packages/builder/src/analytics/constants.js
+++ b/packages/builder/src/analytics/constants.js
@@ -9,6 +9,9 @@ export const Events = {
CREATED: "Datasource Created",
UPDATED: "Datasource Updated",
},
+ QUERIES: {
+ REST: "REST Queries Imported",
+ },
TABLE: {
CREATED: "Table Created",
},
diff --git a/packages/builder/src/analytics/index.js b/packages/builder/src/analytics/index.js
index e9edf38d74..3a4118347d 100644
--- a/packages/builder/src/analytics/index.js
+++ b/packages/builder/src/analytics/index.js
@@ -1,4 +1,4 @@
-import api from "builderStore/api"
+import { API } from "api"
import PosthogClient from "./PosthogClient"
import IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient"
@@ -17,13 +17,11 @@ class AnalyticsHub {
}
async activate() {
- const analyticsStatus = await api.get("/api/analytics")
- const json = await analyticsStatus.json()
-
- // Analytics disabled
- if (!json.enabled) return
-
- this.clients.forEach(client => client.init())
+ // Check analytics are enabled
+ const analyticsStatus = await API.getAnalyticsStatus()
+ if (analyticsStatus.enabled) {
+ this.clients.forEach(client => client.init())
+ }
}
identify(id, metadata) {
diff --git a/packages/builder/src/api.js b/packages/builder/src/api.js
new file mode 100644
index 0000000000..5604db5db8
--- /dev/null
+++ b/packages/builder/src/api.js
@@ -0,0 +1,48 @@
+import {
+ createAPIClient,
+ CookieUtils,
+ Constants,
+} from "@budibase/frontend-core"
+import { store } from "./builderStore"
+import { get } from "svelte/store"
+import { auth } from "./stores/portal"
+
+export const API = createAPIClient({
+ attachHeaders: headers => {
+ // Attach app ID header from store
+ headers["x-budibase-app-id"] = get(store).appId
+
+ // Add csrf token if authenticated
+ const user = get(auth).user
+ if (user?.csrfToken) {
+ headers["x-csrf-token"] = user.csrfToken
+ }
+ },
+
+ onError: error => {
+ const { url, message, status, method, handled } = error || {}
+
+ // Log all API errors to Sentry
+ // analytics.captureException(error)
+
+ // Log any errors that we haven't manually handled
+ if (!handled) {
+ console.error("Unhandled error from API client", error)
+ return
+ }
+
+ // Log all errors to console
+ console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`)
+
+ // Logout on 403's
+ if (status === 403) {
+ // Remove cookies
+ CookieUtils.removeCookie(Constants.Cookies.Auth)
+
+ // Reload after removing cookie, go to login
+ if (!url.includes("self") && !url.includes("login")) {
+ location.reload()
+ }
+ }
+ },
+})
diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js
deleted file mode 100644
index 4bcb9b74c6..0000000000
--- a/packages/builder/src/builderStore/api.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { store } from "./index"
-import { get as svelteGet } from "svelte/store"
-import { removeCookie, Cookies } from "./cookies"
-
-const apiCall =
- method =>
- async (url, body, headers = { "Content-Type": "application/json" }) => {
- headers["x-budibase-app-id"] = svelteGet(store).appId
- const json = headers["Content-Type"] === "application/json"
- const resp = await fetch(url, {
- method: method,
- body: json ? JSON.stringify(body) : body,
- headers,
- })
- if (resp.status === 403) {
- removeCookie(Cookies.Auth)
- // reload after removing cookie, go to login
- if (!url.includes("self") && !url.includes("login")) {
- location.reload()
- }
- }
- return resp
- }
-
-export const post = apiCall("POST")
-export const get = apiCall("GET")
-export const patch = apiCall("PATCH")
-export const del = apiCall("DELETE")
-export const put = apiCall("PUT")
-
-export default {
- post: apiCall("POST"),
- get: apiCall("GET"),
- patch: apiCall("PATCH"),
- delete: apiCall("DELETE"),
- put: apiCall("PUT"),
-}
diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/componentUtils.js
similarity index 80%
rename from packages/builder/src/builderStore/storeUtils.js
rename to packages/builder/src/builderStore/componentUtils.js
index e25949000f..04a87998fe 100644
--- a/packages/builder/src/builderStore/storeUtils.js
+++ b/packages/builder/src/builderStore/componentUtils.js
@@ -127,18 +127,37 @@ const searchComponentTree = (rootComponent, matchComponent) => {
}
/**
- * Searches a component's definition for a setting matching a certin predicate.
+ * Searches a component's definition for a setting matching a certain predicate.
+ * These settings are cached because they cannot change at run time.
*/
+let componentSettingCache = {}
export const getComponentSettings = componentType => {
- const def = store.actions.components.getDefinition(componentType)
- if (!def) {
+ if (!componentType) {
return []
}
- let settings = def.settings?.filter(setting => !setting.section) ?? []
- def.settings
- ?.filter(setting => setting.section)
- .forEach(section => {
- settings = settings.concat(section.settings || [])
- })
+
+ // Ensure whole component name is used
+ if (!componentType.startsWith("@budibase")) {
+ componentType = `@budibase/standard-components/${componentType}`
+ }
+
+ // Check if we have cached this type already
+ if (componentSettingCache[componentType]) {
+ return componentSettingCache[componentType]
+ }
+
+ // Otherwise get the settings and cache them
+ const def = store.actions.components.getDefinition(componentType)
+ let settings = []
+ if (def) {
+ settings = def.settings?.filter(setting => !setting.section) ?? []
+ def.settings
+ ?.filter(setting => setting.section)
+ .forEach(section => {
+ settings = settings.concat(section.settings || [])
+ })
+ }
+ componentSettingCache[componentType] = settings
+
return settings
}
diff --git a/packages/builder/src/builderStore/cookies.js b/packages/builder/src/builderStore/cookies.js
deleted file mode 100644
index a84f1a4f20..0000000000
--- a/packages/builder/src/builderStore/cookies.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export const Cookies = {
- Auth: "budibase:auth",
- CurrentApp: "budibase:currentapp",
-}
-
-export function getCookie(cookieName) {
- return document.cookie.split(";").some(cookie => {
- return cookie.trim().startsWith(`${cookieName}=`)
- })
-}
-
-export function removeCookie(cookieName) {
- if (getCookie(cookieName)) {
- document.cookie = `${cookieName}=; Max-Age=-99999999;`
- }
-}
diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index 9a41ad2afc..edb12c7e74 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -5,7 +5,7 @@ import {
findComponent,
findComponentPath,
getComponentSettings,
-} from "./storeUtils"
+} from "./componentUtils"
import { store } from "builderStore"
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
import {
@@ -15,6 +15,8 @@ import {
encodeJSBinding,
} from "@budibase/string-templates"
import { TableNames } from "../constants"
+import { JSONUtils } from "@budibase/frontend-core"
+import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@@ -61,7 +63,7 @@ export const getComponentBindableProperties = (asset, componentId) => {
/**
* Gets all data provider components above a component.
*/
-export const getDataProviderComponents = (asset, componentId) => {
+export const getContextProviderComponents = (asset, componentId, type) => {
if (!asset || !componentId) {
return []
}
@@ -74,7 +76,18 @@ export const getDataProviderComponents = (asset, componentId) => {
// Filter by only data provider components
return path.filter(component => {
const def = store.actions.components.getDefinition(component._component)
- return def?.context != null
+ if (!def?.context) {
+ return false
+ }
+
+ // If no type specified, return anything that exposes context
+ if (!type) {
+ return true
+ }
+
+ // Otherwise only match components with the specific context type
+ const contexts = Array.isArray(def.context) ? def.context : [def.context]
+ return contexts.find(context => context.type === type) != null
})
}
@@ -143,7 +156,7 @@ export const getDatasourceForProvider = (asset, component) => {
*/
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
- const dataProviders = getDataProviderComponents(asset, componentId)
+ const dataProviders = getContextProviderComponents(asset, componentId)
// Generate bindings for all matching components
return getProviderContextBindings(asset, dataProviders)
@@ -175,6 +188,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
}
let schema
+ let table
let readablePrefix
let runtimeSuffix = context.suffix
@@ -198,7 +212,16 @@ const getProviderContextBindings = (asset, dataProviders) => {
}
const info = getSchemaForDatasource(asset, datasource)
schema = info.schema
- readablePrefix = info.table?.name
+ table = info.table
+
+ // For JSON arrays, use the array name as the readable prefix.
+ // Otherwise use the table name
+ if (datasource.type === "jsonarray") {
+ const split = datasource.label.split(".")
+ readablePrefix = split[split.length - 1]
+ } else {
+ readablePrefix = info.table?.name
+ }
}
if (!schema) {
return
@@ -218,7 +241,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
const fieldSchema = schema[key]
// Make safe runtime binding
- const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
+ const safeKey = key.split(".").map(makePropSafe).join(".")
+ const runtimeBinding = `${safeComponentId}.${safeKey}`
// Optionally use a prefix with readable bindings
let readableBinding = component._instanceName
@@ -236,6 +260,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
// datasource options, based on bindable properties
fieldSchema,
providerId,
+ // Table ID is used by JSON fields to know what table the field is in
+ tableId: table?._id,
})
})
})
@@ -249,10 +275,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
*/
const getUserBindings = () => {
let bindings = []
- const { schema } = getSchemaForDatasource(null, {
- type: "table",
- tableId: TableNames.USERS,
- })
+ const { schema } = getSchemaForTable(TableNames.USERS)
const keys = Object.keys(schema).sort()
const safeUser = makePropSafe("user")
keys.forEach(key => {
@@ -329,23 +352,87 @@ const getUrlBindings = asset => {
}
/**
- * Gets a schema for a datasource object.
+ * Gets all bindable properties exposed in a button actions flow up until
+ * the specified action ID.
*/
-export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
+export const getButtonContextBindings = (actions, actionId) => {
+ // Get the steps leading up to this value
+ const index = actions?.findIndex(action => action.id === actionId)
+ if (index == null || index === -1) {
+ return []
+ }
+ const prevActions = actions.slice(0, index)
+
+ // Generate bindings for any steps which provide context
+ let bindings = []
+ prevActions.forEach((action, idx) => {
+ const def = ActionDefinitions.actions.find(
+ x => x.name === action["##eventHandlerType"]
+ )
+ if (def.context) {
+ def.context.forEach(contextValue => {
+ bindings.push({
+ readableBinding: `Action ${idx + 1}.${contextValue.label}`,
+ runtimeBinding: `actions.${idx}.${contextValue.value}`,
+ })
+ })
+ }
+ })
+ return bindings
+}
+
+/**
+ * Gets the schema for a certain table ID.
+ * The options which can be passed in are:
+ * formSchema: whether the schema is for a form
+ * searchableSchema: whether to generate a searchable schema, which may have
+ * fewer fields than a readable schema
+ * @param tableId the table ID to get the schema for
+ * @param options options for generating the schema
+ * @return {{schema: Object, table: Object}}
+ */
+export const getSchemaForTable = (tableId, options) => {
+ return getSchemaForDatasource(null, { type: "table", tableId }, options)
+}
+
+/**
+ * Gets a schema for a datasource object.
+ * The options which can be passed in are:
+ * formSchema: whether the schema is for a form
+ * searchableSchema: whether to generate a searchable schema, which may have
+ * fewer fields than a readable schema
+ * @param asset the current root client app asset (layout or screen). This is
+ * optional and only needed for "provider" datasource types.
+ * @param datasource the datasource definition
+ * @param options options for generating the schema
+ * @return {{schema: Object, table: Object}}
+ */
+export const getSchemaForDatasource = (asset, datasource, options) => {
+ options = options || {}
let schema, table
if (datasource) {
const { type } = datasource
+ const tables = get(tablesStore).list
- // Determine the source table from the datasource type
+ // Determine the entity which backs this datasource.
+ // "provider" datasources are those targeting another data provider
if (type === "provider") {
const component = findComponent(asset.props, datasource.providerId)
const source = getDatasourceForProvider(asset, component)
- return getSchemaForDatasource(asset, source, isForm)
- } else if (type === "query") {
+ return getSchemaForDatasource(asset, source, options)
+ }
+
+ // "query" datasources are those targeting non-plus datasources or
+ // custom queries
+ else if (type === "query") {
const queries = get(queriesStores).list
table = queries.find(query => query._id === datasource._id)
- } else if (type === "field") {
+ }
+
+ // "field" datasources are array-like fields of rows, such as attachments
+ // or multi-select fields
+ else if (type === "field") {
table = { name: datasource.fieldName }
const { fieldType } = datasource
if (fieldType === "attachment") {
@@ -364,16 +451,34 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
},
}
}
- } else {
- const tables = get(tablesStore).list
+ }
+
+ // "jsonarray" datasources are arrays inside JSON fields
+ else if (type === "jsonarray") {
+ table = tables.find(table => table._id === datasource.tableId)
+ let tableSchema = table?.schema
+ schema = JSONUtils.getJSONArrayDatasourceSchema(tableSchema, datasource)
+ }
+
+ // Otherwise we assume we're targeting an internal table or a plus
+ // datasource, and we can treat it as a table with a schema
+ else {
table = tables.find(table => table._id === datasource.tableId)
}
- // Determine the schema from the table if not already determined
+ // Determine the schema from the backing entity if not already determined
if (table && !schema) {
if (type === "view") {
+ // For views, the schema is pulled from the `views` property of the
+ // table
schema = cloneDeep(table.views?.[datasource.name]?.schema)
- } else if (type === "query" && isForm) {
+ } else if (
+ type === "query" &&
+ (options.formSchema || options.searchableSchema)
+ ) {
+ // For queries, if we are generating a schema for a form or a searchable
+ // schema then we want to use the query parameters rather than the
+ // query schema
schema = {}
const params = table.parameters || []
params.forEach(param => {
@@ -382,14 +487,62 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
}
})
} else {
+ // Otherwise we just want the schema of the table
schema = cloneDeep(table.schema)
}
}
- // Add _id and _rev fields for certain types
- if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
- schema["_id"] = { type: "string" }
- schema["_rev"] = { type: "string" }
+ // Check for any JSON fields so we can add any top level properties
+ if (schema) {
+ let jsonAdditions = {}
+ Object.keys(schema).forEach(fieldKey => {
+ const fieldSchema = schema[fieldKey]
+ if (fieldSchema?.type === "json") {
+ const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
+ fieldSchema,
+ {
+ squashObjects: true,
+ }
+ )
+ Object.keys(jsonSchema).forEach(jsonKey => {
+ jsonAdditions[`${fieldKey}.${jsonKey}`] = {
+ type: jsonSchema[jsonKey].type,
+ nestedJSON: true,
+ }
+ })
+ }
+ })
+ schema = { ...schema, ...jsonAdditions }
+ }
+
+ // Determine if we should add ID and rev to the schema
+ const isInternal = table && !table.sql
+ const isTable = ["table", "link"].includes(datasource.type)
+
+ // ID is part of the readable schema for all tables
+ // Rev is part of the readable schema for internal tables only
+ let addId = isTable
+ let addRev = isTable && isInternal
+
+ // Don't add ID or rev for form schemas
+ if (options.formSchema) {
+ addId = false
+ addRev = false
+ }
+
+ // ID is only searchable for internal tables
+ else if (options.searchableSchema) {
+ addId = isTable && isInternal
+ }
+
+ // Add schema properties if required
+ if (schema) {
+ if (addId) {
+ schema["_id"] = { type: "string" }
+ }
+ if (addRev) {
+ schema["_rev"] = { type: "string" }
+ }
}
// Ensure there are "name" properties for all fields and that field schema
@@ -439,15 +592,58 @@ const buildFormSchema = component => {
return schema
}
+/**
+ * Returns an array of the keys of any state variables which are set anywhere
+ * in the app.
+ */
+export const getAllStateVariables = () => {
+ // Get all component containing assets
+ let allAssets = []
+ allAssets = allAssets.concat(get(store).layouts || [])
+ allAssets = allAssets.concat(get(store).screens || [])
+
+ // Find all button action settings in all components
+ let eventSettings = []
+ allAssets.forEach(asset => {
+ findAllMatchingComponents(asset.props, component => {
+ const settings = getComponentSettings(component._component)
+ settings
+ .filter(setting => setting.type === "event")
+ .forEach(setting => {
+ eventSettings.push(component[setting.key])
+ })
+ })
+ })
+
+ // Extract all state keys from any "update state" actions in each setting
+ let bindingSet = new Set()
+ eventSettings.forEach(setting => {
+ if (!Array.isArray(setting)) {
+ return
+ }
+ setting.forEach(action => {
+ if (
+ action["##eventHandlerType"] === "Update State" &&
+ action.parameters?.type === "set" &&
+ action.parameters?.key &&
+ action.parameters?.value
+ ) {
+ bindingSet.add(action.parameters.key)
+ }
+ })
+ })
+ return Array.from(bindingSet)
+}
+
/**
* Recurses the input object to remove any instances of bindings.
*/
-export function removeBindings(obj) {
+export const removeBindings = (obj, replacement = "Invalid binding") => {
for (let [key, value] of Object.entries(obj)) {
if (value && typeof value === "object") {
- obj[key] = removeBindings(value)
+ obj[key] = removeBindings(value, replacement)
} else if (typeof value === "string") {
- obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding")
+ obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement)
}
}
return obj
@@ -457,8 +653,8 @@ export function removeBindings(obj) {
* When converting from readable to runtime it can sometimes add too many square brackets,
* this makes sure that doesn't happen.
*/
-function shouldReplaceBinding(currentValue, from, convertTo) {
- if (!currentValue?.includes(from)) {
+const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
+ if (!currentValue?.includes(convertFrom)) {
return false
}
if (convertTo === "readableBinding") {
@@ -467,7 +663,7 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
// this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "")
- const fromNoSpaces = from.replace(/\s+/g, "")
+ const fromNoSpaces = convertFrom.replace(/\s+/g, "")
const invalids = [
`[${fromNoSpaces}]`,
`"${fromNoSpaces}"`,
@@ -476,14 +672,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
return !invalids.find(invalid => noSpaces?.includes(invalid))
}
-function replaceBetween(string, start, end, replacement) {
+/**
+ * Utility function which replaces a string between given indices.
+ */
+const replaceBetween = (string, start, end, replacement) => {
return string.substring(0, start) + replacement + string.substring(end)
}
/**
- * utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
+ * Utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/
-function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
+const bindingReplacement = (
+ bindableProperties,
+ textWithBindings,
+ convertTo
+) => {
// Decide from base64 if using JS
const isJS = isJSBinding(textWithBindings)
if (isJS) {
@@ -548,14 +751,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
* Extracts a component ID from a handlebars expression setting of
* {{ literal [componentId] }}
*/
-function extractLiteralHandlebarsID(value) {
+const extractLiteralHandlebarsID = value => {
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
}
/**
* Converts a readable data binding into a runtime data binding
*/
-export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
+export const readableToRuntimeBinding = (
+ bindableProperties,
+ textWithBindings
+) => {
return bindingReplacement(
bindableProperties,
textWithBindings,
@@ -566,56 +772,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
/**
* Converts a runtime data binding into a readable data binding
*/
-export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
+export const runtimeToReadableBinding = (
+ bindableProperties,
+ textWithBindings
+) => {
return bindingReplacement(
bindableProperties,
textWithBindings,
"readableBinding"
)
}
-
-/**
- * Returns an array of the keys of any state variables which are set anywhere
- * in the app.
- */
-export const getAllStateVariables = () => {
- let allComponents = []
-
- // Find all onClick settings in all layouts
- get(store).layouts.forEach(layout => {
- const components = findAllMatchingComponents(
- layout.props,
- c => c.onClick != null
- )
- allComponents = allComponents.concat(components || [])
- })
-
- // Find all onClick settings in all screens
- get(store).screens.forEach(screen => {
- const components = findAllMatchingComponents(
- screen.props,
- c => c.onClick != null
- )
- allComponents = allComponents.concat(components || [])
- })
-
- // Add state bindings for all state actions
- let bindingSet = new Set()
- allComponents.forEach(component => {
- if (!Array.isArray(component.onClick)) {
- return
- }
- component.onClick.forEach(action => {
- if (
- action["##eventHandlerType"] === "Update State" &&
- action.parameters?.type === "set" &&
- action.parameters?.key &&
- action.parameters?.value
- ) {
- bindingSet.add(action.parameters.key)
- }
- })
- })
-
- return Array.from(bindingSet)
-}
diff --git a/packages/builder/src/builderStore/datasource.js b/packages/builder/src/builderStore/datasource.js
new file mode 100644
index 0000000000..cfdeeac23e
--- /dev/null
+++ b/packages/builder/src/builderStore/datasource.js
@@ -0,0 +1,44 @@
+import { datasources, tables } from "../stores/backend"
+import { IntegrationNames } from "../constants/backend"
+import analytics, { Events } from "../analytics"
+import { get } from "svelte/store"
+import cloneDeep from "lodash/cloneDeepWith"
+
+function prepareData(config) {
+ let datasource = {}
+ let existingTypeCount = get(datasources).list.filter(
+ ds => ds.source === config.type
+ ).length
+
+ let baseName = IntegrationNames[config.type]
+ let name =
+ existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
+
+ datasource.type = "datasource"
+ datasource.source = config.type
+ datasource.config = config.config
+ datasource.name = name
+ datasource.plus = config.plus
+
+ return datasource
+}
+
+export async function saveDatasource(config, skipFetch = false) {
+ const datasource = prepareData(config)
+ // Create datasource
+ const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
+
+ // update the tables incase data source plus
+ await tables.fetch()
+ await datasources.select(resp._id)
+ analytics.captureEvent(Events.DATASOURCE.CREATED, {
+ name: resp.name,
+ source: resp.source,
+ })
+ return resp
+}
+
+export async function createRestDatasource(integration) {
+ const config = cloneDeep(integration)
+ return saveDatasource(config)
+}
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index f32dedd47e..5181e756c6 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -1,15 +1,13 @@
import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
-import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
-import { findComponent } from "./storeUtils"
+import { findComponent } from "./componentUtils"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
-export const hostingStore = getHostingStore()
export const currentAsset = derived(store, $store => {
const type = $store.currentFrontEndType
diff --git a/packages/builder/src/builderStore/loadComponentLibraries.js b/packages/builder/src/builderStore/loadComponentLibraries.js
deleted file mode 100644
index 8bdfcf7538..0000000000
--- a/packages/builder/src/builderStore/loadComponentLibraries.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { get } from "builderStore/api"
-
-/**
- * Fetches the definitions for component library components. This includes
- * their props and other metadata from components.json.
- * @param {string} appId - ID of the currently running app
- */
-export const fetchComponentLibDefinitions = async appId => {
- const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
- try {
- const libDefinitionResponse = await get(LIB_DEFINITION_URL)
- return await libDefinitionResponse.json()
- } catch (err) {
- console.error(`Error fetching component definitions for ${appId}`, err)
- }
-}
diff --git a/packages/builder/src/builderStore/schemaGenerator.js b/packages/builder/src/builderStore/schemaGenerator.js
new file mode 100644
index 0000000000..33115fc997
--- /dev/null
+++ b/packages/builder/src/builderStore/schemaGenerator.js
@@ -0,0 +1,56 @@
+import { FIELDS } from "constants/backend"
+
+function baseConversion(type) {
+ if (type === "string") {
+ return {
+ type: FIELDS.STRING.type,
+ }
+ } else if (type === "boolean") {
+ return {
+ type: FIELDS.BOOLEAN.type,
+ }
+ } else if (type === "number") {
+ return {
+ type: FIELDS.NUMBER.type,
+ }
+ }
+}
+
+function recurse(schemaLevel = {}, objectLevel) {
+ if (!objectLevel) {
+ return null
+ }
+ const baseType = typeof objectLevel
+ if (baseType !== "object") {
+ return baseConversion(baseType)
+ }
+ for (let [key, value] of Object.entries(objectLevel)) {
+ const type = typeof value
+ // check array first, since arrays are objects
+ if (Array.isArray(value)) {
+ const schema = recurse(schemaLevel[key], value[0])
+ if (schema) {
+ schemaLevel[key] = {
+ type: FIELDS.ARRAY.type,
+ schema,
+ }
+ }
+ } else if (type === "object") {
+ const schema = recurse(schemaLevel[key], objectLevel[key])
+ if (schema) {
+ schemaLevel[key] = schema
+ }
+ } else {
+ schemaLevel[key] = baseConversion(type)
+ }
+ }
+ if (!schemaLevel.type) {
+ return { type: FIELDS.JSON.type, schema: schemaLevel }
+ } else {
+ return schemaLevel
+ }
+}
+
+export function generate(object) {
+ return recurse({}, object).schema
+}
diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js
index 7bd0ccca22..b901a71cb1 100644
--- a/packages/builder/src/builderStore/store/automation/index.js
+++ b/packages/builder/src/builderStore/store/automation/index.js
@@ -1,26 +1,40 @@
import { writable } from "svelte/store"
-import api from "../../api"
+import { API } from "api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics"
+const initialAutomationState = {
+ automations: [],
+ blockDefinitions: {
+ TRIGGER: [],
+ ACTION: [],
+ },
+ selectedAutomation: null,
+}
+
+export const getAutomationStore = () => {
+ const store = writable(initialAutomationState)
+ store.actions = automationActions(store)
+ return store
+}
+
const automationActions = store => ({
fetch: async () => {
const responses = await Promise.all([
- api.get(`/api/automations`),
- api.get(`/api/automations/definitions/list`),
+ API.getAutomations(),
+ API.getAutomationDefinitions(),
])
- const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
let selected = state.selectedAutomation?.automation
- state.automations = jsonResponses[0]
+ state.automations = responses[0]
state.blockDefinitions = {
- TRIGGER: jsonResponses[1].trigger,
- ACTION: jsonResponses[1].action,
+ TRIGGER: responses[1].trigger,
+ ACTION: responses[1].action,
}
- // if previously selected find the new obj and select it
+ // If previously selected find the new obj and select it
if (selected) {
- selected = jsonResponses[0].filter(
+ selected = responses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
@@ -36,40 +50,36 @@ const automationActions = store => ({
steps: [],
},
}
- const CREATE_AUTOMATION_URL = `/api/automations`
- const response = await api.post(CREATE_AUTOMATION_URL, automation)
- const json = await response.json()
+ const response = await API.createAutomation(automation)
store.update(state => {
- state.automations = [...state.automations, json.automation]
- store.actions.select(json.automation)
+ state.automations = [...state.automations, response.automation]
+ store.actions.select(response.automation)
return state
})
},
save: async automation => {
- const UPDATE_AUTOMATION_URL = `/api/automations`
- const response = await api.put(UPDATE_AUTOMATION_URL, automation)
- const json = await response.json()
+ const response = await API.updateAutomation(automation)
store.update(state => {
- const newAutomation = json.automation
+ const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
if (existingIdx !== -1) {
- state.automations.splice(existingIdx, 1, newAutomation)
+ state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations]
- store.actions.select(newAutomation)
+ store.actions.select(updatedAutomation)
return state
}
})
},
delete: async automation => {
- const { _id, _rev } = automation
- const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
- await api.delete(DELETE_AUTOMATION_URL)
-
+ await API.deleteAutomation({
+ automationId: automation?._id,
+ automationRev: automation?._rev,
+ })
store.update(state => {
const existingIdx = state.automations.findIndex(
- existing => existing._id === _id
+ existing => existing._id === automation?._id
)
state.automations.splice(existingIdx, 1)
state.automations = [...state.automations]
@@ -78,16 +88,17 @@ const automationActions = store => ({
return state
})
},
- trigger: async automation => {
- const { _id } = automation
- return await api.post(`/api/automations/${_id}/trigger`)
- },
test: async (automation, testData) => {
- const { _id } = automation
- const response = await api.post(`/api/automations/${_id}/test`, testData)
- const json = await response.json()
store.update(state => {
- state.selectedAutomation.testResults = json
+ state.selectedAutomation.testResults = null
+ return state
+ })
+ const result = await API.testAutomation({
+ automationId: automation?._id,
+ testData,
+ })
+ store.update(state => {
+ state.selectedAutomation.testResults = result
return state
})
},
@@ -143,17 +154,3 @@ const automationActions = store => ({
})
},
})
-
-export const getAutomationStore = () => {
- const INITIAL_AUTOMATION_STATE = {
- automations: [],
- blockDefinitions: {
- TRIGGER: [],
- ACTION: [],
- },
- selectedAutomation: null,
- }
- const store = writable(INITIAL_AUTOMATION_STATE)
- store.actions = automationActions(store)
- return store
-}
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index 1f1fb035a4..9ce66db3c0 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
allScreens,
- hostingStore,
currentAsset,
mainLayout,
selectedComponent,
@@ -15,8 +14,7 @@ import {
database,
tables,
} from "stores/backend"
-import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
-import api from "../api"
+import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
@@ -26,8 +24,8 @@ import {
findAllMatchingComponents,
findComponent,
getComponentSettings,
-} from "../storeUtils"
-import { uuid } from "../uuid"
+} from "../componentUtils"
+import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
@@ -66,31 +64,31 @@ export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = {
+ reset: () => {
+ store.set({ ...INITIAL_FRONTEND_STATE })
+ },
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
- const components = await fetchComponentLibDefinitions(application.appId)
- // make sure app isn't locked
- if (
- components &&
- components.status === 400 &&
- components.message?.includes("lock")
- ) {
- throw { ok: false, reason: "locked" }
- }
+
+ // Fetch component definitions.
+ // Allow errors to propagate.
+ let components = await API.fetchComponentLibDefinitions(application.appId)
+
+ // Reset store state
store.update(state => ({
...state,
libraries: application.componentLibraries,
components,
clientFeatures: {
- ...state.clientFeatures,
+ ...INITIAL_FRONTEND_STATE.clientFeatures,
...components.features,
},
name: application.name,
description: application.description,
appId: application.appId,
url: application.url,
- layouts,
- screens,
+ layouts: layouts || [],
+ screens: screens || [],
theme: application.theme || "spectrum--light",
customTheme: application.customTheme,
hasAppPackage: true,
@@ -100,54 +98,45 @@ export const getFrontendStore = () => {
version: application.version,
revertableVersion: application.revertableVersion,
}))
- await hostingStore.actions.fetch()
// Initialise backend stores
- const [_integrations] = await Promise.all([
- api.get("/api/integrations").then(r => r.json()),
- ])
- datasources.init()
- integrations.set(_integrations)
- queries.init()
database.set(application.instance)
- tables.init()
+ await datasources.init()
+ await integrations.init()
+ await queries.init()
+ await tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
- const response = await api.put(`/api/applications/${appId}`, { theme })
- if (response.status === 200) {
- store.update(state => {
- state.theme = theme
- return state
- })
- } else {
- throw new Error("Error updating theme")
- }
+ await API.saveAppMetadata({
+ appId,
+ metadata: { theme },
+ })
+ store.update(state => {
+ state.theme = theme
+ return state
+ })
},
},
customTheme: {
save: async customTheme => {
const appId = get(store).appId
- const response = await api.put(`/api/applications/${appId}`, {
- customTheme,
+ await API.saveAppMetadata({
+ appId,
+ metadata: { customTheme },
+ })
+ store.update(state => {
+ state.customTheme = customTheme
+ return state
})
- if (response.status === 200) {
- store.update(state => {
- state.customTheme = customTheme
- return state
- })
- } else {
- throw new Error("Error updating theme")
- }
},
},
routing: {
fetch: async () => {
- const response = await api.get("/api/routing")
- const json = await response.json()
+ const response = await API.fetchAppRoutes()
store.update(state => {
- state.routes = json.routes
+ state.routes = response.routes
return state
})
},
@@ -171,82 +160,76 @@ export const getFrontendStore = () => {
return state
})
},
- create: async screen => {
- screen = await store.actions.screens.save(screen)
- store.update(state => {
- state.selectedScreenId = screen._id
- state.selectedComponentId = screen.props._id
- state.currentFrontEndType = FrontendTypes.SCREEN
- selectedAccessRole.set(screen.routing.roleId)
- return state
- })
- return screen
- },
save: async screen => {
const creatingNewScreen = screen._id === undefined
- const response = await api.post(`/api/screens`, screen)
- if (response.status !== 200) {
- return
- }
- screen = await response.json()
- await store.actions.routing.fetch()
-
+ const savedScreen = await API.saveScreen(screen)
store.update(state => {
- const foundScreen = state.screens.findIndex(
- el => el._id === screen._id
- )
- if (foundScreen !== -1) {
- state.screens.splice(foundScreen, 1)
+ const idx = state.screens.findIndex(x => x._id === savedScreen._id)
+ if (idx !== -1) {
+ state.screens.splice(idx, 1, savedScreen)
+ } else {
+ state.screens.push(savedScreen)
}
- state.screens.push(screen)
return state
})
- if (creatingNewScreen) {
- store.actions.screens.select(screen._id)
- }
+ // Refresh routes
+ await store.actions.routing.fetch()
- return screen
+ // Select the new screen if creating a new one
+ if (creatingNewScreen) {
+ store.actions.screens.select(savedScreen._id)
+ }
+ return savedScreen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
- const screenDeletePromises = []
+ // Build array of promises to speed up bulk deletions
+ const promises = []
+ screensToDelete.forEach(screen => {
+ // Delete the screen
+ promises.push(
+ API.deleteScreen({
+ screenId: screen._id,
+ screenRev: screen._rev,
+ })
+ )
+ // Remove links to this screen
+ promises.push(
+ store.actions.components.links.delete(
+ screen.routing.route,
+ screen.props._instanceName
+ )
+ )
+ })
+
+ await Promise.all(promises)
+ const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
- for (let screenToDelete of screensToDelete) {
- state.screens = state.screens.filter(
- screen => screen._id !== screenToDelete._id
- )
- screenDeletePromises.push(
- api.delete(
- `/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
- )
- )
- if (screenToDelete._id === state.selectedScreenId) {
- state.selectedScreenId = null
- }
- //remove the link for this screen
- screenDeletePromises.push(
- store.actions.components.links.delete(
- screenToDelete.routing.route,
- screenToDelete.props._instanceName
- )
- )
+ // Remove deleted screens from state
+ state.screens = state.screens.filter(screen => {
+ return !deletedIds.includes(screen._id)
+ })
+ // Deselect the current screen if it was deleted
+ if (deletedIds.includes(state.selectedScreenId)) {
+ state.selectedScreenId = null
}
return state
})
- await Promise.all(screenDeletePromises)
+
+ // Refresh routes
+ await store.actions.routing.fetch()
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
-
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
- await store.actions.screens.save(selectedAsset)
+ return await store.actions.screens.save(selectedAsset)
} else {
- await store.actions.layouts.save(selectedAsset)
+ return await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
@@ -270,25 +253,13 @@ export const getFrontendStore = () => {
})
},
save: async layout => {
- const layoutToSave = cloneDeep(layout)
- const creatingNewLayout = layoutToSave._id === undefined
- const response = await api.post(`/api/layouts`, layoutToSave)
- const savedLayout = await response.json()
-
- // Abort if saving failed
- if (response.status !== 200) {
- return
- }
-
+ const creatingNewLayout = layout._id === undefined
+ const savedLayout = await API.saveLayout(layout)
store.update(state => {
- const layoutIdx = state.layouts.findIndex(
- stateLayout => stateLayout._id === savedLayout._id
- )
- if (layoutIdx >= 0) {
- // update existing layout
- state.layouts.splice(layoutIdx, 1, savedLayout)
+ const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
+ if (idx !== -1) {
+ state.layouts.splice(idx, 1, savedLayout)
} else {
- // save new layout
state.layouts.push(savedLayout)
}
return state
@@ -298,7 +269,6 @@ export const getFrontendStore = () => {
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
-
return savedLayout
},
find: layoutId => {
@@ -308,33 +278,32 @@ export const getFrontendStore = () => {
const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId)
},
- delete: async layoutToDelete => {
- const response = await api.delete(
- `/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
- )
- if (response.status !== 200) {
- const json = await response.json()
- throw new Error(json.message)
+ delete: async layout => {
+ if (!layout?._id) {
+ return
}
+ await API.deleteLayout({
+ layoutId: layout._id,
+ layoutRev: layout._rev,
+ })
store.update(state => {
- state.layouts = state.layouts.filter(
- layout => layout._id !== layoutToDelete._id
- )
- if (layoutToDelete._id === state.selectedLayoutId) {
+ // Select main layout if we deleted the selected layout
+ if (layout._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
+ state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state
})
},
},
components: {
select: component => {
- if (!component) {
+ const asset = get(currentAsset)
+ if (!asset || !component) {
return
}
// If this is the root component, select the asset instead
- const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
if (parent == null) {
const state = get(store)
@@ -397,7 +366,7 @@ export const getFrontendStore = () => {
}
return {
- _id: uuid(),
+ _id: Helpers.uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
@@ -414,16 +383,12 @@ export const getFrontendStore = () => {
componentName,
presetProps
)
- if (!componentInstance) {
+ if (!componentInstance || !asset) {
return
}
// Find parent node to attach this component to
let parentComponent
-
- if (!asset) {
- return
- }
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
@@ -524,7 +489,7 @@ export const getFrontendStore = () => {
}
}
},
- paste: async (targetComponent, mode) => {
+ paste: async (targetComponent, mode, preserveBindings = false) => {
let promises = []
store.update(state => {
// Stop if we have nothing to paste
@@ -536,8 +501,8 @@ export const getFrontendStore = () => {
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
- if (!cut) {
- state.componentToPaste = removeBindings(state.componentToPaste)
+ if (!cut && !preserveBindings) {
+ state.componentToPaste = removeBindings(state.componentToPaste, "")
}
// Clone the component to paste
@@ -551,7 +516,7 @@ export const getFrontendStore = () => {
if (!component) {
return
}
- component._id = uuid()
+ component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
@@ -605,11 +570,6 @@ export const getFrontendStore = () => {
selected._styles.custom = style
await store.actions.preview.saveSelected()
},
- resetStyles: async () => {
- const selected = get(selectedComponent)
- selected._styles = { normal: {}, hover: {}, active: {} }
- await store.actions.preview.saveSelected()
- },
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
@@ -664,7 +624,7 @@ export const getFrontendStore = () => {
newLink = cloneDeep(nav._children[0])
// Set our new props
- newLink._id = uuid()
+ newLink._id = Helpers.uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
diff --git a/packages/builder/src/builderStore/store/hosting.js b/packages/builder/src/builderStore/store/hosting.js
deleted file mode 100644
index fb174c2663..0000000000
--- a/packages/builder/src/builderStore/store/hosting.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { writable } from "svelte/store"
-import api, { get } from "../api"
-
-const INITIAL_HOSTING_UI_STATE = {
- appUrl: "",
- deployedApps: {},
- deployedAppNames: [],
- deployedAppUrls: [],
-}
-
-export const getHostingStore = () => {
- const store = writable({ ...INITIAL_HOSTING_UI_STATE })
- store.actions = {
- fetch: async () => {
- const response = await api.get("/api/hosting/urls")
- const urls = await response.json()
- store.update(state => {
- state.appUrl = urls.app
- return state
- })
- },
- fetchDeployedApps: async () => {
- let deployments = await (await get("/api/hosting/apps")).json()
- store.update(state => {
- state.deployedApps = deployments
- state.deployedAppNames = Object.values(deployments).map(app => app.name)
- state.deployedAppUrls = Object.values(deployments).map(app => app.url)
- return state
- })
- return deployments
- },
- }
- return store
-}
diff --git a/packages/builder/src/builderStore/store/notifications.js b/packages/builder/src/builderStore/store/notifications.js
deleted file mode 100644
index 85e708e92a..0000000000
--- a/packages/builder/src/builderStore/store/notifications.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { writable } from "svelte/store"
-import { generate } from "shortid"
-
-export const notificationStore = writable({
- notifications: [],
-})
-
-export function send(message, type = "default") {
- notificationStore.update(state => {
- state.notifications = [
- ...state.notifications,
- { id: generate(), type, message },
- ]
- return state
- })
-}
-
-export const notifier = {
- danger: msg => send(msg, "danger"),
- warning: msg => send(msg, "warning"),
- info: msg => send(msg, "info"),
- success: msg => send(msg, "success"),
-}
diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js
index 182736a1d5..93aa925aa6 100644
--- a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js
+++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js
@@ -1,4 +1,4 @@
-import { uuid } from "builderStore/uuid"
+import { Helpers } from "@budibase/bbui"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
@@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false)
this._children = []
this._json = {
- _id: uuid(),
+ _id: Helpers.uuid(),
_component: name,
_styles: {
normal: {},
diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js
index 04ff1f4ba2..272f627163 100644
--- a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js
+++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js
@@ -1,5 +1,5 @@
import { BaseStructure } from "./BaseStructure"
-import { uuid } from "builderStore/uuid"
+import { Helpers } from "@budibase/bbui"
export class Screen extends BaseStructure {
constructor() {
@@ -7,7 +7,7 @@ export class Screen extends BaseStructure {
this._json = {
layoutId: "layout_private_master",
props: {
- _id: uuid(),
+ _id: Helpers.uuid(),
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js
index 5b3bc041ff..e748161529 100644
--- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js
+++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js
@@ -137,16 +137,19 @@ const fieldTypeToComponentMap = {
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
+ json: "jsonfield",
}
export function makeDatasourceFormComponents(datasource) {
- const { schema } = getSchemaForDatasource(null, datasource, true)
+ const { schema } = getSchemaForDatasource(null, datasource, {
+ formSchema: true,
+ })
let components = []
let fields = Object.keys(schema || {})
fields.forEach(field => {
const fieldSchema = schema[field]
// skip autocolumns
- if (fieldSchema.autocolumn) {
+ if (fieldSchema.autocolumn || fieldSchema.nestedJSON) {
return
}
const fieldType =
@@ -168,6 +171,11 @@ export function makeDatasourceFormComponents(datasource) {
optionsSource: "schema",
})
}
+ if (fieldType === "longform") {
+ component.customProps({
+ format: "auto",
+ })
+ }
if (fieldType === "array") {
component.customProps({
placeholder: "Choose an option",
diff --git a/packages/builder/src/builderStore/store/theme.js b/packages/builder/src/builderStore/store/theme.js
index fd6b05df59..d4d7460ed2 100644
--- a/packages/builder/src/builderStore/store/theme.js
+++ b/packages/builder/src/builderStore/store/theme.js
@@ -1,4 +1,4 @@
-import { localStorageStore } from "./localStorage"
+import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
@@ -6,7 +6,7 @@ export const getThemeStore = () => {
theme: "darkest",
options: ["lightest", "light", "dark", "darkest"],
}
- const store = localStorageStore("bb-theme", initialValue)
+ const store = createLocalStorageStore("bb-theme", initialValue)
// Update theme class when store changes
store.subscribe(state => {
diff --git a/packages/builder/src/builderStore/uuid.js b/packages/builder/src/builderStore/uuid.js
deleted file mode 100644
index 149da83c68..0000000000
--- a/packages/builder/src/builderStore/uuid.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export function uuid() {
- // always want to make this start with a letter, as this makes it
- // easier to use with template string bindings in the client
- return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
- const r = (Math.random() * 16) | 0,
- v = c == "x" ? r : (r & 0x3) | 0x8
- return v.toString(16)
- })
-}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
index 5ae031e033..4e1e5e1103 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
@@ -6,6 +6,7 @@
Body,
Icon,
Tooltip,
+ notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { admin } from "stores/portal"
@@ -47,15 +48,19 @@
}
async function addBlockToAutomation() {
- const newBlock = $automationStore.selectedAutomation.constructBlock(
- "ACTION",
- actionVal.stepId,
- actionVal
- )
- automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
- await automationStore.actions.save(
- $automationStore.selectedAutomation?.automation
- )
+ try {
+ const newBlock = $automationStore.selectedAutomation.constructBlock(
+ "ACTION",
+ actionVal.stepId,
+ actionVal
+ )
+ automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
+ await automationStore.actions.save(
+ $automationStore.selectedAutomation?.automation
+ )
+ } catch (error) {
+ notifications.error("Error saving automation")
+ }
}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
index 2d6881d652..777fcd710a 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
@@ -30,26 +30,13 @@
}
async function deleteAutomation() {
- await automationStore.actions.delete(
- $automationStore.selectedAutomation?.automation
- )
- notifications.success("Automation deleted.")
- }
-
- async function testAutomation() {
- const result = await automationStore.actions.trigger(
- $automationStore.selectedAutomation.automation
- )
- if (result.status === 200) {
- notifications.success(
- `Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
- )
- } else {
- notifications.error(
- `Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
+ try {
+ await automationStore.actions.delete(
+ $automationStore.selectedAutomation?.automation
)
+ } catch (error) {
+ notifications.error("Error deleting automation")
}
- return result
}
@@ -85,7 +72,7 @@
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
>
-
+