1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-09-19 11:07:47 +00:00
bramw_baserow/premium/web-frontend/modules/baserow_premium/store/view/calendar.js
2024-07-09 07:01:21 +00:00

984 lines
31 KiB
JavaScript

import Vue from 'vue'
import _ from 'lodash'
import moment from '@baserow/modules/core/moment'
import { clone } from '@baserow/modules/core/utils/object'
import { GroupTaskQueue } from '@baserow/modules/core/utils/queue'
import ViewService from '@baserow/modules/database/services/view'
import CalendarService from '@baserow_premium/services/views/calendar'
import {
extractRowMetadata,
getRowSortFunction,
matchSearchFilters,
calculateSingleRowSearchMatches,
} from '@baserow/modules/database/utils/view'
import RowService from '@baserow/modules/database/services/row'
import {
getMonthlyTimestamps,
getUserTimeZone,
} from '@baserow/modules/core/utils/date'
import {
extractRowReadOnlyValues,
prepareNewOldAndUpdateRequestValues,
prepareRowForRequest,
} from '@baserow/modules/database/utils/row'
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
export function populateRow(row, metadata = {}) {
row._ = {
metadata,
// Whether the row should be displayed based on the current activeSearchTerm term.
matchSearch: true,
// Contains the specific field ids which match the activeSearchTerm term.
// Could be empty even when matchSearch is true when there is no
// activeSearchTerm term applied.
fieldSearchMatches: [],
}
return row
}
export function populateDateStack(stack, data) {
Object.assign(stack, {
loading: false,
})
stack.results.forEach((row) => {
const metadata = extractRowMetadata(data, row.id)
populateRow(row, metadata)
})
return stack
}
const updateRowQueue = new GroupTaskQueue()
export const state = () => ({
loading: false,
loadingRows: false,
// The calendar view id that is being displayed
lastCalendarId: null,
// The chosen date field that the
// items will be organized by in the view
dateFieldId: null,
fieldOptions: {},
// dateStack organizes rows by dates (2023-02-21)
// based on the date field with dateFieldId
dateStacks: {},
// How many items per date are fetched
bufferRequestSize: 10,
// Determines currently selected time period
// based on a specific date. For example, if selectedDate
// is today, the calendar view will be showing the
// current month surrounding it.
// It is an instance of moment.
selectedDate: null,
// A user provided optional search term which can be used to filter down rows.
activeSearchTerm: '',
// If true then the activeSearchTerm will be sent to the server to filter rows
// entirely out. When false no server filter will be applied and rows which do not
// have any matching cells will still be displayed.
hideRowsNotMatchingSearch: true,
})
export const mutations = {
RESET(state) {
state.loadingRows = false
state.lastCalendarId = null
state.dateFieldId = null
state.fieldOptions = {}
state.dateStacks = {}
state.activeSearchTerm = ''
state.hideRowsNotMatchingSearch = true
},
SET_SEARCH(state, { activeSearchTerm, hideRowsNotMatchingSearch }) {
state.activeSearchTerm = activeSearchTerm.trim()
state.hideRowsNotMatchingSearch = hideRowsNotMatchingSearch
},
SET_LOADING_ROWS(state, loading) {
state.loadingRows = loading
},
SET_ROW_LOADING(state, { row, value }) {
Vue.set(row._, 'loading', value)
},
SET_ROW_SEARCH_MATCHES(state, { row, matchSearch }) {
row._.matchSearch = matchSearch
},
SET_LAST_CALENDAR_ID(state, calendarId) {
state.lastCalendarId = calendarId
},
SET_DATE_FIELD_ID(state, dateFieldId) {
state.dateFieldId = dateFieldId
},
SET_SELECTED_DATE(state, selectedDate) {
// Store as a unix timestamp so the serialization from Nuxt -> Client works as
// you can't safely serialize and transmit an entire moment object.
state.selectedDate = selectedDate.valueOf()
},
REPLACE_ALL_DATE_STACKS(state, stacks) {
state.dateStacks = stacks
},
ADD_ROWS_TO_STACK(state, { date, count, rows }) {
if (count) {
state.dateStacks[date].count = count
}
state.dateStacks[date].results.push(...rows)
},
ADD_STACK(state, { date, stack }) {
Vue.set(state.dateStacks, date, stack)
},
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)) {
Object.assign(state.fieldOptions[fieldId], values)
} else {
state.fieldOptions = Object.assign({}, state.fieldOptions, {
[fieldId]: values,
})
}
},
DELETE_FIELD_OPTIONS(state, fieldId) {
if (Object.prototype.hasOwnProperty.call(state.fieldOptions, fieldId)) {
delete state.fieldOptions[fieldId]
}
},
ADD_FIELD_TO_ALL_ROWS(state, { field, value }) {
const name = `field_${field.id}`
Object.keys(state.dateStacks).forEach((stack) => {
// We have to replace all the rows by using the map function to make it
// reactive and update immediately.
state.dateStacks[stack].results = state.dateStacks[stack].results.map(
(row) => {
if (
row !== null &&
!Object.prototype.hasOwnProperty.call(row, name)
) {
row[`field_${field.id}`] = value
return { ...row }
}
return row
}
)
})
},
CREATE_ROW(state, { row, stackId, index }) {
state.dateStacks[stackId].results.splice(index, 0, row)
},
UPDATE_ROW(state, { row, values }) {
let updated = false
Object.keys(state.dateStacks).forEach((stack) => {
const rows = state.dateStacks[stack].results
const index = rows.findIndex((item) => item.id === row.id)
if (index !== -1) {
const existingRowState = rows[index]
Object.assign(existingRowState, values)
updated = true
}
})
if (!updated) {
Object.assign(row, values)
}
},
UPDATE_VALUE_OF_ALL_ROWS_IN_STACK(state, { fieldId, stackId, values }) {
const name = `field_${fieldId}`
state.dateStacks[stackId].results.forEach((row) => {
Object.assign(row[name], values)
})
},
MOVE_ROW(
state,
{ currentStackId, currentIndex, targetStackId, targetIndex }
) {
state.dateStacks[targetStackId].results.splice(
targetIndex,
0,
state.dateStacks[currentStackId].results.splice(currentIndex, 1)[0]
)
},
DELETE_ROW(state, { stackId, index }) {
state.dateStacks[stackId].results.splice(index, 1)
},
INCREASE_COUNT(state, { stackId }) {
state.dateStacks[stackId].count++
},
DECREASE_COUNT(state, { stackId }) {
state.dateStacks[stackId].count--
},
UPDATE_ROW_VALUES(state, { row, values }) {
Object.assign(row, values)
},
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
const currentValue = row._.metadata[rowMetadataType]
Vue.set(row._.metadata, rowMetadataType, updateFunction(currentValue))
},
}
export const actions = {
/**
* Resets the store completely throwing away all state.
*/
reset({ dispatch, commit, getters }, _) {
commit('RESET')
},
/**
* Resets the store completely throwing away all state and loads new rows given
* a new view etc.
* rows.
*/
async resetAndFetchInitial(
{ dispatch, commit, getters },
{ calendarId, dateFieldId, fields, includeFieldOptions = true }
) {
commit('RESET')
await dispatch('refreshAndFetchInitial', {
calendarId,
dateFieldId,
fields,
includeFieldOptions,
})
},
/**
* Refreshes the store given new date and view parameters and then refetches the
* rows. Doesn't throw away field options etc.
*/
async refreshAndFetchInitial(
{ dispatch, commit, getters },
{ calendarId, dateFieldId, fields, includeFieldOptions = true }
) {
commit('SET_DATE_FIELD_ID', dateFieldId)
commit('SET_LAST_CALENDAR_ID', calendarId)
await dispatch('fetchInitial', {
includeFieldOptions,
fields,
})
},
/**
* Fetches an initial set of rows and adds that data to the store.
*/
async fetchInitial(
{ dispatch, commit, getters },
{ fields, includeFieldOptions = true }
) {
// We can't guess the users timezone when doing the server side render.
// So we only can know what today is and load rows in the client side when the
// field does not have a forced timezone.
if (!process.server || getters.getFieldTimeZoneIfSet(fields)) {
const timezone = getters.getTimeZone(fields)
const todayOrSelectedDay =
getters.getSelectedDate(fields) == null
? moment.tz(timezone)
: getters.getSelectedDate(fields)
await dispatch('fetchMonthly', {
dateTime: todayOrSelectedDay,
fields,
includeFieldOptions,
})
}
},
/**
* Fetches a set of rows based on the provided datetime.
*/
async fetchMonthly(
{ commit, getters, rootGetters },
{ dateTime, fields, includeFieldOptions = false }
) {
commit('SET_SELECTED_DATE', dateTime)
const df = getters.getDateField(fields)
if (
!df ||
this.$registry.get('field', df.type).canRepresentDate(df) === false
) {
commit('RESET')
return
}
commit('SET_LOADING_ROWS', true)
const { fromTimestamp, toTimestamp } = getMonthlyTimestamps(dateTime)
try {
const { data } = await CalendarService(this.$client).fetchRows({
calendarId: getters.getLastCalendarId,
limit: getters.getBufferRequestSize,
offset: 0,
includeFieldOptions,
fromTimestamp,
toTimestamp,
userTimeZone: getUserTimeZone(),
search: getters.getServerSearchTerm,
searchMode: getDefaultSearchModeFromEnv(this.$config),
publicUrl: rootGetters['page/view/public/getIsPublic'],
publicAuthToken: rootGetters['page/view/public/getAuthToken'],
})
const lastRequest = dateTime.isSame(getters.getSelectedDate(fields))
if (lastRequest) {
Object.keys(data.rows).forEach((key) => {
populateDateStack(data.rows[key], data)
})
commit('REPLACE_ALL_DATE_STACKS', data.rows)
if (includeFieldOptions) {
commit('REPLACE_ALL_FIELD_OPTIONS', data.field_options)
}
commit('SET_LOADING_ROWS', false)
}
} catch (error) {
if (error.handler.code === 'ERROR_CALENDAR_VIEW_HAS_NO_DATE_FIELD') {
commit('RESET')
} else {
throw error
}
} finally {
const lastRequest = dateTime.isSame(getters.getSelectedDate(fields))
if (lastRequest) {
commit('SET_LOADING_ROWS', false)
}
}
},
/**
* This action is called when the users scrolls to the end of the stack. Because
* we don't fetch all the rows, the next set will be fetched when the user reaches
* the end.
*/
async fetchMore(
{ dispatch, commit, getters, rootGetters },
{ date, fields }
) {
const stack = getters.getDateStack(date)
const rows = stack.results
const timezone = getters.getTimeZone(fields)
const fromTimestamp = moment.tz(date, timezone)
const toTimestamp = moment.tz(fromTimestamp, timezone).add(1, 'day')
const { data } = await CalendarService(this.$client).fetchRows({
calendarId: getters.getLastCalendarId,
limit: getters.getBufferRequestSize,
offset: rows.length,
includeFieldOptions: false,
fromTimestamp,
toTimestamp,
userTimeZone: getUserTimeZone(),
search: getters.getServerSearchTerm,
searchMode: getDefaultSearchModeFromEnv(this.$config),
publicUrl: rootGetters['page/view/public/getIsPublic'],
publicAuthToken: rootGetters['page/view/public/getAuthToken'],
})
const newRows = data.rows[date].results
const newCount = data.rows[date].count
newRows.forEach((row) => {
populateRow(row)
})
commit('ADD_ROWS_TO_STACK', { date, count: newCount, rows: newRows })
dispatch('updateSearch', { fields })
},
/**
* 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, getters, rootGetters },
{ newFieldOptions, oldFieldOptions, readOnly = false }
) {
dispatch('forceUpdateAllFieldOptions', newFieldOptions)
const calendarId = getters.getLastCalendarId
if (!readOnly) {
const updateValues = { field_options: newFieldOptions }
try {
await ViewService(this.$client).updateFieldOptions({
viewId: calendarId,
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)
},
/**
* Deletes the field options of the provided field id if they exist.
*/
forceDeleteFieldOptions({ commit }, fieldId) {
commit('DELETE_FIELD_OPTIONS', fieldId)
},
/**
* 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 },
{ order, readOnly = false }
) {
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', {
oldFieldOptions,
newFieldOptions,
readOnly,
})
},
/**
* Updates the field options of a specific field.
*/
async updateFieldOptionsOfField(
{ commit, getters, rootGetters },
{ view, field, values, readOnly = false, undoRedoActionGroupId = null }
) {
commit('UPDATE_FIELD_OPTIONS_OF_FIELD', {
fieldId: field.id,
values,
})
if (!readOnly) {
const calendarId = getters.getLastCalendarId
const oldValues = clone(getters.getAllFieldOptions[field.id])
const updateValues = { field_options: {} }
updateValues.field_options[field.id] = values
try {
await ViewService(this.$client).updateFieldOptions({
viewId: calendarId,
values: updateValues,
undoRedoActionGroupId,
})
} catch (error) {
commit('UPDATE_FIELD_OPTIONS_OF_FIELD', {
fieldId: field.id,
values: oldValues,
})
throw error
}
}
},
/**
* Creates a new row and adds it to the state if needed.
*/
async createNewRow(
{ dispatch, commit, getters },
{ view, table, fields, values }
) {
const preparedRow = prepareRowForRequest(values, fields, this.$registry)
const { data } = await RowService(this.$client).create(
table.id,
preparedRow
)
return await dispatch('createdNewRow', {
view,
values: data,
fields,
})
},
/**
* Can be called when a new row has been created. This action will make sure that
* the state is updated accordingly. If the newly created position is within the
* current buffer (`dateStack.results`), then it will be added there, otherwise, just
* the count is increased.
*
* @param values The values of the newly created row.
* @param row Can be provided when the row already existed within the state.
* In that case, the `_` data will be preserved. Can be useful when
* a row has been updated while being dragged.
*/
async createdNewRow(
{ dispatch, commit, getters, rootGetters },
{ view, values, fields }
) {
const row = clone(values)
populateRow(row)
const matchesFilters = await dispatch('rowMatchesFilters', {
view,
row,
fields,
})
dispatch('updateSearchMatchesForRow', { row, fields })
if (!matchesFilters || !row._.matchSearch) {
return
}
const dateFieldId = getters.getDateFieldIdIfNotTrashed(fields)
const value = row[`field_${dateFieldId}`]
const timezone = getters.getTimeZone(fields)
const stackId = moment.tz(value, timezone).format('YYYY-MM-DD')
const stack = getters.getDateStack(stackId)
if (stack === undefined) {
return
}
const sortings = [{ field: dateFieldId, value: 'ASC', order: 'ASC' }]
const sortedRows = clone(stack.results)
sortedRows.push(row)
sortedRows.sort(getRowSortFunction(this.$registry, sortings, fields))
const index = sortedRows.findIndex((r) => r.id === row.id)
const isLast = index === sortedRows.length - 1
// Because we don't fetch all the rows from the backend, we can't know for sure
// whether or not the row is being added at the right position. Therefore, if
// it's last, we just not add it to the store and wait for the user to fetch the
// next page.
if (!isLast || stack.results.length === stack.count) {
commit('CREATE_ROW', { row, stackId, index })
}
// We always need to increase the count whether row has been added to the store
// or not because the count is for all the rows and not just the ones in the store.
commit('INCREASE_COUNT', { stackId })
},
/**
* Called when the user wants to delete an existing row in the table.
*/
async deleteRow({ commit, dispatch, getters }, { table, view, row, fields }) {
commit('SET_ROW_LOADING', { row, value: true })
try {
await dispatch('deletedExistingRow', {
view,
fields,
row,
})
await RowService(this.$client).delete(table.id, row.id)
} catch (error) {
await dispatch('createdNewRow', {
view,
values: row,
fields,
})
commit('SET_ROW_LOADING', { row, value: false })
throw error
}
},
/**
* Can be called when a row in the table has been deleted. This action will make
* sure that the state is updated accordingly.
*/
async deletedExistingRow(
{ dispatch, commit, getters },
{ view, row, fields }
) {
row = clone(row)
populateRow(row)
dispatch('updateSearchMatchesForRow', { row, fields })
const matchesFilters = await dispatch('rowMatchesFilters', {
view,
row,
fields,
})
if (!matchesFilters || !row._.matchSearch) {
return
}
const dateFieldId = getters.getDateFieldIdIfNotTrashed(fields)
const value = row[`field_${dateFieldId}`]
const timezone = getters.getTimeZone(fields)
const stackId = value
? moment.tz(value, timezone).format('YYYY-MM-DD')
: null
const stack = getters.getDateStack(stackId)
const current = getters.findStackIdAndIndex(row.id)
if (current !== undefined) {
const currentStackId = current[0]
const currentIndex = current[1]
const currentRow = current[2]
commit('DELETE_ROW', { stackId: currentStackId, index: currentIndex })
commit('DECREASE_COUNT', { stackId: currentStackId })
return currentRow
} else if (stack) {
commit('DECREASE_COUNT', { stackId })
}
return null
},
/**
* Can be called when a row in the table has been updated. This action will make sure
* that the state is updated accordingly. If the date field value has
* changed, the row will be moved to the right stack. If the position has changed,
* it will be moved to the right position.
*/
async updatedExistingRow(
{ dispatch, getters, commit },
{ view, row, values, fields }
) {
const dateFieldId = getters.getDateFieldIdIfNotTrashed(fields)
const fieldName = `field_${dateFieldId}`
// First, we virtually need to figure out if the row was in the old stack.
const oldRow = populateRow(clone(row))
const oldRowMatchesFilters = await dispatch('rowMatchesFilters', {
view,
row: oldRow,
fields,
})
dispatch('updateSearchMatchesForRow', { row: oldRow, fields })
const oldValue = oldRow[fieldName]
const timezone = getters.getTimeZone(fields)
const oldStackId = moment.tz(oldValue, timezone).format('YYYY-MM-DD')
const oldStack = getters.getDateStack(oldStackId)
let oldExists = false
let oldExistingIndex = -1
if (oldStack) {
const oldStackResults = clone(oldStack.results)
oldExistingIndex = oldStackResults.findIndex((r) => r.id === oldRow.id)
oldExists =
oldExistingIndex > -1 && oldRowMatchesFilters && oldRow._.matchSearch
}
// Second, we need to figure out if the row should be visible in the new stack.
const newRow = Object.assign(populateRow(clone(row)), values)
const newRowMatchesFilters = await dispatch('rowMatchesFilters', {
view,
row: newRow,
fields,
})
dispatch('updateSearchMatchesForRow', { row: newRow, fields })
const newValue = newRow[fieldName]
const newStackId = moment.tz(newValue, timezone).format('YYYY-MM-DD')
const newStack = getters.getDateStack(newStackId)
let newExists = false
let newIndex = -1
if (newStack) {
const newStackResults = newStack ? clone(newStack.results) : []
const newRowCurrentIndex = newStackResults.findIndex(
(r) => r.id === newRow.id
)
let newStackCount = newStack.count
if (newRowCurrentIndex > -1) {
newStackResults.splice(newRowCurrentIndex, 1)
newStackCount--
}
newStackResults.push(newRow)
newStackCount++
const sortings = [{ field: dateFieldId, value: 'ASC', order: 'ASC' }]
newStackResults.sort(getRowSortFunction(this.$registry, sortings, fields))
newIndex = newStackResults.findIndex((r) => r.id === newRow.id)
const newIsLast = newIndex === newStackResults.length - 1
newExists =
(!newIsLast || newStackResults.length === newStackCount) &&
newRowMatchesFilters &&
newRow._.matchSearch
}
commit('UPDATE_ROW', { row, values })
if (oldExists && newExists) {
commit('MOVE_ROW', {
currentStackId: oldStackId,
currentIndex: oldExistingIndex,
targetStackId: newStackId,
targetIndex: newIndex,
})
commit('DECREASE_COUNT', { stackId: oldStackId })
commit('INCREASE_COUNT', { stackId: newStackId })
} else if (oldExists && !newExists) {
commit('DELETE_ROW', { stackId: oldStackId, index: oldExistingIndex })
commit('DECREASE_COUNT', { stackId: oldStackId })
} else if (!oldExists && newExists) {
commit('CREATE_ROW', {
row: newRow,
stackId: newStackId,
index: newIndex,
})
commit('INCREASE_COUNT', { stackId: newStackId })
}
},
/**
* Check if the provided row matches the provided view filters.
*/
rowMatchesFilters(context, { view, fields, row, overrides = {} }) {
const values = JSON.parse(JSON.stringify(row))
Object.assign(values, overrides)
// The value is always valid if the filters are disabled.
return view.filters_disabled
? true
: matchSearchFilters(
this.$registry,
view.filter_type,
view.filters,
view.filter_groups,
fields,
values
)
},
/**
* Updates the value of a row and make the changes to the store accordingly.
*/
async updateRowValue(
{ commit, dispatch },
{ view, table, row, field, fields, value, oldValue }
) {
const { newRowValues, oldRowValues, updateRequestValues } =
prepareNewOldAndUpdateRequestValues(
row,
fields,
field,
value,
oldValue,
this.$registry
)
dispatch('updateSearchMatchesForRow', { row, fields })
await dispatch('updatedExistingRow', {
view,
row,
values: newRowValues,
fields,
})
// There is a chance that the row is not in the buffer, but it does exist in
// the view. In that case, the `updatedExistingRow` action has not done
// anything. There is a possibility that the row is visible in the row edit
// modal, but then it won't be updated, so we have to update it forcefully.
commit('UPDATE_ROW_VALUES', {
row,
values: { ...newRowValues },
})
try {
// Add the update actual update function to the queue so that the same row
// will never be updated concurrency, and so that the value won't be
// updated if the row hasn't been created yet.
await updateRowQueue.add(async () => {
const { data } = await RowService(this.$client).update(
table.id,
row.id,
updateRequestValues
)
const readOnlyData = extractRowReadOnlyValues(
data,
fields,
this.$registry
)
commit('UPDATE_ROW', { row, values: readOnlyData })
}, row.id)
} catch (error) {
await dispatch('updatedExistingRow', {
view,
row,
values: oldRowValues,
fields,
})
commit('UPDATE_ROW_VALUES', {
row,
values: { ...oldRowValues },
})
throw error
}
},
/**
* Adds a field with a provided value to the rows in the store. This will for
* example be called when a new field has been created.
*/
addField({ commit }, { field, value = null }) {
commit('ADD_FIELD_TO_ALL_ROWS', { field, value })
},
selectDateAndStartLoading({ commit }, { selectedDate }) {
commit('SET_LOADING_ROWS', true)
commit('SET_SELECTED_DATE', selectedDate)
},
async selectRow({ commit, getters, dispatch }, { row, fields }) {
const dateFieldId = getters.getDateFieldIdIfNotTrashed(fields)
if (dateFieldId) {
const value = row[`field_${dateFieldId}`]
const dateTime = moment.tz(value, getters.getTimeZone(fields))
const {
fromTimestamp: currentFromTimestamp,
toTimestamp: currentToTimestamp,
} = getMonthlyTimestamps(getters.getSelectedDate(fields))
// Selecting a new row might be triggered by a navigation to a
// `calendar/ABC/row/XYZ` URL or by the user clicking on a row they can see
// in the calendar view. When triggered by a navigation we almost certainly don't
// right month loaded into the store and so if we don't we fetch all the other
// rows for that month.
if (
!dateTime.isBetween(
currentFromTimestamp,
currentToTimestamp,
null,
'[]'
)
) {
await dispatch('fetchMonthly', {
dateTime,
fields,
})
}
}
},
updateSearch(
{ commit, dispatch, getters, state },
{
fields,
activeSearchTerm = state.activeSearchTerm,
hideRowsNotMatchingSearch = state.hideRowsNotMatchingSearch,
refreshMatchesOnClient = true,
}
) {
commit('SET_SEARCH', { activeSearchTerm, hideRowsNotMatchingSearch })
if (refreshMatchesOnClient) {
getters.getAllRows.forEach((row) =>
dispatch('updateSearchMatchesForRow', {
row,
fields,
forced: true,
})
)
}
},
updateSearchMatchesForRow(
{ commit, getters, rootGetters },
{ row, fields = null, overrides, forced = false }
) {
// Avoid computing search on table loading
if (getters.getActiveSearchTerm || forced) {
const rowSearchMatches = calculateSingleRowSearchMatches(
row,
getters.getActiveSearchTerm,
getters.isHidingRowsNotMatchingSearch,
fields,
this.$registry,
getDefaultSearchModeFromEnv(this.$config),
overrides
)
commit('SET_ROW_SEARCH_MATCHES', rowSearchMatches)
}
},
/**
* Updates a single row's row._.metadata based on the provided rowMetadataType and
* updateFunction.
*/
updateRowMetadata(
{ commit, getters },
{ rowId, rowMetadataType, updateFunction }
) {
const target = getters.findStackIdAndIndex(rowId)
if (target !== undefined) {
const row = target[2]
commit('UPDATE_ROW_METADATA', { row, rowMetadataType, updateFunction })
}
},
}
export const getters = {
getServerSearchTerm(state) {
return state.activeSearchTerm
},
getActiveSearchTerm(state) {
return state.activeSearchTerm
},
isHidingRowsNotMatchingSearch(state) {
return state.hideRowsNotMatchingSearch
},
getLoadingRows(state) {
return state.loadingRows
},
getLastCalendarId(state) {
return state.lastCalendarId
},
getDateFieldIdIfNotTrashed: (state, getters) => (fields) => {
return getters.getDateField(fields)?.id
},
getAllFieldOptions(state) {
return state.fieldOptions
},
getDateStack: (state) => (date) => {
return state.dateStacks[date]
},
getBufferRequestSize(state) {
return state.bufferRequestSize
},
getSelectedDate: (state, getters) => (fields) => {
return state.selectedDate
? moment(state.selectedDate).tz(getters.getTimeZone(fields))
: null
},
getAllRows(state) {
let rows = []
Object.keys(state.dateStacks).forEach((key) => {
rows = rows.concat(state.dateStacks[key].results)
})
return rows
},
findStackIdAndIndex: (state) => (rowId) => {
const stacks = state.dateStacks
const keys = Object.keys(stacks)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const results = stacks[key].results
for (let i2 = 0; i2 < results.length; i2++) {
const result = results[i2]
if (result.id === rowId) {
return [key, i2, result]
}
}
}
},
getDateField: (state) => (fields) => {
const fieldId = state.dateFieldId
if (fieldId) {
return fields.find((field) => field.id === fieldId)
} else {
return null
}
},
getFieldTimeZoneIfSet: (state, getters) => (fields) => {
const dateField = getters.getDateField(fields)
if (dateField?.date_include_time) {
return dateField?.date_force_timezone
} else {
return 'UTC'
}
},
getTimeZone: (state, getters) => (fields) => {
const fieldTimezone = getters.getFieldTimeZoneIfSet(fields)
return fieldTimezone || getUserTimeZone()
},
getCalendarTimezoneAndDateLoaded: (state, getters) => (fields) => {
return getters.getSelectedDate(fields) != null
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}