Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User IndexedDB #1230

Merged
merged 14 commits into from
May 22, 2024
6 changes: 3 additions & 3 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ const routes = {
workspace: require('../mod/workspace/_workspace'),
}

process.env.COOKIE_TTL = process.env.COOKIE_TTL || 36000
process.env.COOKIE_TTL ??= 36000

process.env.TITLE = process.env.TITLE || 'GEOLYTIX | XYZ'
process.env.TITLE ??= 'GEOLYTIX | XYZ'

process.env.DIR = process.env.DIR || ''
process.env.DIR ??= ''

module.exports = async (req, res) => {

Expand Down
2 changes: 2 additions & 0 deletions lib/plugins/_plugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { keyvalue_dictionary } from './keyvalue_dictionary.mjs'
import { locator } from './locator.mjs'
import { login } from './login.mjs'
import { svg_templates } from './svg_templates.mjs'
import { userIDB } from './userIDB.mjs'
import { zoomBtn } from './zoomBtn.mjs'
import { zoomToArea } from './zoomToArea.mjs'

Expand All @@ -31,6 +32,7 @@ const plugins = {
locator,
login,
svg_templates,
userIDB,
zoomBtn,
zoomToArea
}
Expand Down
57 changes: 57 additions & 0 deletions lib/plugins/userIDB.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export function userIDB(plugin, mapview) {

if (!mapp.user?.email) {

console.warn(`The userIDB plugin requires a mapp.user`)
return;
}

// Find the btnColumn element.
const btnColumn = document.getElementById('mapButton');

if (!btnColumn) return;

plugin.title ??= "Update userIDB locale"

// Append the plugin btn to the btnColumn.
btnColumn.append(mapp.utils.html.node`
<button
title=${plugin.title}
onclick=${()=>{

mapview.locale.layers.forEach(layer => {

if (typeof layer !== 'object') return;

updateLayer(layer, mapview.layers[layer.key])
})

mapp.utils.userIndexedDB.put('locales', mapview.locale)

alert(`User ${mapp.user.email} IndexedDB updated.`)

}}>
<div class="mask-icon" style="mask-image:url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/rule_settings/default/24px.svg)">`);
}

function updateLayer(layer, _layer) {

if (!_layer) return;

Object.keys(_layer).forEach(key => {

if (_layer[key] === undefined) return;

if (_layer[key] === null) {
layer[key] = null;
}

if (typeof _layer[key] === 'function') return;

if (typeof _layer[key] === 'object') return;

layer[key] = _layer[key]
})

return layer;
}
3 changes: 3 additions & 0 deletions lib/utils/_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import style from './style.mjs'

import * as svgSymbols from './svgSymbols.mjs'

import * as userIndexedDB from './userIndexedDB.mjs'

import * as gazetteer from './gazetteer.mjs'

import {default as verticeGeoms} from './verticeGeoms.mjs'
Expand Down Expand Up @@ -91,6 +93,7 @@ export default {
queryParams,
style,
svgSymbols,
userIndexedDB,
verticeGeoms,
xhr,
}
250 changes: 250 additions & 0 deletions lib/utils/userIndexedDB.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* ### mapp.utils.userIndexedDB
* This [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Terminology) implementation allows to store, get, and update a locale object from the 'locales' object store in a user indexedDB.
*
* There are many different operations that the indexedDB can handle. Typically the operations are CRUD.
*
* The `userIndexDB` methods have all been moved to the `mapp.utils` object.
*
* The logic for initialisation for the userIndexedDB object is the following:
* - `userIndexedDB.open(store)` will open a DB with the following name `${mapp.user.email} - {mapp.user.title}.
* - The database will not be created if there is a pre-existing DB.
* - The creation will trigger the `onupgradeneeded` event which checks whether the request `store` exists in the userIndexedDB.
*
* The `process.env.TITLE` will be added to the user object in the cookie module.
* The `user.title` is required to generate a unique indexedDB for each user[email/instance[title]]
*
* All object stores use the key value as a keypath for object indicies.
*
* Adding the url parameter `useridb=true` will ask the default script to get the keyed locale from the user indexedDB.
* The userLocale will be assigned as locale if available.
*
* ```js
* if (mapp.hooks.current.useridb) {

let userLocale = await mapp.utils.userIndexedDB.get('locales', locale.key)

if (!userLocale) {
await mapp.utils.userIndexedDB.add('locales', locale)
} else {
locale = userLocale
}
}
* ```
*
* The userIDB plugin adds a button to put [update] the locale in the user indexedDB.
*
* ```js
* export function userIDB(plugin, mapview) {

// Find the btnColumn element.
const btnColumn = document.getElementById('mapButton');

if (!btnColumn) return;

// Append the plugin btn to the btnColumn.
btnColumn.append(mapp.utils.html.node`
<button
title="Update userIDB locale"
onclick=${()=>{

mapp.utils.userIndexedDB.put('locales', mapview.locale)

}}>
<div class="mask-icon" style="mask-image:url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/rule_settings/default/24px.svg)">`);
}
* ```
* This can be tested but updating the mapview.locale in another plugin, e.g. dark_mode
*
* The enabled status will be stored with the local applying the setting when the locale is loaded with the useridb=true url param.
*
* ```js
* mapp.plugins.dark_mode = (plugin, mapview) => {

// Get the map button
const mapButton = document.getElementById("mapButton");

// If mapbutton doesn't exist, return (for custom views).
if (!mapButton) return;

// toggle dark_mode if enabled in config.
mapview.locale.dark_mode.enabled && toggleDarkMode()

// If the button container exists, append the dark mode button.
mapButton.append(mapp.utils.html.node`
<button
title="Color Mode"
class="btn-color-mode"
onclick=${()=>{
mapview.locale.dark_mode.enabled = toggleDarkMode()
}}>
<div class="mask-icon">`);
}
* ```
*
* @module /utils/userIndexedDB
*/

let IDB

/**
* @param {Object} store
* @returns {Promise} OpenDBPromise
*/
export async function openDB(_store) {

const store = _store.toString()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lekker!


const OpenDBPromise = new Promise((resolve, reject) => {

// will create a new database if db/version doesn't exist.
const IDBRequest = indexedDB.open(`${mapp.user.email} - ${mapp.user.title}`, 3);

IDBRequest.onerror = (event) => {
console.error(IDBRequest.error)
resolve(IDBRequest)
};

IDBRequest.onsuccess = (event) => {

if (!event.target.result.objectStoreNames.contains(store)) {

console.warn(`UserIDB doesn't contain "${store}" objectStore.`)

IDB = null
resolve()
}

IDB = event.target.result
resolve()
}

// will be called on database versioning.
IDBRequest.onupgradeneeded = (event) => {

// onsuccess method will be called after the object store is created.
event.target.result.createObjectStore(store, { keyPath: 'key' });
};
})

await OpenDBPromise

return OpenDBPromise
}

/**
* - deletes the user indexedDB.
* @function deleteDB
*/
export function deleteDB() {

if (!mapp.user) return;

indexedDB.deleteDatabase(`${mapp.user.email} - ${mapp.user.title}`)

console.log('Database deleted')
}

/**
* - Adds the keyed object to the named store.
* - The key will be returned on success.
* - Adding the same keyed object twice will result in an error.
* @function add
* @param {Object} store
* @param {Object} obj
* @returns {Promise} addPromise
*/
export async function add(store, obj) {

if (!IDB) await openDB(store)

const addPromise = new Promise((resolve, reject) => {

const IDBTransaction = IDB.transaction([store], 'readwrite');

const objectStore = IDBTransaction.objectStore(store)

const IDBRequest = objectStore.add(obj);

IDBRequest.onerror = (event) => {
console.error(IDBRequest.error)
resolve(IDBRequest)
};

IDBRequest.onsuccess = (event) => {
resolve(event.target.result)
};
})

await addPromise

return addPromise
}

/**
* - Gets the keyed object from the named store.
* @param {Object} store
* @param {string} key
* @returns {Promise} getPromise
*/
export async function get(store, key) {

if (!IDB) await openDB(store)

const getPromise = new Promise((resolve, reject) => {

const IDBTransaction = IDB.transaction([store], 'readwrite');

const objectStore = IDBTransaction.objectStore(store)

const IDBRequest = objectStore.get(key);

IDBRequest.onerror = (event) => {
console.error(IDBRequest.error)
reject(IDBRequest)
};

IDBRequest.onsuccess = (event) => {
resolve(IDBRequest.result)
};
})

await getPromise

return getPromise
}

/**
* - puts the keyed object to the named store.
* - This will override the existing keyed object.
* - Updates work by replacing (put) the same keyed object into an user indexedDB.
* @param {Object} store
* @param {Object} obj
* @returns {Promise} updatePromise
*/
export async function put(store, obj) {

if (!IDB) await openDB(store)

const updatePromise = new Promise((resolve, reject) => {

const IDBTransaction = IDB.transaction([store], 'readwrite');

const objectStore = IDBTransaction.objectStore(store)

const IDBRequest = objectStore.put(obj);

IDBRequest.onerror = (event) => {
console.error(IDBRequest.error)
reject(IDBRequest)
};

IDBRequest.onsuccess = (event) => {
resolve(IDBRequest.result)
};
})

await updatePromise

return updatePromise
}
3 changes: 3 additions & 0 deletions mod/user/cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ module.exports = async (req, res) => {
}

const user = rows[0]

// Assign title identifier to user object.
user.title = process.env.TITLE

if (user.blocked) {
res.setHeader('Set-Cookie', `${process.env.TITLE}=null;HttpOnly;Max-Age=0;Path=${process.env.DIR || '/'}`)
Expand Down
13 changes: 12 additions & 1 deletion public/views/_default.html
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
const locales = await mapp.utils.xhr(`${mapp.host}/api/workspace/locales`);

// Get locale with list of layers from Workspace API.
const locale = await mapp.utils.xhr(
let locale = await mapp.utils.xhr(
`${mapp.host}/api/workspace/locale?locale=${mapp.hooks.current.locale || locales[0]?.key}&layers=true`);

if (locale instanceof Error) {
Expand All @@ -568,6 +568,17 @@
${mapp.dictionary.no_locales}`)
}

if (mapp.hooks.current.useridb) {

let userLocale = await mapp.utils.userIndexedDB.get('locales', locale.key)

if (!userLocale) {
await mapp.utils.userIndexedDB.add('locales', locale)
} else {
locale = userLocale
}
}

// Add locale dropdown to layers panel if multiple locales are accessible.
if (locales.length > 1) {
const localesDropdown = mapp.ui.elements.dropdown({
Expand Down
Loading