Merge pull request #15255 from Budibase/typing/stores-grid-rows
Typing grid rows stores
This commit is contained in:
commit
92f92d2c62
|
@ -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/*": [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Row } from "@budibase/types"
|
||||||
|
|
||||||
|
export type UIRow = Row & {
|
||||||
|
_id: string
|
||||||
|
}
|
Loading…
Reference in New Issue