Add settings to grid block for filtering and sorting, and refactor grid logic to allow external control of these stores

This commit is contained in:
Andrew Kingston 2023-06-19 09:54:24 +01:00
parent 6438d23bed
commit 68f21274ed
9 changed files with 111 additions and 28 deletions

View File

@ -4,6 +4,9 @@
const { columns, tableId, filter, table } = getContext("grid")
// Wipe filter whenever table ID changes to avoid using stale filters
$: $tableId, filter.set([])
const onFilter = e => {
filter.set(e.detail || [])
}

View File

@ -5237,6 +5237,7 @@
"width": 600,
"height": 400
},
"info": "Grid Block is only compatible with internal or SQL tables",
"settings": [
{
"type": "table",
@ -5244,6 +5245,23 @@
"key": "table",
"required": true
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field/sortable",
"label": "Sort column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort order",
"key": "sortOrder",
"options": ["Ascending", "Descending"],
"defaultValue": "Ascending"
},
{
"type": "boolean",
"label": "Add rows",

View File

@ -9,6 +9,9 @@
export let allowEditRows = true
export let allowDeleteRows = true
export let stripeRows = false
export let filter = null
export let sortColumn = null
export let sortOrder = null
const component = getContext("component")
const { styleable, API, builderStore } = getContext("sdk")
@ -25,6 +28,9 @@
{allowEditRows}
{allowDeleteRows}
{stripeRows}
{filter}
{sortColumn}
{sortOrder}
showControls={false}
allowExpandRows={false}
allowSchemaChanges={false}

View File

@ -42,6 +42,8 @@
let candidateIndex
let lastSearchId
let searching = false
let valuesHeight = 0
let container
$: oneRowOnly = schema?.relationshipType === "one-to-many"
$: editable = focused && !readonly
@ -138,6 +140,8 @@
const open = async () => {
isOpen = true
valuesHeight = container.getBoundingClientRect().height
console.log(valuesHeight)
// Find the primary display for the related table
if (!primaryDisplay) {
@ -243,7 +247,7 @@
</script>
<div class="wrapper" class:editable class:focused style="--color:{color};">
<div class="container">
<div class="container" bind:this={container}>
<div
class="values"
class:wrap={editable || contentLines > 1}
@ -290,6 +294,7 @@
class:invertY
on:wheel|stopPropagation
use:clickOutside={close}
style="--values-height:{valuesHeight}px;"
>
<div class="search">
<Input

View File

@ -1,6 +1,5 @@
<script>
import { setContext, onMount } from "svelte"
import { writable } from "svelte/store"
import { fade } from "svelte/transition"
import { clickOutside, ProgressCircle } from "@budibase/bbui"
import { createEventManagers } from "../lib/events"
@ -30,6 +29,7 @@
GutterWidth,
DefaultRowHeight,
} from "../lib/constants"
import { memo } from "../../../utils"
export let API = null
export let tableId = null
@ -43,19 +43,29 @@
export let collaboration = true
export let showAvatars = true
export let showControls = true
export let filter = null
export let sortColumn = null
export let sortOrder = null
// Unique identifier for DOM nodes inside this instance
const rand = Math.random()
// State stores
const tableIdStore = writable(tableId)
const schemaOverridesStore = writable(schemaOverrides)
const config = writable({
// Stores derived from props.
// We use memo here to ensure redundant store reactions don't fire and cause
// wasted API calls.
const tableIdStore = memo(tableId)
const schemaOverridesStore = memo(schemaOverrides)
const filterStore = memo(filter)
const sortStore = memo({
column: sortColumn,
order: sortOrder,
})
const config = memo({
allowAddRows,
allowSchemaChanges,
allowExpandRows,
allowEditRows,
allowDeleteRows,
allowSchemaChanges,
stripeRows,
showControls,
})
@ -67,6 +77,8 @@
config,
tableId: tableIdStore,
schemaOverrides: schemaOverridesStore,
filter: filterStore,
sort: sortStore,
}
context = { ...context, ...createEventManagers() }
context = attachStores(context)
@ -83,9 +95,14 @@
gridFocused,
} = context
// Keep stores up to date
// Keep prop-derived stores up to date
$: tableIdStore.set(tableId)
$: schemaOverridesStore.set(schemaOverrides)
$: filterStore.set(filter)
$: sortStore.set({
column: sortColumn,
order: sortOrder,
})
$: config.set({
allowAddRows,
allowSchemaChanges,
@ -115,10 +132,10 @@
id="grid-{rand}"
class:is-resizing={$isResizing}
class:is-reordering={$isReordering}
class:stripe={$config.stripeRows}
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
class:stripe={stripeRows}
on:mouseenter={() => gridFocused.set(true)}
on:mouseleave={() => gridFocused.set(false)}
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
>
{#if showControls}
<div class="controls">
@ -170,6 +187,7 @@
</div>
<style>
/* Core grid */
.grid {
/* Variables */
--grid-background: var(--spectrum-global-color-gray-50);
@ -182,7 +200,6 @@
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
--cell-font-size: 14px;
--controls-height: 50px;
flex: 1 1 auto;
display: flex;
flex-direction: column;
@ -206,6 +223,7 @@
--cell-background-alt: var(--spectrum-global-color-gray-75);
}
/* Data layers */
.grid-data-outer,
.grid-data-inner {
flex: 1 1 auto;

View File

@ -2,19 +2,13 @@ import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui"
import { NewRowID, RowPageSize } from "../lib/constants"
const initialSortState = {
column: null,
order: "ascending",
}
import { tick } from "svelte"
export const createStores = () => {
const rows = writable([])
const table = writable(null)
const filter = writable([])
const loading = writable(false)
const loaded = writable(false)
const sort = writable(initialSortState)
const rowChangeCache = writable({})
const inProgressChanges = writable({})
const hasNextPage = writable(false)
@ -46,10 +40,8 @@ export const createStores = () => {
rows,
rowLookupMap,
table,
filter,
loaded,
loading,
sort,
rowChangeCache,
inProgressChanges,
hasNextPage,
@ -97,15 +89,18 @@ export const deriveStores = context => {
// Reset everything when table ID changes
let unsubscribe = null
let lastResetKey = null
tableId.subscribe($tableId => {
tableId.subscribe(async $tableId => {
// Unsub from previous fetch if one exists
unsubscribe?.()
fetch.set(null)
instanceLoaded.set(false)
loading.set(true)
// Reset state
filter.set([])
// Tick to allow other reactive logic to update stores when table ID changes
// before proceeding. This allows us to wipe filters etc if needed.
await tick()
const $filter = get(filter)
const $sort = get(sort)
// Create new fetch model
const newFetch = fetchData({
@ -115,9 +110,9 @@ export const deriveStores = context => {
tableId: $tableId,
},
options: {
filter: [],
sortColumn: initialSortState.column,
sortOrder: initialSortState.order,
filter: $filter,
sortColumn: $sort.column,
sortOrder: $sort.order,
limit: RowPageSize,
paginate: true,
},

View File

@ -136,8 +136,10 @@ export default class DataFetch {
this.options.sortOrder = "ascending"
}
// If no sort column, use the primary display and fallback to first column
if (!this.options.sortColumn) {
// If no sort column, or an invalid sort column is provided, use the primary
// display and fallback to first column
const sortValid = this.options.sortColumn && schema[this.options.sortColumn]
if (!sortValid) {
let newSortColumn
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
newSortColumn = definition.primaryDisplay

View File

@ -3,4 +3,5 @@ export * as JSONUtils from "./json"
export * as CookieUtils from "./cookies"
export * as RoleUtils from "./roles"
export * as Utils from "./utils"
export { memo } from "./memo"
export { createWebsocket } from "./websocket"

View File

@ -0,0 +1,35 @@
import { writable, get } from "svelte/store"
// A simple svelte store which deeply compares all changes and ensures that
// subscribed children will only fire when a new value is actually set
export const memo = initialValue => {
const store = writable(initialValue)
const tryUpdateValue = (newValue, currentValue) => {
// Sanity check for primitive equality
if (currentValue === newValue) {
return
}
// Otherwise deep compare via JSON stringify
const currentString = JSON.stringify(currentValue)
const newString = JSON.stringify(newValue)
if (currentString !== newString) {
store.set(newValue)
}
}
return {
subscribe: store.subscribe,
set: newValue => {
const currentValue = get(store)
tryUpdateValue(newValue, currentValue)
},
update: updateFn => {
const currentValue = get(store)
let mutableCurrentValue = JSON.parse(JSON.stringify(currentValue))
const newValue = updateFn(mutableCurrentValue)
tryUpdateValue(newValue, currentValue)
},
}
}