Merge pull request #15255 from Budibase/typing/stores-grid-rows

Typing grid rows stores
This commit is contained in:
Adria Navarro 2024-12-31 11:34:05 +01:00 committed by GitHub
commit 92f92d2c62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 278 additions and 80 deletions

View File

@ -2,6 +2,7 @@
"extends": "./tsconfig.build.json", "extends": "./tsconfig.build.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"lib": ["ESNext"],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@budibase/*": [ "@budibase/*": [

View File

@ -16,7 +16,16 @@ interface DerivedColumnStore {
hasNonAutoColumn: Readable<boolean> hasNonAutoColumn: Readable<boolean>
} }
export type Store = ColumnStore & DerivedColumnStore interface ColumnActions {
columns: ColumnStore["columns"] & {
actions: {
changeAllColumnWidths: (width: number) => Promise<void>
isReadonly: (column: UIColumn) => boolean
}
}
}
export type Store = ColumnStore & DerivedColumnStore & ColumnActions
export const createStores = (): ColumnStore => { export const createStores = (): ColumnStore => {
const columns = writable<UIColumn[]>([]) const columns = writable<UIColumn[]>([])
@ -95,7 +104,7 @@ export const deriveStores = (context: StoreContext): DerivedColumnStore => {
} }
} }
export const createActions = (context: StoreContext) => { export const createActions = (context: StoreContext): ColumnActions => {
const { columns, datasource } = context const { columns, datasource } = context
// Updates the width of all columns // Updates the width of all columns

View File

@ -3,12 +3,12 @@ import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils" import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { import {
Row,
SaveRowRequest, SaveRowRequest,
SaveTableRequest, SaveTableRequest,
UIDatasource, UIDatasource,
UIFieldMutation, UIFieldMutation,
UIFieldSchema, UIFieldSchema,
UIRow,
UpdateViewRequest, UpdateViewRequest,
ViewV2Type, ViewV2Type,
} from "@budibase/types" } from "@budibase/types"
@ -327,7 +327,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
} }
// Deletes rows from the datasource // Deletes rows from the datasource
const deleteRows = async (rows: Row[]) => { const deleteRows = async (rows: UIRow[]) => {
return await getAPI()?.actions.deleteRows(rows) return await getAPI()?.actions.deleteRows(rows)
} }

View File

@ -1,8 +1,8 @@
import { import {
Row,
SaveRowRequest, SaveRowRequest,
SaveTableRequest, SaveTableRequest,
UIDatasource, UIDatasource,
UIRow,
UpdateViewRequest, UpdateViewRequest,
} from "@budibase/types" } from "@budibase/types"
@ -10,10 +10,10 @@ interface DatasourceBaseActions<
TSaveDefinitionRequest = UpdateViewRequest | SaveTableRequest TSaveDefinitionRequest = UpdateViewRequest | SaveTableRequest
> { > {
saveDefinition: (newDefinition: TSaveDefinitionRequest) => Promise<void> saveDefinition: (newDefinition: TSaveDefinitionRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<Row | void> addRow: (row: SaveRowRequest) => Promise<UIRow | undefined>
updateRow: (row: SaveRowRequest) => Promise<Row | void> updateRow: (row: SaveRowRequest) => Promise<UIRow | undefined>
deleteRows: (rows: Row[]) => Promise<void> deleteRows: (rows: UIRow[]) => Promise<void>
getRow: (id: string) => Promise<Row | void> getRow: (id: string) => Promise<UIRow | void>
isDatasourceValid: (datasource: UIDatasource) => boolean | void isDatasourceValid: (datasource: UIDatasource) => boolean | void
canUseColumn: (name: string) => boolean | void canUseColumn: (name: string) => boolean | void
} }

View File

@ -106,7 +106,7 @@ export const initialise = (context: StoreContext) => {
if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
return return
} }
$fetch.update({ $fetch?.update({
filter: $allFilters, filter: $allFilters,
}) })
}) })
@ -120,7 +120,7 @@ export const initialise = (context: StoreContext) => {
if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
return return
} }
$fetch.update({ $fetch?.update({
sortOrder: $sort.order || SortOrder.ASCENDING, sortOrder: $sort.order || SortOrder.ASCENDING,
sortColumn: $sort.column, sortColumn: $sort.column,
}) })

View File

@ -31,7 +31,8 @@ export const createActions = (context: StoreContext): TableActions => {
...row, ...row,
tableId: get(datasource)?.tableId, tableId: get(datasource)?.tableId,
} }
return await API.saveRow(row, SuppressErrors) const newRow = await API.saveRow(row, SuppressErrors)
return { ...newRow, _id: newRow._id! }
} }
const deleteRows = async (rows: Row[]) => { const deleteRows = async (rows: Row[]) => {
@ -52,7 +53,12 @@ export const createActions = (context: StoreContext): TableActions => {
}, },
paginate: false, paginate: false,
}) })
return res?.rows?.[0] const row = res?.rows?.[0]
if (!row) {
return
}
return { ...row, _id: row._id! }
} }
const canUseColumn = (name: string) => { const canUseColumn = (name: string) => {

View File

@ -34,8 +34,10 @@ export const createActions = (context: StoreContext): ViewActions => {
tableId: $datasource?.tableId, tableId: $datasource?.tableId,
_viewId: $datasource?.id, _viewId: $datasource?.id,
} }
const newRow = await API.saveRow(row, SuppressErrors)
return { return {
...(await API.saveRow(row, SuppressErrors)), ...newRow,
_id: newRow._id!,
_viewId: row._viewId, _viewId: row._viewId,
} }
} }
@ -54,7 +56,12 @@ export const createActions = (context: StoreContext): ViewActions => {
}, },
paginate: false, paginate: false,
}) })
return res?.rows?.[0] const row = res?.rows?.[0]
if (!row) {
return
}
return { ...row, _id: row._id! }
} }
const isDatasourceValid = (datasource: UIDatasource) => { const isDatasourceValid = (datasource: UIDatasource) => {

View File

@ -69,12 +69,10 @@ export type Store = BaseStore &
Clipboard.Store & Clipboard.Store &
Scroll.Store & { Scroll.Store & {
// TODO while typing the rest of stores // TODO while typing the rest of stores
fetch: Writable<any>
sort: Writable<any> sort: Writable<any>
initialFilter: Writable<any> initialFilter: Writable<any>
initialSortColumn: Writable<any> initialSortColumn: Writable<any>
initialSortOrder: Writable<any> initialSortOrder: Writable<any>
rows: Writable<any> & { actions: any }
subscribe: any subscribe: any
config: Writable<any> config: Writable<any>
dispatch: (event: string, data: any) => any dispatch: (event: string, data: any) => any
@ -82,13 +80,11 @@ export type Store = BaseStore &
schemaOverrides: Writable<any> schemaOverrides: Writable<any>
gridID: string gridID: string
props: Writable<any> props: Writable<any>
rowLookupMap: Writable<any>
width: Writable<number> width: Writable<number>
fixedRowHeight: Writable<number> fixedRowHeight: Writable<number>
rowChangeCache: Readable<any>
bounds: Readable<any> bounds: Readable<any>
height: Readable<number> height: Readable<number>
} } & Rows.Store
export const attachStores = (context: Store): Store => { export const attachStores = (context: Store): Store => {
// Atomic store creation // Atomic store creation

View File

@ -1,4 +1,4 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get, Writable, Readable } from "svelte/store"
import { fetchData } from "../../../fetch" import { fetchData } from "../../../fetch"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { import {
@ -10,10 +10,74 @@ import {
import { tick } from "svelte" import { tick } from "svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { sleep } from "../../../utils/utils" import { sleep } from "../../../utils/utils"
import { FieldType } from "@budibase/types" import { FieldType, Row, UIFetchAPI, UIRow } from "@budibase/types"
import { getRelatedTableValues } from "../../../utils" import { getRelatedTableValues } from "../../../utils"
import { Store as StoreContext } from "."
export const createStores = () => { interface IndexedUIRow extends UIRow {
__idx: number
}
interface RowStore {
rows: Writable<UIRow[]>
fetch: Writable<UIFetchAPI | null>
loaded: Writable<boolean>
refreshing: Writable<boolean>
loading: Writable<boolean>
rowChangeCache: Writable<Record<string, Record<string, any>>>
inProgressChanges: Writable<Record<string, number>>
hasNextPage: Writable<boolean>
error: Writable<string | null>
}
interface RowDerivedStore {
rows: RowStore["rows"]
rowLookupMap: Readable<Record<string, IndexedUIRow>>
}
interface RowActionStore {
rows: RowStore["rows"] & {
actions: {
addRow: (params: {
row: Row
idx: number
bubble: boolean
notify: boolean
}) => Promise<UIRow | undefined>
duplicateRow: (row: UIRow) => Promise<UIRow | undefined>
bulkDuplicate: (
rowsToDupe: UIRow[],
progressCallback: (progressPercentage: number) => void
) => Promise<UIRow[]>
updateValue: (params: {
rowId: string
column: string
value: any
apply: boolean
}) => Promise<void>
applyRowChanges: (params: {
rowId: string
changes?: Record<string, any>
updateState?: boolean
handleErrors?: boolean
}) => Promise<UIRow | undefined>
deleteRows: (rowsToDelete: UIRow[]) => Promise<void>
loadNextPage: () => void
refreshRow: (id: string) => Promise<void>
replaceRow: (id: string, row: UIRow | undefined) => void
refreshData: () => Promise<void>
cleanRow: (row: UIRow) => Row
bulkUpdate: (
changeMap: Record<string, Record<string, any>>,
progressCallback: (progressPercentage: number) => void
) => Promise<void>
}
}
}
export type Store = RowStore & RowDerivedStore & RowActionStore
export const createStores = (): RowStore => {
const rows = writable([]) const rows = writable([])
const loading = writable(false) const loading = writable(false)
const loaded = writable(false) const loaded = writable(false)
@ -47,7 +111,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): RowDerivedStore => {
const { rows, enrichedSchema } = context const { rows, enrichedSchema } = context
// Enrich rows with an index property and any pending changes // Enrich rows with an index property and any pending changes
@ -57,12 +121,16 @@ export const deriveStores = context => {
const customColumns = Object.values($enrichedSchema || {}).filter( const customColumns = Object.values($enrichedSchema || {}).filter(
f => f.related f => f.related
) )
return $rows.map((row, idx) => ({ return $rows.map<IndexedUIRow>((row, idx) => ({
...row, ...row,
__idx: idx, __idx: idx,
...customColumns.reduce((map, column) => { ...customColumns.reduce<Record<string, string>>((map, column) => {
const fromField = $enrichedSchema[column.related.field] const fromField = $enrichedSchema![column.related!.field]
map[column.name] = getRelatedTableValues(row, column, fromField) map[column.name] = getRelatedTableValues(
row,
{ ...column, related: column.related! },
fromField
)
return map return map
}, {}), }, {}),
})) }))
@ -71,7 +139,7 @@ export const deriveStores = context => {
// 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(enrichedRows, $enrichedRows => { const rowLookupMap = derived(enrichedRows, $enrichedRows => {
let map = {} let map: Record<string, IndexedUIRow> = {}
for (let i = 0; i < $enrichedRows.length; i++) { for (let i = 0; i < $enrichedRows.length; i++) {
map[$enrichedRows[i]._id] = $enrichedRows[i] map[$enrichedRows[i]._id] = $enrichedRows[i]
} }
@ -87,7 +155,7 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext): RowActionStore => {
const { const {
rows, rows,
rowLookupMap, rowLookupMap,
@ -114,11 +182,11 @@ export const createActions = context => {
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
// 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: Record<string, boolean> = {}
// Reset everything when datasource changes // Reset everything when datasource changes
let unsubscribe = null let unsubscribe: (() => void) | null = null
let lastResetKey = null let lastResetKey: string | null = null
datasource.subscribe(async $datasource => { datasource.subscribe(async $datasource => {
// Unsub from previous fetch if one exists // Unsub from previous fetch if one exists
unsubscribe?.() unsubscribe?.()
@ -157,7 +225,7 @@ export const createActions = context => {
}) })
// Subscribe to changes of this fetch model // Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe(async $fetch => { unsubscribe = newFetch.subscribe(async ($fetch: UIFetchAPI) => {
if ($fetch.error) { if ($fetch.error) {
// Present a helpful error to the user // Present a helpful error to the user
let message = "An unknown error occurred" let message = "An unknown error occurred"
@ -214,7 +282,15 @@ export const createActions = context => {
// Handles validation errors from the rows API and updates local validation // Handles validation errors from the rows API and updates local validation
// state, storing error messages against relevant cells // state, storing error messages against relevant cells
const handleValidationError = (rowId, error) => { const handleValidationError = (
rowId: string,
error:
| string
| {
message?: string
json: { validationErrors: Record<string, string | undefined> }
}
) => {
let errorString let errorString
if (typeof error === "string") { if (typeof error === "string") {
errorString = error errorString = error
@ -224,7 +300,11 @@ export const createActions = context => {
// If the server doesn't reply with a valid error, assume that the source // If the server doesn't reply with a valid error, assume that the source
// of the error is the focused cell's column // of the error is the focused cell's column
if (!error?.json?.validationErrors && errorString) { if (
typeof error !== "string" &&
!error?.json?.validationErrors &&
errorString
) {
const { field: focusedColumn } = parseCellID(get(focusedCellId)) const { field: focusedColumn } = parseCellID(get(focusedCellId))
if (focusedColumn) { if (focusedColumn) {
error = { error = {
@ -236,7 +316,7 @@ export const createActions = context => {
} }
} }
} }
if (error?.json?.validationErrors) { if (typeof error !== "string" && error?.json?.validationErrors) {
// Normal validation errors // Normal validation errors
const keys = Object.keys(error.json.validationErrors) const keys = Object.keys(error.json.validationErrors)
const $columns = get(columns) const $columns = get(columns)
@ -251,11 +331,11 @@ export const createActions = context => {
missingColumns.push(column) missingColumns.push(column)
} }
} }
const { json } = error
// Process errors for columns that we have // Process errors for columns that we have
for (let column of erroredColumns) { for (let column of erroredColumns) {
// Ensure we have a valid error to display // Ensure we have a valid error to display
let err = error.json.validationErrors[column] let err = json.validationErrors[column]
if (Array.isArray(err)) { if (Array.isArray(err)) {
err = err[0] err = err[0]
} }
@ -287,9 +367,19 @@ export const createActions = context => {
} }
// Adds a new row // Adds a new row
const addRow = async ({ row, idx, bubble = false, notify = true }) => { const addRow = async ({
row,
idx,
bubble = false,
notify = true,
}: {
row: Row
idx: number
bubble: boolean
notify: boolean
}) => {
try { try {
const newRow = await datasource.actions.addRow(row) const newRow = (await datasource.actions.addRow(row))!
// Update state // Update state
if (idx != null) { if (idx != null) {
@ -306,7 +396,7 @@ export const createActions = context => {
get(notifications).success("Row created successfully") get(notifications).success("Row created successfully")
} }
return newRow return newRow
} catch (error) { } catch (error: any) {
if (bubble) { if (bubble) {
throw error throw error
} else { } else {
@ -317,7 +407,7 @@ export const createActions = context => {
} }
// Duplicates a row, inserting the duplicate row after the existing one // Duplicates a row, inserting the duplicate row after the existing one
const duplicateRow = async row => { const duplicateRow = async (row: UIRow) => {
let clone = cleanRow(row) let clone = cleanRow(row)
delete clone._id delete clone._id
delete clone._rev delete clone._rev
@ -330,17 +420,20 @@ export const createActions = context => {
}) })
get(notifications).success("Duplicated 1 row") get(notifications).success("Duplicated 1 row")
return duped return duped
} catch (error) { } catch (error: any) {
handleValidationError(row._id, error) handleValidationError(row._id, error)
validation.actions.focusFirstRowError(row._id) validation.actions.focusFirstRowError(row._id)
} }
} }
// Duplicates multiple rows, inserting them after the last source row // Duplicates multiple rows, inserting them after the last source row
const bulkDuplicate = async (rowsToDupe, progressCallback) => { const bulkDuplicate = async (
rowsToDupe: UIRow[],
progressCallback: (progressPercentage: number) => void
) => {
// Find index of last row // Find index of last row
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const indices = rowsToDupe.map(row => $rowLookupMap[row._id]?.__idx) const indices = rowsToDupe.map(row => $rowLookupMap[row._id!]?.__idx)
const index = Math.max(...indices) const index = Math.max(...indices)
const count = rowsToDupe.length const count = rowsToDupe.length
@ -357,8 +450,9 @@ export const createActions = context => {
let failed = 0 let failed = 0
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
try { try {
saved.push(await datasource.actions.addRow(clones[i])) const newRow = (await datasource.actions.addRow(clones[i]))!
rowCacheMap[saved._id] = true saved.push(newRow)
rowCacheMap[newRow._id] = true
await sleep(50) // Small sleep to ensure we avoid rate limiting await sleep(50) // Small sleep to ensure we avoid rate limiting
} catch (error) { } catch (error) {
failed++ failed++
@ -385,7 +479,7 @@ export const createActions = context => {
// Replaces a row in state with the newly defined row, handling updates, // Replaces a row in state with the newly defined row, handling updates,
// addition and deletion // addition and deletion
const replaceRow = (id, row) => { const replaceRow = (id: string, row: UIRow | undefined) => {
// Get index of row to check if it exists // Get index of row to check if it exists
const $rows = get(rows) const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
@ -410,22 +504,22 @@ export const createActions = context => {
} }
// Refreshes a specific row // Refreshes a specific row
const refreshRow = async id => { const refreshRow = async (id: string) => {
try { try {
const row = await datasource.actions.getRow(id) const row = await datasource.actions.getRow(id)
replaceRow(id, row) replaceRow(id, row!)
} catch { } catch {
// Do nothing - we probably just don't support refreshing individual rows // Do nothing - we probably just don't support refreshing individual rows
} }
} }
// Refreshes all data // Refreshes all data
const refreshData = () => { const refreshData = async () => {
get(fetch)?.getInitialData() await get(fetch)?.getInitialData()
} }
// Checks if a changeset for a row actually mutates the row or not // Checks if a changeset for a row actually mutates the row or not
const changesAreValid = (row, changes) => { const changesAreValid = (row: UIRow, changes: Record<string, any>) => {
const columns = Object.keys(changes || {}) const columns = Object.keys(changes || {})
if (!row || !columns.length) { if (!row || !columns.length) {
return false return false
@ -437,7 +531,7 @@ export const createActions = context => {
// Patches a row with some changes in local state, and returns whether a // Patches a row with some changes in local state, and returns whether a
// valid pending change was made or not // valid pending change was made or not
const stashRowChanges = (rowId, changes) => { const stashRowChanges = (rowId: string, changes: Record<string, any>) => {
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const $columnLookupMap = get(columnLookupMap) const $columnLookupMap = get(columnLookupMap)
const row = $rowLookupMap[rowId] const row = $rowLookupMap[rowId]
@ -477,13 +571,18 @@ export const createActions = context => {
changes = null, changes = null,
updateState = true, updateState = true,
handleErrors = true, handleErrors = true,
}: {
rowId: string
changes?: Record<string, any> | null
updateState?: boolean
handleErrors?: boolean
}) => { }) => {
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const row = $rowLookupMap[rowId] const row = $rowLookupMap[rowId]
if (row == null) { if (row == null) {
return return
} }
let savedRow let savedRow: UIRow | undefined = undefined
// Save change // Save change
try { try {
@ -496,13 +595,13 @@ export const createActions = context => {
// Update row // Update row
const stashedChanges = get(rowChangeCache)[rowId] const stashedChanges = get(rowChangeCache)[rowId]
const newRow = { ...cleanRow(row), ...stashedChanges, ...changes } const newRow = { ...cleanRow(row), ...stashedChanges, ...changes }
savedRow = await datasource.actions.updateRow(newRow) savedRow = (await datasource.actions.updateRow(newRow))!
// Update row state after a successful change // Update row state after a successful change
if (savedRow?._id) { if (savedRow?._id) {
if (updateState) { if (updateState) {
rows.update(state => { rows.update(state => {
state[row.__idx] = savedRow state[row.__idx] = savedRow!
return state.slice() return state.slice()
}) })
} }
@ -521,7 +620,7 @@ export const createActions = context => {
}) })
return state return state
}) })
} catch (error) { } catch (error: any) {
if (handleErrors) { if (handleErrors) {
handleValidationError(rowId, error) handleValidationError(rowId, error)
validation.actions.focusFirstRowError(rowId) validation.actions.focusFirstRowError(rowId)
@ -537,14 +636,27 @@ export const createActions = context => {
} }
// Updates a value of a row // Updates a value of a row
const updateValue = async ({ rowId, column, value, apply = true }) => { const updateValue = async ({
rowId,
column,
value,
apply = true,
}: {
rowId: string
column: string
value: any
apply: boolean
}) => {
const success = stashRowChanges(rowId, { [column]: value }) const success = stashRowChanges(rowId, { [column]: value })
if (success && apply) { if (success && apply) {
await applyRowChanges({ rowId }) await applyRowChanges({ rowId })
} }
} }
const bulkUpdate = async (changeMap, progressCallback) => { const bulkUpdate = async (
changeMap: Record<string, Record<string, any>>,
progressCallback: (progressPercentage: number) => void
) => {
const rowIds = Object.keys(changeMap || {}) const rowIds = Object.keys(changeMap || {})
const count = rowIds.length const count = rowIds.length
if (!count) { if (!count) {
@ -613,7 +725,7 @@ export const createActions = context => {
} }
// Deletes an array of rows // Deletes an array of rows
const deleteRows = async rowsToDelete => { const deleteRows = async (rowsToDelete: UIRow[]) => {
if (!rowsToDelete?.length) { if (!rowsToDelete?.length) {
return return
} }
@ -628,7 +740,7 @@ export const createActions = context => {
// Local handler to process new rows inside the fetch, and append any new // Local handler to process new rows inside the fetch, and append any new
// rows to state that we haven't encountered before // rows to state that we haven't encountered before
const handleNewRows = (newRows, resetRows) => { const handleNewRows = (newRows: Row[], resetRows?: boolean) => {
if (resetRows) { if (resetRows) {
rowCacheMap = {} rowCacheMap = {}
} }
@ -645,20 +757,20 @@ export const createActions = context => {
newRow._id = generateRowID() newRow._id = generateRowID()
} }
if (!rowCacheMap[newRow._id]) { if (!rowCacheMap[newRow._id!]) {
rowCacheMap[newRow._id] = true rowCacheMap[newRow._id!] = true
rowsToAppend.push(newRow) rowsToAppend.push(newRow)
} }
} }
if (resetRows) { if (resetRows) {
rows.set(rowsToAppend) rows.set(rowsToAppend as UIRow[])
} else if (rowsToAppend.length) { } else if (rowsToAppend.length) {
rows.update(state => [...state, ...rowsToAppend]) rows.update(state => [...state, ...(rowsToAppend as UIRow[])])
} }
} }
// Local handler to remove rows from state // Local handler to remove rows from state
const handleRemoveRows = rowsToRemove => { const handleRemoveRows = (rowsToRemove: UIRow[]) => {
const deletedIds = rowsToRemove.map(row => row._id) const deletedIds = rowsToRemove.map(row => row._id)
// We deliberately do not remove IDs from the cache map as the data may // We deliberately do not remove IDs from the cache map as the data may
@ -675,11 +787,11 @@ export const createActions = context => {
// Cleans a row by removing any internal grid metadata from it. // Cleans a row by removing any internal grid metadata from it.
// Call this before passing a row to any sort of external flow. // Call this before passing a row to any sort of external flow.
const cleanRow = row => { const cleanRow = (row: UIRow) => {
let clone = { ...row } let clone: Row = { ...row }
delete clone.__idx delete clone.__idx
delete clone.__metadata delete clone.__metadata
if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id)) { if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id!)) {
delete clone._id delete clone._id
} }
return clone return clone
@ -706,7 +818,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { const {
rowChangeCache, rowChangeCache,
inProgressChanges, inProgressChanges,
@ -733,7 +845,9 @@ export const initialise = context => {
if (!id) { if (!id) {
return return
} }
const { rowId, field } = parseCellID(id) let { rowId, field } = parseCellID(id)
rowId = rowId!
field = field!
const hasChanges = field in (get(rowChangeCache)[rowId] || {}) const hasChanges = field in (get(rowChangeCache)[rowId] || {})
const hasErrors = validation.actions.rowHasErrors(rowId) const hasErrors = validation.actions.rowHasErrors(rowId)
const isSavingChanges = get(inProgressChanges)[rowId] const isSavingChanges = get(inProgressChanges)[rowId]

View File

@ -40,12 +40,21 @@ export interface UIDerivedStore {
compact: Readable<boolean> compact: Readable<boolean>
selectedRowCount: Readable<number> selectedRowCount: Readable<number>
isSelectingCells: Readable<boolean> isSelectingCells: Readable<boolean>
selectedCells: Readable<string[][]> & { actions: any } selectedCells: Readable<string[][]>
selectedCellMap: Readable<Record<string, boolean>> selectedCellMap: Readable<Record<string, boolean>>
selectedCellCount: Readable<number> selectedCellCount: Readable<number>
} }
export type Store = UIStore & UIDerivedStore interface UIActionStore {
selectedCells: UIDerivedStore["selectedCells"] & {
actions: {
clear: () => void
selectRange: (source: string | null, target: string | null) => void
}
}
}
export type Store = UIStore & UIDerivedStore & UIActionStore
export const createStores = (context: StoreContext): UIStore => { export const createStores = (context: StoreContext): UIStore => {
const { props } = context const { props } = context
@ -82,7 +91,7 @@ export const createStores = (context: StoreContext): UIStore => {
} }
} }
export const deriveStores = (context: StoreContext) => { export const deriveStores = (context: StoreContext): UIDerivedStore => {
const { const {
focusedCellId, focusedCellId,
rows, rows,
@ -97,14 +106,14 @@ export const deriveStores = (context: StoreContext) => {
// Derive the current focused row ID // Derive the current focused row ID
const focusedRowId = derived(focusedCellId, $focusedCellId => { const focusedRowId = derived(focusedCellId, $focusedCellId => {
return parseCellID($focusedCellId).rowId return parseCellID($focusedCellId).rowId ?? null
}) })
// Derive the row that contains the selected cell // Derive the row that contains the selected cell
const focusedRow = derived( const focusedRow = derived(
[focusedRowId, rowLookupMap], [focusedRowId, rowLookupMap],
([$focusedRowId, $rowLookupMap]) => { ([$focusedRowId, $rowLookupMap]) => {
if ($focusedRowId === undefined) { if ($focusedRowId === null) {
return return
} }

View File

@ -10,7 +10,17 @@ interface DerivedValidationStore {
validationRowLookupMap: Readable<Record<string, string[]>> validationRowLookupMap: Readable<Record<string, string[]>>
} }
export type Store = ValidationStore & DerivedValidationStore interface ValidationActions {
validation: ValidationStore["validation"] & {
actions: {
setError: (cellId: string | undefined, error: string) => void
rowHasErrors: (rowId: string) => boolean
focusFirstRowError: (rowId: string) => void
}
}
}
export type Store = ValidationStore & DerivedValidationStore & ValidationActions
// Normally we would break out actions into the explicit "createActions" // Normally we would break out actions into the explicit "createActions"
// function, but for validation all these actions are pure so can go into // function, but for validation all these actions are pure so can go into
@ -49,7 +59,7 @@ export const deriveStores = (context: StoreContext): DerivedValidationStore => {
} }
} }
export const createActions = (context: StoreContext) => { export const createActions = (context: StoreContext): ValidationActions => {
const { validation, focusedCellId, validationRowLookupMap } = context const { validation, focusedCellId, validationRowLookupMap } = context
const setError = (cellId: string | undefined, error: string) => { const setError = (cellId: string | undefined, error: string) => {

View File

@ -2,6 +2,7 @@
"extends": "../../tsconfig.build.json", "extends": "../../tsconfig.build.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["ESNext"],
"module": "preserve", "module": "preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"outDir": "./dist", "outDir": "./dist",

View File

@ -0,0 +1,38 @@
import {
Row,
SortOrder,
UIDatasource,
UILegacyFilter,
UISearchFilter,
} from "@budibase/types"
export interface UIFetchAPI {
definition: UIDatasource
getInitialData: () => Promise<void>
loading: any
loaded: boolean
resetKey: string | null
error: any
hasNextPage: boolean
nextPage: () => Promise<void>
rows: Row[]
options?: {
datasource?: {
tableId: string
id: string
}
}
update: ({
sortOrder,
sortColumn,
}: {
sortOrder?: SortOrder
sortColumn?: string
filter?: UILegacyFilter[] | UISearchFilter
}) => any
}

View File

@ -4,3 +4,5 @@ export * from "./table"
export * from "./view" export * from "./view"
export * from "./user" export * from "./user"
export * from "./filters" export * from "./filters"
export * from "./rows"
export * from "./fetch"

View File

@ -0,0 +1,5 @@
import { Row } from "@budibase/types"
export type UIRow = Row & {
_id: string
}