1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Merge branch '2105-change-cookie-values-stored-for-last-remembered-view-id' into 'develop'

Resolve "Change cookie values stored for last remembered view ID"

Closes 

See merge request 
This commit is contained in:
Eimantas Stonys 2024-01-10 09:38:00 +00:00
commit f83c40e5de
4 changed files with 255 additions and 185 deletions
changelog/entries/unreleased/refactor
web-frontend
modules/database
store
utils
test/unit/database

View file

@ -0,0 +1,7 @@
{
"type": "refactor",
"message": "Change cookie values stored for last remembered view ID to shorter ones.",
"issue_number": 2105,
"bullet_points": [],
"created_at": "2023-12-28"
}

View file

@ -1,7 +1,10 @@
import { v1 as uuidv1 } from 'uuid'
import { StoreItemLookupError } from '@baserow/modules/core/errors'
import { uuid, isSecureURL } from '@baserow/modules/core/utils/string'
import { fitInCookie } from '@baserow/modules/database/utils/view'
import { uuid } from '@baserow/modules/core/utils/string'
import {
readDefaultViewIdFromCookie,
saveDefaultViewIdInCookie,
} from '@baserow/modules/database/utils/view'
import ViewService from '@baserow/modules/database/services/view'
import FilterService from '@baserow/modules/database/services/filter'
import DecorationService from '@baserow/modules/database/services/decoration'
@ -308,19 +311,13 @@ export const mutations = {
groupBy._.loading = value
},
/**
* Data for defaultViewId for $cookies:
* [
* {table_id: table1Id, id: view1Id},
* {table_id: table2Id, id: view2Id},
* . . .
* ]
* Data for defaultViewId for Vuex store:
* {
* defaultViewId: view1Id,
* }
*/
SET_DEFAULT_VIEW(state, data) {
state.defaultViewId = data
SET_DEFAULT_VIEW_ID(state, viewId) {
state.defaultViewId = viewId
},
}
@ -354,7 +351,10 @@ export const actions = {
commit('SET_LOADING', false)
// Get the default view for the table.
dispatch('getDefaultView', { tableId: table.id })
const defaultViewId = readDefaultViewIdFromCookie(this.$cookies, table.id)
if (defaultViewId !== null) {
commit('SET_DEFAULT_VIEW_ID', defaultViewId)
}
} catch (error) {
commit('SET_ITEMS', [])
commit('SET_LOADING', false)
@ -539,9 +539,10 @@ export const actions = {
*/
select({ commit, dispatch }, view) {
commit('SET_SELECTED', view)
commit('SET_DEFAULT_VIEW_ID', view.id)
// Set the default view for the table.
dispatch('setDefaultView', { view })
saveDefaultViewIdInCookie(this.$cookies, view, this.$config)
dispatch(
'undoRedo/updateCurrentScopeSet',
@ -577,74 +578,6 @@ export const actions = {
}
return dispatch('select', view)
},
/**
* Gets the default view from cookies (if it exists) OR the first view
* otherwise, sets it in Vuex store
*/
getDefaultView({ commit, getters }, { tableId }) {
try {
const defaultViewIdData = this.$cookies.get('defaultViewId') || []
const foundView = defaultViewIdData.find(
(view) => view.table_id === tableId
)
if (foundView) {
const view = getters.get(foundView.id)
commit('SET_DEFAULT_VIEW', view?.id)
} else {
commit('SET_DEFAULT_VIEW', null)
}
} catch (error) {
// in case of any exception, set default view to null, this should load the
// first view:
commit('SET_DEFAULT_VIEW', null)
}
},
/**
* Updates the default view for table in cookies and in Vuex store
*/
setDefaultView({ commit }, { view }) {
const defaultViewIdData = this.$cookies.get('defaultViewId') || []
try {
// Find the existing object with the same table_id, if it exists
const existingViewIndex = defaultViewIdData.findIndex(
(obj) => obj.table_id === view.table_id
)
if (existingViewIndex !== -1) {
// If existingView is found, remove it from the array
const existingView = defaultViewIdData.splice(existingViewIndex, 1)[0]
// Update the id of the existing object
existingView.id = view.id
// Add the existingView back to the end of the array
defaultViewIdData.push(existingView)
} else {
if (view.id === view.slug) {
// we are viewing a public view so ignore for the purposes of setting
// a default view
return
}
// Add a new object for the table_id
defaultViewIdData.push({ table_id: view.table_id, id: view.id })
}
} catch (error) {
const defaultViewIdData = []
// in case of any exception, set default view to the current view:
defaultViewIdData.push({ table_id: view.table_id, id: view.id })
} finally {
// Limit the number of views to remember (based on the max. cookie size)
const fittedList = fitInCookie('defaultViewId', defaultViewIdData)
const secure = isSecureURL(this.$config.PUBLIC_WEB_FRONTEND_URL)
this.$cookies.set('defaultViewId', fittedList, {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: this.$config.BASEROW_FRONTEND_SAME_SITE_COOKIE,
secure,
})
commit('SET_DEFAULT_VIEW', view.id)
}
},
/**
* Changes the loading state of a specific filter.
*/

View file

@ -1,10 +1,12 @@
import { firstBy } from 'thenby'
import BigNumber from 'bignumber.js'
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
import { escapeRegExp } from '@baserow/modules/core/utils/string'
import { escapeRegExp, isSecureURL } from '@baserow/modules/core/utils/string'
import { SearchModes } from '@baserow/modules/database/utils/search'
import { convertStringToMatchBackendTsvectorData } from '@baserow/modules/database/search/regexes'
export const DEFAULT_VIEW_ID_COOKIE_NAME = 'defaultViewId'
/**
* Generates a sort function based on the provided sortings.
*/
@ -498,23 +500,6 @@ export function utf8ByteSize(str) {
}
}
/**
* Limit the size of a cookie's value by removing elements from an array
* until it fits within the maximum allowed cookie size.
*/
export function fitInCookie(name, list) {
const result = []
for (let i = list.length - 1; i >= 0; i--) {
result.unshift(list[i])
const serialized = encodeURIComponent(JSON.stringify(result))
if (utf8ByteSize(serialized) > 4096) {
result.shift() // Remove the last added item as it caused the size to exceed the limit
break
}
}
return result
}
/**
* Return the view that has been visited most recently or the first
* available one that is capable of displaying the provided row data if required.
@ -543,3 +528,131 @@ export function extractRowMetadata(data, rowId) {
const metadata = data.row_metadata || {}
return metadata[rowId] || {}
}
/**
* Limit the size of a cookie's value by removing elements from an array
* until it fits within the maximum allowed cookie size. The array is
* assumed to be ordered by least important to most important, so the first
* elements are removed first.
*
* @param {Array} arrayOfValues - The array of values to encode.
* @param {Function} encodingFunc - The function to use to encode the array.
* @param {Number} maxLength - The maximum allowed length of the encoded value string.
* @returns {String} - The serialized value to save in the cookie with the
* max number of elements that fit in, or an empty string if none fit.
*/
export function fitInCookieEncoded(
arrayOfValues,
encodingFunc,
maxLength = 2048
) {
for (let i = 0, l = arrayOfValues.length; i < l; i++) {
const encoded = encodingFunc(arrayOfValues.slice(i))
// The encoded URI will be serialized when saved in the cookie, so we
// need to encode it first to get the correct byte size.
const serialized = encodeURIComponent(encoded)
if (utf8ByteSize(serialized) < maxLength) {
return encoded
}
}
return ''
}
export function decodeDefaultViewIdPerTable(value) {
// backward compatibility, we used to store the array of default views
// with a slightly different format
if (Array.isArray(value)) {
return value.map((item) => ({
tableId: item.table_id,
viewId: item.id,
}))
}
const data = []
for (const item of value.split(',')) {
const [tableId, viewId] = item.split(':')
if (tableId !== undefined && viewId !== undefined) {
data.push({ tableId: parseInt(tableId), viewId: parseInt(viewId) })
}
}
return data
}
export function encodeDefaultViewIdPerTable(data) {
return data.map(({ tableId, viewId }) => `${tableId}:${viewId}`).join(',')
}
/**
* Reads the default view for table from cookies.
*
* @param {Object} cookies - The cookies object.
* @param {Number} tableId - The id of the table.
* @param {String} cookieName - The name of the cookie.
* @returns {Number|null} - The id of the default view for the table, or null if there
* is no default view for the table.
*/
export function readDefaultViewIdFromCookie(
cookies,
tableId,
cookieName = DEFAULT_VIEW_ID_COOKIE_NAME
) {
try {
const cookieValue = cookies.get(cookieName) || ''
const defaultViews = decodeDefaultViewIdPerTable(cookieValue)
const defaultView = defaultViews.find((view) => view.tableId === tableId)
return defaultView ? defaultView.viewId : null
} catch (error) {
return null
}
}
/**
* Updates the default view for table in cookies (if it exists) or creates a new one if
* it doesn't. The entry will be placed at the end of the list as the most recently
* visited view. If the entire list does not fit in the cookie, the oldest entries (the
* first ones) will be removed.
*
* @param {Object} cookies - The cookies object.
* @param {Object} view - The view object.
* @param {Object} config - The config object.
* @param {String} cookieName - The name of the cookie.
*/
export function saveDefaultViewIdInCookie(
cookies,
view,
config,
cookieName = DEFAULT_VIEW_ID_COOKIE_NAME
) {
const cookieValue = cookies.get(cookieName) || ''
let defaultViews = decodeDefaultViewIdPerTable(cookieValue)
function createEntry(view) {
return { tableId: view.table_id, viewId: view.id }
}
try {
const index = defaultViews.findIndex((obj) => obj.tableId === view.table_id)
if (index !== -1) {
const existingView = defaultViews.splice(index, 1)[0]
existingView.viewId = view.id
defaultViews.push(existingView)
} else if (view.id !== view.slug) {
defaultViews.push(createEntry(view))
}
} catch (error) {
defaultViews = [createEntry(view)]
} finally {
const fittedListEncoded = fitInCookieEncoded(
defaultViews,
encodeDefaultViewIdPerTable
)
const secure = isSecureURL(config.PUBLIC_WEB_FRONTEND_URL)
cookies.set(cookieName, fittedListEncoded, {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: config.BASEROW_FRONTEND_SAME_SITE_COOKIE,
secure,
})
}
}

View file

@ -1,8 +1,13 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import Table from '@baserow/modules/database/pages/table'
import { utf8ByteSize } from '@baserow/modules/database/utils/view'
import {
DEFAULT_VIEW_ID_COOKIE_NAME,
readDefaultViewIdFromCookie,
decodeDefaultViewIdPerTable,
encodeDefaultViewIdPerTable,
} from '@baserow/modules/database/utils/view'
// Mock out debounce so we dont have to wait or simulate waiting for the various
// Mock out debounce so we don't have to wait or simulate waiting for the various
// debounces in the search functionality.
jest.mock('lodash/debounce', () => jest.fn((fn) => fn))
@ -34,12 +39,12 @@ describe('View Tests', () => {
})
test('Default view is being set correctly initially', async () => {
const allCookies = testApp.store.$cookies
const { application, table, views } =
await givenATableInTheServerWithMultipleViews()
const gridView = views[0]
const galleryView = views[1]
// The first view is the Grid view, the Default view is the Gallery view which
// is going to be rendered initially:
const tableComponent = await testApp.mount(Table, {
@ -50,19 +55,16 @@ describe('View Tests', () => {
},
})
const allCookies = testApp.store.$cookies
const tableId = gridView.table_id
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
// Check if cookie is updated correctly (default view):
const cookieValue = allCookies.get('defaultViewId')
expect(cookieValue.length).toBe(1)
const defaultViewIdObject = cookieValue.find(
(obj) => obj.table_id === tableId
)
expect(defaultViewIdObject.table_id).toBe(tableId)
expect(defaultViewIdObject.id).toBe(galleryView.id)
const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
expect(defaultViewId).not.toBe(null)
const defaultView = testApp.store.getters['view/get'](defaultViewId)
expect(defaultView.table_id).toBe(tableId)
expect(defaultView.id).toBe(galleryView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
// Check if component is rendered:
@ -93,13 +95,11 @@ describe('View Tests', () => {
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
// Check if cookie is updated correctly (default view):
const cookieValue = allCookies.get('defaultViewId')
expect(cookieValue.length).toBe(1)
const defaultViewIdObject = cookieValue.find(
(obj) => obj.table_id === tableId
)
expect(defaultViewIdObject.table_id).toBe(tableId)
expect(defaultViewIdObject.id).toBe(galleryView.id)
const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
expect(defaultViewId).not.toBe(null)
const defaultView = testApp.store.getters['view/get'](defaultViewId)
expect(defaultView.table_id).toBe(tableId)
expect(defaultView.id).toBe(galleryView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
// Check if component is rendered:
@ -112,13 +112,18 @@ describe('View Tests', () => {
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
// Check if cookie is updated correctly (default view):
const updatedCookieValue = allCookies.get('defaultViewId')
expect(updatedCookieValue.length).toBe(1)
const updatedDefaultViewIdObject = updatedCookieValue.find(
(obj) => obj.table_id === tableId
const updatedCookieValue = decodeDefaultViewIdPerTable(
allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(updatedDefaultViewIdObject.table_id).toBe(tableId)
expect(updatedDefaultViewIdObject.id).toBe(gridView.id)
expect(updatedCookieValue.length).toBe(1)
const updatedDefaultViewId = readDefaultViewIdFromCookie(
allCookies,
tableId
)
const updatedDefaultView =
testApp.store.getters['view/get'](updatedDefaultViewId)
expect(updatedDefaultView.table_id).toBe(tableId)
expect(updatedDefaultView.id).toBe(gridView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(gridView.id)
})
@ -146,13 +151,18 @@ describe('View Tests', () => {
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(firstTableGridView.id)
// Check if cookie is updated correctly (default view):
const cookieValue = allCookies.get('defaultViewId')
expect(cookieValue.length).toBe(1)
const defaultViewIdObject = cookieValue.find(
(obj) => obj.table_id === firstTableGridView.table_id
const cookieValue = decodeDefaultViewIdPerTable(
allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(defaultViewIdObject.table_id).toBe(firstTableGridView.table_id)
expect(defaultViewIdObject.id).toBe(firstTableGridView.id)
expect(cookieValue.length).toBe(1)
const defaultViewId = readDefaultViewIdFromCookie(
allCookies,
firstTableGridView.table_id
)
expect(defaultViewId).not.toBe(null)
const defaultView = testApp.store.getters['view/get'](defaultViewId)
expect(defaultView.table_id).toBe(firstTableGridView.table_id)
expect(defaultView.id).toBe(firstTableGridView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(firstTableGridView.id)
// Check if component is rendered:
@ -176,23 +186,26 @@ describe('View Tests', () => {
testApp.store.dispatch('view/selectById', secondTableGridView.id)
const allCookiesAfterChangingTable = testApp.store.$cookies
const cookieValueAfterChangingTable =
allCookiesAfterChangingTable.get('defaultViewId')
const cookieValueAfterChangingTable = decodeDefaultViewIdPerTable(
allCookiesAfterChangingTable.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(secondTableGridView.id)
// Check if cookie is updated correctly (default view):
expect(cookieValueAfterChangingTable.length).toBe(2)
const defaultViewIdObjectAfterChangingTable =
cookieValueAfterChangingTable.find(
(obj) => obj.table_id === secondTableGridView.table_id
)
expect(defaultViewIdObjectAfterChangingTable.table_id).toBe(
const defaultViewIdAfterChangingTable = readDefaultViewIdFromCookie(
allCookies,
secondTableGridView.table_id
)
expect(defaultViewIdObjectAfterChangingTable.id).toBe(
secondTableGridView.id
expect(defaultViewIdAfterChangingTable).not.toBe(null)
const defaultViewAfterChangingTable = testApp.store.getters['view/get'](
defaultViewIdAfterChangingTable
)
expect(defaultViewAfterChangingTable.table_id).toBe(
secondTableGridView.table_id
)
expect(defaultViewAfterChangingTable.id).toBe(secondTableGridView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(secondTableGridView.id)
// Check if component is rendered:
@ -218,26 +231,27 @@ describe('View Tests', () => {
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(firstTableGridView.id)
// Check if cookie is updated correctly (default view):
const cookieValueAfterSwitchingBack =
allCookiesAfterSwitchingBack.get('defaultViewId')
const cookieValueAfterSwitchingBack = decodeDefaultViewIdPerTable(
allCookiesAfterSwitchingBack.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(cookieValueAfterSwitchingBack.length).toBe(2)
const defaultViewIdObjectAfterSwitchingBack =
cookieValueAfterSwitchingBack.find(
(obj) => obj.table_id === firstTableGridView.table_id
)
expect(defaultViewIdObjectAfterSwitchingBack.table_id).toBe(
const defaultViewIdAfterSwitchingBack = readDefaultViewIdFromCookie(
allCookiesAfterSwitchingBack,
firstTableGridView.table_id
)
expect(defaultViewIdObjectAfterSwitchingBack.id).toBe(firstTableGridView.id)
expect(defaultViewIdAfterSwitchingBack).not.toBe(null)
const defaultViewAfterSwitchingBack = testApp.store.getters['view/get'](
defaultViewIdAfterSwitchingBack
)
expect(defaultViewAfterSwitchingBack.table_id).toBe(
firstTableGridView.table_id
)
expect(defaultViewAfterSwitchingBack.id).toBe(firstTableGridView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(firstTableGridView.id)
expect(cookieValueAfterSwitchingBack[cookieValue.length - 1]).toMatchObject(
defaultViewIdObjectAfterSwitchingBack
)
})
test('Default view is being set correctly initially only from cookie', async () => {
test('Default view is being set correctly only from cookie', async () => {
// set the cookie, render table without view id passed in, this should render
// the default (Gallery) view
const { application, table, views } =
@ -252,10 +266,13 @@ describe('View Tests', () => {
// Set the cookie for defaultView manually:
const defaultViewIdData = []
defaultViewIdData.push({
table_id: galleryView.table_id,
id: galleryView.id,
tableId: galleryView.table_id,
viewId: galleryView.id,
})
allCookies.set('defaultViewId', defaultViewIdData)
allCookies.set(
DEFAULT_VIEW_ID_COOKIE_NAME,
encodeDefaultViewIdPerTable(defaultViewIdData)
)
// The first view is the Grid view, the Default view is the Gallery view,
// we're not rendering any view initially and Default view (Gallery view)
@ -270,13 +287,15 @@ describe('View Tests', () => {
// Check if Vuex store is updated correctly (first view):
expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
// Check if cookie is updated correctly (default view):
const cookieValue = allCookies.get('defaultViewId')
expect(cookieValue.length).toBe(1)
const defaultViewIdObject = cookieValue.find(
(obj) => obj.table_id === tableId
const cookieValue = decodeDefaultViewIdPerTable(
allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(defaultViewIdObject.table_id).toBe(tableId)
expect(defaultViewIdObject.id).toBe(galleryView.id)
expect(cookieValue.length).toBe(1)
const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
expect(defaultViewId).not.toBe(null)
const defaultView = testApp.store.getters['view/get'](defaultViewId)
expect(defaultView.table_id).toBe(tableId)
expect(defaultView.id).toBe(galleryView.id)
// Check if Vuex store is updated correctly (default view):
expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
// Check if component is rendered:
@ -290,28 +309,24 @@ describe('View Tests', () => {
const gridView = views[0]
const allCookies = testApp.store.$cookies
// Calculate the size needed to exceed 4096 bytes
const targetSize = 10000 // Choose a size greater than 4096
// Generate random data to fill up the cookie
// Our cookie has a limit of 2kb, so we need to generate enough data to fill it up
// For sure one entry will need more than 1 byte, so we can't just generate 2048
// entries
const targetSize = 2048
const randomData = []
let totalSize = 0
let i = 0
while (totalSize < targetSize) {
const randomTableId = i++
const randomViewId = i++
const entry = { table_id: randomTableId, id: randomViewId }
const entrySize = utf8ByteSize(encodeURIComponent(JSON.stringify(entry)))
if (totalSize + entrySize <= targetSize) {
randomData.push(entry)
totalSize += entrySize
} else {
break
}
for (let i = 0; i < targetSize; i++) {
const randomTableId = i
const randomViewId = i
const entry = { tableId: randomTableId, viewId: randomViewId }
randomData.push(entry)
}
allCookies.set('defaultViewId', randomData)
const allCookies = testApp.store.$cookies
allCookies.set(
DEFAULT_VIEW_ID_COOKIE_NAME,
encodeDefaultViewIdPerTable(randomData)
)
const originalDataLength = randomData.length
// Mount the component, which should update the cookies
@ -324,14 +339,14 @@ describe('View Tests', () => {
})
// The Default view is the Grid view and it should be set (appended) in the cookie
const cookieValue = allCookies.get('defaultViewId')
const cookieValue = decodeDefaultViewIdPerTable(
allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(cookieValue.length).toBeGreaterThan(0)
const defaultViewIdObject = cookieValue.find(
(obj) => obj.table_id === gridView.table_id
)
expect(defaultViewIdObject.table_id).toBe(gridView.table_id)
expect(defaultViewIdObject.id).toBe(gridView.id)
const defaultViewIdObject = cookieValue[cookieValue.length - 1]
expect(defaultViewIdObject.tableId).toBe(gridView.table_id)
expect(defaultViewIdObject.viewId).toBe(gridView.id)
// Check if gridView is set as the last view in the array
expect(cookieValue[cookieValue.length - 1]).toMatchObject(
@ -339,7 +354,9 @@ describe('View Tests', () => {
)
// Ensure that the first element is removed from the cookie array
const updatedCookieValue = allCookies.get('defaultViewId')
const updatedCookieValue = decodeDefaultViewIdPerTable(
allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
)
expect(updatedCookieValue).not.toContainEqual(randomData[0])
expect(updatedCookieValue.length).toBeLessThan(originalDataLength)
})