Add virtual rendering to table to increase performance and remove grid component
This commit is contained in:
parent
16ce2d4a07
commit
892bbcd07a
|
@ -82,18 +82,45 @@ const createScreen = table => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const grid = new Component("@budibase/standard-components/datagrid")
|
const spectrumTable = new Component("@budibase/standard-components/table")
|
||||||
.customProps({
|
.customProps({
|
||||||
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
|
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
|
||||||
editable: false,
|
theme: "spectrum--lightest",
|
||||||
theme: "alpine",
|
showAutoColumns: false,
|
||||||
height: "540",
|
quiet: false,
|
||||||
pagination: true,
|
size: "spectrum--medium",
|
||||||
detailUrl: `${rowListUrl(table)}/:id`,
|
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")
|
const mainContainer = new Component("@budibase/standard-components/container")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[
|
[
|
||||||
"container",
|
"container",
|
||||||
"dataprovider",
|
"dataprovider",
|
||||||
"datagrid",
|
|
||||||
"table",
|
"table",
|
||||||
"repeater",
|
"repeater",
|
||||||
"button",
|
"button",
|
||||||
|
|
|
@ -8,47 +8,6 @@
|
||||||
"transitionable": true,
|
"transitionable": true,
|
||||||
"settings": []
|
"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": {
|
"screenslot": {
|
||||||
"name": "Screenslot",
|
"name": "Screenslot",
|
||||||
"icon": "ri-artboard-2-line",
|
"icon": "ri-artboard-2-line",
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
||||||
"@budibase/bbui": "^1.58.13",
|
"@budibase/bbui": "^1.58.13",
|
||||||
"@budibase/svelte-ag-grid": "^1.0.4",
|
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/button": "^3.0.1",
|
"@spectrum-css/button": "^3.0.1",
|
||||||
"@spectrum-css/checkbox": "^3.0.1",
|
"@spectrum-css/checkbox": "^3.0.1",
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import AttachmentList from "../../attachments/AttachmentList.svelte"
|
|
||||||
export let files
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AttachmentList {files} on:delete />
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import { DatePicker } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DatePicker />
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ loadSpectrumIcons()
|
||||||
|
|
||||||
export { default as container } from "./Container.svelte"
|
export { default as container } from "./Container.svelte"
|
||||||
export { default as dataprovider } from "./DataProvider.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 screenslot } from "./ScreenSlot.svelte"
|
||||||
export { default as button } from "./Button.svelte"
|
export { default as button } from "./Button.svelte"
|
||||||
export { default as repeater } from "./Repeater.svelte"
|
export { default as repeater } from "./Repeater.svelte"
|
||||||
|
|
|
@ -4,4 +4,10 @@
|
||||||
export let value
|
export let value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{dayjs(value).format('MMMM D YYYY, HH:mm')}
|
<div>{dayjs(value).format('MMMM D YYYY, HH:mm')}</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
div {
|
div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 320px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,22 +14,42 @@
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, Provider } = getContext("sdk")
|
const { styleable, Provider } = getContext("sdk")
|
||||||
|
|
||||||
|
// Config
|
||||||
|
const rowHeight = 55
|
||||||
|
const headerHeight = 36
|
||||||
|
const rowPreload = 5
|
||||||
|
const maxRows = 100
|
||||||
|
|
||||||
|
// Sorting state
|
||||||
let sortColumn
|
let sortColumn
|
||||||
let sortOrder
|
let sortOrder
|
||||||
|
|
||||||
$: rows = dataProvider?.rows ?? []
|
// Table state
|
||||||
$: contentStyle = getContentStyle(rowCount, rows.length)
|
|
||||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
|
||||||
$: loaded = dataProvider?.loaded ?? false
|
$: 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 ?? {}
|
$: schema = dataProvider?.schema ?? {}
|
||||||
$: fields = getFields(schema, columns, showAutoColumns)
|
$: fields = getFields(schema, columns, showAutoColumns)
|
||||||
|
|
||||||
const getContentStyle = (rowCount, dataCount) => {
|
// Scrolling state
|
||||||
if (!rowCount) {
|
let timeout
|
||||||
|
let nextScrollTop = 0
|
||||||
|
let scrollTop = 0
|
||||||
|
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
|
||||||
|
$: lastVisibleRow = calculateLastVisibleRow(
|
||||||
|
firstVisibleRow,
|
||||||
|
visibleRowCount,
|
||||||
|
rows.length
|
||||||
|
)
|
||||||
|
|
||||||
|
const getContentStyle = (visibleRows, scroll) => {
|
||||||
|
if (!scroll) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const actualCount = Math.min(rowCount, dataCount)
|
return `height: ${headerHeight - 1 + visibleRows * (rowHeight + 1)}px;`
|
||||||
return `height: ${35 + actualCount * 56}px;`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||||
|
@ -71,69 +91,101 @@
|
||||||
})
|
})
|
||||||
return columns.concat(autoColumns)
|
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>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
{#if loaded}
|
||||||
<div
|
<div use:styleable={$component.styles}>
|
||||||
lang="en"
|
<div
|
||||||
dir="ltr"
|
on:scroll={onScroll}
|
||||||
class:quiet
|
lang="en"
|
||||||
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
|
dir="ltr"
|
||||||
<div class="content" style={contentStyle}>
|
class:quiet
|
||||||
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
|
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||||
<thead class="spectrum-Table-head">
|
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
|
||||||
<tr>
|
<div class="content" style={contentStyle}>
|
||||||
{#if $component.children}
|
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
|
||||||
<th class="spectrum-Table-headCell">
|
<thead class="spectrum-Table-head">
|
||||||
<div class="spectrum-Table-headCell-content" />
|
<tr>
|
||||||
</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 $component.children}
|
{#if $component.children}
|
||||||
<td class="spectrum-Table-cell spectrum-Table-cell--divider">
|
<th class="spectrum-Table-headCell">
|
||||||
<div class="spectrum-Table-cell-content">
|
<div class="spectrum-Table-headCell-content" />
|
||||||
<Provider data={row}>
|
</th>
|
||||||
<slot />
|
|
||||||
</Provider>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<td class="spectrum-Table-cell">
|
<th
|
||||||
<div class="spectrum-Table-cell-content">
|
class="spectrum-Table-headCell is-sortable"
|
||||||
<CellRenderer schema={schema[field]} value={row[field]} />
|
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>
|
</div>
|
||||||
</td>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
</tbody>
|
<tbody class="spectrum-Table-body">
|
||||||
</table>
|
{#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>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum {
|
.spectrum {
|
||||||
|
@ -148,12 +200,22 @@
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
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 {
|
th {
|
||||||
vertical-align: bottom;
|
vertical-align: middle;
|
||||||
height: 36px;
|
height: var(--header-height);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: var(--spectrum-global-color-gray-100);
|
background-color: var(--spectrum-global-color-gray-100);
|
||||||
|
@ -167,6 +229,24 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
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 {
|
td {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
@ -185,7 +265,7 @@
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
|
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
|
||||||
}
|
}
|
||||||
.spectrum-Table-cell-content {
|
.spectrum-Table-cell-content {
|
||||||
height: 55px;
|
height: var(--row-height);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -193,16 +273,4 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
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>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue