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

Table V2
This commit is contained in:
Andrew Kingston 2022-02-23 14:46:27 +00:00 committed by GitHub
commit b2ab0954fc
26 changed files with 2374 additions and 1028 deletions

View File

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

View File

@ -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;

View File

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

View File

@ -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}>
<div class="center">
<img src={attachment.url} alt={attachment.extension} /> <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);

View File

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

View File

@ -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'};">
<svelte:component
this={renderer}
{row}
{schema}
value={cellValue}
on:clickrelationship
>
<slot /> <slot />
</svelte:component> </svelte:component>
</div>
{/if} {/if}
<style>
div {
display: contents;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
$: sortedRows = sortRows(rows, sortColumn, sortOrder) loaded,
$: fields = getFields(schema, showAutoColumns) height,
$: showEditColumn = allowEditRows || allowSelectRows rows.length,
rowCount,
// Scrolling state rowHeight
let timeout
let nextScrollTop = 0
let scrollTop = 0
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
$: lastVisibleRow = calculateLastVisibleRow(
firstVisibleRow,
visibleRowCount,
rows.length
) )
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
// Reset state when data changes $: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: rows.length, reset() $: gridStyle = getGridStyle(fields, schema, showEditColumn)
const reset = () => { $: showEditColumn = allowEditRows || allowSelectRows
nextScrollTop = 0 $: cellStyles = computeCellStyles(schema)
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,33 +203,57 @@
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
{#if !loaded} class="wrapper"
<div class="loading" style={contentStyle} /> class:wrapper--quiet={quiet}
{:else} class:wrapper--compact={compact}
<div bind:offsetHeight={height}
on:scroll={onScroll}
class:quiet
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`} style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
class="container" >
> {#if !loaded}
<div style={contentStyle}> <div class="loading" style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}> <ProgressCircle />
</div>
{:else}
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
{#if fields.length} {#if fields.length}
<thead class="spectrum-Table-head"> <div class="spectrum-Table-head">
<tr>
{#if showEditColumn} {#if showEditColumn}
<th class="spectrum-Table-headCell"> <div
<div class="spectrum-Table-headCell-content"> class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
>
{editColumnTitle || ""} {editColumnTitle || ""}
</div> </div>
</th>
{/if} {/if}
{#each fields as field} {#each fields as field}
<th <div
class="spectrum-Table-headCell" class="spectrum-Table-headCell"
class:spectrum-Table-headCell--alignCenter={schema[field]
.align === "Center"}
class:spectrum-Table-headCell--alignRight={schema[field].align ===
"Right"}
class:is-sortable={schema[field].sortable !== false} class:is-sortable={schema[field].sortable !== false}
class:is-sorted-desc={sortColumn === field && class:is-sorted-desc={sortColumn === field &&
sortOrder === "Descending"} sortOrder === "Descending"}
@ -247,7 +261,6 @@
sortOrder === "Ascending"} sortOrder === "Ascending"}
on:click={() => sortBy(schema[field])} on:click={() => sortBy(schema[field])}
> >
<div class="spectrum-Table-headCell-content">
<div class="title">{getDisplayName(schema[field])}</div> <div class="title">{getDisplayName(schema[field])}</div>
{#if schema[field]?.autocolumn} {#if schema[field]?.autocolumn}
<svg <svg
@ -276,26 +289,20 @@
</svg> </svg>
{/if} {/if}
</div> </div>
</th>
{/each} {/each}
</tr> </div>
</thead>
{/if} {/if}
<tbody class="spectrum-Table-body"> {#if sortedRows?.length}
{#if sortedRows?.length && fields.length}
{#each sortedRows as row, idx} {#each sortedRows as row, idx}
<tr <div
class="spectrum-Table-row"
on:click={() => dispatch("click", row)} on:click={() => dispatch("click", row)}
on:click={() => toggleSelectRow(row)} on:click={() => toggleSelectRow(row)}
class="spectrum-Table-row"
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}
> >
{#if idx >= firstVisibleRow && idx <= lastVisibleRow}
{#if showEditColumn} {#if showEditColumn}
<td <div
class="spectrum-Table-cell spectrum-Table-cell--divider" class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
> >
<div class="spectrum-Table-cell-content">
<SelectEditRenderer <SelectEditRenderer
data={row} data={row}
selected={selectedRows.includes(row)} selected={selectedRows.includes(row)}
@ -305,15 +312,13 @@
{allowEditRows} {allowEditRows}
/> />
</div> </div>
</td>
{/if} {/if}
{#each fields as field} {#each fields as field}
<td <div
class="spectrum-Table-cell" class="spectrum-Table-cell"
class:spectrum-Table-cell--divider={!!schema[field] class:spectrum-Table-cell--divider={!!schema[field].divider}
.divider} style={cellStyles[field]}
> >
<div class="spectrum-Table-cell-content">
<CellRenderer <CellRenderer
{customRenderers} {customRenderers}
{row} {row}
@ -324,59 +329,107 @@
<slot /> <slot />
</CellRenderer> </CellRenderer>
</div> </div>
</td>
{/each} {/each}
{/if} </div>
</tr>
{/each} {/each}
{:else} {:else}
<tr class="placeholder-row"> <div class="placeholder" class:placeholder--no-fields={!fields?.length}>
{#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"> <div class="placeholder-content">
<svg <svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" /> <use xlink:href="#spectrum-icon-18-Table" />
</svg> </svg>
<div>No rows found</div> <div>No rows found</div>
</div> </div>
</div> </div>
</tr>
{/if} {/if}
</tbody>
</table>
</div>
</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 {
--table-bg: var(--spectrum-alias-background-color-transparent);
}
.wrapper--compact {
--cell-padding: var(--spectrum-global-dimension-size-150);
} }
.container { /* Loading */
height: 100%; .loading {
position: relative; display: grid;
place-items: center;
min-height: 100px;
}
/* Table */
.spectrum-Table {
width: 100%;
border-radius: 0;
display: grid;
overflow: auto; overflow: auto;
} }
.container.quiet {
border: none;
}
table {
width: 100%;
}
/* 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; /* Table rows */
transition: opacity 0.2s ease; .spectrum-Table-row {
display: contents;
}
.spectrum-Table-row:hover .spectrum-Table-cell {
/*background-color: var(--hover-bg) !important;*/
}
.spectrum-Table-row:hover .spectrum-Table-cell:after {
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);
} }
th { /* Table cells */
vertical-align: middle; .spectrum-Table-cell {
height: var(--header-height); flex: 1 1 auto;
position: sticky; padding: 0 calc(var(--cell-padding) / 1.33);
top: 0; border-top: none;
z-index: 2; border-bottom: none;
background-color: var(--spectrum-alias-background-color-secondary); border-radius: 0;
border-bottom: 1px solid text-overflow: ellipsis;
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.spectrum-Table-headCell-content {
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);
background-color: var(--table-bg);
z-index: 1;
} }
.spectrum-Table-headCell-content .title { .spectrum-Table-cell--divider {
overflow: hidden; padding-right: var(--cell-padding);
text-overflow: ellipsis; }
.spectrum-Table-cell--divider + .spectrum-Table-cell {
padding-left: var(--cell-padding);
}
.spectrum-Table-cell--edit {
position: sticky;
left: 0;
z-index: 2;
}
.spectrum-Table-cell:after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: transparent;
top: 0;
left: 0;
pointer-events: none;
transition: background-color
var(--spectrum-global-animation-duration-100, 0.13s) ease-in-out;
} }
.placeholder-row { /* Placeholder */
position: relative;
height: 150px;
}
.placeholder-row td {
border-top: none !important;
border-bottom: none !important;
}
.placeholder-offset {
width: 1px;
}
.placeholder { .placeholder {
top: 0;
height: 100%;
left: 0;
width: 100%;
position: absolute;
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>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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,9 +16,9 @@ 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", () => {
@ -43,9 +42,18 @@ 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")
.contains("Choose an option")
.click()
.then(() => {
cy.wait(1000) cy.wait(1000)
if (i == 0) { if (i == 0) {
cy.get(".spectrum-Popover").contains("Admin").click() cy.get(".spectrum-Popover").contains("Admin").click()
@ -57,12 +65,16 @@ filterTests(['smoke', 'all'], () => {
cy.get(".spectrum-Popover").contains("Basic").click() cy.get(".spectrum-Popover").contains("Basic").click()
} }
cy.wait(1000) cy.wait(1000)
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) 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")
@ -71,35 +83,61 @@ filterTests(['smoke', 'all'], () => {
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")
.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.get(".spectrum-Picker").eq(1).click({ force: true })
cy.wait(500) cy.wait(500)
cy.get(".spectrum-Popover").contains("No Access").click() cy.get(".spectrum-Popover").contains("No Access").click()
}) })
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) 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")
.eq(4)
.within(() => {
cy.get(".spectrum-Switch-input").click({ force: true }) 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")
.contains("No Access")
.parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found") 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")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table-body").eq(0).find('tr').eq(i).contains("Admin") cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.eq(i)
.contains("Admin")
cy.wait(500) cy.wait(500)
} }
}) })
@ -107,26 +145,36 @@ filterTests(['smoke', 'all'], () => {
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")
.eq(4)
.within(() => {
cy.get(".spectrum-Switch-input").click({ force: true }) 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")
.contains("Configure roles")
.parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found") 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")
.contains("Delete user")
.click({ force: true })
.then(() => {
// Confirm deletion within modal // Confirm deletion within modal
cy.wait(500) cy.wait(500)
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete user").click({force: true}) cy.get(".spectrum-Button")
.contains("Delete user")
.click({ force: true })
cy.wait(4000) cy.wait(4000)
}) })
}) })
cy.get(".spectrum-Table-body").should("not.have.text", "bbuser") cy.get(".spectrum-Table").should("not.have.text", "bbuser")
}) })
}) })
}) })

View File

@ -1,9 +1,8 @@
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(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.createTestApp()
@ -16,44 +15,52 @@ filterTests(['all'], () => {
// Select MySQL data source // Select MySQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept('**/datasources').as('datasource') cy.intercept("**/datasources").as("datasource")
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Save and fetch tables")
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions // Intercept Request after button click & apply assertions
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource").its('response.body') cy.get("@datasource")
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:3306') .its("response.body")
cy.get("@datasource").its('response.body') .should(
.should('have.property', 'status', 500) "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", () => { it("should add MySQL data source and fetch tables", () => {
// Add & configure MySQL data source // Add & configure MySQL 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-body").eq(0) cy.get(".spectrum-Table")
.find('tr') .eq(0)
.its('length') .find(".spectrum-Table-row")
.should('be.gt', 0) .its("length")
.should("be.gt", 0)
}) })
it("should check table fetching error", () => { it("should check table fetching error", () => {
// MySQL test data source contains tables without primary keys // MySQL test data source contains tables without primary keys
cy.get(".spectrum-InLineAlert") cy.get(".spectrum-InLineAlert")
.should('contain', 'Error fetching tables') .should("contain", "Error fetching tables")
.and('contain', 'No primary key constraint found') .and("contain", "No primary key constraint found")
}) })
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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Popover").contains("One").click()
@ -70,16 +77,19 @@ filterTests(['all'], () => {
cy.reload() cy.reload()
}) })
// Confirm table length & column name // Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 1) .its("length")
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") .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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click() cy.get(".spectrum-Popover").contains("Many").click()
@ -99,92 +109,113 @@ filterTests(['all'], () => {
cy.wait(1000) cy.wait(1000)
}) })
// Confirm table length & relationship name // Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 2) .its("length")
cy.get(".spectrum-Table-cell") .should("eq", 2)
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
}) })
it("should delete relationships", () => { it("should delete relationships", () => {
// Delete both relationships // Delete both relationships
cy.get(".spectrum-Table-body") cy.get(".spectrum-Table")
.eq(1).find('tr').its('length') .eq(1)
.then((len) => { .find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table-body").eq(1).within(() => { cy.get(".spectrum-Table")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click() cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500) cy.wait(500)
}) })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) cy.get(".spectrum-Button")
.contains("Delete")
.click({ force: true })
}) })
cy.reload() cy.reload()
} }
// Confirm relationships no longer exist // Confirm relationships no longer exist
cy.get(".spectrum-Body").should('contain', 'No relationships configured') 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")
.eq(0)
.within(() => {
cy.get("input").type(queryName) cy.get("input").type(queryName)
}) })
// Insert Query within Fields section // Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0) cy.get(".CodeMirror textarea")
.eq(0)
.type("SELECT * FROM books", { force: true }) .type("SELECT * FROM books", { force: true })
// Intercept query execution // Intercept query execution
cy.intercept('**/queries/preview').as('query') cy.intercept("**/queries/preview").as("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.wait("@query") cy.wait("@query")
// Assert against Status Code & Body // Assert against Status Code & Body
cy.get("@query").its('response.statusCode') cy.get("@query").its("response.statusCode").should("eq", 200)
.should('eq', 200) cy.get("@query").its("response.body").should("not.be.empty")
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query // Save query
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', queryName) 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")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".icon").eq(1).click({ force: true })
}) })
// Select and confirm duplication // Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)') 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")
.eq(0)
.within(() => {
cy.get("input").clear().type(queryRename) cy.get("input").clear().type(queryRename)
}) })
// Save query // Save query
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")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".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")
.contains("Delete Query")
.click({ force: true })
cy.wait(1000) cy.wait(1000)
} }
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should('not.contain', queryName) cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should('not.contain', queryRename) cy.get(".nav-item").should("not.contain", queryRename)
}) })
} }
}) })

View File

@ -1,9 +1,8 @@
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(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.createTestApp()
@ -16,51 +15,60 @@ filterTests(['all'], () => {
// Select Oracle data source // Select Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Skip table fetch - no config added // Skip table fetch - no config added
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.get(".spectrum-Button")
.contains("Skip table fetch")
.click({ force: true })
cy.wait(500) cy.wait(500)
// Confirm config contains localhost // Confirm config contains localhost
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', 'localhost') cy.get(".spectrum-Textfield-input")
.eq(1)
.should("have.value", "localhost")
// Add another Oracle data source, configure & skip table fetch // Add another Oracle data source, configure & skip table fetch
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource, true) cy.addDatasourceConfig(datasource, true)
// Confirm config and no tables // Confirm config and no tables
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', Cypress.env("oracle").HOST) cy.get(".spectrum-Textfield-input")
cy.get(".spectrum-Body").eq(2).should('contain', 'No tables found.') .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", () => { 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)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept('**/datasources').as('datasource') cy.intercept("**/datasources").as("datasource")
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Save and fetch tables")
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions // Intercept Request after button click & apply assertions
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource").its('response.body') cy.get("@datasource")
.should('have.property', 'status', 500) .its("response.body")
.should("have.property", "status", 500)
}) })
it("should add Oracle data source and fetch tables", () => { it("should add Oracle data source and fetch tables", () => {
// Add & configure Oracle data source // Add & configure Oracle 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-body").eq(0) cy.get(".spectrum-Table")
.find('tr') .eq(0)
.its('length') .find(".spectrum-Table-row")
.should('be.gt', 0) .its("length")
.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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Popover").contains("One").click()
@ -77,16 +85,19 @@ filterTests(['all'], () => {
cy.reload() cy.reload()
}) })
// Confirm table length & column name // Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 1) .its("length")
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") .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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click() cy.get(".spectrum-Popover").contains("Many").click()
@ -105,91 +116,114 @@ filterTests(['all'], () => {
cy.reload() cy.reload()
}) })
// Confirm table length & relationship name // Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 2) .its("length")
cy.get(".spectrum-Table-cell") .should("eq", 2)
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
}) })
it("should delete relationships", () => { it("should delete relationships", () => {
// Delete both relationships // Delete both relationships
cy.get(".spectrum-Table-body") cy.get(".spectrum-Table")
.eq(1).find('tr').its('length') .eq(1)
.then((len) => { .find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table-body").eq(1).within(() => { cy.get(".spectrum-Table")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click() cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500) cy.wait(500)
}) })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) cy.get(".spectrum-Button")
.contains("Delete")
.click({ force: true })
}) })
cy.reload() cy.reload()
} }
// Confirm relationships no longer exist // Confirm relationships no longer exist
cy.get(".spectrum-Body").should('contain', 'No relationships configured') 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")
.eq(0)
.within(() => {
cy.get("input").type(queryName) cy.get("input").type(queryName)
}) })
// Insert Query within Fields section // Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0) cy.get(".CodeMirror textarea")
.eq(0)
.type("SELECT * FROM JOBS", { force: true }) .type("SELECT * FROM JOBS", { force: true })
// Intercept query execution // Intercept query execution
cy.intercept('**/queries/preview').as('query') cy.intercept("**/queries/preview").as("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.wait("@query") cy.wait("@query")
// Assert against Status Code & Body // Assert against Status Code & Body
cy.get("@query").its('response.statusCode') cy.get("@query").its("response.statusCode").should("eq", 200)
.should('eq', 200) cy.get("@query").its("response.body").should("not.be.empty")
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query // Save query
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', queryName) cy.get(".nav-item").should("contain", queryName)
}) })
it("should duplicate a query", () => { it("should duplicate a query", () => {
// Get query nav item // Get query nav item
cy.get(".nav-item").contains(queryName).parent().within(() => { cy.get(".nav-item")
.contains(queryName)
.parent()
.within(() => {
cy.get(".spectrum-Icon").eq(1).click({ force: true }) cy.get(".spectrum-Icon").eq(1).click({ force: true })
}) })
// Select and confirm duplication // Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)') 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")
.eq(0)
.within(() => {
cy.get("input").clear().type(queryRename) cy.get("input").clear().type(queryRename)
}) })
// Save query // Save query
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 query nav item - QueryName // Get query nav item - QueryName
cy.get(".nav-item").contains(queryName).parent().within(() => { cy.get(".nav-item")
.contains(queryName)
.parent()
.within(() => {
cy.get(".spectrum-Icon").eq(1).click({ force: true }) 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")
.contains("Delete Query")
.click({ force: true })
cy.wait(1000) cy.wait(1000)
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should('not.contain', queryName) cy.get(".nav-item").should("not.contain", queryName)
}) })
} }
}) })

View File

@ -1,9 +1,8 @@
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(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.createTestApp()
@ -16,37 +15,45 @@ filterTests(['all'], () => {
// Select PostgreSQL data source // Select PostgreSQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept('**/datasources').as('datasource') cy.intercept("**/datasources").as("datasource")
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Save and fetch tables")
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions // Intercept Request after button click & apply assertions
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource").its('response.body') cy.get("@datasource")
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:5432') .its("response.body")
cy.get("@datasource").its('response.body') .should(
.should('have.property', 'status', 500) "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", () => { 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-body").eq(0) cy.get(".spectrum-Table")
.find('tr') .eq(0)
.its('length') .find(".spectrum-Table-row")
.should('be.gt', 0) .its("length")
.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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click() cy.get(".spectrum-Popover").contains("One").click()
@ -63,16 +70,19 @@ filterTests(['all'], () => {
cy.reload() cy.reload()
}) })
// Confirm table length & column name // Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 1) .its("length")
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS") .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")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click() cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click() cy.get(".spectrum-Popover").contains("Many").click()
@ -91,19 +101,24 @@ filterTests(['all'], () => {
cy.reload() cy.reload()
}) })
// Confirm table length & relationship name // Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1) cy.get(".spectrum-Table")
.find('tr') .eq(1)
.its('length') .find(".spectrum-Table-row")
.should('eq', 2) .its("length")
cy.get(".spectrum-Table-cell") .should("eq", 2)
.should('contain', "LOCATIONS through COUNTRIES → REGIONS") 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")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click() cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500) cy.wait(500)
}) })
@ -112,32 +127,36 @@ filterTests(['all'], () => {
}) })
cy.reload() cy.reload()
// Confirm relationship was deleted // Confirm relationship was deleted
cy.get(".spectrum-Table-body") cy.get(".spectrum-Table")
.eq(1).find('tr').its('length').should('eq', 1) .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")
.eq(0)
.within(() => {
cy.get("input").type(queryName) cy.get("input").type(queryName)
}) })
// Insert Query within Fields section // Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0) cy.get(".CodeMirror textarea")
.eq(0)
.type("SELECT * FROM books", { force: true }) .type("SELECT * FROM books", { force: true })
// Intercept query execution // Intercept query execution
cy.intercept('**/queries/preview').as('query') cy.intercept("**/queries/preview").as("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.wait("@query") cy.wait("@query")
// Assert against Status Code & Body // Assert against Status Code & Body
cy.get("@query").its('response.statusCode') cy.get("@query").its("response.statusCode").should("eq", 200)
.should('eq', 200) cy.get("@query").its("response.body").should("not.be.empty")
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query // Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".hierarchy-items-container").should('contain', queryName) cy.get(".hierarchy-items-container").should("contain", queryName)
}) })
it("should switch to schema with no tables", () => { it("should switch to schema with no tables", () => {
@ -146,10 +165,10 @@ filterTests(['all'], () => {
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", () => {
@ -157,44 +176,59 @@ filterTests(['all'], () => {
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")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".icon").eq(1).click({ force: true })
}) })
// Select and confirm duplication // Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)') 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")
.eq(0)
.within(() => {
cy.get("input").clear().type(queryRename) cy.get("input").clear().type(queryRename)
}) })
@ -202,36 +236,46 @@ filterTests(['all'], () => {
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")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".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")
.contains("Delete Query")
.click({ force: true })
cy.wait(1000) cy.wait(1000)
} }
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should('not.contain', queryName) cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should('not.contain', queryRename) cy.get(".nav-item").should("not.contain", queryRename)
}) })
const switchSchema = (schema) => { const switchSchema = schema => {
// Edit configuration - Change Schema // Edit configuration - Change Schema
cy.get(".spectrum-Textfield").eq(6).within(() => { cy.get(".spectrum-Textfield")
cy.get('input').clear().type(schema) .eq(6)
.within(() => {
cy.get("input").clear().type(schema)
}) })
// Save configuration & fetch // Save configuration & fetch
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.get(".spectrum-Button").contains("Fetch tables").click({ force: true }) cy.get(".spectrum-Button")
.contains("Fetch tables")
.click({ force: true })
// Click fetch tables again within modal // Click fetch tables again within modal
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Fetch tables").click({ force: true }) cy.get(".spectrum-Button")
.contains("Fetch tables")
.click({ force: true })
}) })
cy.reload() cy.reload()
cy.wait(5000) cy.wait(5000)

View File

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

View File

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

View File

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

View File

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

View File

@ -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 /> <div class="buttons wide">
<div class="buttons"> <Button secondary icon="Add" on:click={addColumn}>Add column</Button>
<Button secondary icon="Add" on:click={addColumn}> <Button secondary quiet on:click={addAllColumns}>
Add column Add all columns
</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;

View File

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

View File

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

View File

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

View File

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