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

99.36% Statements 315/317
96.87% Branches 62/64
100% Functions 16/16
99.36% Lines 315/317

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 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 11x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 52x 52x 52x 52x 52x 52x 488x 488x 52x 52x 126x 126x 126x 126x 126x 126x 126x 126x 126x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 23x 23x 23x 23x 23x 550x 550x 23x 23x 23x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 1x 5x 5x 5x 5x 5x 6x 1x 1x 1x 4x 4x 6x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 2x 126x 126x 126x 126x 126x 126x 126x 126x 126x 150x 16x 16x 16x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 3x 3x 2x 2x 2x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 7x 1x 1x 6x 7x 7x 7x 7x 7x 7x 5x 3x 3x 2x 5x 7x 7x 7x 7x 7x 7x 7x     7x 7x 7x 7x 2x 2x 7x 7x 7x 7x 7x 7x 7x 6x 6x 5x 6x 6x 5x 6x 126x 126x 126x 126x 126x 126x 126x 126x 126x 19x 22x 7x 7x 7x 22x 12x 12x 12x 22x 126x 126x 126x 126x 126x 126x 126x 126x 1x 1x 1x 126x 126x  
/*!
 * WikiLambda Vue editor: Pinia store for frontend error-related state, actions, mutations and getters
 *
 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
 * @license MIT
 */
'use strict';
 
const Constants = require( '../../Constants.js' );
 
const { extractErrorData } = require( '../../utils/errorUtils.js' );
const { getValueFromCanonicalZMap } = require( '../../utils/schemata.js' );
const ErrorData = require( '../classes/ErrorData.js' );
 
module.exports = {
	state: {
		/**
		 * Collection of ErrorData objects by errorId.
		 *
		 * The errorId is an internal string identifier that
		 * uniquely points at a sub-zObject represented by a
		 * component. The root errorId ('main') identifies the
		 * whole object.
		 *
		 * This permits that different app granularity levels
		 * present errors in different ways. For example, a
		 * text field that represents a terminal ZString object
		 * can have an error state. This will be saved in the
		 * error module with the unique internal errorId that
		 * idetifies that sub-object. The component will use
		 * that errorId to grab all the errors associated to
		 * that field.
		 *
		 * Similarly, there can be a number of errors that are
		 * general and page-wide. These errors will be saved
		 * in the state using the root errorId and can be
		 * presented in a top-level component such as the
		 * Publish dialog window.
		 */
		errors: {}
	},
 
	getters: {
		/**
		 * Returns all the stored error Ids (which are the key paths
		 * to the failed object.
		 *
		 * @param {Object} state
		 * @return {Array}
		 */
		getErrorPaths: function ( state ) {
			return Object.keys( state.errors );
		},
 
		/**
		 * Returns all the errors for a given errorId.
		 * If error type is passed as second parameter, returns only
		 * the errors of the type ("error" or "warning").
		 *
		 * @param {Object} state
		 * @return {Function}
		 */
		getErrors: function ( state ) {
			/**
			 * @param {string} errorId
			 * @param {string|undefined} type
			 * @return {Array}
			 */
			const findErrors = ( errorId, type = undefined ) => {
				const allErrors = state.errors[ errorId ] || [];
				return type ? allErrors.filter( ( error ) => error.type === type ) : allErrors;
			};
			return findErrors;
		},
 
		/**
		 * Returns if there are errors for a given errorId that have a specific message key.
		 *
		 * @param {Object} state
		 * @return {Function}
		 */
		hasErrorByKey: function ( state ) {
			/**
			 * @param {string} errorId
			 * @param {string} errorMessageKey
			 * @return {boolean}
			 */
			const findErrorByKey = ( errorId, errorMessageKey ) => {
				const allErrors = state.errors[ errorId ] || [];
				return !!allErrors.some( ( error ) => error.messageKey === errorMessageKey );
			};
			return findErrorByKey;
		},
 
		/**
		 * Returns the errors that are child to this field.
		 * E.g. main.Z2K2.Z14K3.Z16K1 is child to main.Z2K2.Z14K3
		 *
		 * @param {Object} state
		 * @return {Function}
		 */
		getChildErrorKeys: function ( state ) {
			/**
			 * @param {string} keyPath
			 * @return {Array}
			 */
			const findChildErrors = ( keyPath ) => {
				const errorPaths = Object.keys( state.errors );
				return errorPaths.filter( ( path ) => path.startsWith( keyPath + '.' ) );
			};
			return findChildErrors;
		}
	},
 
	actions: {
		/**
		 * Builds an ErrorData object and stores it for a given errorId.
		 * Any error that's set for the errorId 'main' is considered page-wide.
		 *
		 * @param {Object} payload
		 * @param {string} payload.errorId
		 * @param {string} payload.errorMessage
		 * @param {string} payload.errorMessageKey
		 * @param {string[]} payload.errorParams
		 * @param {string} payload.errorType literal string: "error" or "warning"
		 */
		setError: function ( payload ) {
			const {
				errorId = Constants.STORED_OBJECTS.MAIN,
				errorMessage,
				errorMessageKey,
				errorParams = [],
				errorType = Constants.ERROR_TYPES.ERROR,
				isPermanent = false
			} = payload;
 
			const errorData = ErrorData.buildErrorData( {
				errorMessage,
				errorMessageKey,
				errorParams,
				errorType,
				isPermanent
			} );
 
			this.errors[ errorId ] = this.errors[ errorId ] || [];
			this.errors[ errorId ].push( errorData );
		},
 
		/**
		 * Extracts the error data from metadata, builds the error message and
		 * runs the callback function to set the error with the available error
		 * message or the fallback message if metadata error wasn't found.
		 *
		 * @param {Object} payload
		 * @param {Object|null} payload.metadata - ZMap with metadata which may contain an errors key
		 * @param {Object} fallbackErrorData - error to set if no errors key is found
		 * @param {Function} errorHandler - function for handling the error when found
		 */
		handleMetadataError: function ( payload ) {
			const {
				metadata,
				fallbackErrorData,
				errorHandler
			} = payload;
 
			// If metadata is null, apply fallback error and exit
			if ( !metadata ) {
				errorHandler( fallbackErrorData );
				return;
			}
 
			const error = getValueFromCanonicalZMap( metadata, 'errors' );
			const errorData = extractErrorData( error );
 
			// If metadata doesn't have error information, apply fallback error and exit
			if ( !errorData ) {
				errorHandler( fallbackErrorData );
				return;
			}
 
			// If error is Z500/generic and has a string (Z500)K1, show that as error message
			if ( errorData.errorType === Constants.Z_GENERIC_ERROR && errorData.stringArgs.length ) {
				errorHandler( { errorMessage: errorData.stringArgs[ 0 ].value } );
				return;
			}
 
			// Finally, asynchronously fetch errorType and, if fetched,
			// set its label as error message
			this.fetchZids( { zids: [ errorData.errorType ] } ).then( () => {
				if ( this.getStoredObject( errorData.errorType ) ) {
					errorHandler( { errorMessage: this.getLabelData( errorData.errorType ).label } );
				} else {
					errorHandler( fallbackErrorData );
				}
			} );
		},
 
		/**
		 * Clears all errors for a given errorId
		 *
		 * @param {string} errorId
		 * @param {boolean} isPermanent
		 */
		clearErrors: function ( errorId, isPermanent = false ) {
			if ( errorId in this.errors ) {
				this.errors[ errorId ] = this.errors[ errorId ]
					.filter( ( error ) => error.isPermanent !== isPermanent );
			}
		},
 
		/**
		 * Clears all errors for a given errorId that have a specific key
		 *
		 * @param {Object} payload
		 * @param {string} payload.errorId
		 * @param {string} payload.errorMessageKey
		 */
		clearErrorsByKey: function ( payload ) {
			const { errorId, errorMessageKey } = payload;
			if ( errorId in this.errors ) {
				this.errors[ errorId ] = this.errors[ errorId ]
					.filter( ( error ) => error.messageKey !== errorMessageKey );
			}
		},
 
		/**
		 * Relocates field errors when a typed-list item is moved, so that an
		 * item's errors (and its descendants') follow it to its new position
		 * instead of staying with the array index (T431714).
		 *
		 * Errors are keyed by the item's dotted keyPath, so moving the item at
		 * `index` to `newIndex` under `listKeyPath` reproduces the array splice
		 * on those keys: the moved item's index becomes `newIndex` and every
		 * index the splice shifts is remapped by one. Indices outside the
		 * affected range, and non-numeric children, are left untouched.
		 *
		 * @param {string|Array} listKeyPath keyPath of the list (not the item)
		 * @param {number} index Current index of the moved item
		 * @param {number} newIndex Target index of the moved item
		 */
		relocateErrorsForListMove: function ( listKeyPath, index, newIndex ) {
			if ( index === newIndex ) {
				return;
			}
 
			const prefix = Array.isArray( listKeyPath ) ? listKeyPath.join( '.' ) : listKeyPath;
			const low = Math.min( index, newIndex );
			const high = Math.max( index, newIndex );
 
			// Map an old item index to its index after the splice
			const remap = ( itemIndex ) => {
				if ( itemIndex === index ) {
					return newIndex;
				}
				// The items between the old and new position shift by one
				return index < newIndex ? itemIndex - 1 : itemIndex + 1;
			};
 
			// Collect the affected error entries before mutating, so a swap
			// doesn't clobber a key that another entry is about to move into
			const moved = [];
			for ( const path of Object.keys( this.errors ) ) {
				if ( !path.startsWith( prefix + '.' ) ) {
					continue;
				}
				const rest = path.slice( prefix.length + 1 );
				const dotIndex = rest.indexOf( '.' );
				const itemIndex = Number( dotIndex === -1 ? rest : rest.slice( 0, dotIndex ) );
				if ( !Number.isInteger( itemIndex ) || itemIndex < low || itemIndex > high ) {
					continue;
				}
				const tail = dotIndex === -1 ? '' : rest.slice( dotIndex );
				moved.push( {
					oldPath: path,
					newPath: `${ prefix }.${ remap( itemIndex ) }${ tail }`,
					value: this.errors[ path ]
				} );
			}
 
			moved.forEach( ( entry ) => {
				delete this.errors[ entry.oldPath ];
			} );
			moved.forEach( ( entry ) => {
				this.errors[ entry.newPath ] = entry.value;
			} );
		},
 
		/**
		 * Clears field validation errors (doesn't clear global ones)
		 * that are children of the given path. Does not clear global (main) errors.
		 *
		 * @param {string} parentPath
		 */
		clearValidationErrors: function ( parentPath = Constants.STORED_OBJECTS.MAIN ) {
			for ( const errorId in this.errors ) {
				if ( errorId === Constants.STORED_OBJECTS.MAIN ) {
					// Don't clear global errors
					continue;
				}
				if ( errorId.startsWith( `${ parentPath }.` ) || ( errorId === parentPath ) ) {
					// Clear errors that are, or start with parentPath
					this.errors[ errorId ] = [];
				}
			}
		},
 
		/**
		 * Clears all errors.
		 *
		 * @param {boolean} isPermanent
		 */
		clearAllErrors: function ( isPermanent = false ) {
			this.clearValidationErrors();
			this.clearErrors( Constants.STORED_OBJECTS.MAIN, isPermanent );
		}
	}
};