All files / ext.wikilambda.app/store/stores clipboard.js

100% Statements 224/224
94.59% Branches 35/37
100% Functions 8/8
100% Lines 224/224

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 1x 1x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 25x 25x 25x 25x 25x 25x 25x 25x 25x 1x 1x 1x 1x 1x 25x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 3x 3x 3x 3x 3x 3x 3x 3x 2x 2x 2x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 126x 126x 126x 126x 126x 1x 1x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 119x 119x 8x 8x 111x 111x 119x 56x 56x 56x 56x 15x 15x 3x 3x 12x 12x 12x 12x 15x 4x 4x 12x 12x 41x 41x 41x 41x 85x 41x 41x 41x 41x 55x 55x 55x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 2x 2x 2x 2x 2x 2x 8x 8x 8x 8x 10x 2x 2x 2x 2x 2x 2x 2x 2x 2x 6x 6x 6x 6x 6x 6x 10x 10x 10x 10x 10x 10x 10x 10x 126x 126x  
/*!
 * WikiLambda Vue editor: Pinia store for frontend clipboard feature
 *
 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
 * @license MIT
 */
'use strict';
 
const Constants = require( '../../Constants.js' );
const eventLogUtils = require( '../../utils/eventLogUtils.js' );
const { extractZIDs } = require( '../../utils/schemata.js' );
const { tryJsonParse } = require( '../../utils/miscUtils.js' );
const { getScaffolding } = require( '../../utils/typeUtils.js' );
const { getZObjectType, getZArgumentReferenceTerminalValue } = require( '../../utils/zobjectUtils.js' );
 
const STORAGE_KEY = 'ext-wikilambda-app-clipboard';
 
module.exports = {
	state: {
		clipboardItems: []
	},
 
	getters: {
		getClipboardItems: function ( state ) {
			return state.clipboardItems;
		}
	},
 
	actions: {
		/**
		 * Initializes the clipboard store with the content saved in mw.storage
		 * and creates an event listener to keep the storage updated across tabs.
		 * This action should be run once in the app, so it will be called from the
		 * initializeView action called in the App.vue component, on mount.
		 */
		initializeClipboard: function () {
			// Initialize Pinia store with mw.storage clipboard
			const storedClipboard = tryJsonParse( mw.storage.get( STORAGE_KEY ) );
			this.clipboardItems = storedClipboard || [];
 
			const zids = extractZIDs( storedClipboard );
			this.fetchZids( { zids } );
 
			// Add event listener to update other tabs in real time
			window.addEventListener( 'storage', ( event ) => {
				if ( event.key === STORAGE_KEY ) {
					this.clipboardItems = tryJsonParse( event.newValue ) || [];
					const moreZids = extractZIDs( this.clipboardItems );
					this.fetchZids( { zids: moreZids } );
				}
			} );
		},
		/**
		 * Store a zobject fragment in the zobject block clipboard, which has a Pinia store
		 * module and is also persisted in the local storage using the mediawiki.storage module.
		 *
		 * @param {Object} payload
		 * @param {string|undefined} payload.originKey - key from copy action, for naming
		 * @param {Object|string|undefined} payload.originSlotType - expected type, in canonical form
		 * @param {Object|string|undefined} payload.value - zobject block to copy, in hybrid form
		 */
		copyToClipboard: function ( payload ) {
			const { originKey, originSlotType } = payload;
			const value = JSON.parse( JSON.stringify( payload.value ) );
 
			const objectType = getZObjectType( value );
			const resolvingType = this.getResolvingType( value );
			// On edge cases like blank or partially unconfigured objects,
			// resolving type can be undefined:
			if ( resolvingType ) {
				const zids = extractZIDs( resolvingType );
				this.fetchZids( { zids } );
			}
 
			// For the item id, get the label of the key and assign a number.
			const keyLabel = this.getLabelData( originKey ).label;
			const repeated = this.clipboardItems.filter( ( i ) => i.itemId.startsWith( `${ keyLabel }#` ) ).length;
			const itemId = `${ keyLabel }#${ repeated + 1 }`;
 
			const clipboardItem = {
				itemId,
				originKey,
				originSlotType,
				value,
				objectType,
				resolvingType
			};
 
			// Update Pinia store
			this.clipboardItems.unshift( clipboardItem );
 
			// Update mw.storage
			mw.storage.set( STORAGE_KEY, JSON.stringify( this.clipboardItems ) );
 
			// Submit copy interaction
			const interactionData = {
				zobjectid: this.isAbstractContent() ? this.getAbstractWikiId : this.getCurrentZObjectId,
				zobjecttype: this.isAbstractContent() ? 'abstractwiki' : this.getCurrentZObjectType,
				zlang: this.getUserLangZid || null
			};
			eventLogUtils.submitInteraction( 'copy', interactionData );
		},
		/**
		 * Clears the store and the data stored in the browser localStorage
		 */
		clearClipboard: function () {
			this.clipboardItems = [];
			mw.storage.set( STORAGE_KEY, JSON.stringify( [] ) );
		},
		/**
		 * Given a zobject fragment and a destination keyPath, it resets the
		 * argument reference objects so that the final object is always
		 * contextually valid:
		 * * If the destination keyPath belongs to a implementation composition
		 *   it keeps untouched all the argument references to the target function
		 *   arguments, and resets the rest to blank argument references.
		 * * If the destination keyPath belongs to an abstract fragment,
		 *   it keeps untouched all the Z825 argument references, and resets the
		 *   rest to blank argument references.
		 * * If the destination keyPath is not a composition or abstract fragment
		 *   it removes the Z18 objects.
		 *
		 * @param {Object | Array | string} initialValue - object to copy, in its hybrid form
		 * @param {string} destinationKeyPath
		 * @return {Object | Array | string}
		 */
		cleanClipboardItem: function ( initialValue, destinationKeyPath ) {
			/**
			 * Helper function: recursively resets argument reference objects of
			 * a given object value and given a destination context. The context
			 * object must contain two properties:
			 * * allowsArgs: true if the destination slot is a composition or an abstract fragment.
			 * * allowedArgs: with the argument reference keys that are allowed, if any.
			 *
			 * @param {Object | Array | string} value
			 * @param {Object} context
			 * @return {Object | Array | string}
			 */
			const resetContextArgRefs = ( value, context ) => {
				// Array: walk each item and reset
				if ( Array.isArray( value ) ) {
					return value.map( ( item ) => resetContextArgRefs( item, context ) );
				}
 
				// Object: make sure any argument references are contextually correct
				if ( value && typeof value === 'object' ) {
 
					// Type is argument reference; reset if needed
					const type = getZObjectType( value );
					if ( type === Constants.Z_ARGUMENT_REFERENCE ) {
						// If context does not allow arg references, return empty object
						if ( !context.allowsArgs ) {
							return getScaffolding( Constants.Z_OBJECT );
						}
						// If context allows arg references, reset to blank if the key doesn't
						// belong to the destination context, or leave intact if it does
						const key = getZArgumentReferenceTerminalValue( value );
						const copy = JSON.parse( JSON.stringify( value ) );
						if ( !context.allowedArgs.includes( key ) ) {
							copy[ Constants.Z_ARGUMENT_REFERENCE_KEY ] = getScaffolding( Constants.Z_STRING );
						}
						return copy;
					}
 
					// Not an argument reference; recurse over all keys
					const cleaned = {};
					Object.keys( value ).forEach( ( key ) => {
						cleaned[ key ] = resetContextArgRefs( value[ key ], context );
					} );
 
					return cleaned;
				}
 
				// Any other type: return untouched
				return value;
			};
 
			/**
			 * Helper function: gathers context properties given a destination key path
			 *
			 * @param {string} keyPath
			 * @return {Object}
			 */
			const getContextArgumentKeys = ( keyPath ) => {
				// If keyPath belongs to an implementation composition, the available
				// argument references are the arg keys of the selected target function.
				const isComposition = keyPath.split( '.' ).includes( Constants.Z_IMPLEMENTATION_COMPOSITION );
				if ( isComposition ) {
					const args = this.getInputsOfFunctionZid( this.getCurrentTargetFunctionZid );
					return {
						allowsArgs: true,
						allowedArgs: args.map( ( arg ) => arg[ Constants.Z_ARGUMENT_KEY ] )
					};
				}
 
				// If keyPath belongs to an abstract fragment, the available argument
				// references are the arguments of Z825/Abstract render function
				const isAbstractFragment = keyPath.split( '.' )[ 0 ] === Constants.STORED_OBJECTS.ABSTRACT;
				if ( isAbstractFragment ) {
					return {
						allowsArgs: true,
						allowedArgs: [
							Constants.Z_ABSTRACT_RENDER_FUNCTION_QID,
							Constants.Z_ABSTRACT_RENDER_FUNCTION_LANGUAGE,
							Constants.Z_ABSTRACT_RENDER_FUNCTION_DATE
						]
					};
				}
 
				// Else, argument keys not allowed
				return {
					allowsArgs: false,
					allowedArgs: []
				};
			};
 
			// Get array of arguments for the destination context
			const destinationContext = getContextArgumentKeys( destinationKeyPath );
 
			// Call internal reset function with context
			return resetContextArgRefs( initialValue, destinationContext );
		}
	}
};