Browse Source

Merge branch 'develop' of github.com:Budibase/budibase into feature/budibase-api

pull/4732/head
mike12345567 4 years ago
parent
commit
f2c2c903e5
  1. 4
      .eslintignore
  2. 6
      .github/workflows/budibase_ci.yml
  3. 1
      .gitignore
  4. 2
      .prettierignore
  5. 2
      lerna.json
  6. 2
      packages/backend-core/package.json
  7. 2
      packages/backend-core/src/migrations/index.js
  8. 3
      packages/bbui/package.json
  9. 1
      packages/bbui/src/Button/Button.svelte
  10. 5
      packages/bbui/src/ColorPicker/ColorPicker.svelte
  11. 21
      packages/bbui/src/Drawer/Drawer.svelte
  12. 15
      packages/bbui/src/Table/AttachmentRenderer.svelte
  13. 4
      packages/bbui/src/Table/BoldRenderer.svelte
  14. 32
      packages/bbui/src/Table/CellRenderer.svelte
  15. 9
      packages/bbui/src/Table/CodeRenderer.svelte
  16. 4
      packages/bbui/src/Table/DateTimeRenderer.svelte
  17. 8
      packages/bbui/src/Table/InternalRenderer.svelte
  18. 3
      packages/bbui/src/Table/StringRenderer.svelte
  19. 586
      packages/bbui/src/Table/Table.svelte
  20. 935
      packages/bbui/yarn.lock
  21. 7
      packages/builder/cypress.json
  22. 24
      packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
  23. 62
      packages/builder/cypress/integration/addRadioButtons.spec.js
  24. 51
      packages/builder/cypress/integration/autoScreensUI.spec.js
  25. 43
      packages/builder/cypress/integration/changeAppIconAndColour.spec.js
  26. 18
      packages/builder/cypress/integration/createApp.spec.js
  27. 109
      packages/builder/cypress/integration/createAutomation.spec.js
  28. 100
      packages/builder/cypress/integration/createBinding.spec.js
  29. 151
      packages/builder/cypress/integration/createComponents.spec.js
  30. 34
      packages/builder/cypress/integration/createScreen.js
  31. 164
      packages/builder/cypress/integration/createTable.spec.js
  32. 10
      packages/builder/cypress/integration/createUser.spec.js
  33. 180
      packages/builder/cypress/integration/createUserAndRoles.spec.js
  34. 272
      packages/builder/cypress/integration/createView.spec.js
  35. 161
      packages/builder/cypress/integration/customThemingProperties.spec.js
  36. 43
      packages/builder/cypress/integration/datasources/datasourceWizard.spec.js
  37. 222
      packages/builder/cypress/integration/datasources/mySql.spec.js
  38. 230
      packages/builder/cypress/integration/datasources/oracle.spec.js
  39. 285
      packages/builder/cypress/integration/datasources/postgreSql.spec.js
  40. 43
      packages/builder/cypress/integration/datasources/rest.spec.js
  41. 116
      packages/builder/cypress/integration/queryLevelTransformers.spec.js
  42. 216
      packages/builder/cypress/integration/renameAnApplication.spec.js
  43. 67
      packages/builder/cypress/integration/revertApp.spec.js
  44. 275
      packages/builder/cypress/support/commands.js
  45. 16
      packages/builder/cypress/support/filterTests.js
  46. 14
      packages/builder/cypress/support/queryLevelTransformerFunction.js
  47. 31
      packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
  48. 4
      packages/builder/nuxt.config.js
  49. 14
      packages/builder/package.json
  50. 2
      packages/builder/src/components/backend/DataTable/DataTable.svelte
  51. 2
      packages/builder/src/components/backend/DataTable/Table.svelte
  52. 3
      packages/builder/src/components/design/NavigationPanel/ScreenWizard.svelte
  53. 66
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellDrawer.svelte
  54. 34
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellEditor.svelte
  55. 57
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnDrawer.svelte
  56. 29
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnEditor.svelte
  57. 2
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
  58. 1417
      packages/builder/yarn.lock
  59. 2
      packages/cli/package.json
  60. 13
      packages/client/manifest.json
  61. 21
      packages/client/package.json
  62. 2
      packages/client/src/components/app/blocks/TableBlock.svelte
  63. 13
      packages/client/src/components/app/table/Table.svelte
  64. 1413
      packages/client/yarn.lock
  65. 4
      packages/frontend-core/package.json
  66. 8
      packages/server/package.json
  67. 5
      packages/server/src/automations/steps/filter.js
  68. 2
      packages/string-templates/package.json
  69. 9
      packages/string-templates/src/index.js
  70. 4
      packages/string-templates/test/basic.spec.js
  71. 6
      packages/worker/package.json
  72. 2
      packages/worker/src/api/controllers/system/environment.js

4
.eslintignore

@ -4,4 +4,6 @@ dist
packages/server/builder packages/server/builder
packages/server/coverage packages/server/coverage
packages/server/client packages/server/client
packages/builder/.routify packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js

6
.github/workflows/budibase_ci.yml

@ -43,4 +43,8 @@ jobs:
verbose: true verbose: true
# TODO: parallelise this # TODO: parallelise this
- run: yarn test:e2e:ci - name: Cypress run
uses: cypress-io/github-action@v2
with:
install: false
command: yarn test:e2e:ci

1
.gitignore

@ -97,3 +97,4 @@ hosting/proxy/.generated-nginx.prod.conf
bin/ bin/
hosting/.generated* hosting/.generated*
packages/builder/cypress.env.json

2
.prettierignore

@ -6,3 +6,5 @@ packages/server/builder
packages/server/coverage packages/server/coverage
packages/server/client packages/server/client
packages/builder/.routify packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js

2
lerna.json

@ -1,5 +1,5 @@
{ {
"version": "1.0.73", "version": "1.0.76-alpha.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

2
packages/backend-core/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

2
packages/backend-core/src/migrations/index.js

@ -36,7 +36,7 @@ const runMigration = async (CouchDB, migration, options = {}) => {
if (migrationType === exports.MIGRATION_TYPES.GLOBAL) { if (migrationType === exports.MIGRATION_TYPES.GLOBAL) {
dbNames = [getGlobalDBName()] dbNames = [getGlobalDBName()]
} else if (migrationType === exports.MIGRATION_TYPES.APP) { } else if (migrationType === exports.MIGRATION_TYPES.APP) {
const apps = await getAllApps(CouchDB, migration.opts) const apps = await getAllApps(migration.opts)
dbNames = apps.map(app => app.appId) dbNames = apps.map(app => app.appId)
} else { } else {
throw new Error( throw new Error(

3
packages/bbui/package.json

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,6 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.72-alpha.0",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

1
packages/bbui/src/Button/Button.svelte

@ -29,6 +29,7 @@
{disabled} {disabled}
on:click|preventDefault on:click|preventDefault
on:mouseover={() => (showTooltip = true)} on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)} on:mouseleave={() => (showTooltip = false)}
> >
{#if icon} {#if icon}

5
packages/bbui/src/ColorPicker/ColorPicker.svelte

@ -10,6 +10,7 @@
export let value export let value
export let size = "M" export let size = "M"
export let spectrumTheme export let spectrumTheme
export let alignRight = false
let open = false let open = false
@ -133,6 +134,7 @@
use:clickOutside={() => (open = false)} use:clickOutside={() => (open = false)}
transition:fly={{ y: -20, duration: 200 }} transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:spectrum-Popover--align-right={alignRight}
> >
{#each categories as category} {#each categories as category}
<div class="category"> <div class="category">
@ -250,6 +252,9 @@
align-items: stretch; align-items: stretch;
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.spectrum-Popover--align-right {
right: 0;
}
.colors { .colors {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;

21
packages/bbui/src/Drawer/Drawer.svelte

@ -1,5 +1,4 @@
<script> <script>
import { slide } from "svelte/transition"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
@ -7,7 +6,9 @@
export let title export let title
export let fillWidth export let fillWidth
let visible = false let visible = false
export function show() { export function show() {
if (visible) { if (visible) {
return return
@ -21,11 +22,27 @@
} }
visible = false visible = false
} }
const easeInOutQuad = x => {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
}
// Use a custom svelte transition here because the built-in slide
// transition has a horrible overshoot
const slide = () => {
return {
duration: 360,
css: t => {
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
return `transform: translateY(${translation}%);`
},
}
}
</script> </script>
{#if visible} {#if visible}
<Portal> <Portal>
<section class:fillWidth class="drawer" transition:slide> <section class:fillWidth class="drawer" transition:slide|local>
<header> <header>
<div class="text"> <div class="text">
<Heading size="XS">{title}</Heading> <Heading size="XS">{title}</Heading>

15
packages/bbui/src/Table/AttachmentRenderer.svelte

@ -17,14 +17,16 @@
{#each attachments as attachment} {#each attachments as attachment}
{#if isImage(attachment.extension)} {#if isImage(attachment.extension)}
<Link quiet target="_blank" href={attachment.url}> <Link quiet target="_blank" href={attachment.url}>
<img src={attachment.url} alt={attachment.extension} /> <div class="center">
<img src={attachment.url} alt={attachment.extension} />
</div>
</Link> </Link>
{:else} {:else}
<Tooltip text={attachment.name} direction="right"> <Tooltip text={attachment.name} direction="right">
<div class="file"> <div class="file">
<Link quiet target="_blank" href={attachment.url} <Link quiet target="_blank" href={attachment.url}>
>{attachment.extension}</Link {attachment.extension}
> </Link>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
@ -38,12 +40,15 @@
height: 32px; height: 32px;
max-width: 64px; max-width: 64px;
} }
.center,
.file { .file {
height: 32px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
}
.file {
height: 32px;
padding: 0 8px; padding: 0 8px;
color: var(--spectrum-global-color-gray-800); color: var(--spectrum-global-color-gray-800);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);

4
packages/bbui/src/Table/BoldRenderer.svelte

@ -7,5 +7,9 @@
<style> <style>
.bold { .bold {
font-weight: bold; font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: var(--max-cell-width);
} }
</style> </style>

32
packages/bbui/src/Table/CellRenderer.svelte

@ -6,6 +6,7 @@
import AttachmentRenderer from "./AttachmentRenderer.svelte" import AttachmentRenderer from "./AttachmentRenderer.svelte"
import ArrayRenderer from "./ArrayRenderer.svelte" import ArrayRenderer from "./ArrayRenderer.svelte"
import InternalRenderer from "./InternalRenderer.svelte" import InternalRenderer from "./InternalRenderer.svelte"
import { processStringSync } from "@budibase/string-templates"
export let row export let row
export let schema export let schema
@ -28,10 +29,33 @@
$: type = schema?.type ?? "string" $: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: 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 })
}
</script> </script>
{#if renderer && (customRenderer || (value != null && value !== ""))} {#if renderer && (customRenderer || (cellValue != null && cellValue !== ""))}
<svelte:component this={renderer} {row} {schema} {value} on:clickrelationship> <div style="--max-cell-width: {schema.width ? 'none' : '200px'};">
<slot /> <svelte:component
</svelte:component> this={renderer}
{row}
{schema}
value={cellValue}
on:clickrelationship
>
<slot />
</svelte:component>
</div>
{/if} {/if}
<style>
div {
display: contents;
}
</style>

9
packages/bbui/src/Table/CodeRenderer.svelte

@ -3,3 +3,12 @@
</script> </script>
<code>{value}</code> <code>{value}</code>
<style>
code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: var(--max-cell-width);
}
</style>

4
packages/bbui/src/Table/DateTimeRenderer.svelte

@ -17,6 +17,8 @@
<style> <style>
div { div {
width: 200px; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
</style> </style>

8
packages/bbui/src/Table/InternalRenderer.svelte

@ -43,11 +43,3 @@
<div on:click|stopPropagation={onClick}> <div on:click|stopPropagation={onClick}>
<Icon size="S" name="Copy" /> <Icon size="S" name="Copy" />
</div> </div>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
width: 150px;
}
</style>

3
packages/bbui/src/Table/StringRenderer.svelte

@ -8,6 +8,7 @@
div { div {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 150px; white-space: nowrap;
max-width: var(--max-cell-width);
} }
</style> </style>

586
packages/bbui/src/Table/Table.svelte

@ -4,6 +4,7 @@
import CellRenderer from "./CellRenderer.svelte" import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep, deepGet } from "../helpers" import { cloneDeep, deepGet } from "../helpers"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
/** /**
* The expected schema is our normal couch schemas for our tables. * 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 * sortable: Set to false to disable sorting data by a certain column
* editable: Set to false to disable editing a certain column if the * editable: Set to false to disable editing a certain column if the
* allowEditColumns prop is true * 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 data = []
export let schema = {} export let schema = {}
@ -28,13 +34,14 @@
export let editColumnTitle = "Edit" export let editColumnTitle = "Edit"
export let customRenderers = [] export let customRenderers = []
export let disableSorting = false export let disableSorting = false
export let autoSortColumns = true
export let compact = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
// Config // Config
const rowHeight = 55
const headerHeight = 36 const headerHeight = 36
const rowPreload = 5 $: rowHeight = compact ? 46 : 55
// Sorting state // Sorting state
let sortColumn let sortColumn
@ -45,32 +52,20 @@
let loaded = false let loaded = false
$: schema = fixSchema(schema) $: schema = fixSchema(schema)
$: if (!loading) loaded = true $: if (!loading) loaded = true
$: rows = data ?? [] $: fields = getFields(schema, showAutoColumns, autoSortColumns)
$: visibleRowCount = getVisibleRowCount(loaded, height, rows.length, rowCount) $: rows = fields?.length ? data || [] : []
$: contentStyle = getContentStyle(visibleRowCount, rowCount) $: visibleRowCount = getVisibleRowCount(
loaded,
height,
rows.length,
rowCount,
rowHeight
)
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
$: sortedRows = sortRows(rows, sortColumn, sortOrder) $: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: fields = getFields(schema, showAutoColumns) $: gridStyle = getGridStyle(fields, schema, showEditColumn)
$: showEditColumn = allowEditRows || allowSelectRows $: showEditColumn = allowEditRows || allowSelectRows
$: cellStyles = computeCellStyles(schema)
// Scrolling state
let timeout
let nextScrollTop = 0
let scrollTop = 0
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
$: lastVisibleRow = calculateLastVisibleRow(
firstVisibleRow,
visibleRowCount,
rows.length
)
// Reset state when data changes
$: rows.length, reset()
const reset = () => {
nextScrollTop = 0
scrollTop = 0
clearTimeout(timeout)
timeout = null
}
const fixSchema = schema => { const fixSchema = schema => {
let fixedSchema = {} let fixedSchema = {}
@ -90,7 +85,7 @@
return fixedSchema return fixedSchema
} }
const getVisibleRowCount = (loaded, height, allRows, rowCount) => { const getVisibleRowCount = (loaded, height, allRows, rowCount, rowHeight) => {
if (!loaded) { if (!loaded) {
return rowCount || 0 return rowCount || 0
} }
@ -100,11 +95,28 @@
return Math.min(allRows, Math.ceil(height / rowHeight)) return Math.min(allRows, Math.ceil(height / rowHeight))
} }
const getContentStyle = (visibleRows, rowCount) => { const getContentStyle = (visibleRows, rowCount, rowHeight) => {
if (!rowCount || !visibleRows) { if (!rowCount || !visibleRows) {
return "" 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) => { const sortRows = (rows, sortColumn, sortOrder) => {
@ -143,14 +155,14 @@
return name || "" return name || ""
} }
const getFields = (schema, showAutoColumns) => { const getFields = (schema, showAutoColumns, autoSortColumns) => {
let columns = [] let columns = []
let autoColumns = [] let autoColumns = []
Object.entries(schema || {}).forEach(([field, fieldSchema]) => { Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
if (!field || !fieldSchema) { if (!field || !fieldSchema) {
return return
} }
if (!fieldSchema?.autocolumn) { if (!autoSortColumns || !fieldSchema?.autocolumn) {
columns.push(fieldSchema) columns.push(fieldSchema)
} else if (showAutoColumns) { } else if (showAutoColumns) {
autoColumns.push(fieldSchema) autoColumns.push(fieldSchema)
@ -171,28 +183,6 @@
.map(column => column.name) .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) => { const editColumn = (e, field) => {
e.stopPropagation() e.stopPropagation()
dispatch("editcolumn", field) dispatch("editcolumn", field)
@ -213,170 +203,233 @@
selectedRows = [...selectedRows, row] 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
}
</script> </script>
<div class="wrapper" bind:offsetHeight={height}> <div
class="wrapper"
class:wrapper--quiet={quiet}
class:wrapper--compact={compact}
bind:offsetHeight={height}
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
>
{#if !loaded} {#if !loaded}
<div class="loading" style={contentStyle} /> <div class="loading" style={contentStyle}>
<ProgressCircle />
</div>
{:else} {:else}
<div <div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
on:scroll={onScroll} {#if fields.length}
class:quiet <div class="spectrum-Table-head">
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`} {#if showEditColumn}
class="container" <div
> class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
<div style={contentStyle}> >
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}> {editColumnTitle || ""}
{#if fields.length} </div>
<thead class="spectrum-Table-head">
<tr>
{#if showEditColumn}
<th class="spectrum-Table-headCell">
<div class="spectrum-Table-headCell-content">
{editColumnTitle || ""}
</div>
</th>
{/if}
{#each fields as field}
<th
class="spectrum-Table-headCell"
class:is-sortable={schema[field].sortable !== false}
class:is-sorted-desc={sortColumn === field &&
sortOrder === "Descending"}
class:is-sorted-asc={sortColumn === field &&
sortOrder === "Ascending"}
on:click={() => sortBy(schema[field])}
>
<div class="spectrum-Table-headCell-content">
<div class="title">{getDisplayName(schema[field])}</div>
{#if schema[field]?.autocolumn}
<svg
class="spectrum-Icon spectrum-Table-autoIcon"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-MagicWand" />
</svg>
{/if}
{#if sortColumn === field}
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Arrow100" />
</svg>
{/if}
{#if allowEditColumns && schema[field]?.editable !== false}
<svg
class="spectrum-Icon spectrum-Table-editIcon"
focusable="false"
on:click={e => editColumn(e, field)}
>
<use xlink:href="#spectrum-icon-18-Edit" />
</svg>
{/if}
</div>
</th>
{/each}
</tr>
</thead>
{/if} {/if}
<tbody class="spectrum-Table-body"> {#each fields as field}
{#if sortedRows?.length && fields.length} <div
{#each sortedRows as row, idx} class="spectrum-Table-headCell"
<tr class:spectrum-Table-headCell--alignCenter={schema[field]
on:click={() => dispatch("click", row)} .align === "Center"}
on:click={() => toggleSelectRow(row)} class:spectrum-Table-headCell--alignRight={schema[field].align ===
class="spectrum-Table-row" "Right"}
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow} class:is-sortable={schema[field].sortable !== false}
class:is-sorted-desc={sortColumn === field &&
sortOrder === "Descending"}
class:is-sorted-asc={sortColumn === field &&
sortOrder === "Ascending"}
on:click={() => sortBy(schema[field])}
>
<div class="title">{getDisplayName(schema[field])}</div>
{#if schema[field]?.autocolumn}
<svg
class="spectrum-Icon spectrum-Table-autoIcon"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-MagicWand" />
</svg>
{/if}
{#if sortColumn === field}
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
focusable="false"
aria-hidden="true"
> >
{#if idx >= firstVisibleRow && idx <= lastVisibleRow} <use xlink:href="#spectrum-css-icon-Arrow100" />
{#if showEditColumn} </svg>
<td {/if}
class="spectrum-Table-cell spectrum-Table-cell--divider" {#if allowEditColumns && schema[field]?.editable !== false}
> <svg
<div class="spectrum-Table-cell-content"> class="spectrum-Icon spectrum-Table-editIcon"
<SelectEditRenderer focusable="false"
data={row} on:click={e => editColumn(e, field)}
selected={selectedRows.includes(row)} >
onToggleSelection={() => toggleSelectRow(row)} <use xlink:href="#spectrum-icon-18-Edit" />
onEdit={e => editRow(e, row)} </svg>
{allowSelectRows} {/if}
{allowEditRows} </div>
/> {/each}
</div> </div>
</td> {/if}
{/if} {#if sortedRows?.length}
{#each fields as field} {#each sortedRows as row, idx}
<td <div
class="spectrum-Table-cell" class="spectrum-Table-row"
class:spectrum-Table-cell--divider={!!schema[field] on:click={() => dispatch("click", row)}
.divider} on:click={() => toggleSelectRow(row)}
> >
<div class="spectrum-Table-cell-content"> {#if showEditColumn}
<CellRenderer <div
{customRenderers} class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
{row} >
schema={schema[field]} <SelectEditRenderer
value={deepGet(row, field)} data={row}
on:clickrelationship selected={selectedRows.includes(row)}
> onToggleSelection={() => toggleSelectRow(row)}
<slot /> onEdit={e => editRow(e, row)}
</CellRenderer> {allowSelectRows}
</div> {allowEditRows}
</td> />
{/each} </div>
{/if}
</tr>
{/each}
{:else}
<tr class="placeholder-row">
{#if showEditColumn}
<td class="placeholder-offset" />
{/if}
{#each fields as field}
<td />
{/each}
<div class="placeholder" class:has-fields={fields.length > 0}>
<div class="placeholder-content">
<svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
</tr>
{/if} {/if}
</tbody> {#each fields as field}
</table> <div
</div> class="spectrum-Table-cell"
class:spectrum-Table-cell--divider={!!schema[field].divider}
style={cellStyles[field]}
>
<CellRenderer
{customRenderers}
{row}
schema={schema[field]}
value={deepGet(row, field)}
on:clickrelationship
>
<slot />
</CellRenderer>
</div>
{/each}
</div>
{/each}
{:else}
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
<div class="placeholder-content">
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>
/* Wrapper */
.wrapper { .wrapper {
background-color: var(--spectrum-alias-background-color-secondary);
overflow: hidden;
position: relative; position: relative;
z-index: 0; z-index: 0;
--table-bg: var(--spectrum-global-color-gray-50);
--table-border: 1px solid var(--spectrum-alias-border-color-mid);
--cell-padding: var(--spectrum-global-dimension-size-250);
} }
.wrapper--quiet {
.container { --table-bg: var(--spectrum-alias-background-color-transparent);
height: 100%;
position: relative;
overflow: auto;
} }
.container.quiet { .wrapper--compact {
border: none; --cell-padding: var(--spectrum-global-dimension-size-150);
} }
table {
/* Loading */
.loading {
display: grid;
place-items: center;
min-height: 100px;
}
/* Table */
.spectrum-Table {
width: 100%; width: 100%;
border-radius: 0;
display: grid;
overflow: auto;
} }
/* Header */
.spectrum-Table-head {
display: contents;
}
.spectrum-Table-head > :first-child {
border-left: 1px solid transparent;
padding-left: var(--cell-padding);
}
.spectrum-Table-head > :last-child {
border-right: 1px solid transparent;
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell {
height: var(--header-height);
position: sticky;
top: 0;
text-overflow: ellipsis;
white-space: nowrap;
background-color: var(--spectrum-alias-background-color-secondary);
z-index: 2;
border-bottom: var(--table-border);
padding: 0 calc(var(--cell-padding) / 1.33);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
user-select: none;
}
.spectrum-Table-headCell--alignCenter {
justify-content: center;
}
.spectrum-Table-headCell--alignRight {
justify-content: flex-end;
}
.spectrum-Table-headCell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
padding-left: var(--cell-padding);
}
.spectrum-Table-headCell--edit {
position: sticky;
left: 0;
z-index: 3;
}
.spectrum-Table-headCell .title {
overflow: hidden;
text-overflow: ellipsis;
}
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
opacity: 1;
transition: opacity 0.2s ease;
}
.spectrum-Table-headCell .spectrum-Icon { .spectrum-Table-headCell .spectrum-Icon {
pointer-events: all; pointer-events: all;
margin-left: var( margin-left: var(
@ -392,63 +445,93 @@
.spectrum-Table-editIcon { .spectrum-Table-editIcon {
opacity: 0; opacity: 0;
} }
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
opacity: 1;
transition: opacity 0.2s ease;
}
th { /* Table rows */
vertical-align: middle; .spectrum-Table-row {
height: var(--header-height); display: contents;
position: sticky; }
top: 0; .spectrum-Table-row:hover .spectrum-Table-cell {
z-index: 2; /*background-color: var(--hover-bg) !important;*/
background-color: var(--spectrum-alias-background-color-secondary); }
border-bottom: 1px solid .spectrum-Table-row:hover .spectrum-Table-cell:after {
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)); background-color: var(--spectrum-alias-highlight-hover);
}
.wrapper--quiet .spectrum-Table-row {
border-left: none;
border-right: none;
}
.spectrum-Table-row > :first-child {
border-left: var(--table-border);
padding-left: var(--cell-padding);
}
.spectrum-Table-row > :last-child {
border-right: var(--table-border);
padding-right: var(--cell-padding);
} }
.spectrum-Table-headCell-content {
/* Table cells */
.spectrum-Table-cell {
flex: 1 1 auto;
padding: 0 calc(var(--cell-padding) / 1.33);
border-top: none;
border-bottom: none;
border-radius: 0;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
height: var(--row-height);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
user-select: none; gap: 4px;
} border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
.spectrum-Table-headCell-content .title { background-color: var(--table-bg);
overflow: hidden; z-index: 1;
text-overflow: ellipsis;
} }
.spectrum-Table-cell--divider {
.placeholder-row { padding-right: var(--cell-padding);
position: relative;
height: 150px;
} }
.placeholder-row td { .spectrum-Table-cell--divider + .spectrum-Table-cell {
border-top: none !important; padding-left: var(--cell-padding);
border-bottom: none !important;
} }
.placeholder-offset { .spectrum-Table-cell--edit {
width: 1px; position: sticky;
left: 0;
z-index: 2;
} }
.placeholder { .spectrum-Table-cell:after {
top: 0; content: "";
position: absolute;
width: 100%;
height: 100%; height: 100%;
background-color: transparent;
top: 0;
left: 0; left: 0;
width: 100%; pointer-events: none;
position: absolute; transition: background-color
var(--spectrum-global-animation-duration-100, 0.13s) ease-in-out;
}
/* Placeholder */
.placeholder {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: var(--table-border);
border-top: none;
grid-column: 1 / -1;
background-color: var(--table-bg);
} }
.placeholder.has-fields { .placeholder--no-fields {
top: var(--header-height); border-top: var(--table-border);
height: calc(100% - var(--header-height)); }
.wrapper--quiet .placeholder {
border-left: none;
border-right: none;
} }
.placeholder-content { .placeholder-content {
padding: 20px; padding: 40px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -466,41 +549,4 @@
); );
text-align: center; text-align: center;
} }
tbody {
z-index: 1;
}
tbody tr {
height: var(--row-height);
}
tbody tr.hidden {
height: calc(var(--row-height) + 1px);
}
td {
padding-top: 0;
padding-bottom: 0;
border-bottom: none;
border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
border-radius: 0;
}
tr:first-child td {
border-top: none;
}
tr:last-child td {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
td.spectrum-Table-cell--divider {
width: 1px;
}
.spectrum-Table-cell-content {
height: var(--row-height);
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
}
</style> </style>

935
packages/bbui/yarn.lock

File diff suppressed because it is too large

7
packages/builder/cypress.json

@ -1,9 +1,10 @@
{ {
"baseUrl": "http://localhost:10001/builder/", "baseUrl": "http://localhost:10001",
"video": true, "video": false,
"projectId": "bmbemn", "projectId": "bmbemn",
"env": { "env": {
"PORT": "10001", "PORT": "10001",
"JWT_SECRET": "test" "JWT_SECRET": "test",
"HOST_IP": ""
} }
} }

24
packages/builder/cypress/integration/addMultiOptionDatatype.spec.js

@ -1,14 +1,17 @@
context("Add Multi-Option Datatype", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new table, with data", () => { filterTests(['all'], () => {
cy.createTable("Multi Data") context("Add Multi-Option Datatype", () => {
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5") before(() => {
cy.addRowMultiValue(["1", "2", "3", "4", "5"]) 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", () => { it("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend() cy.navigateToFrontend()
@ -39,6 +42,7 @@ context("Add Multi-Option Datatype", () => {
cy.getComponent(componentId) cy.getComponent(componentId)
.find(".spectrum-Picker-label") .find(".spectrum-Picker-label")
.contains("(5)") .contains("(5)")
})
}) })
}) })
}) })

62
packages/builder/cypress/integration/addRadioButtons.spec.js

@ -1,35 +1,39 @@
context("Add Radio Buttons", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
})
it("should add Radio Buttons options picker on form, add data, and confirm", () => { filterTests(['all'], () => {
cy.navigateToFrontend() context("Add Radio Buttons", () => {
cy.addComponent("Form", "Form") before(() => {
cy.addComponent("Form", "Options Picker").then((componentId) => { cy.login()
// Provide field setting cy.createTestApp()
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)
}) })
})
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
const addRadioButtonData = (totalRadioButtons) => { cy.navigateToFrontend()
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => { cy.addComponent("Form", "Form")
cy.get('.spectrum-Popover').contains('Custom') 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) .wait(500)
.click() .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)
}
})
}) })

51
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')
})
}
})
})

43
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')
})
})
})
})

18
packages/builder/cypress/integration/createApp.spec.js

@ -1,8 +1,12 @@
context("Create an Application", () => { import filterTests from '../support/filterTests'
it("should create a new application", () => {
cy.login() filterTests(['smoke', 'all'], () => {
cy.createTestApp() context("Create an Application", () => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) it("should create a new application", () => {
cy.contains("Cypress Tests").should("exist") cy.login()
}) cy.createTestApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.contains("Cypress Tests").should("exist")
})
})
}) })

109
packages/builder/cypress/integration/createAutomation.spec.js

@ -1,66 +1,69 @@
context("Create a automation", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
})
// https://on.cypress.io/interacting-with-elements filterTests(['smoke', 'all'], () => {
it("should create a automation", () => { context("Create a automation", () => {
cy.createTestTableWithData() before(() => {
cy.wait(2000) cy.login()
cy.contains("Automate").click() cy.createTestApp()
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 it("should create a automation", () => {
cy.contains("Setup").click() cy.createTestTableWithData()
cy.get(".spectrum-Picker-label").click() cy.wait(2000)
cy.wait(500) cy.contains("Automate").click()
cy.contains("dog").click() cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
cy.wait(2000) cy.get(".modal-inner-wrapper").within(() => {
// Create action cy.get("input").type("Add Row")
cy.get(".block > .spectrum-Icon").click() cy.contains("Row Created").click({ force: true })
cy.get(".modal-inner-wrapper").within(() => { cy.wait(500)
cy.wait(1000) cy.get(".spectrum-Button--cta").click()
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("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
cy.get(".spectrum-Textfield-input")
.eq(1)
.type("11")
cy.contains("Finish and test automation").click()
cy.get(".modal-inner-wrapper").within(() => { // Setup trigger
cy.wait(1000) cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
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.get(".spectrum-Picker-label").click()
cy.contains("dog").click() cy.contains("dog").click()
cy.wait(1000)
cy.get(".spectrum-Textfield-input") cy.get(".spectrum-Textfield-input")
.first() .first()
.type("automationGoodboy") .type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
cy.get(".spectrum-Textfield-input") cy.get(".spectrum-Textfield-input")
.eq(1) .eq(1)
.type("11") .type("11")
cy.get(".spectrum-Textfield-input") cy.contains("Finish and test automation").click()
.eq(2)
.type("123456") cy.get(".modal-inner-wrapper").within(() => {
cy.get(".spectrum-Textfield-input") cy.wait(1000)
.eq(3) cy.get(".spectrum-Picker-label").click()
.type("123456") cy.contains("dog").click()
cy.contains("Test").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")
}) })
cy.contains("Data").click()
cy.contains("automationGoodboy")
}) })
}) })

100
packages/builder/cypress/integration/createBinding.spec.js

@ -1,58 +1,62 @@
context("Create Bindings", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
it("should add a current user binding", () => { filterTests(['smoke', 'all'], () => {
cy.addComponent("Elements", "Paragraph").then(() => { context("Create Bindings", () => {
addSettingBinding("text", "Current User._id") before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
}) })
})
it("should handle an invalid binding", () => { it("should add a current user binding", () => {
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.addComponent("Elements", "Paragraph").then(() => {
// Cypress needs to escape curly brackets addSettingBinding("text", "Current User._id")
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", () => { it("should handle an invalid binding", () => {
const paramName = "foo" cy.addComponent("Elements", "Paragraph").then(componentId => {
cy.createScreen("Test Param", `/test/:${paramName}`) // Cypress needs to escape curly brackets
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.get("[data-cy=setting-text] input")
addSettingBinding("text", `URL.${paramName}`) .type("{{}{{}{{} Current User._id {}}{}}")
// The builder preview pages don't have a real URL, so all we can do .blur()
// is check that we were able to bind to the property, and that the cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
// component exists on the page })
cy.getComponent(componentId).should("have.text", "New Paragraph")
}) })
})
it("should add a binding with a handlebars helper", () => { it("should add a URL param binding", () => {
cy.addComponent("Elements", "Paragraph").then(componentId => { const paramName = "foo"
// Cypress needs to escape curly brackets cy.createScreen("Test Param", `/test/:${paramName}`)
cy.get("[data-cy=setting-text] input") cy.addComponent("Elements", "Paragraph").then(componentId => {
.type("{{}{{} add 1 2 {}}{}}") addSettingBinding("text", `URL.${paramName}`)
.blur() // The builder preview pages don't have a real URL, so all we can do
cy.getComponent(componentId).should("have.text", "3") // 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")
})
}) })
})
})
const addSettingBinding = (setting, bindingText, clickOption = true) => { it("should add a binding with a handlebars helper", () => {
cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click() cy.addComponent("Elements", "Paragraph").then(componentId => {
cy.get(".drawer").within(() => { // Cypress needs to escape curly brackets
if (clickOption) { cy.get("[data-cy=setting-text] input")
cy.contains(bindingText).click() .type("{{}{{} add 1 2 {}}{}}")
cy.get("textarea").should("have.value", `{{ ${bindingText} }}`) .blur()
} else { cy.getComponent(componentId).should("have.text", "3")
cy.get("textarea").type(bindingText) })
} })
cy.contains("Save").click()
}) })
}
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()
})
}
})

151
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 // TODO for now components are skipped, might not be good to keep doing this
xcontext("Create Components", () => {
let headlineId
before(() => { import filterTests from "../support/filterTests"
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 container", () => { filterTests(['all'], () => {
cy.addComponent(null, "Container").then(componentId => { xcontext("Create Components", () => {
cy.getComponent(componentId).should("exist") let headlineId
})
})
it("should add a headline", () => { before(() => {
cy.addComponent("Elements", "Headline").then(componentId => { cy.login()
headlineId = componentId cy.createTestApp()
cy.getComponent(headlineId).should("exist") cy.createTable("dog")
cy.addColumn("dog", "name", "Text")
cy.addColumn("dog", "age", "Number")
cy.addColumn("dog", "type", "Options")
cy.navigateToFrontend()
}) })
})
it("should change the text of the headline", () => { it("should add a container", () => {
const text = "Lorem ipsum dolor sit amet." cy.addComponent(null, "Container").then(componentId => {
cy.get("[data-cy=Settings]").click() cy.getComponent(componentId).should("exist")
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", () => { it("should add a headline", () => {
cy.get("[data-cy=Design]").click() cy.addComponent("Elements", "Headline").then(componentId => {
cy.contains("Typography").click() headlineId = componentId
cy.get("[data-cy=font-size-prop-control]").click() cy.getComponent(headlineId).should("exist")
cy.contains("60px").click() })
cy.getComponent(headlineId).should("have.css", "font-size", "60px") })
})
it("should create a form and reset to match schema", () => { it("should change the text of the headline", () => {
cy.addComponent("Form", "Form").then(() => { const text = "Lorem ipsum dolor sit amet."
cy.get("[data-cy=Settings]").click() cy.get("[data-cy=Settings]").click()
cy.get("[data-cy=setting-dataSource]") cy.get("[data-cy=setting-text] input")
.contains("Choose option") .type(text)
.click() .blur()
cy.get(".dropdown") cy.getComponent(headlineId).should("have.text", text)
.contains("dog") })
.click()
cy.addComponent("Form", "Field Group").then(fieldGroupId => { 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.get("[data-cy=Settings]").click()
cy.contains("Update Form Fields").click() cy.get("[data-cy=setting-dataSource]")
cy.get(".modal") .contains("Choose option")
.get("button.primary") .click()
cy.get(".dropdown")
.contains("dog")
.click() .click()
cy.getComponent(fieldGroupId).within(() => { cy.addComponent("Form", "Field Group").then(fieldGroupId => {
cy.contains("name").should("exist") cy.get("[data-cy=Settings]").click()
cy.contains("age").should("exist") cy.contains("Update Form Fields").click()
cy.contains("type").should("exist") 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", () => { it("deletes a component", () => {
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.addComponent("Elements", "Paragraph").then(componentId => {
cy.get("[data-cy=setting-_instanceName] input") cy.get("[data-cy=setting-_instanceName] input")
.type(componentId) .type(componentId)
.blur() .blur()
cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({ cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
force: true, 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")
}) })
}) })
}) })

34
packages/builder/cypress/integration/createScreen.js

@ -1,21 +1,25 @@
context("Screen Tests", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
it("Should successfully create a screen", () => { filterTests(["smoke", "all"], () => {
cy.createScreen("Test Screen", "/test") context("Screen Tests", () => {
cy.get(".nav-items-container").within(() => { before(() => {
cy.contains("/test").should("exist") cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
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", () => { it("Should update the url", () => {
cy.createScreen("Test Screen", "test with spaces") cy.createScreen("Test Screen", "test with spaces")
cy.get(".nav-items-container").within(() => { cy.get(".nav-items-container").within(() => {
cy.contains("/test-with-spaces").should("exist") cy.contains("/test-with-spaces").should("exist")
})
}) })
}) })
}) })

164
packages/builder/cypress/integration/createTable.spec.js

@ -1,74 +1,110 @@
context("Create a Table", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new Table", () => { filterTests(["smoke", "all"], () => {
cy.createTable("dog") context("Create a Table", () => {
cy.wait(1000) before(() => {
// Check if Table exists cy.login()
cy.get(".table-title h1").should("have.text", "dog") cy.createTestApp()
}) })
it("adds a new column to the table", () => { it("should create a new Table", () => {
cy.addColumn("dog", "name", "Text") cy.createTable("dog")
cy.contains("name").should("be.visible") cy.wait(1000)
}) // Check if Table exists
cy.get(".table-title h1").should("have.text", "dog")
})
it("creates a row in the table", () => { it("adds a new column to the table", () => {
cy.addRow(["Rover"]) cy.addColumn("dog", "name", "Text")
cy.contains("Rover").should("be.visible") cy.contains("name").should("be.visible")
}) })
it("updates a column on the table", () => { it("creates a row in the table", () => {
cy.get(".title").click() cy.addRow(["Rover"])
cy.get(".spectrum-Table-editIcon > use").click() cy.contains("Rover").should("be.visible")
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("updates a column on the table", () => {
it("edits a row", () => { cy.get(".title").click()
cy.contains("button", "Edit").click({ force: true }) cy.get(".spectrum-Table-editIcon > use").click()
cy.wait(1000) cy.get("input").eq(1).type("updated", { force: true })
cy.get(".spectrum-Modal input").clear() // Unset table display column
cy.get(".spectrum-Modal input").type("Updated") cy.get(".spectrum-Switch-input").eq(1).click()
cy.contains("Save").click() cy.contains("Save Column").click()
cy.contains("Updated").should("have.text", "Updated") cy.contains("nameupdated ").should("contain", "nameupdated")
}) })
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("deletes a column", () => { it("edits a row", () => {
cy.get(".title").click() cy.contains("button", "Edit").click({ force: true })
cy.get(".spectrum-Table-editIcon > use").click() cy.wait(1000)
cy.contains("Delete").click() cy.get(".spectrum-Modal input").clear()
cy.wait(50) cy.get(".spectrum-Modal input").type("Updated")
cy.get(`[data-cy="delete-column-confirm"]`).type("nameupdated") cy.contains("Save").click()
cy.contains("Delete Column").click() cy.contains("Updated").should("have.text", "Updated")
cy.contains("nameupdated").should("not.exist") })
})
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("deletes a table", () => { if (Cypress.env("TEST_ENV")) {
cy.get(".nav-item") // No Pagination in CI - Test env only for the next two tests
.contains("dog") it("Adds 15 rows and checks pagination", () => {
.parents(".nav-item") // 10 rows per page, 15 rows should create 2 pages within table
.first() const totalRows = 16
.within(() => { for (let i = 1; i < totalRows; i++) {
cy.get(".actions .spectrum-Icon").click({ force: true }) 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") it("Deletes rows and checks pagination", () => {
cy.contains("Delete Table").click() // Delete rows, removing second page of rows from table
cy.contains("dog").should("not.exist") 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")
})
}) })
}) })

10
packages/builder/cypress/integration/createUser.spec.js

@ -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")
})
})

180
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")
})
})
})

272
packages/builder/cypress/integration/createView.spec.js

@ -1,152 +1,156 @@
context("Create a View", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login() filterTests(['smoke', 'all'], () => {
cy.createTestApp() context("Create a View", () => {
cy.createTable("data") before(() => {
cy.addColumn("data", "group", "Text") cy.login()
cy.addColumn("data", "age", "Number") cy.createTestApp()
cy.addColumn("data", "rating", "Number") cy.createTable("data")
cy.addColumn("data", "group", "Text")
// 6 Rows cy.addColumn("data", "age", "Number")
cy.addRow(["Students", 25, 1]) cy.addColumn("data", "rating", "Number")
cy.addRow(["Students", 20, 3])
cy.addRow(["Students", 18, 6]) // 6 Rows
cy.addRow(["Students", 25, 2]) cy.addRow(["Students", 25, 1])
cy.addRow(["Teachers", 49, 5]) cy.addRow(["Students", 20, 3])
cy.addRow(["Teachers", 36, 3]) cy.addRow(["Students", 18, 6])
}) cy.addRow(["Students", 25, 2])
cy.addRow(["Teachers", 49, 5])
it("creates a view", () => { cy.addRow(["Teachers", 36, 3])
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 => { it("creates a view", () => {
expect($headers).to.have.length(3) cy.contains("Create view").click()
const headers = Array.from($headers).map(header => cy.get(".modal-inner-wrapper").within(() => {
header.textContent.trim() cy.get("input").type("Test View")
) cy.get("button").contains("Create View").click({ force: true })
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"]) })
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", () => { it("filters the view by age over 10", () => {
cy.contains("Filter").click() cy.contains("Filter").click()
cy.contains("Add Filter").click() cy.contains("Add Filter").click()
cy.get(".modal-inner-wrapper").within(() => { cy.get(".modal-inner-wrapper").within(() => {
cy.get(".spectrum-Picker-label").eq(0).click() cy.get(".spectrum-Picker-label").eq(0).click()
cy.contains("age").click({ force: true }) cy.contains("age").click({ force: true })
cy.get(".spectrum-Picker-label").eq(1).click() cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("More Than").click({ force: true }) cy.contains("More Than").click({ force: true })
cy.get("input").type(18) cy.get("input").type(18)
cy.contains("Save").click() cy.contains("Save").click()
}) })
cy.get(".spectrum-Table-row").get($values => { cy.get(".spectrum-Table-row").get($values => {
expect($values).to.have.length(5) 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() it("creates a stats calculation view based on age", () => {
cy.contains("age").click({ force: true }) cy.wait(1000)
cy.contains("Calculate").click()
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".modal-inner-wrapper").within(() => {
}) cy.get(".spectrum-Picker-label").eq(0).click()
cy.wait(1000) cy.contains("Statistics").click()
cy.get(".title").then($headers => { cy.get(".spectrum-Picker-label").eq(1).click()
expect($headers).to.have.length(7) cy.contains("age").click({ force: true })
const headers = Array.from($headers).map(header =>
header.textContent.trim() cy.get(".spectrum-Button").contains("Save").click({ force: true })
) })
expect(removeSpacing(headers)).to.deep.eq([ cy.wait(1000)
"field",
"sum", cy.get(".title").then($headers => {
"min", expect($headers).to.have.length(7)
"max", const headers = Array.from($headers).map(header =>
"count", header.textContent.trim()
"sumsqr", )
"avg", 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"])
})
}) })
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", () => { it("groups the view by group", () => {
cy.contains("Group by").click() cy.contains("Group by").click()
cy.get(".modal-inner-wrapper").within(() => { cy.get(".modal-inner-wrapper").within(() => {
cy.get(".spectrum-Picker-label").eq(0).click() cy.get(".spectrum-Picker-label").eq(0).click()
cy.contains("group").click() cy.contains("group").click()
cy.contains("Save").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",
])
})
}) })
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", () => { it("renames a view", () => {
cy.contains(".nav-item", "Test View") cy.contains(".nav-item", "Test View")
.find(".actions .icon") .find(".actions .icon")
.click({ force: true }) .click({ force: true })
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click() cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
cy.get(".modal-inner-wrapper").within(() => { cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").type(" Updated") cy.get("input").type(" Updated")
cy.contains("Save").click() cy.contains("Save").click()
})
cy.wait(1000)
cy.contains("Test View Updated").should("be.visible")
}) })
cy.wait(1000)
cy.contains("Test View Updated").should("be.visible")
})
it("deletes a view", () => { it("deletes a view", () => {
cy.contains(".nav-item", "Test View Updated") cy.contains(".nav-item", "Test View Updated")
.find(".actions .icon") .find(".actions .icon")
.click({ force: true }) .click({ force: true })
cy.contains("Delete").click() cy.contains("Delete").click()
cy.contains("Delete View").click() cy.contains("Delete View").click()
cy.wait(500) cy.wait(500)
cy.contains("TestView Updated").should("not.exist") cy.contains("TestView Updated").should("not.exist")
})
}) })
})
function removeSpacing(headers) { function removeSpacing(headers) {
let newHeaders = [] let newHeaders = []
for (let header of headers) { for (let header of headers) {
newHeaders.push(header.replace(/\s\s+/g, " ")) newHeaders.push(header.replace(/\s\s+/g, " "))
}
return newHeaders
} }
return newHeaders })
}

161
packages/builder/cypress/integration/customThemingProperties.spec.js

@ -1,84 +1,87 @@
xcontext("Custom Theming Properties", () => { import filterTests from "../support/filterTests"
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
/* Default Values: filterTests(['all'], () => {
Button roundness = Large xcontext("Custom Theming Properties", () => {
Accent colour = Blue 600 before(() => {
Accent colour (hover) = Blue 500 cy.login()
Navigation bar background colour = Gray 100 cy.createTestApp()
Navigation bar text colour = Gray 800 */ cy.navigateToFrontend()
it("should reset the color property values", () => { })
// Open Theme modal and change colours
cy.get(".spectrum-ActionButton-label").contains("Theme").click() /* Default Values:
cy.get(".spectrum-Picker").contains("Large").click() Button roundness = Large
.parents() Accent colour = Blue 600
.get(".spectrum-Menu-itemLabel").contains("None").click() Accent colour (hover) = Blue 500
changeThemeColors() Navigation bar background colour = Gray 100
// Reset colours Navigation bar text colour = Gray 800 */
cy.get(".spectrum-Button-label").contains("Reset").click({force: true}) it("should reset the color property values", () => {
// Check values have reset // Open Theme modal and change colours
checkThemeColorDefaults() cy.get(".spectrum-ActionButton-label").contains("Theme").click()
}) cy.get(".spectrum-Picker").contains("Large").click()
.parents()
/* Button Roundness Values: .get(".spectrum-Menu-itemLabel").contains("None").click()
None = 0 changeThemeColors()
Small = 4px // Reset colours
Medium = 8px cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
Large = 16px */ // Check values have reset
it("should test button roundness", () => { checkThemeColorDefaults()
const buttonRoundnessValues = ["0", "4px", "8px", "16px"] })
cy.wait(1000)
// Add button, change roundness and confirm value /* Button Roundness Values:
cy.addComponent("Button", null).then((componentId) => { None = 0
buttonRoundnessValues.forEach(function (item, index){ Small = 4px
cy.get(".spectrum-ActionButton-label").contains("Theme").click() Medium = 8px
cy.get(".setting").contains("Button roundness").parent() Large = 16px */
.get(".select-wrapper").click() it("should test button roundness", () => {
cy.get(".spectrum-Popover").find('li').eq(index).click() const buttonRoundnessValues = ["0", "4px", "8px", "16px"]
cy.get(".spectrum-Button").contains("View changes").click({force: true}) cy.wait(1000)
cy.reload() // Add button, change roundness and confirm value
cy.getComponent(componentId) cy.addComponent("Button", null).then((componentId) => {
.parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`) 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"]')
}
}) })

43
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)
})
}
})
})

222
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)
})
}
})
})

230
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)
})
}
})
})

285
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)
}
}
})
})

43
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)
})
})
})

116
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')
})
})
})
})

216
packages/builder/cypress/integration/renameAnApplication.spec.js

@ -1,103 +1,133 @@
context("Rename an App", () => { import filterTests from "../support/filterTests"
beforeEach(() => {
cy.login()
cy.createTestApp()
})
it("should rename an unpublished application", () => { filterTests(['all'], () => {
const appRename = "Cypress Renamed" context("Rename an App", () => {
// Rename app, Search for app, Confirm name was changed beforeEach(() => {
cy.get(".home-logo").click() cy.login()
renameApp(appRename) cy.createTestApp()
cy.searchForApplication(appRename)
cy.get(".appTable").find(".title").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})
}) })
// Rename app, Search for app, Confirm name was changed
cy.get(".home-logo").click()
renameApp(appRename, true)
cy.searchForApplication(appRename)
cy.get(".appTable").find(".title").should("have.length", 1)
})
it("Should try to rename an application to have no name", () => { it("should rename an unpublished application", () => {
cy.get(".home-logo").click() const appName = "Cypress Tests"
renameApp(" ", false, true) const appRename = "Cypress Renamed"
// Close modal and confirm name has not been changed // Rename app, Search for app, Confirm name was changed
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() cy.get(".home-logo").click()
cy.searchForApplication("Cypress Tests") renameApp(appName, appRename)
cy.get(".appTable").find(".title").should("have.length", 1) 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)
})
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(() => {
// 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(`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 validate application names", () => { xit("Should create two applications with the same name", () => {
// App name must be letters, numbers and spaces only // It is not possible to have applications with the same name
// This test checks numbers and special characters specifically const appName = "Cypress Tests"
const numberName = 12345 cy.visit(`${Cypress.config().baseUrl}/builder`)
const specialCharName = "£$%^" cy.wait(500)
cy.get(".home-logo").click() cy.get(".spectrum-Button").contains("Create app").click({force: true})
renameApp(numberName) cy.contains(/Start from scratch/).click()
cy.searchForApplication(numberName) cy.get(".spectrum-Modal")
cy.get(".appTable").find(".title").should("have.length", 1) .within(() => {
renameApp(specialCharName) cy.get("input").eq(0).type(appName)
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
}) cy.get(".error").should("have.text", "Another app with the same name already exists")
})
const renameApp = (appName, published, noName) => { })
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
.its("body") it("should validate application names", () => {
.then(val => { // App name must be letters, numbers and spaces only
if (val.length > 0) { // This test checks numbers and special characters specifically
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() const appName = "Cypress Tests"
// Check for when an app is published const numberName = 12345
if (published == true){ const specialCharName = "£$%^"
// Should not have Edit as option, will unpublish app cy.get(".home-logo").click()
cy.should("not.have.value", "Edit") renameApp(appName, numberName)
cy.get(".spectrum-Menu").contains("Unpublish").click() cy.reload()
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() cy.wait(1000)
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() cy.searchForApplication(numberName)
} cy.get(".appTable").find(".title").should("have.length", 1)
cy.contains("Edit").click() cy.reload()
cy.get(".spectrum-Modal") cy.wait(1000)
.within(() => { renameApp(numberName, specialCharName)
if (noName == true){ cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
cy.get("input").clear() // Set app name back to Cypress Tests
cy.get(".spectrum-Dialog-grid").click() cy.reload()
.contains("App name must be letters, numbers and spaces only") cy.wait(1000)
return cy 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.contains("Edit").click()
cy.get("input").eq(0).type(appName).should("have.value", appName).blur() cy.get(".spectrum-Modal")
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true}) .within(() => {
cy.wait(500) 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)
})
}
})
} }
}) })
}
}) })

67
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')
})
})
})
})

275
packages/builder/cypress/support/commands.js

@ -10,7 +10,7 @@ Cypress.on("uncaught:exception", () => {
}) })
Cypress.Commands.add("login", () => { Cypress.Commands.add("login", () => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(2000) cy.wait(2000)
cy.url().then(url => { cy.url().then(url => {
if (url.includes("builder/admin")) { if (url.includes("builder/admin")) {
@ -33,37 +33,69 @@ Cypress.Commands.add("login", () => {
}) })
Cypress.Commands.add("createApp", name => { Cypress.Commands.add("createApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
cy.request(`${Cypress.config().baseUrl}api/applications?status=all`) cy.get(".spectrum-Button").contains("Create app").click({ force: true })
.its("body")
.then(body => {
if (body.length > 0) {
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
}
})
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.get(".spectrum-ButtonGroup").contains("Create app").click()
cy.wait(7000) cy.wait(5000)
}) })
cy.createTable("Cypress Tests", true)
}) })
Cypress.Commands.add("deleteApp", appName => { Cypress.Commands.add("deleteApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000) cy.wait(2000)
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get( cy.searchForApplication(name)
".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon" cy.get(".appTable").within(() => {
).click() cy.get(".spectrum-Icon").eq(1).click()
cy.contains("Delete").click()
cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName)
cy.get(".spectrum-Button--warning").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()
})
} }
}) })
}) })
@ -72,6 +104,7 @@ Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.deleteApp(appName) cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.") cy.createApp(appName, "This app is used for Cypress testing.")
cy.createScreen("home", "home")
}) })
Cypress.Commands.add("createTestTableWithData", () => { Cypress.Commands.add("createTestTableWithData", () => {
@ -80,10 +113,18 @@ Cypress.Commands.add("createTestTableWithData", () => {
cy.addColumn("dog", "age", "Number") cy.addColumn("dog", "age", "Number")
}) })
Cypress.Commands.add("createTable", tableName => { Cypress.Commands.add("createTable", (tableName, initialTable) => {
cy.contains("Budibase DB").click() if (!initialTable) {
cy.contains("Create new table").click() 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.get(".spectrum-Modal").within(() => {
cy.wait(1000) cy.wait(1000)
cy.get("input").first().type(tableName).blur() cy.get("input").first().type(tableName).blur()
@ -190,22 +231,49 @@ Cypress.Commands.add("navigateToFrontend", () => {
cy.wait(1000) cy.wait(1000)
cy.contains("Design").click() cy.contains("Design").click()
cy.get(".spectrum-Search").type("/") cy.get(".spectrum-Search").type("/")
cy.createScreen("home", "home")
cy.addComponent("Elements", "Headline")
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) => { Cypress.Commands.add("createScreen", (screenName, route) => {
cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click() cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get(".item").first().click() cy.get(".item").contains("Blank").click()
cy.get(".spectrum-Button--cta").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(".spectrum-Modal").within(() => {
cy.get("input").first().clear().type(screenName) for (let i = 0; i < values.length; i++) {
cy.get("input").eq(1).clear().type(route) cy.get("input").eq(i).type(values[i]).blur()
cy.get(".spectrum-Button--cta").click() }
cy.wait(2000) cy.get(".spectrum-ButtonGroup").contains("Create").click()
}) })
}) })
@ -243,7 +311,144 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
}) })
Cypress.Commands.add("searchForApplication", appName => { Cypress.Commands.add("searchForApplication", appName => {
cy.get(".spectrum-Textfield").within(() => { cy.wait(1000)
cy.get("input").eq(0).type(appName) // 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)
}) })

16
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

14
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 }))

31
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" }
})

4
packages/builder/nuxt.config.js

@ -0,0 +1,4 @@
export default {
ssr: false,
target: "static",
}

14
packages/builder/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -11,7 +11,7 @@
"rollup": "rollup -c -w", "rollup": "rollup -c -w",
"cy:setup": "ts-node ./cypress/ts/setup.ts", "cy:setup": "ts-node ./cypress/ts/setup.ts",
"cy:setup:ci": "node ./cypress/setup.js", "cy:setup:ci": "node ./cypress/setup.js",
"cy:run": "cypress run", "cy:run": "xvfb-run cypress run --headed --browser chrome",
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run:ci": "cypress run --record", "cy:run:ci": "cypress run --record",
"cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run", "cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
@ -64,10 +64,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.73", "@budibase/bbui": "^1.0.76-alpha.3",
"@budibase/client": "^1.0.73", "@budibase/client": "^1.0.76-alpha.3",
"@budibase/frontend-core": "^1.0.73", "@budibase/frontend-core": "^1.0.76-alpha.3",
"@budibase/string-templates": "^1.0.73", "@budibase/string-templates": "^1.0.76-alpha.3",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
@ -94,7 +94,7 @@
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0", "@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"cypress": "^5.1.0", "cypress": "^9.3.1",
"cypress-terminal-report": "^1.4.1", "cypress-terminal-report": "^1.4.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",

2
packages/builder/src/components/backend/DataTable/DataTable.svelte

@ -98,7 +98,7 @@
tableId={id} tableId={id}
data={$fetch.rows} data={$fetch.rows}
bind:hideAutocolumns bind:hideAutocolumns
loading={$fetch.loading} loading={!$fetch.loaded}
on:sort={onSort} on:sort={onSort}
allowEditing allowEditing
disableSorting disableSorting

2
packages/builder/src/components/backend/DataTable/Table.svelte

@ -136,7 +136,7 @@
</div> </div>
</div> </div>
{#key tableId} {#key tableId}
<div class="table-wrapper" in:fade={{ delay: 200, duration: 100 }}> <div class="table-wrapper">
<Table <Table
{data} {data}
{schema} {schema}

3
packages/builder/src/components/design/NavigationPanel/ScreenWizard.svelte

@ -12,11 +12,12 @@
let screenName = "" let screenName = ""
let url = "" let url = ""
let selectedScreens = [] let selectedScreens = []
let roleId = $selectedAccessRole || "BASIC"
let showProgressCircle = false let showProgressCircle = false
let routeError let routeError
let createdScreens = [] let createdScreens = []
$: roleId = $selectedAccessRole || "BASIC"
const createScreens = async () => { const createScreens = async () => {
for (let screen of selectedScreens) { for (let screen of selectedScreens) {
let test = screen.create() let test = screen.create()

66
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellDrawer.svelte

@ -0,0 +1,66 @@
<script>
import {
Input,
Select,
ColorPicker,
DrawerContent,
Layout,
Label,
} from "@budibase/bbui"
import { store } from "builderStore"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let column
</script>
<DrawerContent>
<div class="container">
<Layout noPadding gap="S">
<Input bind:value={column.width} label="Width" placeholder="Auto" />
<Select
label="Alignment"
bind:value={column.align}
options={["Left", "Center", "Right"]}
placeholder="Default"
/>
<DrawerBindableInput
label="Value"
value={column.template}
on:change={e => (column.template = e.detail)}
placeholder={`{{ Value }}`}
bindings={[
{
readableBinding: "Value",
runtimeBinding: "[value]",
},
]}
/>
<Layout noPadding gap="XS">
<Label>Background color</Label>
<ColorPicker
value={column.background}
on:change={e => (column.background = e.detail)}
alignRight
spectrumTheme={$store.theme}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Text color</Label>
<ColorPicker
value={column.color}
on:change={e => (column.color = e.detail)}
alignRight
spectrumTheme={$store.theme}
/>
</Layout>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 240px;
margin: 0 auto;
}
</style>

34
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellEditor.svelte

@ -0,0 +1,34 @@
<script>
import { Drawer, Button, Icon } from "@budibase/bbui"
import CellDrawer from "./CellDrawer.svelte"
export let column
let boundValue
let drawer
$: updateBoundValue(column)
const updateBoundValue = value => {
boundValue = { ...value }
}
const open = () => {
updateBoundValue(column)
drawer.show()
}
const save = () => {
column = boundValue
drawer.hide()
}
</script>
<Icon name="Settings" hoverable size="S" on:click={open} />
<Drawer bind:this={drawer} title="Table Columns">
<svelte:fragment slot="description">
"{column.name}" column settings
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<CellDrawer slot="body" bind:column={boundValue} />
</Drawer>

57
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnDrawer.svelte

@ -4,17 +4,19 @@
Icon, Icon,
DrawerContent, DrawerContent,
Layout, Layout,
Input,
Select, Select,
Label, Label,
Body, Body,
Input,
} from "@budibase/bbui" } from "@budibase/bbui"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action" import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid" import { generate } from "shortid"
import CellEditor from "./CellEditor.svelte"
export let columns = [] export let columns = []
export let options = [] export let options = []
export let schema = {}
const flipDurationMs = 150 const flipDurationMs = 150
let dragDisabled = true let dragDisabled = true
@ -61,11 +63,23 @@
dragDisabled = true dragDisabled = true
} }
const addAllColumns = () => {
let newColumns = columns || []
options.forEach(field => {
const fieldSchema = schema[field]
const hasCol = columns && columns.findIndex(x => x.name === field) !== -1
if (!fieldSchema?.autocolumn && !hasCol) {
newColumns.push({
name: field,
displayName: field,
})
}
})
columns = newColumns
}
const reset = () => { const reset = () => {
columns = options.map(col => ({ columns = []
name: col,
displayName: col,
}))
} }
</script> </script>
@ -79,6 +93,7 @@
<Label size="L">Column</Label> <Label size="L">Column</Label>
<Label size="L">Label</Label> <Label size="L">Label</Label>
<div /> <div />
<div />
</div> </div>
<div <div
class="columns" class="columns"
@ -108,6 +123,7 @@
on:change={e => (column.displayName = e.detail)} on:change={e => (column.displayName = e.detail)}
/> />
<Input bind:value={column.displayName} placeholder="Label" /> <Input bind:value={column.displayName} placeholder="Label" />
<CellEditor bind:column />
<Icon <Icon
name="Close" name="Close"
hoverable hoverable
@ -121,19 +137,25 @@
</Layout> </Layout>
{:else} {:else}
<div class="column"> <div class="column">
<div /> <div class="wide">
<Body size="S">Add the first column to your table.</Body> <Body size="S">
By default, all table columns will automatically be shown.
<br />
You can manually control which columns are included in your table,
and their appearance, by adding them below.
</Body>
</div>
</div> </div>
{/if} {/if}
<div class="columns"> <div class="column">
<div class="column"> <div class="buttons wide">
<div /> <Button secondary icon="Add" on:click={addColumn}>Add column</Button>
<div class="buttons"> <Button secondary quiet on:click={addAllColumns}>
<Button secondary icon="Add" on:click={addColumn}> Add all columns
Add column </Button>
</Button> {#if columns?.length}
<Button secondary quiet on:click={reset}>Reset columns</Button> <Button secondary quiet on:click={reset}>Reset columns</Button>
</div> {/if}
</div> </div>
</div> </div>
</Layout> </Layout>
@ -156,7 +178,7 @@
.column { .column {
gap: var(--spacing-l); gap: var(--spacing-l);
display: grid; display: grid;
grid-template-columns: 20px 1fr 1fr 20px; grid-template-columns: 20px 1fr 1fr auto auto;
align-items: center; align-items: center;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms; transition: background-color ease-in-out 130ms;
@ -168,6 +190,9 @@
display: grid; display: grid;
place-items: center; place-items: center;
} }
.wide {
grid-column: 2 / -1;
}
.buttons { .buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

29
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnEditor.svelte

@ -18,22 +18,30 @@
let boundValue let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, datasource).schema $: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {}) $: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options) $: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue) $: updateBoundValue(sanitisedValue)
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => { const updateBoundValue = value => {
boundValue = cloneDeep(value) boundValue = cloneDeep(value)
} }
const getValidColumns = (columns, options) => { const getValidColumns = (columns, options) => {
// If no columns then default to all columns
if (!Array.isArray(columns) || !columns.length) { if (!Array.isArray(columns) || !columns.length) {
return options.map(col => ({ return []
name: col,
displayName: col,
}))
} }
// We need to account for legacy configs which would just be an array // We need to account for legacy configs which would just be an array
// of strings // of strings
@ -48,17 +56,22 @@
}) })
} }
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => { const save = () => {
dispatch("change", getValidColumns(boundValue, options)) dispatch("change", getValidColumns(boundValue, options))
drawer.hide() drawer.hide()
} }
</script> </script>
<ActionButton on:click={drawer.show}>Configure columns</ActionButton> <ActionButton on:click={open}>Configure columns</ActionButton>
<Drawer bind:this={drawer} title="Table Columns"> <Drawer bind:this={drawer} title="Table Columns">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure the columns in your table. Configure the columns in your table.
</svelte:fragment> </svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button> <Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} /> <ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer> </Drawer>

2
packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte

@ -90,7 +90,7 @@
</script> </script>
<DrawerContent> <DrawerContent>
<div className="container"> <div class="container">
<Layout noPadding> <Layout noPadding>
<Body size="S"> <Body size="S">
{#if !filters?.length} {#if !filters?.length}

1417
packages/builder/yarn.lock

File diff suppressed because it is too large

2
packages/cli/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

13
packages/client/manifest.json

@ -2679,7 +2679,8 @@
"type": "columns", "type": "columns",
"label": "Columns", "label": "Columns",
"key": "columns", "key": "columns",
"dependsOn": "dataProvider" "dependsOn": "dataProvider",
"nested": true
}, },
{ {
"type": "select", "type": "select",
@ -2702,6 +2703,11 @@
"label": "Quiet", "label": "Quiet",
"key": "quiet" "key": "quiet"
}, },
{
"type": "boolean",
"label": "Compact",
"key": "compact"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Show auto columns", "label": "Show auto columns",
@ -2957,6 +2963,11 @@
"label": "Quiet table variant", "label": "Quiet table variant",
"key": "quiet" "key": "quiet"
}, },
{
"type": "boolean",
"label": "Compact",
"key": "compact"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Show auto columns", "label": "Show auto columns",

21
packages/client/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,12 +19,25 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.73", "@budibase/bbui": "^1.0.76-alpha.3",
"@budibase/frontend-core": "^1.0.73", "@budibase/frontend-core": "^1.0.76-alpha.3",
"@budibase/string-templates": "^1.0.73", "@budibase/string-templates": "^1.0.76-alpha.3",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",
"@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/tag": "^3.1.4",
"@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1",
"dayjs": "^1.10.5",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-polyfill-node": "^0.8.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte": "^3.38.2",
"svelte-apexcharts": "^1.0.2",
"svelte-flatpickr": "^3.1.0",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {

2
packages/client/src/components/app/blocks/TableBlock.svelte

@ -16,6 +16,7 @@
export let showAutoColumns export let showAutoColumns
export let rowCount export let rowCount
export let quiet export let quiet
export let compact
export let size export let size
export let linkRows export let linkRows
export let linkURL export let linkURL
@ -162,6 +163,7 @@
showAutoColumns, showAutoColumns,
rowCount, rowCount,
quiet, quiet,
compact,
size, size,
linkRows, linkRows,
linkURL, linkURL,

13
packages/client/src/components/app/table/Table.svelte

@ -14,6 +14,7 @@
export let linkURL export let linkURL
export let linkColumn export let linkColumn
export let linkPeek export let linkPeek
export let compact
const component = getContext("component") const component = getContext("component")
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk") const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
@ -72,6 +73,7 @@
order: 0, order: 0,
sortable: false, sortable: false,
divider: true, divider: true,
width: "auto",
} }
} }
@ -84,8 +86,13 @@
if (UnsortableTypes.includes(schema[columnName].type)) { if (UnsortableTypes.includes(schema[columnName].type)) {
newSchema[columnName].sortable = false newSchema[columnName].sortable = false
} }
if (field?.displayName) {
newSchema[columnName].displayName = field?.displayName // Add additional settings like width etc
if (typeof field === "object") {
newSchema[columnName] = {
...newSchema[columnName],
...field,
}
} }
}) })
return newSchema return newSchema
@ -119,12 +126,14 @@
{loading} {loading}
{rowCount} {rowCount}
{quiet} {quiet}
{compact}
{customRenderers} {customRenderers}
allowSelectRows={false} allowSelectRows={false}
allowEditRows={false} allowEditRows={false}
allowEditColumns={false} allowEditColumns={false}
showAutoColumns={true} showAutoColumns={true}
disableSorting disableSorting
autoSortColumns={!columns?.length}
on:sort={onSort} on:sort={onSort}
on:click={onClick} on:click={onClick}
> >

1413
packages/client/yarn.lock

File diff suppressed because it is too large

4
packages/frontend-core/package.json

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.73", "@budibase/bbui": "^1.0.76-alpha.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

8
packages/server/package.json

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -74,9 +74,9 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.73", "@budibase/backend-core": "^1.0.76-alpha.3",
"@budibase/client": "^1.0.73", "@budibase/client": "^1.0.76-alpha.3",
"@budibase/string-templates": "^1.0.73", "@budibase/string-templates": "^1.0.76-alpha.3",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

5
packages/server/src/automations/steps/filter.js

@ -16,10 +16,11 @@ exports.FilterConditions = FilterConditions
exports.PrettyFilterConditions = PrettyFilterConditions exports.PrettyFilterConditions = PrettyFilterConditions
exports.definition = { exports.definition = {
name: "Filter", name: "Condition",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}", tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
icon: "Branch2", icon: "Branch2",
description: "Filter any automations which do not meet certain conditions", description:
"Conditionally halt automations which do not meet certain conditions",
type: "LOGIC", type: "LOGIC",
internal: true, internal: true,
stepId: "FILTER", stepId: "FILTER",

2
packages/string-templates/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

9
packages/string-templates/src/index.js

@ -167,8 +167,13 @@ module.exports.disableEscaping = string => {
if (matches == null) { if (matches == null) {
return string return string
} }
for (let match of matches) {
string = string.replace(match, `{${match}}`) // find the unique set
const unique = [...new Set(matches)]
for (let match of unique) {
// add a negative lookahead to exclude any already
const regex = new RegExp(`${match}(?!})`, "g")
string = string.replace(regex, `{${match}}`)
} }
return string return string
} }

4
packages/string-templates/test/basic.spec.js

@ -194,5 +194,9 @@ describe("check that disabling escaping function works", () => {
it("should work with a combination", () => { it("should work with a combination", () => {
expect(disableEscaping("{{ name }} welcome to {{{ platform }}}")).toEqual("{{{ name }}} welcome to {{{ platform }}}") expect(disableEscaping("{{ name }} welcome to {{{ platform }}}")).toEqual("{{{ name }}} welcome to {{{ platform }}}")
}) })
it("should work with multiple escaped", () => {
expect(disableEscaping("{{ name }} welcome to {{ name }}")).toEqual("{{{ name }}} welcome to {{{ name }}}")
})
}) })

6
packages/worker/package.json

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.73", "version": "1.0.76-alpha.3",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -34,8 +34,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.73", "@budibase/backend-core": "^1.0.76-alpha.3",
"@budibase/string-templates": "^1.0.73", "@budibase/string-templates": "^1.0.76-alpha.3",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",

2
packages/worker/src/api/controllers/system/environment.js

@ -7,6 +7,6 @@ exports.fetch = async ctx => {
accountPortalUrl: env.ACCOUNT_PORTAL_URL, accountPortalUrl: env.ACCOUNT_PORTAL_URL,
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
// in test need to pretend its in production for the UI (Cypress) // in test need to pretend its in production for the UI (Cypress)
isDev: env.isDev(), isDev: env.isDev() && !env.isTest(),
} }
} }

Loading…
Cancel
Save