Skip to content

Commit

Permalink
Merge pull request #1230 from dbauszus-glx/user-IndexedDB
Browse files Browse the repository at this point in the history
User IndexedDB
  • Loading branch information
RobAndrewHurst authored May 22, 2024
2 parents c80aa89 + 2c6aec1 commit 5d410fc
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 4 deletions.
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()

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

0 comments on commit 5d410fc

Please sign in to comment.