@ou-imdt/create
Version:
Command line tool to create team boilerplate.
417 lines (403 loc) • 14.9 kB
JavaScript
import { getDefaultParams, getUrlParams, VleModule } from '@ou-imdt/utils';
import { defineStore } from 'pinia';
// Initialize VLE utility module for handling Virtual Learning Environment operations
const vleUtils = new VleModule();
export const useGlobalStore = defineStore('store', {
state: () => ({
/**
* External configuration data loaded from external sources.
* @type {Object<string, any>}
* @default {}
*/
externalConfigurationData: {},
/**
* Indicates whether the activity has finished loading and is ready for user interaction.
* @type {boolean}
* @default false
*/
activityLoaded: false,
/**
* @type {Boolean} Indicates if running inside the VLE.
* @default false (Offline)
*/
isOnline: vleUtils.onVLE(),
/**
* Object containing the default VLE parameters as key-value pairs.
* '_c', '_i', '_p', '_a', '_s', '_u'
* @type {Object<string, string>}
*/
defaultVLEParams: getDefaultParams(),
/**
* Object containing the URL parameters as key-value pairs.
* @type {Object<string, string>}
*/
urlParams: { _u: 'default', ...getUrlParams() },
/**
* A mapping of parameter keys to their resolved file attachment URLs.
* @type {Object<string, string>}
*/
fileAttachments: {},
/**
* A mapping of parameter keys to their resolved folder attachment URLs.
* @type {Object<string, string>}
*/
folderAttachments: {},
/**
* User-specific data objects.
* @type {Object<string, any>}
*/
userData: {
default: {}
},
/**
* Global data objects.
*/
globalData: {
default: {}
},
/**
* The current user ID.
* @type {string}
*/
currentUserId: 'default'
}),
actions: {
/**
* Outputs error to console if in development environment or verbose is set to true
* @param msg
* @return void
*/
error (...msg) {
this.urlParams.verbose || !this.isOnline ? console.error(...msg) : null;
},
/**
* Outputs log to console if in development environment or verbose is set to true
* @param msg
* @return void
*/
log (...msg) {
this.urlParams.verbose || !this.isOnline ? console.log(...msg) : null;
},
/**
* Saves store.userData to the VLE.
* @return {Promise<void>} A promise that resolves on successful save or rejects on error.
*/
saveUserData () {
return vleUtils
.saveData({
user: true,
values: this.serialiseDataForSave(this.userData)
})
.then(
() => {},
reason => this.error(`${this.isOnline ? 'VLE' : 'Local'} Save failed: ${reason}`)
);
},
/**
* Saves store.globalData to the VLE.
* @param {boolean} all - If true, saves all global data; otherwise saves only current user's data
* @return {Promise<void>} A promise that resolves on successful save or rejects on error.
*/
saveGlobalData(all = false) {
// Prepare data for saving - workaround for serialization issues
let datatosave = {};
if (all) {
datatosave = this.globalData;
} else {
// Save only the current user's global data
datatosave[this.urlParams._u] = this.globalData[this.urlParams._u];
}
return vleUtils
.saveData({
user: false,
values: this.serialiseDataForSave(datatosave)
})
.then(
() => {},
(reason) => this.error(`${this.isOnline ? 'VLE' : 'Local'} Save failed: ${reason}`)
);
},
/**
* Converts objects to be saved into a format that can be accepted by the VLE. {string: string}
* @param {Object} obj - The object to serialize
* @return {Object<string, string>} Serialized object with JSON stringified values
*/
serialiseDataForSave (obj) {
let serialisedSaveData = {};
Object.entries(obj).forEach(ent => {
serialisedSaveData[ent[0]] = JSON.stringify(ent[1]);
});
return serialisedSaveData;
},
/**
* Retrieves all single user data on the VLE and assigns it to store.userData
* @returns {Promise<void>} A promise that resolves when user data is fetched and assigned, or rejects on error.
*/
getUserData () {
this.currentUserId = this.urlParams._u;
return vleUtils
.loadAllData({
user: true
})
.then(
response => {
Object.entries(response).forEach(ent => {
// Handle different key formats for online vs offline modes
this.isOnline
? (this.userData[ent[0]] = JSON.parse(ent[1]))
: (this.userData[ent[0].split(':')[1]] = JSON.parse(ent[1]));
});
},
reason => this.error(`User data fetch failed: ${reason}`)
);
},
/**
* Retrieves all global data on the VLE and assigns it to store.globalData
* @returns {Promise<void>} A promise that resolves when global data is fetched and assigned, or rejects on error.
*/
getGlobalData() {
this.currentUserId = this.urlParams._u;
return vleUtils
.loadAllData({
user: false
})
.then(
(response) => {
Object.entries(response).forEach((ent) => {
// Handle different key formats for online vs offline modes
this.isOnline
? (this.globalData[ent[0]] = JSON.parse(ent[1]))
: (this.globalData[ent[0].split('global:')[1]] = JSON.parse(ent[1]));
});
// Ensure current user has a global data object
if (!this.globalData[this.urlParams._u]) {
this.globalData[this.urlParams._u] = {};
}
},
(reason) => this.error(`Global data fetch failed: ${reason}`)
);
},
/**
* Parses the iframe URL and stores the URL of attachments in store.fileAttachments
* @return void
*/
splitOutAttachments () {
Object.keys(this.urlParams).forEach(x => {
const value = this.urlParams[x];
// Check if parameter contains a file attachment URL (http + zip)
if (value.includes('http') && value.includes('zip')) {
this.fileAttachments[x] = value;
delete this.urlParams[x];
}
});
},
/**
* Fetches External data from data.json if available
* @returns {Promise<void>} A promise that resolves when timeline data is fetched and assigned
*/
async fetchExternalData() {
try {
let urlMap;
let dataFile = '';
// Get data folder contents if running on VLE server
if (VLE.serverversion) {
await new Promise((resolve, reject) => {
VLE.get_folder('data', url => {
// Create a map of file paths to URLs for easy lookup
urlMap = url.reduce((acc, obj) => {
acc[obj.path] = obj.url;
return acc;
}, {});
// Check for configuration and style files
if(urlMap['data.json'] ){
dataFile = urlMap['data.json']
}
if(urlMap['overrides.css'] ){
this.addOverrideStyles(urlMap);
}
resolve(urlMap);
}, () => {
const error = 'Unable to get data folder (incorrect url)';
this.error(error);
reject(error);
});
});
}
// Fetch and process external configuration data
if(dataFile !== ''){
const response = await fetch(dataFile);
this.externalConfigurationData = await response.json();
this.replaceSquareBrackets(urlMap)
this.replaceCurlyBrackets()
}
this.activityLoaded = true;
} catch (error) {
this.error('Error loading timeline data:', error);
}
},
/**
* Replaces curly bracketed strings in external configuration data with their corresponding attachment URLs
* @description If running on server, replaces all instances of {{attachmentName}} in string values
* with the corresponding URL from urlParams. For example {{attachment1}} would be replaced with the
* actual URL path stored in urlParams.attachment1
*/
replaceCurlyBrackets() {
if (VLE.serverversion) {
this.externalConfigurationData = this.processObjectForCurlyBrackets(this.externalConfigurationData);
}
},
/**
* Replaces square bracketed strings in external configuration data with their corresponding URLs
* @description If running on server, replaces all instances of [[filename]] in string values
* with the mapped URL from the data folder. For example [[image.jpg]] would be replaced with the
* actual URL path to image.jpg
* @param {Object} urlMap - Map of file paths to their corresponding URLs
*/
replaceSquareBrackets(urlMap = {}) {
if (VLE.serverversion) {
this.externalConfigurationData = this.processObjectForSquareBrackets(this.externalConfigurationData, urlMap);
}
},
/**
* Recursively processes an object to replace curly bracketed strings
* @param {any} obj - The object or value to process
* @returns {any} The processed object with replacements made
*/
processObjectForCurlyBrackets(obj) {
if (typeof obj === 'string') {
let processedString = obj;
this.findCurlyBracketedStrings(obj).forEach(instance => {
// Extract attachment name from bracketed string
const cleanInstance = instance.replace('{{', '').replace('}}', '').trim();
const instanceParts = cleanInstance.split('/');
const attachmentName = instanceParts[0];
const attachmentUrl = this.urlParams[attachmentName];
if (attachmentUrl) {
// Replace both spaced and non-spaced bracket formats
processedString = processedString
.replace(`{{ ${cleanInstance} }}`, attachmentUrl)
.replace(`{{${cleanInstance}}}`, attachmentUrl);
}
});
return processedString;
} else if (Array.isArray(obj)) {
return obj.map(item => this.processObjectForCurlyBrackets(item));
} else if (obj !== null && typeof obj === 'object') {
const processedObj = {};
Object.keys(obj).forEach(key => {
processedObj[key] = this.processObjectForCurlyBrackets(obj[key]);
});
return processedObj;
}
return obj;
},
/**
* Recursively processes an object to replace square bracketed strings
* @param {any} obj - The object or value to process
* @param {Object} urlMap - Map of file paths to their corresponding URLs
* @returns {any} The processed object with replacements made
*/
processObjectForSquareBrackets(obj, urlMap = {}) {
if (typeof obj === 'string') {
let processedString = obj;
this.findBracketedStrings(obj).forEach(instance => {
// Extract filename from bracketed string
const cleanInstance = instance.replace('[[', '').replace(']]', '').trim();
const urlValue = urlMap[cleanInstance];
if (urlValue) {
console.log('Replacing:', instance, 'with:', urlValue);
// Replace both spaced and non-spaced bracket formats
processedString = processedString
.replace(`[[ ${cleanInstance} ]]`, urlValue)
.replace(`[[${cleanInstance}]]`, urlValue);
}
});
return processedString;
} else if (Array.isArray(obj)) {
return obj.map(item => this.processObjectForSquareBrackets(item, urlMap));
} else if (obj !== null && typeof obj === 'object') {
const processedObj = {};
Object.keys(obj).forEach(key => {
processedObj[key] = this.processObjectForSquareBrackets(obj[key], urlMap);
});
return processedObj;
}
return obj;
},
/**
* Finds all square bracketed strings in text using regex
* @param {string} text - Text to search for bracketed strings
* @returns {string[]} Array of found bracketed strings
*/
findBracketedStrings(text) {
const regex = /\[\[(.*?)]]/g;
return text.match(regex) || [];
},
/**
* Finds all curly bracketed strings in text using regex
* @param {string} text - Text to search for bracketed strings
* @returns {string[]} Array of found bracketed strings
*/
findCurlyBracketedStrings(text) {
const regex = /\{\{(.*?)\}\}/g;
return text.match(regex) || [];
},
/**
* Adds a stylesheet link element to the document head for custom CSS overrides
* @param {Object} urlMap - Map of file paths to their corresponding URLs
* @description If overrides.css exists in the URL map, creates and appends a link
* element to load those custom styles. The stylesheet applies to both screen and print media.
*/
addOverrideStyles(urlMap) {
if (urlMap['overrides.css']) {
let link = document.createElement("link");
link.href = urlMap['overrides.css'];
link.type = "text/css";
link.rel = "stylesheet";
link.media = "screen,print";
document.head.append(link);
}
},
/**
* Parses the iframe URL and stores the contents of folders in store.folderAttachments
* @returns {Promise<any>}
*/
splitOutFolders () {
return new Promise(resolve => {
Object.keys(this.urlParams).forEach(x => {
const value = this.vleParams[x];
// Check if parameter contains a zip folder attachment
if (value.includes('zip')) {
// Get folder contents from VLE
window.VLE.get_folder(x, url => {
this.folderAttachments[x] = url;
});
delete this.urlParams[x];
}
});
resolve('Split');
});
},
/**
* Outputs warning to console if in development environment or verbose is set to true
* @param msg
* @return void
*/
warn (...msg) {
this.urlParams.verbose || !this.isOnline ? console.warn(...msg) : null;
},
/**
* Runs the helper functions required to initialise a widget.
* Loads user data, global data, and processes attachments in sequence.
* @returns {Promise<void>}
*/
async prepareWidget () {
await this.getUserData();
await this.getGlobalData();
this.splitOutAttachments();
await this.splitOutFolders().catch(() =>
this.error('Unable to split folders (incorrect url)')
);
}
}
});