Browse Source

Merge pull request #4608 from Budibase/table-cell-config

Table V2
master
Andrew Kingston 5 years ago
committed by GitHub
parent
commit
cd3fd172cc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/bbui/package.json
  2. 5
      packages/bbui/src/ColorPicker/ColorPicker.svelte
  3. 21
      packages/bbui/src/Drawer/Drawer.svelte
  4. 15
      packages/bbui/src/Table/AttachmentRenderer.svelte
  5. 4
      packages/bbui/src/Table/BoldRenderer.svelte
  6. 32
      packages/bbui/src/Table/CellRenderer.svelte
  7. 9
      packages/bbui/src/Table/CodeRenderer.svelte
  8. 4
      packages/bbui/src/Table/DateTimeRenderer.svelte
  9. 8
      packages/bbui/src/Table/InternalRenderer.svelte
  10. 3
      packages/bbui/src/Table/StringRenderer.svelte
  11. 586
      packages/bbui/src/Table/Table.svelte
  12. 935
      packages/bbui/yarn.lock
  13. 8
      packages/builder/cypress/integration/createTable.spec.js
  14. 188
      packages/builder/cypress/integration/createUserAndRoles.spec.js
  15. 381
      packages/builder/cypress/integration/datasources/mySql.spec.js
  16. 394
      packages/builder/cypress/integration/datasources/oracle.spec.js
  17. 474
      packages/builder/cypress/integration/datasources/postgreSql.spec.js
  18. 2
      packages/builder/src/components/backend/DataTable/DataTable.svelte
  19. 2
      packages/builder/src/components/backend/DataTable/Table.svelte
  20. 66
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellDrawer.svelte
  21. 34
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/CellEditor.svelte
  22. 57
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnDrawer.svelte
  23. 29
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ColumnEditor/ColumnEditor.svelte
  24. 13
      packages/client/manifest.json
  25. 2
      packages/client/src/components/app/blocks/TableBlock.svelte
  26. 13
      packages/client/src/components/app/table/Table.svelte

1
packages/bbui/package.json

@ -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",

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

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

@ -1,6 +1,6 @@
import filterTests from "../support/filterTests" import filterTests from "../support/filterTests"
filterTests(['smoke', 'all'], () => { filterTests(["smoke", "all"], () => {
context("Create a Table", () => { context("Create a Table", () => {
before(() => { before(() => {
cy.login() cy.login()
@ -55,7 +55,7 @@ filterTests(['smoke', 'all'], () => {
it("Adds 15 rows and checks pagination", () => { it("Adds 15 rows and checks pagination", () => {
// 10 rows per page, 15 rows should create 2 pages within table // 10 rows per page, 15 rows should create 2 pages within table
const totalRows = 16 const totalRows = 16
for (let i = 1; i < totalRows; i++){ for (let i = 1; i < totalRows; i++) {
cy.addRow([i]) cy.addRow([i])
} }
cy.wait(1000) cy.wait(1000)
@ -71,14 +71,14 @@ filterTests(['smoke', 'all'], () => {
// Delete rows, removing second page of rows from table // Delete rows, removing second page of rows from table
const deleteRows = 5 const deleteRows = 5
cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.get(".spectrum-Checkbox-input").check({ force: true })
cy.get(".spectrum-Table-body") cy.get(".spectrum-Table")
cy.contains("Delete 5 row(s)").click() cy.contains("Delete 5 row(s)").click()
cy.get(".spectrum-Modal").contains("Delete").click() cy.get(".spectrum-Modal").contains("Delete").click()
cy.wait(1000) cy.wait(1000)
// Confirm table only has one page // Confirm table only has one page
cy.get(".spectrum-Pagination").within(() => { cy.get(".spectrum-Pagination").within(() => {
cy.get(".spectrum-ActionButton").eq(1).should('not.be.enabled') cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled")
}) })
}) })
} }

188
packages/builder/cypress/integration/createUserAndRoles.spec.js

@ -1,7 +1,6 @@
import filterTests from "../support/filterTests" import filterTests from "../support/filterTests"
filterTests(["smoke", "all"], () => {
filterTests(['smoke', 'all'], () => {
context("Create a User and Assign Roles", () => { context("Create a User and Assign Roles", () => {
before(() => { before(() => {
cy.login() cy.login()
@ -9,7 +8,7 @@ filterTests(['smoke', 'all'], () => {
it("should create a user", () => { it("should create a user", () => {
cy.createUser("bbuser@test.com") cy.createUser("bbuser@test.com")
cy.get(".spectrum-Table-body").should('contain', 'bbuser') cy.get(".spectrum-Table").should("contain", "bbuser")
}) })
it("should confirm there is No Access for a New User", () => { it("should confirm there is No Access for a New User", () => {
@ -17,24 +16,24 @@ filterTests(['smoke', 'all'], () => {
cy.contains("bbuser").click() cy.contains("bbuser").click()
cy.wait(500) cy.wait(500)
// Get No Access table - Confirm it has apps in it // Get No Access table - Confirm it has apps in it
cy.get(".spectrum-Table").eq(1).should('not.contain', 'No rows found') cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found")
// Get Configure Roles table - Confirm it has no apps // Get Configure Roles table - Confirm it has no apps
cy.get(".spectrum-Table").eq(0).contains('No rows found') cy.get(".spectrum-Table").eq(0).contains("No rows found")
}) })
it("should assign role types", () => { it("should assign role types", () => {
// 3 apps minimum required - to assign an app to each role type // 3 apps minimum required - to assign an app to each role type
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length < 3) { if (val.length < 3) {
for (let i = 1; i < 3; i++) { for (let i = 1; i < 3; i++) {
const uuid = () => Cypress._.random(0, 1e6) const uuid = () => Cypress._.random(0, 1e6)
const name = uuid() const name = uuid()
cy.createApp(name) cy.createApp(name)
}
} }
} })
})
// Navigate back to the user // Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
@ -43,90 +42,139 @@ filterTests(['smoke', 'all'], () => {
cy.get(".spectrum-Table").contains("bbuser").click() cy.get(".spectrum-Table").contains("bbuser").click()
cy.wait(1000) cy.wait(1000)
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
cy.get(".spectrum-Table-body").eq(1).find('tr').eq(0).click() cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
cy.wait(500) cy.wait(500)
cy.get(".spectrum-Dialog-grid").contains("Choose an option").click().then(() => { cy.get(".spectrum-Dialog-grid")
cy.wait(1000) .contains("Choose an option")
if (i == 0) { .click()
cy.get(".spectrum-Popover").contains("Admin").click() .then(() => {
} cy.wait(1000)
if (i == 1) { if (i == 0) {
cy.get(".spectrum-Popover").contains("Power").click() cy.get(".spectrum-Popover").contains("Admin").click()
} }
if (i == 2) { if (i == 1) {
cy.get(".spectrum-Popover").contains("Basic").click() cy.get(".spectrum-Popover").contains("Power").click()
} }
cy.wait(1000) if (i == 2) {
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) 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 // Confirm roles exist within Configure roles table
cy.wait(2000) cy.wait(2000)
cy.get(".spectrum-Table-body").eq(0).within((assginedRoles) => { cy.get(".spectrum-Table")
.eq(0)
.within(assginedRoles => {
expect(assginedRoles).to.contain("Admin") expect(assginedRoles).to.contain("Admin")
expect(assginedRoles).to.contain("Power") expect(assginedRoles).to.contain("Power")
expect(assginedRoles).to.contain("Basic") expect(assginedRoles).to.contain("Basic")
}) })
}) })
it("should unassign role types", () => { it("should unassign role types", () => {
// Set each app within Configure roles table to 'No Access' // Set each app within Configure roles table to 'No Access'
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').then((len) => { cy.get(".spectrum-Table")
for (let i = 0; i < len; i ++){ .eq(0)
cy.get(".spectrum-Table-body").eq(0).find('tr').eq(0).click().then(() => { .find(".spectrum-Table-row")
cy.get(".spectrum-Picker").eq(1).click({ force: true }) .its("length")
cy.wait(500) .then(len => {
cy.get(".spectrum-Popover").contains("No Access").click() for (let i = 0; i < len; i++) {
}) cy.get(".spectrum-Table")
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) .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) cy.wait(1000)
} }
}) })
// Confirm Configure roles table no longer has any apps in it // Confirm Configure roles table no longer has any apps in it
cy.get(".spectrum-Table-body").eq(0).contains('No rows found') cy.get(".spectrum-Table").eq(0).contains("No rows found")
}) })
it("should enable Developer access", () => { it("should enable Developer access", () => {
// Enable Developer access // Enable Developer access
cy.get(".field").eq(4).within(() => { cy.get(".field")
cy.get(".spectrum-Switch-input").click({ force: true }) .eq(4)
}) .within(() => {
cy.get(".spectrum-Switch-input").click({ force: true })
})
// No Access table should now be empty // No Access table should now be empty
cy.get(".container").contains("No Access").parent().within(() => { cy.get(".container")
cy.get(".spectrum-Table").contains("No rows found") .contains("No Access")
}) .parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found")
})
// Each app within Configure roles should have Admin access // Each app within Configure roles should have Admin access
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').then((len) => { cy.get(".spectrum-Table")
for (let i = 0; i < len; i++) { .eq(0)
cy.get(".spectrum-Table-body").eq(0).find('tr').eq(i).contains("Admin") .find(".spectrum-Table-row")
cy.wait(500) .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", () => { it("should disable Developer access", () => {
// Disable Developer access // Disable Developer access
cy.get(".field").eq(4).within(() => { cy.get(".field")
cy.get(".spectrum-Switch-input").click({ force: true }) .eq(4)
}) .within(() => {
cy.get(".spectrum-Switch-input").click({ force: true })
})
// Configure roles table should now be empty // Configure roles table should now be empty
cy.get(".container").contains("Configure roles").parent().within(() => { cy.get(".container")
cy.get(".spectrum-Table").contains("No rows found") .contains("Configure roles")
}) .parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found")
})
}) })
it("should delete a user", () => { it("should delete a user", () => {
// Click Delete user button // Click Delete user button
cy.get(".spectrum-Button").contains("Delete user").click({force: true}).then(() => { cy.get(".spectrum-Button")
// Confirm deletion within modal .contains("Delete user")
cy.wait(500) .click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { .then(() => {
cy.get(".spectrum-Button").contains("Delete user").click({force: true}) // Confirm deletion within modal
cy.wait(4000) 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")
cy.get(".spectrum-Table-body").should("not.have.text", "bbuser")
}) })
}) })
}) })

381
packages/builder/cypress/integration/datasources/mySql.spec.js

@ -1,191 +1,222 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("MySQL Datasource Testing", () => { context("MySQL Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "MySQL"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add MySQL data source without configuration", () => {
cy.login() // Select MySQL data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
}) // Attempt to fetch tables without applying configuration
const datasource = "MySQL" cy.intercept("**/datasources").as("datasource")
const queryName = "Cypress Test Query" cy.get(".spectrum-Button")
const queryRename = "CT Query Rename" .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 without configuration", () => { it("should add MySQL data source and fetch tables", () => {
// Select MySQL data source // Add & configure MySQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource")
cy.intercept('**/datasources').as('datasource') cy.addDatasourceConfig(datasource)
cy.get(".spectrum-Button") // Check response from datasource after adding configuration
.contains("Save and fetch tables") cy.wait("@datasource")
.click({ force: true }) cy.get("@datasource").its("response.statusCode").should("eq", 200)
// Intercept Request after button click & apply assertions // Confirm fetch tables was successful
cy.wait("@datasource") cy.get(".spectrum-Table")
cy.get("@datasource").its('response.body') .eq(0)
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:3306') .find(".spectrum-Table-row")
cy.get("@datasource").its('response.body') .its("length")
.should('have.property', 'status', 500) .should("be.gt", 0)
}) })
it("should add MySQL data source and fetch tables", () => { it("should check table fetching error", () => {
// Add & configure MySQL data source // MySQL test data source contains tables without primary keys
cy.selectExternalDatasource(datasource) cy.get(".spectrum-InLineAlert")
cy.intercept('**/datasources').as('datasource') .should("contain", "Error fetching tables")
cy.addDatasourceConfig(datasource) .and("contain", "No primary key constraint found")
// 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-body").eq(0)
.find('tr')
.its('length')
.should('be.gt', 0)
})
it("should check table fetching error", () => { it("should define a One relationship type", () => {
// MySQL test data source contains tables without primary keys // Select relationship type & configure
cy.get(".spectrum-InLineAlert") cy.get(".spectrum-Button")
.should('contain', 'Error fetching tables') .contains("Define relationship")
.and('contain', 'No primary key constraint found') .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 One relationship type", () => { it("should define a Many relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Button")
cy.get(".spectrum-Dialog-grid").within(() => { .contains("Define relationship")
cy.get(".spectrum-Picker").eq(0).click() .click({ force: true })
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("REGIONS").click() cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(3).click() cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(4).click() cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("COUNTRIES").click()
// Save relationship & reload page cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.reload() cy.get(".spectrum-Picker").eq(5).click()
}) cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Confirm table length & column name // Save relationship & reload page
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Button").contains("Save").click({ force: true })
.find('tr') cy.reload()
.its('length') cy.wait(1000)
.should('eq', 1) })
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") // 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 define a Many relationship type", () => { it("should delete relationships", () => {
// Select relationship type & configure // Delete both relationships
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Table")
cy.get(".spectrum-Dialog-grid").within(() => { .eq(1)
cy.get(".spectrum-Picker").eq(0).click() .find(".spectrum-Table-row")
cy.get(".spectrum-Popover").contains("Many").click() .its("length")
cy.get(".spectrum-Picker").eq(1).click() .then(len => {
cy.get(".spectrum-Popover").contains("LOCATIONS").click() for (let i = 0; i < len; i++) {
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Table")
cy.get(".spectrum-Popover").contains("REGIONS").click() .eq(1)
cy.get(".spectrum-Picker").eq(3).click() .within(() => {
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Table-row").eq(0).click()
cy.get(".spectrum-Picker").eq(4).click() cy.wait(500)
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-Dialog-grid").within(() => {
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Button")
.find('tr') .contains("Delete")
.its('length') .click({ force: true })
.should('eq', 2) })
cy.get(".spectrum-Table-cell") cy.reload()
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") }
}) // Confirm relationships no longer exist
cy.get(".spectrum-Body").should(
it("should delete relationships", () => { "contain",
// Delete both relationships "No relationships configured"
cy.get(".spectrum-Table-body") )
.eq(1).find('tr').its('length') })
.then((len) => { })
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table-body").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", () => { it("should add a query", () => {
// Add query // Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true }) cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item").eq(0).within(() => { cy.get(".spectrum-Form-item")
cy.get("input").type(queryName) .eq(0)
}) .within(() => {
// Insert Query within Fields section cy.get("input").type(queryName)
cy.get(".CodeMirror textarea").eq(0) })
.type("SELECT * FROM books", { force: true }) // Insert Query within Fields section
// Intercept query execution cy.get(".CodeMirror textarea")
cy.intercept('**/queries/preview').as('query') .eq(0)
cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) .type("SELECT * FROM books", { force: true })
cy.wait(500) // Intercept query execution
cy.wait("@query") cy.intercept("**/queries/preview").as("query")
// Assert against Status Code & Body cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.get("@query").its('response.statusCode') cy.wait(500)
.should('eq', 200) cy.wait("@query")
cy.get("@query").its('response.body') // Assert against Status Code & Body
.should('not.be.empty') cy.get("@query").its("response.statusCode").should("eq", 200)
// Save query cy.get("@query").its("response.body").should("not.be.empty")
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) // Save query
cy.get(".nav-item").should('contain', queryName) cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
}) cy.get(".nav-item").should("contain", queryName)
})
it("should duplicate a query", () => { it("should duplicate a query", () => {
// Get last nav item - The query // Get last nav item - The query
cy.get(".nav-item").last().within(() => { cy.get(".nav-item")
cy.get(".icon").eq(1).click({ force: true }) .last()
}) .within(() => {
// Select and confirm duplication cy.get(".icon").eq(1).click({ force: true })
cy.get(".spectrum-Menu").contains("Duplicate").click() })
cy.get(".nav-item").should('contain', queryName + ' (1)') // 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", () => { it("should edit a query name", () => {
// Rename query // Rename query
cy.get(".spectrum-Form-item").eq(0).within(() => { cy.get(".spectrum-Form-item")
cy.get("input").clear().type(queryRename) .eq(0)
}) .within(() => {
// Save query cy.get("input").clear().type(queryRename)
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) })
cy.get(".nav-item").should('contain', 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", () => { it("should delete a query", () => {
// Get last nav item - The query // Get last nav item - The query
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
cy.get(".nav-item").last().within(() => { cy.get(".nav-item")
cy.get(".icon").eq(1).click({ force: true }) .last()
}) .within(() => {
// Select Delete cy.get(".icon").eq(1).click({ force: true })
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)
}) })
// 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)
})
}
})
}) })

394
packages/builder/cypress/integration/datasources/oracle.spec.js

@ -1,196 +1,230 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("Oracle Datasource Testing", () => { context("Oracle Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "Oracle"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add Oracle data source and skip table fetch", () => {
cy.login() // Select Oracle data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
}) // Skip table fetch - no config added
const datasource = "Oracle" cy.get(".spectrum-Button")
const queryName = "Cypress Test Query" .contains("Skip table fetch")
const queryRename = "CT Query Rename" .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 skip table fetch", () => { it("Should add Oracle data source and fetch tables without configuration", () => {
// Select Oracle data source // Select Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Skip table fetch - no config added // Attempt to fetch tables without applying configuration
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.intercept("**/datasources").as("datasource")
cy.wait(500) cy.get(".spectrum-Button")
// Confirm config contains localhost .contains("Save and fetch tables")
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', 'localhost') .click({ force: true })
// Add another Oracle data source, configure & skip table fetch // Intercept Request after button click & apply assertions
cy.selectExternalDatasource(datasource) cy.wait("@datasource")
cy.addDatasourceConfig(datasource, true) cy.get("@datasource")
// Confirm config and no tables .its("response.body")
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', Cypress.env("oracle").HOST) .should("have.property", "status", 500)
cy.get(".spectrum-Body").eq(2).should('contain', 'No tables found.') })
})
it("Should add Oracle data source and fetch tables without configuration", () => { it("should add Oracle data source and fetch tables", () => {
// Select Oracle data source // Add & configure Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource")
cy.intercept('**/datasources').as('datasource') cy.addDatasourceConfig(datasource)
cy.get(".spectrum-Button") // Check response from datasource after adding configuration
.contains("Save and fetch tables") cy.wait("@datasource")
.click({ force: true }) cy.get("@datasource").its("response.statusCode").should("eq", 200)
// Intercept Request after button click & apply assertions // Confirm fetch tables was successful
cy.wait("@datasource") cy.get(".spectrum-Table")
cy.get("@datasource").its('response.body') .eq(0)
.should('have.property', 'status', 500) .find(".spectrum-Table-row")
}) .its("length")
.should("be.gt", 0)
})
it("should add Oracle data source and fetch tables", () => { it("should define a One relationship type", () => {
// Add & configure Oracle data source // Select relationship type & configure
cy.selectExternalDatasource(datasource) cy.get(".spectrum-Button")
cy.intercept('**/datasources').as('datasource') .contains("Define relationship")
cy.addDatasourceConfig(datasource) .click({ force: true })
// Check response from datasource after adding configuration cy.get(".spectrum-Dialog-grid").within(() => {
cy.wait("@datasource") cy.get(".spectrum-Picker").eq(0).click()
cy.get("@datasource").its('response.statusCode') cy.get(".spectrum-Popover").contains("One").click()
.should('eq', 200) cy.get(".spectrum-Picker").eq(1).click()
// Confirm fetch tables was successful cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Table-body").eq(0) cy.get(".spectrum-Picker").eq(2).click()
.find('tr') cy.get(".spectrum-Popover").contains("REGION_ID").click()
.its('length') cy.get(".spectrum-Picker").eq(3).click()
.should('be.gt', 0) 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 One relationship type", () => { it("should define a Many relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Button")
cy.get(".spectrum-Dialog-grid").within(() => { .contains("Define relationship")
cy.get(".spectrum-Picker").eq(0).click() .click({ force: true })
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("REGIONS").click() cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(3).click() cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(4).click() cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("COUNTRIES").click()
// Save relationship & reload page cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.reload() cy.get(".spectrum-Picker").eq(5).click()
}) cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Confirm table length & column name // Save relationship & reload page
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Button").contains("Save").click({ force: true })
.find('tr') cy.reload()
.its('length') })
.should('eq', 1) // Confirm table length & relationship name
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") 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 define a Many relationship type", () => { it("should delete relationships", () => {
// Select relationship type & configure // Delete both relationships
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Table")
cy.get(".spectrum-Dialog-grid").within(() => { .eq(1)
cy.get(".spectrum-Picker").eq(0).click() .find(".spectrum-Table-row")
cy.get(".spectrum-Popover").contains("Many").click() .its("length")
cy.get(".spectrum-Picker").eq(1).click() .then(len => {
cy.get(".spectrum-Popover").contains("LOCATIONS").click() for (let i = 0; i < len; i++) {
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Table")
cy.get(".spectrum-Popover").contains("REGIONS").click() .eq(1)
cy.get(".spectrum-Picker").eq(3).click() .within(() => {
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Table-row").eq(0).click()
cy.get(".spectrum-Picker").eq(4).click() cy.wait(500)
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-Dialog-grid").within(() => {
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Button")
.find('tr') .contains("Delete")
.its('length') .click({ force: true })
.should('eq', 2) })
cy.get(".spectrum-Table-cell") cy.reload()
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") }
}) // Confirm relationships no longer exist
cy.get(".spectrum-Body").should(
"contain",
"No relationships configured"
)
})
})
it("should delete relationships", () => { it("should add a query", () => {
// Delete both relationships // Add query
cy.get(".spectrum-Table-body") cy.get(".spectrum-Button").contains("Add query").click({ force: true })
.eq(1).find('tr').its('length') cy.get(".spectrum-Form-item")
.then((len) => { .eq(0)
for (let i = 0; i < len; i++) { .within(() => {
cy.get(".spectrum-Table-body").eq(1).within(() => { cy.get("input").type(queryName)
cy.get(".spectrum-Table-row").eq(0).click() })
cy.wait(500) // Insert Query within Fields section
}) cy.get(".CodeMirror textarea")
cy.get(".spectrum-Dialog-grid").within(() => { .eq(0)
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) .type("SELECT * FROM JOBS", { force: true })
}) // Intercept query execution
cy.reload() cy.intercept("**/queries/preview").as("query")
} cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
// Confirm relationships no longer exist cy.wait(500)
cy.get(".spectrum-Body").should('contain', 'No relationships configured') 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 add a query", () => { it("should duplicate a query", () => {
// Add query // Get query nav item
cy.get(".spectrum-Button").contains("Add query").click({ force: true }) cy.get(".nav-item")
cy.get(".spectrum-Form-item").eq(0).within(() => { .contains(queryName)
cy.get("input").type(queryName) .parent()
}) .within(() => {
// Insert Query within Fields section cy.get(".spectrum-Icon").eq(1).click({ force: true })
cy.get(".CodeMirror textarea").eq(0) })
.type("SELECT * FROM JOBS", { force: true }) // Select and confirm duplication
// Intercept query execution cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.intercept('**/queries/preview').as('query') cy.get(".nav-item").should("contain", queryName + " (1)")
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", () => { it("should edit a query name", () => {
// Get query nav item // Rename query
cy.get(".nav-item").contains(queryName).parent().within(() => { cy.get(".spectrum-Form-item")
cy.get(".spectrum-Icon").eq(1).click({ force: true }) .eq(0)
}) .within(() => {
// Select and confirm duplication cy.get("input").clear().type(queryRename)
cy.get(".spectrum-Menu").contains("Duplicate").click() })
cy.get(".nav-item").should('contain', queryName + ' (1)') // Save query
}) cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should("contain", queryRename)
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", () => { it("should delete a query", () => {
// Get query nav item - QueryName // Get query nav item - QueryName
cy.get(".nav-item").contains(queryName).parent().within(() => { cy.get(".nav-item")
cy.get(".spectrum-Icon").eq(1).click({ force: true }) .contains(queryName)
}) .parent()
.within(() => {
cy.get(".spectrum-Icon").eq(1).click({ force: true })
})
// Select Delete // Select Delete
cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button").contains("Delete Query").click({ force: true }) cy.get(".spectrum-Button")
cy.wait(1000) .contains("Delete Query")
.click({ force: true })
cy.wait(1000)
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should('not.contain', queryName) cy.get(".nav-item").should("not.contain", queryName)
}) })
} }
}) })
}) })

474
packages/builder/cypress/integration/datasources/postgreSql.spec.js

@ -1,241 +1,285 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("PostgreSQL Datasource Testing", () => { context("PostgreSQL Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "PostgreSQL"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add PostgreSQL data source without configuration", () => {
cy.login() // Select PostgreSQL data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
}) // Attempt to fetch tables without applying configuration
const datasource = "PostgreSQL" cy.intercept("**/datasources").as("datasource")
const queryName = "Cypress Test Query" cy.get(".spectrum-Button")
const queryRename = "CT Query Rename" .contains("Save and fetch tables")
.click({ force: true })
it("Should add PostgreSQL data source without configuration", () => { // Intercept Request after button click & apply assertions
// Select PostgreSQL data source cy.wait("@datasource")
cy.selectExternalDatasource(datasource) cy.get("@datasource")
// Attempt to fetch tables without applying configuration .its("response.body")
cy.intercept('**/datasources').as('datasource') .should(
cy.get(".spectrum-Button") "have.property",
.contains("Save and fetch tables") "message",
.click({ force: true }) "connect ECONNREFUSED 127.0.0.1:5432"
// Intercept Request after button click & apply assertions )
cy.wait("@datasource") cy.get("@datasource")
cy.get("@datasource").its('response.body') .its("response.body")
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:5432') .should("have.property", "status", 500)
cy.get("@datasource").its('response.body') })
.should('have.property', 'status', 500)
})
it("should add PostgreSQL data source and fetch tables", () => { it("should add PostgreSQL data source and fetch tables", () => {
// Add & configure PostgreSQL data source // Add & configure PostgreSQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.intercept('**/datasources').as('datasource') cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)
// Check response from datasource after adding configuration // Check response from datasource after adding configuration
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource").its('response.statusCode') cy.get("@datasource").its("response.statusCode").should("eq", 200)
.should('eq', 200) // Confirm fetch tables was successful
// Confirm fetch tables was successful cy.get(".spectrum-Table")
cy.get(".spectrum-Table-body").eq(0) .eq(0)
.find('tr') .find(".spectrum-Table-row")
.its('length') .its("length")
.should('be.gt', 0) .should("be.gt", 0)
}) })
it("should define a One relationship type", () => { it("should define a One relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Button")
cy.get(".spectrum-Dialog-grid").within(() => { .contains("Define relationship")
cy.get(".spectrum-Picker").eq(0).click() .click({ force: true })
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("REGIONS").click() cy.get(".spectrum-Popover").contains("One").click()
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click() cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.get(".spectrum-Picker").eq(4).click() cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("COUNTRIES").click()
// Save relationship & reload page cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.reload() // Save relationship & reload page
}) cy.get(".spectrum-Button").contains("Save").click({ force: true })
// Confirm table length & column name cy.reload()
cy.get(".spectrum-Table-body").eq(1) })
.find('tr') // Confirm table length & column name
.its('length') cy.get(".spectrum-Table")
.should('eq', 1) .eq(1)
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") .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", () => { it("should define a Many relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Button")
cy.get(".spectrum-Dialog-grid").within(() => { .contains("Define relationship")
cy.get(".spectrum-Picker").eq(0).click() .click({ force: true })
cy.get(".spectrum-Popover").contains("Many").click() cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("LOCATIONS").click() cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGIONS").click() cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(3).click() cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(4).click() cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click() cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(5).click() cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
// Save relationship & reload page cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.reload() // Save relationship & reload page
}) cy.get(".spectrum-Button").contains("Save").click({ force: true })
// Confirm table length & relationship name cy.reload()
cy.get(".spectrum-Table-body").eq(1) })
.find('tr') // Confirm table length & relationship name
.its('length') cy.get(".spectrum-Table")
.should('eq', 2) .eq(1)
cy.get(".spectrum-Table-cell") .find(".spectrum-Table-row")
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") .its("length")
}) .should("eq", 2)
cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
})
it("should delete a relationship", () => { it("should delete a relationship", () => {
cy.get(".hierarchy-items-container").contains(datasource).click() cy.get(".hierarchy-items-container").contains(datasource).click()
cy.reload() cy.reload()
// Delete one relationship // Delete one relationship
cy.get(".spectrum-Table-body").eq(1).within(() => { cy.get(".spectrum-Table")
cy.get(".spectrum-Table-row").eq(0).click() .eq(1)
cy.wait(500) .within(() => {
}) cy.get(".spectrum-Table-row").eq(0).click()
cy.get(".spectrum-Dialog-grid").within(() => { cy.wait(500)
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) })
}) cy.get(".spectrum-Dialog-grid").within(() => {
cy.reload() cy.get(".spectrum-Button").contains("Delete").click({ force: true })
// Confirm relationship was deleted })
cy.get(".spectrum-Table-body") cy.reload()
.eq(1).find('tr').its('length').should('eq', 1) // Confirm relationship was deleted
}) cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
})
it("should add a query", () => { it("should add a query", () => {
// Add query // Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true }) cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item").eq(0).within(() => { cy.get(".spectrum-Form-item")
cy.get("input").type(queryName) .eq(0)
}) .within(() => {
// Insert Query within Fields section cy.get("input").type(queryName)
cy.get(".CodeMirror textarea").eq(0) })
.type("SELECT * FROM books", { force: true }) // Insert Query within Fields section
// Intercept query execution cy.get(".CodeMirror textarea")
cy.intercept('**/queries/preview').as('query') .eq(0)
cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) .type("SELECT * FROM books", { force: true })
cy.wait(500) // Intercept query execution
cy.wait("@query") cy.intercept("**/queries/preview").as("query")
// Assert against Status Code & Body cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.get("@query").its('response.statusCode') cy.wait(500)
.should('eq', 200) cy.wait("@query")
cy.get("@query").its('response.body') // Assert against Status Code & Body
.should('not.be.empty') cy.get("@query").its("response.statusCode").should("eq", 200)
// Save query cy.get("@query").its("response.body").should("not.be.empty")
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) // Save query
cy.get(".hierarchy-items-container").should('contain', queryName) 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", () => { it("should switch to schema with no tables", () => {
// Switch Schema - To one without any tables // Switch Schema - To one without any tables
cy.get(".hierarchy-items-container").contains(datasource).click() cy.get(".hierarchy-items-container").contains(datasource).click()
switchSchema("randomText") switchSchema("randomText")
// No tables displayed // No tables displayed
cy.get(".spectrum-Body").eq(2).should('contain', 'No tables found') cy.get(".spectrum-Body").eq(2).should("contain", "No tables found")
// Previously created query should be visible // Previously created query should be visible
cy.get(".spectrum-Table-body").should('contain', queryName) cy.get(".spectrum-Table").should("contain", queryName)
}) })
it("should switch schemas", () => { it("should switch schemas", () => {
// Switch schema - To one with tables // Switch schema - To one with tables
switchSchema("1") switchSchema("1")
// Confirm tables exist - Check for specific one // Confirm tables exist - Check for specific one
cy.get(".spectrum-Table-body").eq(0).should('contain', 'test') cy.get(".spectrum-Table").eq(0).should("contain", "test")
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').should('eq', 1) cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
// Confirm specific table visible within left nav bar // Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should('contain', 'test') cy.get(".hierarchy-items-container").should("contain", "test")
// Switch back to public schema // Switch back to public schema
switchSchema("public") switchSchema("public")
// Confirm tables exist - again // Confirm tables exist - again
cy.get(".spectrum-Table-body").eq(0).should('contain', 'REGIONS') cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS")
cy.get(".spectrum-Table-body").eq(0) cy.get(".spectrum-Table")
.find('tr').its('length').should('be.gt', 1) .eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("be.gt", 1)
// Confirm specific table visible within left nav bar // Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should('contain', 'REGIONS') cy.get(".hierarchy-items-container").should("contain", "REGIONS")
// No relationships and one query // No relationships and one query
cy.get(".spectrum-Body").eq(3).should('contain', 'No relationships configured.') cy.get(".spectrum-Body")
cy.get(".spectrum-Table-body").eq(1).should('contain', queryName) .eq(3)
}) .should("contain", "No relationships configured.")
cy.get(".spectrum-Table").eq(1).should("contain", queryName)
})
it("should duplicate a query", () => { it("should duplicate a query", () => {
// Get last nav item - The query // Get last nav item - The query
cy.get(".nav-item").last().within(() => { cy.get(".nav-item")
cy.get(".icon").eq(1).click({ force: true }) .last()
}) .within(() => {
// Select and confirm duplication cy.get(".icon").eq(1).click({ force: true })
cy.get(".spectrum-Menu").contains("Duplicate").click() })
cy.get(".nav-item").should('contain', queryName + ' (1)') // 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", () => { it("should edit a query name", () => {
// Access query // Access query
cy.get(".hierarchy-items-container").contains(queryName + ' (1)').click() cy.get(".hierarchy-items-container")
.contains(queryName + " (1)")
.click()
// Rename query // Rename query
cy.get(".spectrum-Form-item").eq(0).within(() => { cy.get(".spectrum-Form-item")
cy.get("input").clear().type(queryRename) .eq(0)
}) .within(() => {
cy.get("input").clear().type(queryRename)
})
// Run and Save query // Run and Save query
cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500) cy.wait(500)
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryRename) cy.get(".nav-item").should("contain", queryRename)
}) })
it("should delete a query", () => { it("should delete a query", () => {
// Get last nav item - The query // Get last nav item - The query
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
cy.get(".nav-item").last().within(() => { cy.get(".nav-item")
cy.get(".icon").eq(1).click({ force: true }) .last()
}) .within(() => {
// Select Delete cy.get(".icon").eq(1).click({ force: true })
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)
}) })
// Select Delete
const switchSchema = (schema) => { cy.get(".spectrum-Menu").contains("Delete").click()
// Edit configuration - Change Schema cy.get(".spectrum-Button")
cy.get(".spectrum-Textfield").eq(6).within(() => { .contains("Delete Query")
cy.get('input').clear().type(schema) .click({ force: true })
}) cy.wait(1000)
// 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)
}
} }
}) // 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)
}
}
})
}) })

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}

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>

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",

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

Loading…
Cancel
Save