Refactor grid to split up stores and provide better separation of datasource-specific logic

This commit is contained in:
Andrew Kingston 2023-08-03 11:18:19 +01:00
parent ab47e49dd9
commit e3cf0667be
19 changed files with 262 additions and 166 deletions

View File

@ -61,7 +61,7 @@
allowDeleteRows={!isUsersTable} allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
on:updatetable={handleGridTableUpdate} on:updatedatasource={handleGridTableUpdate}
> >
<svelte:fragment slot="filter"> <svelte:fragment slot="filter">
<GridFilterButton /> <GridFilterButton />

View File

@ -2,22 +2,22 @@
import TableFilterButton from "../TableFilterButton.svelte" import TableFilterButton from "../TableFilterButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, tableId, filter, table } = getContext("grid") const { columns, datasource, filter, definition } = getContext("grid")
// Wipe filter whenever table ID changes to avoid using stale filters // Wipe filter whenever table ID changes to avoid using stale filters
$: $tableId, filter.set([]) $: $datasource, filter.set([])
const onFilter = e => { const onFilter = e => {
filter.set(e.detail || []) filter.set(e.detail || [])
} }
</script> </script>
{#key $tableId} {#key $datasource}
<TableFilterButton <TableFilterButton
schema={$table?.schema} schema={$definition?.schema}
filters={$filter} filters={$filter}
on:change={onFilter} on:change={onFilter}
disabled={!$columns.length} disabled={!$columns.length}
tableId={$tableId} tableId={$datasource.tableId}
/> />
{/key} {/key}

View File

@ -4,12 +4,12 @@
export let disabled = false export let disabled = false
const { rows, datasource, table } = getContext("grid") const { rows, datasource, definition } = getContext("grid")
</script> </script>
<ImportButton <ImportButton
{disabled} {disabled}
tableId={$datasource.tableId} tableId={$datasource?.tableId}
tableType={$table?.type} tableType={$definition?.type}
on:importrows={rows.actions.refreshData} on:importrows={rows.actions.refreshData}
/> />

View File

@ -2,7 +2,7 @@
import ManageAccessButton from "../ManageAccessButton.svelte" import ManageAccessButton from "../ManageAccessButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { tableId } = getContext("grid") const { datasource } = getContext("grid")
</script> </script>
<ManageAccessButton resourceId={$tableId} /> <ManageAccessButton resourceId={$datasource?.tableId} />

View File

@ -2,12 +2,12 @@
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte" import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { table, rows } = getContext("grid") const { definition, rows } = getContext("grid")
</script> </script>
{#if $table} {#if $definition}
<ExistingRelationshipButton <ExistingRelationshipButton
table={$table} table={$definition}
on:updatecolumns={() => rows.actions.refreshData()} on:updatecolumns={() => rows.actions.refreshData()}
/> />
{/if} {/if}

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte" import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
const { rows } = getContext("grid") const { datasource } = getContext("grid")
</script> </script>
<CreateEditColumn on:updatecolumns={rows.actions.refreshDatasourceDefinition} /> <CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />

View File

@ -8,8 +8,14 @@
SmallRowHeight, SmallRowHeight,
} from "../lib/constants" } from "../lib/constants"
const { stickyColumn, columns, rowHeight, table, fixedRowHeight } = const {
getContext("grid") stickyColumn,
columns,
rowHeight,
definition,
fixedRowHeight,
datasource,
} = getContext("grid")
// Some constants for column width options // Some constants for column width options
const smallColSize = 120 const smallColSize = 120
@ -60,8 +66,8 @@
] ]
const changeRowHeight = height => { const changeRowHeight = height => {
columns.actions.saveTable({ datasource.actions.saveDefinition({
...$table, ...$definition,
rowHeight: height, rowHeight: height,
}) })
} }

View File

@ -1 +1,2 @@
export { default as Grid } from "./layout/Grid.svelte" export { default as Grid } from "./layout/Grid.svelte"
export { DatasourceType } from "./lib/constants"

View File

@ -1,3 +1,7 @@
export const DatasourceType = {
Table: "table",
ViewV2: "viewV2",
}
export const Padding = 246 export const Padding = 246
export const MaxCellRenderHeight = 222 export const MaxCellRenderHeight = 222
export const ScrollBarSize = 8 export const ScrollBarSize = 8

View File

@ -69,8 +69,7 @@ export const deriveStores = context => {
} }
export const createActions = context => { export const createActions = context => {
const { table, columns, stickyColumn, API, dispatch, config, datasource } = const { columns, stickyColumn, config, datasource, definition } = context
context
// Checks if we have a certain column by name // Checks if we have a certain column by name
const hasColumn = column => { const hasColumn = column => {
@ -79,13 +78,13 @@ export const createActions = context => {
return $columns.some(col => col.name === column) || $sticky?.name === column return $columns.some(col => col.name === column) || $sticky?.name === column
} }
// Updates the tables primary display column // Updates the datasources primary display column
const changePrimaryDisplay = async column => { const changePrimaryDisplay = async column => {
if (!get(config).canEditPrimaryDisplay) { if (!get(config).canEditPrimaryDisplay) {
return return
} }
return await saveTable({ return await datasource.actions.saveDefinition({
...get(table), ...get(definition),
primaryDisplay: column, primaryDisplay: column,
}) })
} }
@ -107,14 +106,14 @@ export const createActions = context => {
await saveChanges() await saveChanges()
} }
// Persists column changes by saving metadata against table schema // Persists column changes by saving metadata against datasource schema
const saveChanges = async () => { const saveChanges = async () => {
const $columns = get(columns) const $columns = get(columns)
const $table = get(table) const $definition = get(definition)
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
const newSchema = cloneDeep($table.schema) const newSchema = cloneDeep($definition.schema)
// Build new updated table schema // Build new updated datasource schema
Object.keys(newSchema).forEach(column => { Object.keys(newSchema).forEach(column => {
// Respect order specified by columns // Respect order specified by columns
const index = $columns.findIndex(x => x.name === column) const index = $columns.findIndex(x => x.name === column)
@ -134,28 +133,10 @@ export const createActions = context => {
} }
}) })
await saveTable({ ...$table, schema: newSchema }) await datasource.actions.saveDefinition({
} ...$definition,
schema: newSchema,
const saveTable = async newTable => { })
const $config = get(config)
const $datasource = get(datasource)
// Update local state
table.set(newTable)
// Update server
if ($config.canSaveSchema) {
if ($datasource.type === "table") {
await API.saveTable(newTable)
} else if ($datasource.type === "viewV2") {
await API.viewV2.update({ ...newTable })
}
}
// Broadcast change to external state can be updated, as this change
// will not be received by the builder websocket because we caused it ourselves
dispatch("updatetable", newTable)
} }
return { return {
@ -164,7 +145,6 @@ export const createActions = context => {
actions: { actions: {
hasColumn, hasColumn,
saveChanges, saveChanges,
saveTable,
changePrimaryDisplay, changePrimaryDisplay,
changeAllColumnWidths, changeAllColumnWidths,
}, },
@ -173,51 +153,7 @@ export const createActions = context => {
} }
export const initialise = context => { export const initialise = context => {
const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } = const { definition, columns, stickyColumn, schema } = context
context
const schema = derived(
[table, schemaOverrides, columnWhitelist],
([$table, $schemaOverrides, $columnWhitelist]) => {
if (!$table?.schema) {
return null
}
let newSchema = { ...$table?.schema }
// Edge case to temporarily allow deletion of duplicated user
// fields that were saved with the "disabled" flag set.
// By overriding the saved schema we ensure only overrides can
// set the disabled flag.
// TODO: remove in future
Object.keys(newSchema).forEach(field => {
newSchema[field] = {
...newSchema[field],
disabled: false,
}
})
// Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) {
newSchema[field] = {
...newSchema[field],
...$schemaOverrides[field],
}
}
})
// Apply whitelist if specified
if ($columnWhitelist?.length) {
Object.keys(newSchema).forEach(key => {
if (!$columnWhitelist.includes(key)) {
delete newSchema[key]
}
})
}
return newSchema
}
)
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
schema.subscribe($schema => { schema.subscribe($schema => {
@ -226,12 +162,12 @@ export const initialise = context => {
stickyColumn.set(null) stickyColumn.set(null)
return return
} }
const $table = get(table) const $definition = get(definition)
// Find primary display // Find primary display
let primaryDisplay let primaryDisplay
if ($table.primaryDisplay && $schema[$table.primaryDisplay]) { if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) {
primaryDisplay = $table.primaryDisplay primaryDisplay = $definition.primaryDisplay
} }
// Get field list // Get field list

View File

@ -1,5 +1,6 @@
import { derivedMemo } from "../../../utils" import { derivedMemo } from "../../../utils"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { DatasourceType } from "../lib/constants"
export const deriveStores = context => { export const deriveStores = context => {
const { props, hasNonAutoColumn } = context const { props, hasNonAutoColumn } = context
@ -28,8 +29,7 @@ export const deriveStores = context => {
} }
// Disable some features if we're editing a view // Disable some features if we're editing a view
if ($props.datasource?.type === "viewV2") { if ($props.datasource?.type === DatasourceType.ViewV2) {
config.canEditPrimaryDisplay = false
config.canEditColumns = false config.canEditColumns = false
} }

View File

@ -0,0 +1,107 @@
import { derived, get, writable } from "svelte/store"
export const createStores = () => {
const definition = writable(null)
return {
definition,
}
}
export const deriveStores = context => {
const { definition, schemaOverrides, columnWhitelist } = context
const schema = derived(
[definition, schemaOverrides, columnWhitelist],
([$definition, $schemaOverrides, $columnWhitelist]) => {
if (!$definition?.schema) {
return null
}
let newSchema = { ...$definition?.schema }
// Edge case to temporarily allow deletion of duplicated user
// fields that were saved with the "disabled" flag set.
// By overriding the saved schema we ensure only overrides can
// set the disabled flag.
// TODO: remove in future
Object.keys(newSchema).forEach(field => {
newSchema[field] = {
...newSchema[field],
disabled: false,
}
})
// Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) {
newSchema[field] = {
...newSchema[field],
...$schemaOverrides[field],
}
}
})
// Apply whitelist if specified
if ($columnWhitelist?.length) {
Object.keys(newSchema).forEach(key => {
if (!$columnWhitelist.includes(key)) {
delete newSchema[key]
}
})
}
return newSchema
}
)
return {
schema,
}
}
export const createActions = context => {
const { datasource, definition, API, config, dispatch } = context
// Refreshes the datasource definition
const refreshDefinition = async () => {
const $datasource = get(datasource)
if ($datasource.type === "table") {
definition.set(await API.fetchTableDefinition($datasource.tableId))
} else if ($datasource.type === "viewV2") {
// const definition = await API.viewsV2.(get(tableId))
// table.set(definition)
}
}
// Saves the datasource definition
const saveDefinition = async newDefinition => {
const $config = get(config)
const $datasource = get(datasource)
// Update local state
definition.set(newDefinition)
// Update server
if ($config.canSaveSchema) {
if ($datasource.type === "table") {
await API.saveTable(newDefinition)
} else if ($datasource.type === "viewV2") {
await API.viewV2.update({ ...newDefinition })
}
}
// Broadcast change to external state can be updated, as this change
// will not be received by the builder websocket because we caused it ourselves
dispatch("updatedefinition", newDefinition)
}
return {
datasource: {
...datasource,
actions: {
refreshDefinition,
saveDefinition,
},
},
}
}

View File

@ -15,13 +15,18 @@ import * as Config from "./config"
import * as Sort from "./sort" import * as Sort from "./sort"
import * as Filter from "./filter" import * as Filter from "./filter"
import * as Notifications from "./notifications" import * as Notifications from "./notifications"
import * as Table from "./table"
import * as ViewV2 from "./viewV2"
import * as Datasource from "./datsource"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
// Common stores
Notifications, Notifications,
Sort, Sort,
Filter, Filter,
Bounds, Bounds,
Scroll, Scroll,
Datasource,
Columns, Columns,
Rows, Rows,
UI, UI,
@ -34,6 +39,10 @@ const DependencyOrderedStores = [
Pagination, Pagination,
Clipboard, Clipboard,
Config, Config,
// Datasource specific stores
Table,
ViewV2,
] ]
export const attachStores = context => { export const attachStores = context => {

View File

@ -7,13 +7,13 @@ const SuppressErrors = true
export const createStores = () => { export const createStores = () => {
const rows = writable([]) const rows = writable([])
const table = writable(null)
const loading = writable(false) const loading = writable(false)
const loaded = writable(false) const loaded = writable(false)
const rowChangeCache = writable({}) const rowChangeCache = writable({})
const inProgressChanges = writable({}) const inProgressChanges = writable({})
const hasNextPage = writable(false) const hasNextPage = writable(false)
const error = writable(null) const error = writable(null)
const fetch = writable(null)
// Generate a lookup map to quick find a row by ID // Generate a lookup map to quick find a row by ID
const rowLookupMap = derived(rows, $rows => { const rowLookupMap = derived(rows, $rows => {
@ -51,8 +51,8 @@ export const createStores = () => {
...rows, ...rows,
subscribe: enrichedRows.subscribe, subscribe: enrichedRows.subscribe,
}, },
fetch,
rowLookupMap, rowLookupMap,
table,
loaded, loaded,
loading, loading,
rowChangeCache, rowChangeCache,
@ -66,7 +66,7 @@ export const createActions = context => {
const { const {
rows, rows,
rowLookupMap, rowLookupMap,
table, definition,
filter, filter,
loading, loading,
sort, sort,
@ -82,14 +82,14 @@ export const createActions = context => {
hasNextPage, hasNextPage,
error, error,
notifications, notifications,
fetch,
} = context } = context
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
const fetch = writable(null)
// Local cache of row IDs to speed up checking if a row exists // Local cache of row IDs to speed up checking if a row exists
let rowCacheMap = {} let rowCacheMap = {}
// Reset everything when table ID changes // Reset everything when datasource changes
let unsubscribe = null let unsubscribe = null
let lastResetKey = null let lastResetKey = null
datasource.subscribe(async $datasource => { datasource.subscribe(async $datasource => {
@ -100,11 +100,11 @@ export const createActions = context => {
loading.set(true) loading.set(true)
// Abandon if we don't have a valid datasource // Abandon if we don't have a valid datasource
if (!$datasource?.tableId) { if (!$datasource) {
return return
} }
// Tick to allow other reactive logic to update stores when table ID changes // Tick to allow other reactive logic to update stores when datasource changes
// before proceeding. This allows us to wipe filters etc if needed. // before proceeding. This allows us to wipe filters etc if needed.
await tick() await tick()
const $filter = get(filter) const $filter = get(filter)
@ -142,7 +142,7 @@ export const createActions = context => {
const previousResetKey = lastResetKey const previousResetKey = lastResetKey
lastResetKey = $fetch.resetKey lastResetKey = $fetch.resetKey
// If resetting rows due to a table change, wipe data and wait for // If resetting rows due to a datasource change, wipe data and wait for
// derived stores to compute. This prevents stale data being passed // derived stores to compute. This prevents stale data being passed
// to cells when we save the new schema. // to cells when we save the new schema.
if (!$instanceLoaded && previousResetKey) { if (!$instanceLoaded && previousResetKey) {
@ -152,16 +152,17 @@ export const createActions = context => {
// Reset state properties when dataset changes // Reset state properties when dataset changes
if (!$instanceLoaded || resetRows) { if (!$instanceLoaded || resetRows) {
table.set($fetch.definition) definition.set($fetch.definition)
sort.set({
column: $fetch.sortColumn, // sort.set({
order: $fetch.sortOrder, // column: $fetch.sortColumn,
}) // order: $fetch.sortOrder,
// })
} }
// Reset scroll state when data changes // Reset scroll state when data changes
if (!$instanceLoaded) { if (!$instanceLoaded) {
// Reset both top and left for a new table ID // Reset both top and left for a new datasource ID
instanceLoaded.set(true) instanceLoaded.set(true)
scroll.set({ top: 0, left: 0 }) scroll.set({ top: 0, left: 0 })
} else if (resetRows) { } else if (resetRows) {
@ -169,8 +170,6 @@ export const createActions = context => {
scroll.update(state => ({ ...state, top: 0 })) scroll.update(state => ({ ...state, top: 0 }))
} }
// For views we always update the filter to match the definition
// Process new rows // Process new rows
handleNewRows($fetch.rows, resetRows) handleNewRows($fetch.rows, resetRows)
@ -182,23 +181,6 @@ export const createActions = context => {
fetch.set(newFetch) fetch.set(newFetch)
}) })
// Update fetch when filter or sort config changes
filter.subscribe($filter => {
if (get(datasource)?.type === "table") {
get(fetch)?.update({
filter: $filter,
})
}
})
sort.subscribe($sort => {
if (get(datasource)?.type === "table") {
get(fetch)?.update({
sortOrder: $sort.order,
sortColumn: $sort.column,
})
}
})
// Gets a row by ID // Gets a row by ID
const getRow = id => { const getRow = id => {
const index = get(rowLookupMap)[id] const index = get(rowLookupMap)[id]
@ -506,17 +488,6 @@ export const createActions = context => {
get(fetch)?.nextPage() get(fetch)?.nextPage()
} }
// Refreshes the schema of the data fetch subscription
const refreshDatasourceDefinition = async () => {
const $datasource = get(datasource)
if ($datasource.type === "table") {
table.set(await API.fetchTableDefinition($datasource.tableId))
} else if ($datasource.type === "viewV2") {
// const definition = await API.viewsV2.(get(tableId))
// table.set(definition)
}
}
// Checks if we have a row with a certain ID // Checks if we have a row with a certain ID
const hasRow = id => { const hasRow = id => {
if (id === NewRowID) { if (id === NewRowID) {
@ -550,21 +521,7 @@ export const createActions = context => {
refreshRow, refreshRow,
replaceRow, replaceRow,
refreshData, refreshData,
refreshDatasourceDefinition,
}, },
}, },
} }
} }
export const initialise = context => {
const { table, filter, datasource } = context
// For views, always keep the UI for filter and sorting up to date with the
// latest view definition
table.subscribe($definition => {
if (!$definition || get(datasource)?.type !== "viewV2") {
return
}
filter.set($definition.query)
})
}

View File

@ -16,7 +16,8 @@ export const createStores = context => {
} }
export const initialise = context => { export const initialise = context => {
const { sort, initialSortColumn, initialSortOrder } = context const { sort, initialSortColumn, initialSortOrder, table, datasource } =
context
// Reset sort when initial sort props change // Reset sort when initial sort props change
initialSortColumn.subscribe(newSortColumn => { initialSortColumn.subscribe(newSortColumn => {

View File

@ -0,0 +1,24 @@
import { get } from "svelte/store"
export const initialise = context => {
const { datasource, fetch, filter, sort } = context
// Update fetch when filter changes
filter.subscribe($filter => {
if (get(datasource)?.type === "table") {
get(fetch)?.update({
filter: $filter,
})
}
})
// Update fetch when sorting changes
sort.subscribe($sort => {
if (get(datasource)?.type === "table") {
get(fetch)?.update({
sortOrder: $sort.order,
sortColumn: $sort.column,
})
}
})
}

View File

@ -131,7 +131,7 @@ export const initialise = context => {
focusedCellId, focusedCellId,
selectedRows, selectedRows,
hoveredRowId, hoveredRowId,
table, definition,
rowHeight, rowHeight,
fixedRowHeight, fixedRowHeight,
} = context } = context
@ -187,9 +187,9 @@ export const initialise = context => {
}) })
// Pull row height from table as long as we don't have a fixed height // Pull row height from table as long as we don't have a fixed height
table.subscribe($table => { definition.subscribe($definition => {
if (!get(fixedRowHeight)) { if (!get(fixedRowHeight)) {
rowHeight.set($table?.rowHeight || DefaultRowHeight) rowHeight.set($definition?.rowHeight || DefaultRowHeight)
} }
}) })
@ -198,7 +198,7 @@ export const initialise = context => {
if (height) { if (height) {
rowHeight.set(height) rowHeight.set(height)
} else { } else {
rowHeight.set(get(table)?.rowHeight || DefaultRowHeight) rowHeight.set(get(definition)?.rowHeight || DefaultRowHeight)
} }
}) })
} }

View File

@ -0,0 +1,43 @@
import { get } from "svelte/store"
export const initialise = context => {
const { definition, datasource, sort, rows } = context
// For views, keep sort state in line with the view definition
definition.subscribe($definition => {
if (!$definition || get(datasource)?.type !== "viewV2") {
return
}
const $sort = get(sort)
if (
$definition.sort?.field !== $sort?.column ||
$definition.sort?.order !== $sort?.order
) {
sort.set({
column: $definition.sort?.field,
order: $definition.sort?.order,
})
}
})
// When sorting changes, ensure view definition is kept up to date
sort.subscribe(async $sort => {
const $view = get(definition)
if (!$view || get(datasource)?.type !== "viewV2") {
return
}
if (
$sort?.column !== $view.sort?.field ||
$sort?.order !== $view.sort?.order
) {
await datasource.actions.saveDefinition({
...$view,
sort: {
field: $sort.column,
order: $sort.order,
},
})
await rows.actions.refreshData()
}
})
}

View File

@ -116,8 +116,16 @@ export default class DataFetch {
async getInitialData() { async getInitialData() {
const { datasource, filter, paginate } = this.options const { datasource, filter, paginate } = this.options
// Fetch datasource definition and determine feature flags // Fetch datasource definition and extract filter and sort if configured
const definition = await this.getDefinition(datasource) const definition = await this.getDefinition(datasource)
if (definition?.sort?.field) {
this.options.sortColumn = definition.sort.field
}
if (definition?.sort?.order) {
this.options.sortOrder = definition.sort.order
}
// Determine feature flags
const features = this.determineFeatureFlags(definition) const features = this.determineFeatureFlags(definition)
this.features = { this.features = {
supportsSearch: !!features?.supportsSearch, supportsSearch: !!features?.supportsSearch,