Add virtual rendering to table to increase performance and remove grid component

This commit is contained in:
Andrew Kingston 2021-03-25 14:40:29 +00:00
parent 4b4288d982
commit f84302ed89
20 changed files with 188 additions and 821 deletions

View File

@ -82,18 +82,45 @@ const createScreen = table => {
},
})
const grid = new Component("@budibase/standard-components/datagrid")
const spectrumTable = new Component("@budibase/standard-components/table")
.customProps({
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
editable: false,
theme: "alpine",
height: "540",
pagination: true,
detailUrl: `${rowListUrl(table)}/:id`,
theme: "spectrum--lightest",
showAutoColumns: false,
quiet: false,
size: "spectrum--medium",
rowCount: 8,
})
.instanceName("Grid")
.instanceName(`${table.name} Table`)
provider.addChild(grid)
const safeTableId = makePropSafe(spectrumTable._json._id)
const safeRowId = makePropSafe("_id")
const viewButton = new Component("@budibase/standard-components/button")
.customProps({
text: "View",
onClick: [
{
"##eventHandlerType": "Navigate To",
parameters: {
url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`,
},
},
],
})
.instanceName("View Button")
.normalStyle({
background: "transparent",
"font-family": "Inter, sans-serif",
"font-weight": "500",
color: "#888",
"border-width": "0",
})
.hoverStyle({
color: "#4285f4",
})
spectrumTable.addChild(viewButton)
provider.addChild(spectrumTable)
const mainContainer = new Component("@budibase/standard-components/container")
.normalStyle({

View File

@ -1,7 +1,6 @@
[
"container",
"dataprovider",
"datagrid",
"table",
"repeater",
"button",

View File

@ -8,47 +8,6 @@
"transitionable": true,
"settings": []
},
"datagrid": {
"name": "Grid",
"description": "A datagrid component with functionality to add, remove and edit rows.",
"icon": "ri-grid-line",
"styleable": true,
"settings": [
{
"type": "dataProvider",
"label": "Data",
"key": "dataProvider"
},
{
"type": "detailScreen",
"label": "Detail URL",
"key": "detailUrl"
},
{
"type": "boolean",
"label": "Editable",
"key": "editable"
},
{
"type": "select",
"label": "Theme",
"key": "theme",
"options": ["alpine", "alpine-dark", "balham", "balham-dark", "material"],
"defaultValue": "alpine"
},
{
"type": "number",
"label": "Height",
"key": "height",
"defaultValue": "500"
},
{
"type": "boolean",
"label": "Pagination",
"key": "pagination"
}
]
},
"screenslot": {
"name": "Screenslot",
"icon": "ri-artboard-2-line",

View File

@ -41,7 +41,6 @@
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
"@budibase/bbui": "^1.58.13",
"@budibase/svelte-ag-grid": "^1.0.4",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/button": "^3.0.1",
"@spectrum-css/checkbox": "^3.0.1",

View File

@ -1,6 +0,0 @@
<script>
import AttachmentList from "../../attachments/AttachmentList.svelte"
export let files
</script>
<AttachmentList {files} on:delete />

View File

@ -1,180 +0,0 @@
<script>
// Import valueSetters and custom renderers
import { number } from "./valueSetters"
import { getRenderer } from "./customRenderer"
import { isEmpty } from "lodash/fp"
import { getContext } from "svelte"
import AgGrid from "@budibase/svelte-ag-grid"
import {
TextButton as DeleteButton,
Icon,
Modal,
ModalContent,
} from "@budibase/bbui"
// These maps need to be set up to handle whatever types that are used in the tables.
const setters = new Map([["number", number]])
const SDK = getContext("sdk")
const component = getContext("component")
const { API, styleable } = SDK
export let dataProvider
export let editable
export let theme = "alpine"
export let height = 500
export let pagination
export let detailUrl
// Add setting height as css var to allow grid to use correct height
$: gridStyles = {
...$component.styles,
normal: {
...$component.styles.normal,
["--grid-height"]: `${height}px`,
},
}
$: setUpGrid(dataProvider)
$: dataLoaded = dataProvider?.loaded
$: data = dataProvider?.rows
// These can never change at runtime so don't need to be reactive
let canEdit = editable && datasource && datasource.type !== "view"
let canAddDelete = editable && datasource && datasource.type === "table"
let modal
let columnDefs
let selectedRows = []
let table
let options = {
defaultColDef: {
flex: 1,
minWidth: 150,
filter: true,
},
rowSelection: canEdit ? "multiple" : false,
suppressFieldDotNotation: true,
suppressRowClickSelection: !canEdit,
paginationAutoPageSize: true,
pagination,
}
async function setUpGrid(dataProvider) {
if (!dataProvider) {
return
}
const { schema } = dataProvider
columnDefs = Object.keys(schema).map((key, i) => {
return {
headerCheckboxSelection: i === 0 && canEdit,
checkboxSelection: i === 0 && canEdit,
valueSetter: setters.get(schema[key].type),
headerName: key,
field: key,
hide: shouldHideField(key),
sortable: true,
editable: canEdit && schema[key].type !== "link",
cellRenderer: getRenderer(schema[key], canEdit, SDK),
autoHeight: true,
}
})
if (detailUrl) {
columnDefs = [
...columnDefs,
{
headerName: "Detail",
field: "_id",
minWidth: 100,
width: 100,
flex: 0,
editable: false,
sortable: false,
cellRenderer: getRenderer(
{
type: "_id",
options: { detailUrl },
},
false,
SDK
),
autoHeight: true,
pinned: "left",
filter: false,
},
]
}
}
const shouldHideField = name => {
if (name.startsWith("_")) return true
// always 'row'
if (name === "type") return true
// tables are always tied to a single tableId, this is irrelevant
if (name === "tableId") return true
return false
}
const handleUpdate = ({ detail }) => {
data[detail.row] = detail.data
updateRow(detail.data)
}
const updateRow = async row => {
await API.updateRow(row)
}
const deleteRows = async () => {
await API.deleteRows({ rows: selectedRows, tableId: datasource.name })
data = data.filter(row => !selectedRows.includes(row))
selectedRows = []
}
</script>
<div class="container" use:styleable={gridStyles}>
{#if dataLoaded}
{#if canAddDelete}
<div class="controls">
{#if selectedRows.length > 0}
<DeleteButton text small on:click={modal.show()}>
<Icon name="addrow" />
Delete
{selectedRows.length}
row(s)
</DeleteButton>
{/if}
</div>
{/if}
<AgGrid
{theme}
{options}
{data}
{columnDefs}
on:update={handleUpdate}
on:select={({ detail }) => (selectedRows = detail)} />
{/if}
<Modal bind:this={modal}>
<ModalContent
title="Confirm Row Deletion"
confirmText="Delete"
onConfirm={deleteRows}>
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span>
</ModalContent>
</Modal>
</div>
<style>
.container :global(.ag-pinned-left-header .ag-header-cell-label) {
justify-content: center;
}
.controls {
min-height: 15px;
margin-bottom: var(--spacing-s);
display: grid;
grid-gap: var(--spacing-s);
grid-template-columns: auto auto;
justify-content: start;
}
</style>

View File

@ -1,37 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import Modal from "./Modal.svelte"
const dispatch = createEventDispatcher()
let anchor
let dropdown
export let table
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Add New Row</h5>
<Modal
{table}
onClosed={dropdown.hide}
on:newRow={() => dispatch('newRow')} />
</DropdownMenu>
<style>
div {
display: grid;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View File

@ -1,144 +0,0 @@
<script>
import { getContext, onMount, createEventDispatcher } from "svelte"
import { Button, Label, DatePicker, RichText } from "@budibase/bbui"
import Dropzone from "../../attachments/Dropzone.svelte"
import debounce from "lodash.debounce"
const dispatch = createEventDispatcher()
const { fetchRow, saveRow, routeStore } = getContext("sdk")
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
export let table
export let onClosed
let row = { tableId: table._id }
let schema = table.schema
let saved = false
let rowId
let isNew = true
let errors = {}
$: fields = schema ? Object.keys(schema) : []
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in row)) {
row[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
}
const response = await saveRow(row)
if (!response.error) {
// store.update(state => {
// state[table._id] = state[table._id]
// ? [...state[table._id], json]
// : [json]
// return state
// })
errors = {}
// wipe form, if new row, otherwise update
// table to get new _rev
row = isNew ? { tableId: table._id } : response
onClosed()
dispatch("newRow")
} else {
errors = [response.error]
}
})
onMount(async () => {
const routeParams = $routeStore.routeParams
rowId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !rowId || rowId === "new"
if (isNew) {
row = { tableId: table }
return
}
row = await fetchRow({ tableId: table._id, rowId })
})
</script>
<div class="actions">
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<form on:submit|preventDefault>
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select bind:value={row[field]}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={row[field]} />
{:else if schema[field].type === 'boolean'}
<input class="input" type="checkbox" bind:checked={row[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={row[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={row[field]} />
{:else if schema[field].type === 'longform'}
<RichText bind:value={row[field]} />
{:else if schema[field].type === 'attachment'}
<Dropzone bind:files={row[field]} />
{/if}
</div>
<hr />
{/each}
</form>
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
<style>
.actions {
padding: var(--spacing-l) var(--spacing-xl);
}
footer {
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View File

@ -1,5 +0,0 @@
<script>
import { DatePicker } from "@budibase/bbui"
</script>
<DatePicker />

View File

@ -1,75 +0,0 @@
<script>
import { onMount } from "svelte"
export let columnName
export let row
export let SDK
const { API } = SDK
$: count =
row && columnName && Array.isArray(row[columnName])
? row[columnName].length
: 0
let linkedRows = []
let displayColumn
onMount(async () => {
linkedRows = await API.fetchRelationshipData({
tableId: row.tableId,
rowId: row._id,
fieldName: columnName,
})
if (linkedRows && linkedRows.length) {
const table = await API.fetchTableDefinition(linkedRows[0].tableId)
if (table && table.primaryDisplay) {
displayColumn = table.primaryDisplay
}
}
})
async function fetchLinkedRowsData(row, columnName) {
if (!row || !row._id) {
return []
}
return await API.fetchRelationshipData({
tableId: row.tableId,
rowId: row._id,
fieldName: columnName,
})
}
</script>
<div class="container">
{#if linkedRows && linkedRows.length && displayColumn}
{#each linkedRows as linkedRow}
{#if linkedRow[displayColumn] != null && linkedRow[displayColumn] !== ''}
<div class="linked-row">{linkedRow[displayColumn]}</div>
{/if}
{/each}
{:else}{count} related row(s){/if}
</div>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
}
/* This styling is opinionated to ensure these always look consistent */
.linked-row {
color: white;
background-color: #616161;
border-radius: var(--border-radius-xs);
padding: var(--spacing-xs) var(--spacing-s) calc(var(--spacing-xs) + 1px)
var(--spacing-s);
line-height: 1;
font-size: 0.8em;
font-family: var(--font-sans);
font-weight: 500;
}
</style>

View File

@ -1,32 +0,0 @@
<script>
export let columnName
export let row
$: items = row?.[columnName] || []
</script>
<div class="container">
{#each items as item}
<div class="item">{item?.primaryDisplay ?? ''}</div>
{/each}
</div>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
}
.item {
font-size: var(--font-size-xs);
padding: var(--spacing-xs) var(--spacing-s);
border: 1px solid var(--grey-5);
color: var(--grey-7);
line-height: normal;
border-radius: 4px;
}
</style>

View File

@ -1,17 +0,0 @@
<script>
import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
export let options
$: dispatch("change", value)
</script>
<Select label={false} bind:value>
<option value="">Choose an option</option>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</Select>

View File

@ -1,19 +0,0 @@
<script>
import { Button } from "@budibase/bbui"
export let url
export let SDK
const { linkable } = SDK
let link
</script>
<a href={url} bind:this={link} use:linkable />
<Button small translucent on:click={() => link.click()}>View</Button>
<style>
a {
display: none;
}
</style>

View File

@ -1,169 +0,0 @@
// Custom renderers to handle special types
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
import AttachmentCell from "./AttachmentCell/Button.svelte"
import ViewDetails from "./ViewDetails/Cell.svelte"
import Select from "./Select/Wrapper.svelte"
import DatePicker from "./DateTime/Wrapper.svelte"
import RelationshipLabel from "./Relationship/RelationshipLabel.svelte"
const renderers = new Map([
["boolean", booleanRenderer],
["attachment", attachmentRenderer],
["options", optionsRenderer],
["link", linkedRowRenderer],
["_id", viewDetailsRenderer],
])
export function getRenderer(schema, editable, SDK) {
if (renderers.get(schema.type)) {
return renderers.get(schema.type)(
schema.options,
schema.constraints,
editable,
SDK
)
} else {
return false
}
}
/* eslint-disable no-unused-vars */
function booleanRenderer(options, constraints, editable, SDK) {
return params => {
const toggle = e => {
params.value = !params.value
params.setValue(e.currentTarget.checked)
}
let input = document.createElement("input")
input.style.display = "grid"
input.style.placeItems = "center"
input.style.height = "100%"
input.type = "checkbox"
input.checked = params.value
if (editable) {
input.addEventListener("click", toggle)
} else {
input.disabled = true
}
return input
}
}
/* eslint-disable no-unused-vars */
function attachmentRenderer(options, constraints, editable, SDK) {
return params => {
const container = document.createElement("div")
const attachmentInstance = new AttachmentCell({
target: container,
props: {
files: params.value || [],
SDK,
},
})
const deleteFile = event => {
const newFilesArray = params.value.filter(file => file !== event.detail)
params.setValue(newFilesArray)
}
attachmentInstance.$on("delete", deleteFile)
return container
}
}
/* eslint-disable no-unused-vars */
function dateRenderer(options, constraints, editable, SDK) {
return function(params) {
const container = document.createElement("div")
const toggle = e => {
params.setValue(e.detail[0][0])
}
// Options need to be passed in with minTime and maxTime! Needs bbui update.
new DatePicker({
target: container,
props: {
value: params.value,
SDK,
},
})
return container
}
}
function optionsRenderer(options, constraints, editable, SDK) {
return params => {
if (!editable) return params.value
const container = document.createElement("div")
container.style.display = "grid"
container.style.placeItems = "center"
container.style.height = "100%"
const change = e => {
params.setValue(e.detail)
}
const selectInstance = new Select({
target: container,
props: {
value: params.value,
options: constraints.inclusion,
SDK,
},
})
selectInstance.$on("change", change)
return container
}
}
/* eslint-disable no-unused-vars */
function linkedRowRenderer(options, constraints, editable, SDK) {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.placeItems = "center"
container.style.height = "100%"
new RelationshipLabel({
target: container,
props: {
row: params.data,
columnName: params.column.colId,
SDK,
},
})
return container
}
}
/* eslint-disable no-unused-vars */
function viewDetailsRenderer(options, constraints, editable, SDK) {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.alignItems = "center"
container.style.height = "100%"
let url = "/"
if (options.detailUrl) {
url = options.detailUrl.replace(":id", params.data._id)
}
if (!url.startsWith("/")) {
url = `/${url}`
}
new ViewDetails({
target: container,
props: {
url,
SDK,
},
})
return container
}
}

View File

@ -1,6 +0,0 @@
// https://www.ag-grid.com/javascript-grid-value-setters/
// These handles values and makes sure they adhere to the data type provided by the table
export const number = params => {
params.data[params.colDef.field] = parseFloat(params.newValue)
return true
}

View File

@ -14,7 +14,6 @@ loadSpectrumIcons()
export { default as container } from "./Container.svelte"
export { default as dataprovider } from "./DataProvider.svelte"
export { default as datagrid } from "./grid/Component.svelte"
export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte"
export { default as repeater } from "./Repeater.svelte"

View File

@ -4,4 +4,10 @@
export let value
</script>
{dayjs(value).format('MMMM D YYYY, HH:mm')}
<div>{dayjs(value).format('MMMM D YYYY, HH:mm')}</div>
<style>
div {
width: 200px;
}
</style>

View File

@ -8,6 +8,6 @@
div {
overflow: hidden;
text-overflow: ellipsis;
max-width: 320px;
width: 150px;
}
</style>

View File

@ -14,22 +14,42 @@
const component = getContext("component")
const { styleable, Provider } = getContext("sdk")
// Config
const rowHeight = 55
const headerHeight = 36
const rowPreload = 5
const maxRows = 100
// Sorting state
let sortColumn
let sortOrder
$: rows = dataProvider?.rows ?? []
$: contentStyle = getContentStyle(rowCount, rows.length)
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
// Table state
$: loaded = dataProvider?.loaded ?? false
$: rows = dataProvider?.rows ?? []
$: visibleRowCount = Math.min(rows.length, rowCount || maxRows, maxRows)
$: scroll = rows.length > visibleRowCount
$: contentStyle = getContentStyle(visibleRowCount, scroll)
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: schema = dataProvider?.schema ?? {}
$: fields = getFields(schema, columns, showAutoColumns)
const getContentStyle = (rowCount, dataCount) => {
if (!rowCount) {
// Scrolling state
let timeout
let nextScrollTop = 0
let scrollTop = 0
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
$: lastVisibleRow = calculateLastVisibleRow(
firstVisibleRow,
visibleRowCount,
rows.length
)
const getContentStyle = (visibleRows, scroll) => {
if (!scroll) {
return ""
}
const actualCount = Math.min(rowCount, dataCount)
return `height: ${35 + actualCount * 56}px;`
return `height: ${headerHeight - 1 + visibleRows * (rowHeight + 1)}px;`
}
const sortRows = (rows, sortColumn, sortOrder) => {
@ -71,69 +91,101 @@
})
return columns.concat(autoColumns)
}
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) => {
return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount)
}
</script>
<div use:styleable={$component.styles}>
<div
lang="en"
dir="ltr"
class:quiet
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
<div class="content" style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
<thead class="spectrum-Table-head">
<tr>
{#if $component.children}
<th class="spectrum-Table-headCell">
<div class="spectrum-Table-headCell-content" />
</th>
{/if}
{#each fields as field}
<th
class="spectrum-Table-headCell is-sortable"
class:is-sorted-desc={sortColumn === field && sortOrder === 'Descending'}
class:is-sorted-asc={sortColumn === field && sortOrder === 'Ascending'}
on:click={() => sortBy(field)}>
<div class="spectrum-Table-headCell-content">
{schema[field]?.name}
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
class:visible={sortColumn === field}
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Arrow100" />
</svg>
</div>
</th>
{/each}
</tr>
</thead>
<tbody class="spectrum-Table-body">
{#each sortedRows as row}
<tr class="spectrum-Table-row">
{#if loaded}
<div use:styleable={$component.styles}>
<div
on:scroll={onScroll}
lang="en"
dir="ltr"
class:quiet
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
<div class="content" style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
<thead class="spectrum-Table-head">
<tr>
{#if $component.children}
<td class="spectrum-Table-cell spectrum-Table-cell--divider">
<div class="spectrum-Table-cell-content">
<Provider data={row}>
<slot />
</Provider>
</div>
</td>
<th class="spectrum-Table-headCell">
<div class="spectrum-Table-headCell-content" />
</th>
{/if}
{#each fields as field}
<td class="spectrum-Table-cell">
<div class="spectrum-Table-cell-content">
<CellRenderer schema={schema[field]} value={row[field]} />
<th
class="spectrum-Table-headCell is-sortable"
class:is-sorted-desc={sortColumn === field && sortOrder === 'Descending'}
class:is-sorted-asc={sortColumn === field && sortOrder === 'Ascending'}
on:click={() => sortBy(field)}>
<div class="spectrum-Table-headCell-content">
<div class="title">{schema[field]?.name}</div>
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
class:visible={sortColumn === field}
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Arrow100" />
</svg>
</div>
</td>
</th>
{/each}
</tr>
{/each}
</tbody>
</table>
</thead>
<tbody class="spectrum-Table-body">
{#each sortedRows as row, idx}
<tr
class="spectrum-Table-row"
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}>
{#if idx < firstVisibleRow || idx > lastVisibleRow}
{:else}
{#if $component.children}
<td
class="spectrum-Table-cell spectrum-Table-cell--divider">
<div class="spectrum-Table-cell-content">
<Provider data={row}>
<slot />
</Provider>
</div>
</td>
{/if}
{#each fields as field}
<td class="spectrum-Table-cell">
<div class="spectrum-Table-cell-content">
<CellRenderer
schema={schema[field]}
value={row[field]} />
</div>
</td>
{/each}
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/if}
<style>
.spectrum {
@ -148,12 +200,22 @@
table {
width: 100%;
}
tbody {
z-index: 1;
.spectrum-Table-sortedIcon {
opacity: 0;
display: block !important;
}
.spectrum-Table-sortedIcon.visible {
opacity: 1;
}
.spectrum,
th {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
th {
vertical-align: bottom;
height: 36px;
vertical-align: middle;
height: var(--header-height);
position: sticky;
top: 0;
background-color: var(--spectrum-global-color-gray-100);
@ -167,6 +229,24 @@
align-items: center;
user-select: none;
}
.spectrum-Table-headCell-content .title {
overflow: hidden;
text-overflow: ellipsis;
}
tbody {
z-index: 1;
}
tbody tr {
height: var(--row-height);
}
tbody tr.hidden {
height: calc(var(--row-height) + 1px);
}
tbody tr.offset {
background-color: red;
display: block;
}
td {
padding-top: 0;
padding-bottom: 0;
@ -185,7 +265,7 @@
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
.spectrum-Table-cell-content {
height: 55px;
height: var(--row-height);
white-space: nowrap;
display: flex;
flex-direction: row;
@ -193,16 +273,4 @@
align-items: center;
gap: 4px;
}
.spectrum-Table-sortedIcon {
opacity: 0;
display: block !important;
}
.spectrum-Table-sortedIcon.visible {
opacity: 1;
}
.spectrum,
th {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
</style>