Merge branch 'master' into budi-7710-user-groups-do-not-fully-support-custom-roles-3
This commit is contained in:
commit
a9ad0229da
|
@ -59,13 +59,13 @@
|
||||||
isReadonly: () => readonly,
|
isReadonly: () => readonly,
|
||||||
getType: () => column.schema.type,
|
getType: () => column.schema.type,
|
||||||
getValue: () => row[column.name],
|
getValue: () => row[column.name],
|
||||||
setValue: (value, options = { save: true }) => {
|
setValue: (value, options = { apply: true }) => {
|
||||||
validation.actions.setError(cellId, null)
|
validation.actions.setError(cellId, null)
|
||||||
updateValue({
|
updateValue({
|
||||||
rowId: row._id,
|
rowId: row._id,
|
||||||
column: column.name,
|
column: column.name,
|
||||||
value,
|
value,
|
||||||
save: options?.save,
|
apply: options?.apply,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,14 +217,14 @@
|
||||||
const type = $focusedCellAPI.getType()
|
const type = $focusedCellAPI.getType()
|
||||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
||||||
// Update the value locally but don't save it yet
|
// Update the value locally but don't save it yet
|
||||||
$focusedCellAPI.setValue(parseInt(key), { save: false })
|
$focusedCellAPI.setValue(parseInt(key), { apply: false })
|
||||||
$focusedCellAPI.focus()
|
$focusedCellAPI.focus()
|
||||||
} else if (
|
} else if (
|
||||||
["string", "barcodeqr", "longform"].includes(type) &&
|
["string", "barcodeqr", "longform"].includes(type) &&
|
||||||
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
|
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
|
||||||
) {
|
) {
|
||||||
// Update the value locally but don't save it yet
|
// Update the value locally but don't save it yet
|
||||||
$focusedCellAPI.setValue(key, { save: false })
|
$focusedCellAPI.setValue(key, { apply: false })
|
||||||
$focusedCellAPI.focus()
|
$focusedCellAPI.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,29 +327,31 @@ export const createActions = context => {
|
||||||
get(fetch)?.getInitialData()
|
get(fetch)?.getInitialData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patches a row with some changes
|
// Checks if a changeset for a row actually mutates the row or not
|
||||||
const updateRow = async (rowId, changes, options = { save: true }) => {
|
const changesAreValid = (row, changes) => {
|
||||||
|
const columns = Object.keys(changes || {})
|
||||||
|
if (!row || !columns.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure there is at least 1 column that creates a difference
|
||||||
|
return columns.some(column => row[column] !== changes[column])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patches a row with some changes in local state, and returns whether a
|
||||||
|
// valid pending change was made or not
|
||||||
|
const stashRowChanges = (rowId, changes) => {
|
||||||
const $rows = get(rows)
|
const $rows = get(rows)
|
||||||
const $rowLookupMap = get(rowLookupMap)
|
const $rowLookupMap = get(rowLookupMap)
|
||||||
const index = $rowLookupMap[rowId]
|
const index = $rowLookupMap[rowId]
|
||||||
const row = $rows[index]
|
const row = $rows[index]
|
||||||
if (index == null || !Object.keys(changes || {}).length) {
|
|
||||||
return
|
// Check this is a valid change
|
||||||
|
if (!row || !changesAreValid(row, changes)) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abandon if no changes
|
// Add change to cache
|
||||||
let same = true
|
|
||||||
for (let column of Object.keys(changes)) {
|
|
||||||
if (row[column] !== changes[column]) {
|
|
||||||
same = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (same) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediately update state so that the change is reflected
|
|
||||||
rowChangeCache.update(state => ({
|
rowChangeCache.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
[rowId]: {
|
[rowId]: {
|
||||||
|
@ -357,26 +359,30 @@ export const createActions = context => {
|
||||||
...changes,
|
...changes,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Stop here if we don't want to persist the change
|
// Saves any pending changes to a row
|
||||||
if (!options?.save) {
|
const applyRowChanges = async rowId => {
|
||||||
|
const $rows = get(rows)
|
||||||
|
const $rowLookupMap = get(rowLookupMap)
|
||||||
|
const index = $rowLookupMap[rowId]
|
||||||
|
const row = $rows[index]
|
||||||
|
if (row == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save change
|
// Save change
|
||||||
try {
|
try {
|
||||||
inProgressChanges.update(state => ({
|
// Mark as in progress
|
||||||
...state,
|
inProgressChanges.update(state => ({ ...state, [rowId]: true }))
|
||||||
[rowId]: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Update row
|
// Update row
|
||||||
const saved = await datasource.actions.updateRow({
|
const changes = get(rowChangeCache)[rowId]
|
||||||
...cleanRow(row),
|
const newRow = { ...cleanRow(row), ...changes }
|
||||||
...get(rowChangeCache)[rowId],
|
const saved = await datasource.actions.updateRow(newRow)
|
||||||
})
|
|
||||||
|
|
||||||
// Update state after a successful change
|
// Update row state after a successful change
|
||||||
if (saved?._id) {
|
if (saved?._id) {
|
||||||
rows.update(state => {
|
rows.update(state => {
|
||||||
state[index] = saved
|
state[index] = saved
|
||||||
|
@ -386,6 +392,8 @@ export const createActions = context => {
|
||||||
// Handle users table edge case
|
// Handle users table edge case
|
||||||
await refreshRow(saved.id)
|
await refreshRow(saved.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wipe row change cache now that we've saved the row
|
||||||
rowChangeCache.update(state => {
|
rowChangeCache.update(state => {
|
||||||
delete state[rowId]
|
delete state[rowId]
|
||||||
return state
|
return state
|
||||||
|
@ -393,15 +401,17 @@ export const createActions = context => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleValidationError(rowId, error)
|
handleValidationError(rowId, error)
|
||||||
}
|
}
|
||||||
inProgressChanges.update(state => ({
|
|
||||||
...state,
|
// Mark as completed
|
||||||
[rowId]: false,
|
inProgressChanges.update(state => ({ ...state, [rowId]: false }))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates a value of a row
|
// Updates a value of a row
|
||||||
const updateValue = async ({ rowId, column, value, save = true }) => {
|
const updateValue = async ({ rowId, column, value, apply = true }) => {
|
||||||
return await updateRow(rowId, { [column]: value }, { save })
|
const success = stashRowChanges(rowId, { [column]: value })
|
||||||
|
if (success && apply) {
|
||||||
|
await applyRowChanges(rowId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes an array of rows
|
// Deletes an array of rows
|
||||||
|
@ -411,9 +421,7 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actually delete rows
|
// Actually delete rows
|
||||||
rowsToDelete.forEach(row => {
|
rowsToDelete.forEach(row => delete row.__idx)
|
||||||
delete row.__idx
|
|
||||||
})
|
|
||||||
await datasource.actions.deleteRows(rowsToDelete)
|
await datasource.actions.deleteRows(rowsToDelete)
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
|
@ -433,7 +441,7 @@ export const createActions = context => {
|
||||||
newRow = newRows[i]
|
newRow = newRows[i]
|
||||||
|
|
||||||
// Ensure we have a unique _id.
|
// Ensure we have a unique _id.
|
||||||
// This means generating one for non DS+, overriting any that may already
|
// This means generating one for non DS+, overwriting any that may already
|
||||||
// exist as we cannot allow duplicates.
|
// exist as we cannot allow duplicates.
|
||||||
if (!$isDatasourcePlus) {
|
if (!$isDatasourcePlus) {
|
||||||
newRow._id = Helpers.uuid()
|
newRow._id = Helpers.uuid()
|
||||||
|
@ -494,7 +502,7 @@ export const createActions = context => {
|
||||||
duplicateRow,
|
duplicateRow,
|
||||||
getRow,
|
getRow,
|
||||||
updateValue,
|
updateValue,
|
||||||
updateRow,
|
applyRowChanges,
|
||||||
deleteRows,
|
deleteRows,
|
||||||
hasRow,
|
hasRow,
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
|
@ -508,7 +516,14 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialise = context => {
|
export const initialise = context => {
|
||||||
const { rowChangeCache, inProgressChanges, previousFocusedRowId } = context
|
const {
|
||||||
|
rowChangeCache,
|
||||||
|
inProgressChanges,
|
||||||
|
previousFocusedRowId,
|
||||||
|
previousFocusedCellId,
|
||||||
|
rows,
|
||||||
|
validation,
|
||||||
|
} = context
|
||||||
|
|
||||||
// Wipe the row change cache when changing row
|
// Wipe the row change cache when changing row
|
||||||
previousFocusedRowId.subscribe(id => {
|
previousFocusedRowId.subscribe(id => {
|
||||||
|
@ -519,4 +534,15 @@ export const initialise = context => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ensure any unsaved changes are saved when changing cell
|
||||||
|
previousFocusedCellId.subscribe(async id => {
|
||||||
|
const rowId = id?.split("-")[0]
|
||||||
|
const hasErrors = validation.actions.rowHasErrors(rowId)
|
||||||
|
const hasChanges = Object.keys(get(rowChangeCache)[rowId] || {}).length > 0
|
||||||
|
const isSavingChanges = get(inProgressChanges)[rowId]
|
||||||
|
if (rowId && !hasErrors && hasChanges && !isSavingChanges) {
|
||||||
|
await rows.actions.applyRowChanges(rowId)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const createStores = context => {
|
||||||
const hoveredRowId = writable(null)
|
const hoveredRowId = writable(null)
|
||||||
const rowHeight = writable(get(props).fixedRowHeight || DefaultRowHeight)
|
const rowHeight = writable(get(props).fixedRowHeight || DefaultRowHeight)
|
||||||
const previousFocusedRowId = writable(null)
|
const previousFocusedRowId = writable(null)
|
||||||
|
const previousFocusedCellId = writable(null)
|
||||||
const gridFocused = writable(false)
|
const gridFocused = writable(false)
|
||||||
const isDragging = writable(false)
|
const isDragging = writable(false)
|
||||||
const buttonColumnWidth = writable(0)
|
const buttonColumnWidth = writable(0)
|
||||||
|
@ -48,6 +49,7 @@ export const createStores = context => {
|
||||||
focusedCellAPI,
|
focusedCellAPI,
|
||||||
focusedRowId,
|
focusedRowId,
|
||||||
previousFocusedRowId,
|
previousFocusedRowId,
|
||||||
|
previousFocusedCellId,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
rowHeight,
|
rowHeight,
|
||||||
gridFocused,
|
gridFocused,
|
||||||
|
@ -129,6 +131,7 @@ export const initialise = context => {
|
||||||
const {
|
const {
|
||||||
focusedRowId,
|
focusedRowId,
|
||||||
previousFocusedRowId,
|
previousFocusedRowId,
|
||||||
|
previousFocusedCellId,
|
||||||
rows,
|
rows,
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
|
@ -181,6 +184,13 @@ export const initialise = context => {
|
||||||
lastFocusedRowId = id
|
lastFocusedRowId = id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Remember the last focused cell ID so that we can store the previous one
|
||||||
|
let lastFocusedCellId = null
|
||||||
|
focusedCellId.subscribe(id => {
|
||||||
|
previousFocusedCellId.set(lastFocusedCellId)
|
||||||
|
lastFocusedCellId = id
|
||||||
|
})
|
||||||
|
|
||||||
// Remove hovered row when a cell is selected
|
// Remove hovered row when a cell is selected
|
||||||
focusedCellId.subscribe(cell => {
|
focusedCellId.subscribe(cell => {
|
||||||
if (cell && get(hoveredRowId)) {
|
if (cell && get(hoveredRowId)) {
|
||||||
|
|
|
@ -1,8 +1,23 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
|
||||||
|
// Normally we would break out actions into the explicit "createActions"
|
||||||
|
// function, but for validation all these actions are pure so can go into
|
||||||
|
// "createStores" instead to make dependency ordering simpler
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const validation = writable({})
|
const validation = writable({})
|
||||||
|
|
||||||
|
// Derive which rows have errors so that we can use that info later
|
||||||
|
const rowErrorMap = derived(validation, $validation => {
|
||||||
|
let map = {}
|
||||||
|
Object.entries($validation).forEach(([key, error]) => {
|
||||||
|
// Extract row ID from all errored cell IDs
|
||||||
|
if (error) {
|
||||||
|
map[key.split("-")[0]] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
const setError = (cellId, error) => {
|
const setError = (cellId, error) => {
|
||||||
if (!cellId) {
|
if (!cellId) {
|
||||||
return
|
return
|
||||||
|
@ -13,11 +28,16 @@ export const createStores = () => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowHasErrors = rowId => {
|
||||||
|
return get(rowErrorMap)[rowId]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validation: {
|
validation: {
|
||||||
...validation,
|
...validation,
|
||||||
actions: {
|
actions: {
|
||||||
setError,
|
setError,
|
||||||
|
rowHasErrors,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue