UNPKG

@ou-imdt/create

Version:

Command line tool to create team boilerplate.

417 lines (403 loc) 14.9 kB
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)') ); } } });