Add virtual rendering to table to increase performance and remove grid component
This commit is contained in:
parent
4b4288d982
commit
f84302ed89
|
@ -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({
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
[
|
||||
"container",
|
||||
"dataprovider",
|
||||
"datagrid",
|
||||
"table",
|
||||
"repeater",
|
||||
"button",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 320px;
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue