1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-21 12:10:32 +00:00
bramw_baserow/web-frontend/modules/database/store/view/grid.js
2021-03-12 13:46:22 +00:00

1098 lines
35 KiB
JavaScript

import Vue from 'vue'
import axios from 'axios'
import _ from 'lodash'
import BigNumber from 'bignumber.js'
import { uuid } from '@baserow/modules/core/utils/string'
import { clone } from '@baserow/modules/core/utils/object'
import GridService from '@baserow/modules/database/services/view/grid'
import RowService from '@baserow/modules/database/services/row'
import {
getRowSortFunction,
rowMatchesFilters,
} from '@baserow/modules/database/utils/view'
export function populateRow(row) {
row._ = {
loading: false,
hover: false,
selectedBy: [],
matchFilters: true,
matchSortings: true,
// Keeping the selected state with the row has the best performance when navigating
// between cells.
selected: false,
selectedFieldId: -1,
}
return row
}
export const state = () => ({
loading: false,
loaded: false,
// The last used grid id.
lastGridId: -1,
// Contains the custom field options per view. Things like the field width are
// stored here.
fieldOptions: {},
// Contains the buffered rows that we keep in memory. Depending on the
// scrollOffset rows will be added or removed from this buffer. Most of the times,
// it will contain 3 times the bufferRequestSize in rows.
rows: [],
// The total amount of rows in the table.
count: 0,
// The height of a single row.
rowHeight: 33,
// The distance to the top in pixels the visible rows should have.
rowsTop: 0,
// The amount of rows that must be visible above and under the middle row.
rowPadding: 16,
// The amount of rows that will be requested per request.
bufferRequestSize: 40,
// The start index of the buffer in the whole table.
bufferStartIndex: 0,
// The limit of the buffer measured from the start index in the whole table.
bufferLimit: 0,
// The start index of the visible rows of the rows in the buffer.
rowsStartIndex: 0,
// The end index of the visible rows of the rows buffer.
rowsEndIndex: 0,
// The last scrollTop when the visibleByScrollTop was called.
scrollTop: 0,
// The last windowHeight when the visibleByScrollTop was called.
windowHeight: 0,
// Indicates if the user is hovering over the add row button.
addRowHover: false,
})
export const mutations = {
SET_LOADING(state, value) {
state.loading = value
},
SET_LOADED(state, value) {
state.loaded = value
},
SET_LAST_GRID_ID(state, gridId) {
state.lastGridId = gridId
},
SET_SCROLL_TOP(state, { scrollTop, windowHeight }) {
state.scrollTop = scrollTop
state.windowHeight = windowHeight
},
CLEAR_ROWS(state) {
state.rows = []
state.rowsTop = 0
state.bufferStartIndex = 0
state.bufferEndIndex = 0
state.bufferLimit = 0
state.rowsStartIndex = 0
state.rowsEndIndex = 0
state.scrollTop = 0
},
/**
* It will add and remove rows to the state based on the provided values. For example
* if prependToRows is a positive number that amount of the provided rows will be
* added to the state. If that number is negative that amount will be removed from
* the state. Same goes for the appendToRows, only then it will be appended.
*/
ADD_ROWS(
state,
{ rows, prependToRows, appendToRows, count, bufferStartIndex, bufferLimit }
) {
state.count = count
state.bufferStartIndex = bufferStartIndex
state.bufferLimit = bufferLimit
if (prependToRows > 0) {
state.rows = [...rows.slice(0, prependToRows), ...state.rows]
}
if (appendToRows > 0) {
state.rows.push(...rows.slice(0, appendToRows))
}
if (prependToRows < 0) {
state.rows = state.rows.splice(Math.abs(prependToRows))
}
if (appendToRows < 0) {
state.rows = state.rows.splice(
0,
state.rows.length - Math.abs(appendToRows)
)
}
},
/**
* Inserts a new row at a specific index.
*/
INSERT_ROW_AT(state, { row, index }) {
state.count++
state.bufferLimit++
const min = new BigNumber(row.order.split('.')[0])
const max = new BigNumber(row.order)
// Decrease all the orders that have already have been inserted before the same
// row.
state.rows.forEach((row) => {
const order = new BigNumber(row.order)
if (order.isGreaterThan(min) && order.isLessThanOrEqualTo(max)) {
row.order = order
.minus(new BigNumber('0.00000000000000000001'))
.toString()
}
})
state.rows.splice(index, 0, row)
},
SET_ROWS_INDEX(state, { startIndex, endIndex, top }) {
state.rowsStartIndex = startIndex
state.rowsEndIndex = endIndex
state.rowsTop = top
},
DELETE_ROW(state, id) {
const index = state.rows.findIndex((item) => item.id === id)
if (index !== -1) {
// A small side effect of the buffered loading is that we don't know for sure if
// the row exists within the view. So the count might need to be decreased
// even though the row is not found. Because we don't want to make another call
// to the backend we only decrease the count if the row is found in the buffer.
// The count is eventually refreshed when the user scrolls within the view.
state.count--
state.bufferLimit--
state.rows.splice(index, 1)
}
},
DELETE_ROW_MOVED_UP(state, id) {
const index = state.rows.findIndex((item) => item.id === id)
if (index !== -1) {
state.bufferStartIndex++
state.bufferLimit--
state.rows.splice(index, 1)
}
},
DELETE_ROW_MOVED_DOWN(state, id) {
const index = state.rows.findIndex((item) => item.id === id)
if (index !== -1) {
state.bufferLimit--
state.rows.splice(index, 1)
}
},
FINALIZE_ROW(state, { oldId, id, order }) {
const index = state.rows.findIndex((item) => item.id === oldId)
if (index !== -1) {
state.rows[index].id = id
state.rows[index].order = order
state.rows[index]._.loading = false
}
},
SET_VALUE(state, { row, field, value }) {
row[`field_${field.id}`] = value
},
UPDATE_ROW(state, { row, values }) {
_.assign(row, values)
},
UPDATE_ROWS(state, { rows }) {
rows.forEach((newRow) => {
const row = state.rows.find((row) => row.id === newRow.id)
if (row !== undefined) {
_.assign(row, newRow)
}
})
},
SORT_ROWS(state, sortFunction) {
state.rows.sort(sortFunction)
// Because all the rows have been sorted again we can safely assume they are all in
// the right order again.
state.rows.forEach((row) => {
if (!row._.matchSortings) {
row._.matchSortings = true
}
})
},
ADD_FIELD(state, { field, value }) {
const name = `field_${field.id}`
state.rows.forEach((row) => {
// We have to use the Vue.set function here to make it reactive immediately.
// If we don't do this the value in the field components of the grid and modal
// don't have the correct value and will act strange.
Vue.set(row, name, value)
})
},
SET_ROW_LOADING(state, { row, value }) {
row._.loading = value
},
REPLACE_ALL_FIELD_OPTIONS(state, fieldOptions) {
state.fieldOptions = fieldOptions
},
UPDATE_ALL_FIELD_OPTIONS(state, fieldOptions) {
state.fieldOptions = _.merge({}, state.fieldOptions, fieldOptions)
},
UPDATE_FIELD_OPTIONS_OF_FIELD(state, { fieldId, values }) {
if (Object.prototype.hasOwnProperty.call(state.fieldOptions, fieldId)) {
_.assign(state.fieldOptions[fieldId], values)
} else {
state.fieldOptions = _.assign({}, state.fieldOptions, {
[fieldId]: values,
})
}
},
DELETE_FIELD_OPTIONS(state, fieldId) {
if (Object.prototype.hasOwnProperty.call(state.fieldOptions, fieldId)) {
delete state.fieldOptions[fieldId]
}
},
SET_ROW_HOVER(state, { row, value }) {
row._.hover = value
},
SET_ROW_MATCH_FILTERS(state, { row, value }) {
row._.matchFilters = value
},
SET_ROW_MATCH_SORTINGS(state, { row, value }) {
row._.matchSortings = value
},
ADD_ROW_SELECTED_BY(state, { row, fieldId }) {
if (!row._.selectedBy.includes(fieldId)) {
row._.selectedBy.push(fieldId)
}
},
REMOVE_ROW_SELECTED_BY(state, { row, fieldId }) {
const index = row._.selectedBy.indexOf(fieldId)
if (index > -1) {
row._.selectedBy.splice(index, 1)
}
},
SET_ADD_ROW_HOVER(state, value) {
state.addRowHover = value
},
SET_SELECTED_CELL(state, { rowId, fieldId }) {
state.rows.forEach((row) => {
if (row._.selected) {
row._.selected = false
row._.selectedFieldId = -1
}
if (row.id === rowId) {
row._.selected = true
row._.selectedFieldId = fieldId
}
})
},
}
// Contains the timeout needed for the delayed delayed scroll top action.
let fireTimeout = null
// Contains a timestamp of the last fire of the related actions to the delayed
// scroll top action.
let lastFire = null
// Contains the
let lastScrollTop = null
let lastRequest = null
let lastRequestOffset = null
let lastRequestLimit = null
let lastSource = null
export const actions = {
/**
* This action calculates which rows we would like to have in the buffer based on
* the scroll top offset and the window height. Based on that is calculates which
* rows we need to fetch compared to what we already have. If we need to fetch
* anything other then we already have or waiting for a new request will be made.
*/
fetchByScrollTop(
{ commit, getters, dispatch },
{ gridId, scrollTop, windowHeight }
) {
commit('SET_LAST_GRID_ID', gridId)
// Calculate what the middle row index of the visible window based on the scroll
// top.
const middle = scrollTop + windowHeight / 2
const countIndex = getters.getCount - 1
const middleRowIndex = Math.min(
Math.max(Math.ceil(middle / getters.getRowHeight) - 1, 0),
countIndex
)
// Calculate the start and end index of the rows that are visible to the user in
// the whole database.
const visibleStartIndex = Math.max(
middleRowIndex - getters.getRowPadding,
0
)
const visibleEndIndex = Math.min(
middleRowIndex + getters.getRowPadding,
countIndex
)
// Calculate the start and end index of the buffer, which are the rows that we
// load in the memory of the browser, based on all the rows in the database.
const bufferRequestSize = getters.getBufferRequestSize
const bufferStartIndex = Math.max(
Math.ceil((visibleStartIndex - bufferRequestSize) / bufferRequestSize) *
bufferRequestSize,
0
)
const bufferEndIndex = Math.min(
Math.ceil((visibleEndIndex + bufferRequestSize) / bufferRequestSize) *
bufferRequestSize,
getters.getCount
)
const bufferLimit = bufferEndIndex - bufferStartIndex
// Determine if the user is scrolling up or down.
const down =
bufferStartIndex > getters.getBufferStartIndex ||
bufferEndIndex > getters.getBufferEndIndex
const up =
bufferStartIndex < getters.getBufferStartIndex ||
bufferEndIndex < getters.getBufferEndIndex
let prependToBuffer = 0
let appendToBuffer = 0
let requestOffset = null
let requestLimit = null
// Calculate how many rows we want to add and remove from the current rows buffer in
// the store if the buffer would transition to the desired state. Also the
// request offset and limit are calculated for the next request based on what we
// currently have in the buffer.
if (down) {
prependToBuffer = Math.max(
-getters.getBufferLimit,
getters.getBufferStartIndex - bufferStartIndex
)
appendToBuffer = Math.min(
bufferLimit,
bufferEndIndex - getters.getBufferEndIndex
)
requestOffset = Math.max(getters.getBufferEndIndex, bufferStartIndex)
requestLimit = appendToBuffer
} else if (up) {
prependToBuffer = Math.min(
bufferLimit,
getters.getBufferStartIndex - bufferStartIndex
)
appendToBuffer = Math.max(
-getters.getBufferLimit,
bufferEndIndex - getters.getBufferEndIndex
)
requestOffset = Math.max(bufferStartIndex, 0)
requestLimit = prependToBuffer
}
// Checks if we need to request anything and if there are any changes since the
// last request we made. If so we need to initialize a new request.
if (
requestLimit > 0 &&
(lastRequestOffset !== requestOffset || lastRequestLimit !== requestLimit)
) {
// If another request is runnig we need to cancel that one because it won't
// what we need at the moment.
if (lastRequest !== null) {
lastSource.cancel('Canceled in favor of new request')
}
// Doing the actual request and remember what we are requesting so we can compare
// it when making a new request.
lastRequestOffset = requestOffset
lastRequestLimit = requestLimit
lastSource = axios.CancelToken.source()
lastRequest = GridService(this.$client)
.fetchRows({
gridId,
offset: requestOffset,
limit: requestLimit,
cancelToken: lastSource.token,
})
.then(({ data }) => {
data.results.forEach((part, index) => {
populateRow(data.results[index])
})
commit('ADD_ROWS', {
rows: data.results,
prependToRows: prependToBuffer,
appendToRows: appendToBuffer,
count: data.count,
bufferStartIndex,
bufferLimit,
})
dispatch('visibleByScrollTop', {
// Somehow we have to explicitly set these values to null.
scrollTop: null,
windowHeight: null,
})
lastRequest = null
})
.catch((error) => {
if (!axios.isCancel(error)) {
lastRequest = null
throw error
}
})
}
},
/**
* Calculates which rows should be visible for the user based on the provided
* scroll top and window height. Because we know what the padding above and below
* the middle row should be and which rows we have in the buffer we can calculate
* what the start and end index for the visible rows in the buffer should be.
*/
visibleByScrollTop(
{ getters, commit },
{ scrollTop = null, windowHeight = null }
) {
if (scrollTop !== null && windowHeight !== null) {
commit('SET_SCROLL_TOP', { scrollTop, windowHeight })
} else {
scrollTop = getters.getScrollTop
windowHeight = getters.getWindowHeight
}
const middle = scrollTop + windowHeight / 2
const countIndex = getters.getCount - 1
const middleRowIndex = Math.min(
Math.max(Math.ceil(middle / getters.getRowHeight) - 1, 0),
countIndex
)
// Calculate the start and end index of the rows that are visible to the user in
// the whole table.
const visibleStartIndex = Math.max(
middleRowIndex - getters.getRowPadding,
0
)
const visibleEndIndex = Math.min(
middleRowIndex + getters.getRowPadding + 1,
getters.getCount
)
// Calculate the start and end index of the buffered rows that are visible for
// the user.
const visibleRowStartIndex =
Math.min(
Math.max(visibleStartIndex, getters.getBufferStartIndex),
getters.getBufferEndIndex
) - getters.getBufferStartIndex
const visibleRowEndIndex =
Math.max(
Math.min(visibleEndIndex, getters.getBufferEndIndex),
getters.getBufferStartIndex
) - getters.getBufferStartIndex
// Calculate the top position of the html element that contains all the rows.
// This element will be placed over the placeholder the correct position of
// those rows.
const top =
Math.min(visibleStartIndex, getters.getBufferEndIndex) *
getters.getRowHeight
// If the index changes from what we already have we can commit the new indexes
// to the state.
if (
visibleRowStartIndex !== getters.getRowsStartIndex ||
visibleRowEndIndex !== getters.getRowsEndIndex ||
top !== getters.getRowsTop
) {
commit('SET_ROWS_INDEX', {
startIndex: visibleRowStartIndex,
endIndex: visibleRowEndIndex,
top,
})
}
},
/**
* This action is called every time the users scrolls which might result in a lot
* of calls. Therefore it will dispatch the related actions, but only every 100
* milliseconds to prevent calling the actions who do a lot of calculating a lot.
*/
fetchByScrollTopDelayed({ dispatch }, { gridId, scrollTop, windowHeight }) {
const fire = (scrollTop, windowHeight) => {
lastFire = new Date().getTime()
if (scrollTop === lastScrollTop) {
return
}
lastScrollTop = scrollTop
dispatch('fetchByScrollTop', { gridId, scrollTop, windowHeight })
dispatch('visibleByScrollTop', { scrollTop, windowHeight })
}
const difference = new Date().getTime() - lastFire
if (difference > 100) {
clearTimeout(fireTimeout)
fire(scrollTop, windowHeight)
} else {
clearTimeout(fireTimeout)
fireTimeout = setTimeout(() => {
fire(scrollTop, windowHeight)
}, 100)
}
},
/**
* Fetches an initial set of rows and adds that data to the store.
*/
async fetchInitial({ dispatch, commit, getters }, { gridId }) {
commit('SET_LAST_GRID_ID', gridId)
const limit = getters.getBufferRequestSize * 2
const { data } = await GridService(this.$client).fetchRows({
gridId,
offset: 0,
limit,
includeFieldOptions: true,
})
data.results.forEach((part, index) => {
populateRow(data.results[index])
})
commit('CLEAR_ROWS')
commit('ADD_ROWS', {
rows: data.results,
prependToRows: 0,
appendToRows: data.results.length,
count: data.count,
bufferStartIndex: 0,
bufferLimit: data.count > limit ? limit : data.count,
})
commit('SET_ROWS_INDEX', {
startIndex: 0,
// @TODO mut calculate how many rows would fit and based on that calculate
// what the end index should be.
endIndex: data.count > 31 ? 31 : data.count,
top: 0,
})
commit('REPLACE_ALL_FIELD_OPTIONS', data.field_options)
},
/**
* Refreshes the current state with fresh data. It keeps the scroll offset the same
* if possible. This can be used when a new filter or sort is created.
*/
async refresh({ dispatch, commit, getters }, { gridId }) {
const response = await GridService(this.$client).fetchCount(gridId)
const count = response.data.count
const limit = getters.getBufferRequestSize * 3
const bufferEndIndex = getters.getBufferEndIndex
const offset =
count >= bufferEndIndex
? getters.getBufferStartIndex
: Math.max(0, count - limit)
const { data } = await GridService(this.$client).fetchRows({
gridId,
offset,
limit,
})
// If there are results we can replace the existing rows so that the user stays
// at the same scroll offset.
data.results.forEach((part, index) => {
populateRow(data.results[index])
})
await commit('ADD_ROWS', {
rows: data.results,
prependToRows: -getters.getBufferLimit,
appendToRows: data.results.length,
count: data.count,
bufferStartIndex: offset,
bufferLimit: data.results.length,
})
},
/**
* Checks if the given row still matches the given view filters. The row's
* matchFilters value is updated accordingly. It is also possible to provide some
* override values that not actually belong to the row to do some preliminary checks.
*/
updateMatchFilters({ commit }, { view, row, overrides = {} }) {
const values = JSON.parse(JSON.stringify(row))
Object.keys(overrides).forEach((key) => {
values[key] = overrides[key]
})
// The value is always valid if the filters are disabled.
const matches = view.filters_disabled
? true
: rowMatchesFilters(
this.$registry,
view.filter_type,
view.filters,
values
)
commit('SET_ROW_MATCH_FILTERS', { row, value: matches })
},
/**
* Checks if the given row index is still the same. The row's matchSortings value is
* updated accordingly. It is also possible to provide some override values that not
* actually belong to the row to do some preliminary checks.
*/
updateMatchSortings(
{ commit, getters, rootGetters },
{ view, row, fields, primary = null, overrides = {} }
) {
const values = JSON.parse(JSON.stringify(row))
Object.keys(overrides).forEach((key) => {
values[key] = overrides[key]
})
const allRows = getters.getAllRows
const currentIndex = getters.getAllRows.findIndex((r) => r.id === row.id)
const sortedRows = JSON.parse(JSON.stringify(allRows))
sortedRows[currentIndex] = values
sortedRows.sort(
getRowSortFunction(this.$registry, view.sortings, fields, primary)
)
const newIndex = sortedRows.findIndex((r) => r.id === row.id)
commit('SET_ROW_MATCH_SORTINGS', { row, value: currentIndex === newIndex })
},
/**
* Updates a grid view field value. It will immediately be updated in the store
* and only if the change request fails it will reverted to give a faster
* experience for the user.
*/
async updateValue(
{ commit, dispatch },
{ table, view, row, field, fields, primary, value, oldValue }
) {
commit('SET_VALUE', { row, field, value })
dispatch('updateMatchFilters', { view, row })
dispatch('updateMatchSortings', { view, fields, primary, row })
const fieldType = this.$registry.get('field', field._.type.type)
const newValue = fieldType.prepareValueForUpdate(field, value)
const values = {}
values[`field_${field.id}`] = newValue
try {
await RowService(this.$client).update(table.id, row.id, values)
} catch (error) {
commit('SET_VALUE', { row, field, value: oldValue })
dispatch('updateMatchFilters', { view, row })
throw error
}
},
/**
* Creates a new row. Based on the default values of the fields a row is created
* which will be added to the store. Only if the request fails the row is removed.
*/
async create(
{ commit, getters, rootGetters, dispatch },
{ view, table, fields, values = {}, before = null }
) {
// Fill the not provided values with the empty value of the field type so we can
// immediately commit the created row to the state.
fields.forEach((field) => {
const name = `field_${field.id}`
if (!(name in values)) {
const fieldType = this.$registry.get('field', field._.type.type)
const empty = fieldType.getEmptyValue(field)
values[name] = empty
}
})
// Populate the row and set the loading state to indicate that the row has not
// yet been added.
const row = _.assign({}, values)
populateRow(row)
row.id = uuid()
row._.loading = true
if (before !== null) {
// If the row has been placed before another row we can specifically insert to
// the row at a calculated index.
const index = getters.getAllRows.findIndex((r) => r.id === before.id)
const change = new BigNumber('0.00000000000000000001')
row.order = new BigNumber(before.order).minus(change).toString()
commit('INSERT_ROW_AT', { row, index })
} else {
// By default the row is inserted at the end.
commit('ADD_ROWS', {
rows: [row],
prependToRows: 0,
appendToRows: 1,
count: getters.getCount + 1,
bufferStartIndex: getters.getBufferStartIndex,
bufferLimit: getters.getBufferLimit + 1,
})
}
// Recalculate all the values.
dispatch('visibleByScrollTop', {
scrollTop: null,
windowHeight: null,
})
// Check if the newly created row matches the filters.
dispatch('updateMatchFilters', { view, row })
// Check if the newly created row matches the sortings.
dispatch('updateMatchSortings', { view, fields, row })
try {
const { data } = await RowService(this.$client).create(
table.id,
values,
before !== null ? before.id : null
)
commit('FINALIZE_ROW', { oldId: row.id, id: data.id, order: data.order })
} catch (error) {
commit('DELETE_ROW', row.id)
throw error
}
},
/**
* Forcefully create a new row without making a call to the backend. It also
* checks if the row matches the filters and sortings and if not it will be
* removed from the buffer.
*/
forceCreate(
{ commit, dispatch, getters },
{ view, fields, primary, values, getScrollTop }
) {
const row = _.assign({}, values)
populateRow(row)
commit('ADD_ROWS', {
rows: [row],
prependToRows: 0,
appendToRows: 1,
count: getters.getCount + 1,
bufferStartIndex: getters.getBufferStartIndex,
bufferLimit: getters.getBufferLimit + 1,
})
dispatch('visibleByScrollTop', {
scrollTop: null,
windowHeight: null,
})
dispatch('updateMatchFilters', { view, row })
dispatch('updateMatchSortings', { view, fields, primary, row })
dispatch('refreshRow', { grid: view, row, fields, primary, getScrollTop })
},
/**
* Forcefully update an existing row without making a call to the backend. It
* could be that the row does not exist in the buffer, but actually belongs in
* there. So after creating or updating the row we can check if it belongs
* there and if not it will be deleted.
*/
forceUpdate(
{ dispatch, commit, getters },
{ view, fields, primary, values, getScrollTop }
) {
const row = getters.getRow(values.id)
if (row === undefined) {
return dispatch('forceCreate', {
view,
fields,
primary,
values,
getScrollTop,
})
} else {
commit('UPDATE_ROW', { row, values })
}
dispatch('updateMatchFilters', { view, row })
dispatch('updateMatchSortings', { view, fields, primary, row })
dispatch('refreshRow', { grid: view, row, fields, primary, getScrollTop })
},
/**
* Deletes an existing row of the provided table. After deleting, the visible rows
* range and the buffer are recalculated because we might need to show different
* rows or add some rows to the buffer.
*/
async delete(
{ commit, dispatch, getters },
{ table, grid, row, getScrollTop }
) {
commit('SET_ROW_LOADING', { row, value: true })
try {
await RowService(this.$client).delete(table.id, row.id)
dispatch('forceDelete', { grid, row, getScrollTop })
} catch (error) {
commit('SET_ROW_LOADING', { row, value: false })
throw error
}
},
/**
* Deletes a row from the store without making a request to the backend. Note that
* this should only be used if the row really isn't visible in the view anymore.
* Otherwise wrong data could be fetched later. This action can also be used when a
* row has been moved outside the current buffer.
*/
forceDelete(
{ commit, dispatch, getters },
{ grid, row, getScrollTop, moved = false }
) {
if (moved === 'up') {
commit('DELETE_ROW_MOVED_UP', row.id)
} else if (moved === 'down') {
commit('DELETE_ROW_MOVED_DOWN', row.id)
} else {
commit('DELETE_ROW', row.id)
}
// We use the provided function to recalculate the scrollTop offset in order
// to get fresh data.
const scrollTop = getScrollTop()
const windowHeight = getters.getWindowHeight
dispatch('fetchByScrollTop', {
gridId: grid.id,
scrollTop,
windowHeight,
})
dispatch('visibleByScrollTop', { scrollTop, windowHeight })
},
/**
* Adds a field with a provided value to the rows in memory.
*/
addField({ commit }, { field, value = null }) {
commit('ADD_FIELD', { field, value })
},
/**
* Updates the field options of a given field and also makes an API request to the
* backend with the changed values. If the request fails the action is reverted.
*/
async updateFieldOptionsOfField(
{ commit },
{ gridId, field, values, oldValues }
) {
commit('UPDATE_FIELD_OPTIONS_OF_FIELD', {
fieldId: field.id,
values,
})
const updateValues = { field_options: {} }
updateValues.field_options[field.id] = values
try {
await GridService(this.$client).update({ gridId, values: updateValues })
} catch (error) {
commit('UPDATE_FIELD_OPTIONS_OF_FIELD', {
fieldId: field.id,
values: oldValues,
})
throw error
}
},
/**
* Updates the field options of a given field in the store. So no API request to
* the backend is made.
*/
setFieldOptionsOfField({ commit }, { field, values }) {
commit('UPDATE_FIELD_OPTIONS_OF_FIELD', {
fieldId: field.id,
values,
})
},
/**
* Replaces all field options with new values and also makes an API request to the
* backend with the changed values. If the request fails the action is reverted.
*/
async updateAllFieldOptions(
{ dispatch },
{ gridId, newFieldOptions, oldFieldOptions }
) {
dispatch('forceUpdateAllFieldOptions', newFieldOptions)
const updateValues = { field_options: newFieldOptions }
try {
await GridService(this.$client).update({ gridId, values: updateValues })
} catch (error) {
dispatch('forceUpdateAllFieldOptions', oldFieldOptions)
throw error
}
},
/**
* Forcefully updates all field options without making a call to the backend.
*/
forceUpdateAllFieldOptions({ commit }, fieldOptions) {
commit('UPDATE_ALL_FIELD_OPTIONS', fieldOptions)
},
/**
* Updates the order of all the available field options. The provided order parameter
* should be an array containing the field ids in the correct order.
*/
async updateFieldOptionsOrder(
{ commit, getters, dispatch },
{ gridId, order }
) {
const oldFieldOptions = clone(getters.getAllFieldOptions)
const newFieldOptions = clone(getters.getAllFieldOptions)
// Update the order of the field options that have not been provided in the order.
// They will get a position that places them after the provided field ids.
let i = 0
Object.keys(newFieldOptions).forEach((fieldId) => {
if (!order.includes(parseInt(fieldId))) {
newFieldOptions[fieldId].order = order.length + i
i++
}
})
// Update create the field options and set the correct order value.
order.forEach((fieldId, index) => {
const id = fieldId.toString()
if (Object.prototype.hasOwnProperty.call(newFieldOptions, id)) {
newFieldOptions[fieldId.toString()].order = index
}
})
return await dispatch('updateAllFieldOptions', {
gridId,
oldFieldOptions,
newFieldOptions,
})
},
/**
* Deletes the field options of the provided field id if they exist.
*/
forceDeleteFieldOptions({ commit }, fieldId) {
commit('DELETE_FIELD_OPTIONS', fieldId)
},
setRowHover({ commit }, { row, value }) {
commit('SET_ROW_HOVER', { row, value })
},
/**
* Adds a field to the list of selected fields of a row. We use this to indicate
* if a row is selected or not.
*/
addRowSelectedBy({ commit }, { row, field }) {
commit('ADD_ROW_SELECTED_BY', { row, fieldId: field.id })
},
/**
* Removes a field from the list of selected fields of a row. We use this to
* indicate if a row is selected or not. If the field is not selected anymore
* and it does not match the filters it can be removed from the store.
*/
removeRowSelectedBy(
{ dispatch, commit },
{ grid, row, field, fields, primary, getScrollTop }
) {
commit('REMOVE_ROW_SELECTED_BY', { row, fieldId: field.id })
dispatch('refreshRow', { grid, row, fields, primary, getScrollTop })
},
/**
* The row is going to be removed or repositioned if the matchFilters and
* matchSortings state is false. It will make the state correct.
*/
refreshRow(
{ dispatch, commit, getters },
{ grid, row, fields, primary, getScrollTop }
) {
if (row._.selectedBy.length === 0 && !row._.matchFilters) {
dispatch('forceDelete', { grid, row, getScrollTop })
return
}
if (row._.selectedBy.length === 0 && !row._.matchSortings) {
const sortFunction = getRowSortFunction(
this.$registry,
grid.sortings,
fields,
primary
)
commit('SORT_ROWS', sortFunction)
// We cannot know for sure if the row has been moved outside the scope of the
// current buffer. Therefore if the row is at the beginning or the end of the
// buffer we are going to remove it. This doesn't matter because the
// fetchByScrollTop action, which is called in the forceDelete action, will fix
// the buffer automatically.
const up = getters.isFirst(row.id) && getters.getBufferStartIndex > 0
const down =
getters.isLast(row.id) && getters.getBufferEndIndex < getters.getCount
if (up || down) {
const moved = up ? 'up' : 'down'
dispatch('forceDelete', { grid, row, getScrollTop, moved })
}
}
},
setAddRowHover({ commit }, value) {
commit('SET_ADD_ROW_HOVER', value)
},
setSelectedCell({ commit }, { rowId, fieldId }) {
commit('SET_SELECTED_CELL', { rowId, fieldId })
},
}
export const getters = {
isLoading(state) {
return state.loading
},
isLoaded(state) {
return state.loaded
},
getLastGridId(state) {
return state.lastGridId
},
getCount(state) {
return state.count
},
getRowHeight(state) {
return state.rowHeight
},
getRowsTop(state) {
return state.rowsTop
},
getRowsLength(state) {
return state.rows.length
},
getPlaceholderHeight(state) {
return state.count * state.rowHeight
},
getRowPadding(state) {
return state.rowPadding
},
getAllRows(state) {
return state.rows
},
getRow: (state) => (id) => {
return state.rows.find((row) => row.id === id)
},
getRows(state) {
return state.rows.slice(state.rowsStartIndex, state.rowsEndIndex)
},
getRowsStartIndex(state) {
return state.rowsStartIndex
},
getRowsEndIndex(state) {
return state.rowsEndIndex
},
getBufferRequestSize(state) {
return state.bufferRequestSize
},
getBufferStartIndex(state) {
return state.bufferStartIndex
},
getBufferEndIndex(state) {
return state.bufferStartIndex + state.bufferLimit
},
getBufferLimit(state) {
return state.bufferLimit
},
getScrollTop(state) {
return state.scrollTop
},
getWindowHeight(state) {
return state.windowHeight
},
getAllFieldOptions(state) {
return state.fieldOptions
},
isFirst: (state) => (id) => {
const index = state.rows.findIndex((row) => row.id === id)
return index === 0
},
isLast: (state) => (id) => {
const index = state.rows.findIndex((row) => row.id === id)
return index === state.rows.length - 1
},
getAddRowHover(state) {
return state.addRowHover
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}