diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 6b583ba..b93cc56 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -38,7 +38,7 @@ "version": "3.2.0" }, "easejs": { - "version": "0.2.8" + "version": "0.2.9" }, "escape-string-regexp": { "version": "1.0.5" diff --git a/src/client/Client.js b/src/client/Client.js index a047945..2ed3748 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -23,6 +23,7 @@ const Class = require( 'easejs' ).Class; const EventEmitter = require( 'events' ).EventEmitter; const DomFieldNotFoundError = require( '../ui/field/DomFieldNotFoundError' ); const UnknownEventError = require( './event/UnknownEventError' ); +const system = require( '../system/client' ); /** @@ -198,30 +199,10 @@ module.exports = Class( 'Client' ) 'private _validatorFormatter': null, /** - * Contains classification match data per field - * - * TODO: Move this to somewhere more appropriate - * - * @type {Object} + * Classification match handler + * @type {Cmatch} */ - 'private _cmatch': {}, - - /** - * Performs classification matching on fields - * - * A field will have a positive match for a given index if all of its - * classes match - * - * @type {FieldClassMatcher} - */ - 'private _classMatcher': null, - - - /** - * Fields that were hidden (including indexes) since the last cmatch clear - * @type {Object} - */ - 'private _cmatchHidden': {}, + 'private _cmatch': null, /** * Automatically discards staging bucket contents @@ -347,9 +328,7 @@ module.exports = Class( 'Client' ) this, this._dataValidator, this.elementStyler, this.dataProxy, jQuery ); - this._classMatcher = this._factory.createFieldClassMatcher( - this.program.whens - ); + this._cmatch = system.cmatch( this.program, this.__inst ); this._validatorFormatter = this._factory.createValidatorFormatter( this.program.meta.qtypes @@ -428,7 +407,7 @@ module.exports = Class( 'Client' ) { client.ui.displayStep( step_id, function() { - client.forceCmatchAction(); + client._cmatch.forceCmatchAction(); } ); client._currentStepId = step_id; @@ -503,7 +482,7 @@ module.exports = Class( 'Client' ) client._quote.setQuickSaveData( data.content.quicksave || {} ); - client._hookClassifier(); + client._cmatch.hookClassifier( client._dataValidator ); // store internal status client._isInternal = client.program.isInternal = @@ -556,285 +535,6 @@ module.exports = Class( 'Client' ) }, - /** - * Force handling of the most recent cmatch data - * - * This can be used to refresh the UI to ensure that it is consistent with - * the cmatch data. - * - * @return {Client} self - */ - 'public forceCmatchAction': function() - { - if ( !( this._cmatch ) ) - { - return this; - } - - this._handleClassMatch( this._cmatch, true ); - - return this; - }, - - - 'private _hookClassifier': function() - { - var _self = this, - program = this.program; - - // clear/initialize cmatches - this._cmatch = {}; - - var cmatchprot = false; - - // set classifier - this._quote - .setClassifier( program.getClassifierKnownFields(), function() - { - return program.classify.apply( program, arguments ); - } ) - .on( 'classify', function( classes ) - { - if ( cmatchprot === true ) - { - _self._handleError( Error( 'cmatch recursion' ) ); - } - - cmatchprot = true; - - // handle field fixes - _self._dataValidator.validate( undefined, classes ) - .catch( e => _self.handleError( e ) ); - - _self._classMatcher.match( classes, function( cmatch ) - { - // it's important that we do this here so that everything - // that uses the cmatch data will consistently benefit - _self._postProcessCmatch( cmatch ); - - // if we're not on a current step, defer - if ( !( _self.ui.getCurrentStep() ) ) - { - _self._cmatch = cmatch; - cmatchprot = false; - return; - } - - _self._handleClassMatch( cmatch ); - cmatchprot = false; - } ); - } ); - }, - - - 'private _postProcessCmatch': function( cmatch ) - { - // for any matches that are scalars (they will have no indexes), loop - // through each field and set the index to the value of 'all' - for ( var field in cmatch ) - { - if ( field === '__classes' ) - { - continue; - } - - var cfield = cmatch[ field ]; - - if ( cfield.indexes.length === 0 ) - { - var data = this.getQuote().getDataByName( field ), - i = data.length; - - // this will do nothing if there is no data found - while ( i-- ) - { - cfield.indexes[ i ] = cfield.all; - } - } - } - - return cmatch; - }, - - - // from UI - 'private _cmatchVisFromUi': function( field, all ) - { - var step = this.getUi().getCurrentStep(); - - if ( !step ) - { - return []; - } - - var group = step.getElementGroup( field ); - if ( !group ) - { - return []; - } - - var i = group.getCurrentIndexCount(), - ret = []; - - while ( i-- ) - { - ret.push( all ); - } - - return ret; - }, - - - 'private _handleClassMatch': function( cmatch, force ) - { - force = !!force; - - this.ui.setCmatch( cmatch ); - - var _self = this, - quote = this.getQuote(), - - // oh dear god...(Demeter, specifically..) - fields = this.getUi().getCurrentStep().getStep() - .getExclusiveFieldNames(); - - - var visq = []; - for ( var field in cmatch ) - { - // ignore fields that are not on the current step - if ( !( fields[ field ] ) ) - { - continue; - } - - // if the match is still false, then we can rest assured - // that nothing has changed (and skip the overhead) - if ( !force - && ( cmatch[ field ] === false ) - && ( _self._cmatch[ field ] === false ) - ) - { - continue; - } - - var show = [], - hide = [], - - cfield = cmatch[ field ], - - vis = cfield.indexes, - cur = ( - ( _self._cmatch[ field ] || {} ).indexes - || [] - ); - - // the system was previously unable to determine the length, so - // let's attempt to get it from the UI - if ( vis.length === 0 ) - { - vis = this._cmatchVisFromUi( field, cfield.all ); - } - - // consider the number of indexes in the bucket first; - // otherwise, we might try to operate on fields that don't - // exist (bucket/class indexes not the same). the check for - // undefined in the first index is a workaround for the explicit - // setting of the length property of the bucket value when - // indexes are removed - var curdata = quote.getDataByName( field ), - fieldn = ( curdata.length > 0 && ( curdata[ 0 ] !== undefined ) ) - ? curdata.length - : vis.length; - - for ( var i = 0; i < fieldn; i++ ) - { - // do not record unchanged indexes as changed - // (avoiding the event overhead) - if ( !force && ( vis[ i ] === cur[ i ] ) ) - { - continue; - } - - ( ( vis[ i ] ) ? show : hide ).push( i ); - } - - if ( show.length ) - { - visq[ field ] = { event_id: 'show', name: field, indexes: show }; - this._mergeCmatchHidden( field, show, false ); - } - - if ( hide.length ) - { - visq[ field ] = { event_id: 'hide', name: field, indexes: hide }; - this._mergeCmatchHidden( field, hide, true ); - } - } - - // it's important to do this before showing/hiding fields, since - // those might trigger events that check the current cmatches - this._cmatch = cmatch; - - - // allow DOM operations to complete before we trigger - // manipulations on it (TODO: this is a workaround for group - // show/hide issues; we need a better solution to guarantee - // order - setTimeout( () => - { - Object.keys( visq ).forEach( field => - { - const { event_id, name, indexes } = visq[ field ]; - - this.handleEvent( event_id, { - elementName: name, - indexes: indexes, - } ); - - this._dapiTrigger( name ); - } ); - }, 25 ); - }, - - - 'private _mergeCmatchHidden': function( name, indexes, hidden ) - { - if ( !( this._cmatchHidden[ name ] ) ) - { - this._cmatchHidden[ name ] = {}; - } - - var cindexes = this._cmatchHidden[ name ]; - - for ( i in indexes ) - { - if ( hidden ) - { - cindexes[ indexes[ i ] ] = i; - } - else - { - delete cindexes[ indexes[ i ] ]; - } - } - - var some = false; - for ( var i in cindexes ) - { - some = true; - break; - } - - if ( !some ) - { - // v8 devs do not recomment delete as it progressively slows down - // property access on the object - this._cmatchHidden[ name ] = undefined; - } - }, - - /** * Hooks quote for performing validations on data change * @@ -862,7 +562,7 @@ module.exports = Class( 'Client' ) name, bucket, diff, - this._cmatch, + this._cmatch.getMatches(), function() { var args = arguments; @@ -937,7 +637,7 @@ module.exports = Class( 'Client' ) { // N.B.: We pass {} as the diff because nothing has actually changed _self.ui.invalidateForm( - validate_callback( bucket, {}, _self._cmatch ) + validate_callback( bucket, {}, _self._cmatch.getMatches() ) ); } ); }, @@ -1009,7 +709,7 @@ module.exports = Class( 'Client' ) // force UI cmatch update, since we may have fields that have been added // that need to be shown/hidden based on the current set of // classifications - this.forceCmatchAction(); + this._cmatch.forceCmatchAction(); }, @@ -1049,7 +749,7 @@ module.exports = Class( 'Client' ) catch ( e ) { // todo: better suited for brokers - this._handleError( Error( + this.handleError( Error( "Error loading program data: " + e.message ) ); @@ -1058,7 +758,7 @@ module.exports = Class( 'Client' ) program.on( 'error', function( e ) { - _self._handleError( e ); + _self.handleError( e ); } ); // handle field updates @@ -1158,7 +858,7 @@ module.exports = Class( 'Client' ) } ) .on( 'error', function( e ) { - _self._handleError( e ); + _self.handleError( e ); } ); return program; @@ -1412,7 +1112,7 @@ module.exports = Class( 'Client' ) // handle context errors root_context.on( 'error', function( e ) { - client._handleError( e ); + client.handleError( e ); } ); // must init after the Ui obj is available @@ -1472,7 +1172,7 @@ module.exports = Class( 'Client' ) } ) .on( 'error', function( e ) { - client._handleError( e ); + client.handleError( e ); } ); return ui.saveStep( function( stepui ) @@ -1929,7 +1629,7 @@ module.exports = Class( 'Client' ) var client = this; // if the step contains invalid data, they must correct it - if ( !( stepui.isValid( this._cmatch ) ) ) + if ( !( stepui.isValid( this._cmatch.getMatches() ) ) ) { // well we didn't get very far callback( false ); @@ -1947,7 +1647,7 @@ module.exports = Class( 'Client' ) // we want to do this before save so that we don't re-mark the bucket as // dirty by populating it with uncommitted data - client._clearCmatchFields(); + client._cmatch.clearCmatchFields(); // give devs the option to disable client-side submit events so we can // test server-side functionality @@ -1959,7 +1659,7 @@ module.exports = Class( 'Client' ) // shouldn't occurr, we should still throw an exception if one is // triggered var failures = this.program.submit( - step_id, bucket, this._cmatch + step_id, bucket, this._cmatch.getMatches() ); if ( failures !== null ) @@ -2072,7 +1772,7 @@ module.exports = Class( 'Client' ) { event.abort(); - _self._handleError( Error( + _self.handleError( Error( 'Save timeout; please try again' ) ); }, 15000 ); @@ -2371,57 +2071,6 @@ module.exports = Class( 'Client' ) }, - 'private _clearCmatchFields': function() - { - var step = this.getUi().getCurrentStep(), - program = this.program; - - // don't bother if we're not yet on a step - if ( !step ) - { - return; - } - - var reset = {}; - for ( var name in step.getStep().getExclusiveFieldNames() ) - { - var data = this._cmatchHidden[ name ]; - - // if there is no data or we have been asked to retain this field's - // value, then do not clear - if ( !data || program.cretain[ name ] ) - { - continue; - } - - // what state is the current data in? - var cur = this.getQuote().getDataByName( name ); - - // we could have done Array.join(',').split(','), but we're trying - // to keep performance sane here - var indexes = []; - for ( var i in data ) - { - // we do *not* want to reset fields that have been removed - if ( cur[ i ] === undefined ) - { - break; - } - - indexes.push( i ); - } - - reset[ name ] = indexes; - } - - // batch reset (limit the number of times events are kicked off) - this._resetFields( reset ); - - // we've done our deed; reset it for the next time around - this._cmatchHidden = {}; - }, - - /** * Perform `forward' validations if needed * @@ -2449,7 +2098,7 @@ module.exports = Class( 'Client' ) var failures = this.program.forward( cur_step_id, bucket, - this._cmatch, + this._cmatch.getMatches(), function( trigger_event, question_id, value ) { client.handleEvent( trigger_event, { stepId: +value } ); @@ -2561,14 +2210,12 @@ module.exports = Class( 'Client' ) /** * Handles client-side events * - * @param String event_name name of the event - * @param Object data data to pass to event - * @param Function callback function to call when event is done (if - * not asynchronous, it'll be called immediately) + * @param {string} event_name name of the event + * @param {Object} data data to pass to event + * @param {function(Object)} callback function to call when event is done + * @param {function(Error)} error_callback function to call if event fails * - * @param Function error_callback function to call if event fails - * - * @return Client self to allow for method chaining + * @return {Client} self to allow for method chaining */ handleEvent: function( event_name, data, callback, error_callback ) { @@ -2577,120 +2224,26 @@ module.exports = Class( 'Client' ) this.emit( this.__self.$('EVENT_TRIGGER'), event_name, data ); - try - { - this._eventHandler.handle( - event_name, function( err, data ) - { - if ( err ) - { - error_callback( err ); - return; - } - - // XXX: move me - if ( event_name === 'rate' ) - { - _self.emit( _self.__self.$('EVENT_POST_RATE'), data ); - } - - callback && callback( data ); - }, data - ); - - // we had no problem handling this event; no need to continue with - // the old event handling system - return; - } - catch ( e ) - { - // segue into the old event handling system - if ( !( Class.isA( UnknownEventError, e ) ) ) + this._eventHandler.handle( + event_name, function( err, data ) { - // ruh roh - this._handleError( e ); - return; - } - } - - // perform event (XXX: replace me; see above) - switch ( event_name ) - { - case 'reset': - var reset = {}; - reset[ data.elementName ] = data.indexes; - - this._resetFields( reset ); - break; - - default: - this._handleError( Error( - 'Unknown client-side event: ' + event_name - ) ); - } - - // call the callback, if one was provided - if ( callback instanceof Function ) - { - callback.call( this ); - } - - return this; - }, - - - /** - * Trigger DataApi event for field FIELD - * - * @param {string} field field name - * - * @return {undefined} - */ - 'private _dapiTrigger': function( field ) - { - var _self = this; - - this.getQuote().visitData( function( bucket ) - { - _self.program.dapi( - _self._currentStepId, - field, - bucket, - {}, - _self._cmatch, - null - ); - } ); - }, - - - 'private _resetFields': function( fields ) - { - var update = {}; - - for ( var field in fields ) - { - var cur = fields[ field ], - cdata = this._quote.getDataByName( field ), - val = this.elementStyler.getDefault( field ); - - var data = []; - for ( var i in cur ) - { - var index = cur[ i ]; - - if ( cdata[ index ] === val ) + if ( err ) { - continue; + error_callback && error_callback( err ); + return this; } - data[ index ] = val; - } + // XXX: move me + if ( event_name === 'rate' ) + { + _self.emit( _self.__self.$('EVENT_POST_RATE'), data ); + } - update[ field ] = data; - } + callback && callback( data ); + }, data + ); - this._quote.setData( update ); + return this; }, @@ -2926,7 +2479,7 @@ module.exports = Class( 'Client' ) // proxy errors this._fieldMonitor.on( 'error', function( e ) { - _self._handleError( e ); + _self.handleError( e ); } ); }, @@ -2960,7 +2513,7 @@ module.exports = Class( 'Client' ) * * @return {undefined} */ - 'private _handleError': function( e ) + 'public handleError': function( e ) { if ( !e ) { @@ -3024,10 +2577,4 @@ module.exports = Class( 'Client' ) { return this.program.id; }, - - - 'public getCmatchData': function() - { - return this._cmatch; - } } ); diff --git a/src/client/ClientDependencyFactory.js b/src/client/ClientDependencyFactory.js index 03b2682..2ca86eb 100644 --- a/src/client/ClientDependencyFactory.js +++ b/src/client/ClientDependencyFactory.js @@ -82,9 +82,8 @@ var Step = require( '../step/Step' ), NavStyler = require( '../ui/nav/NavStyler' ), Sidebar = require( '../ui/sidebar/Sidebar' ), - FieldClassMatcher = require( '../field/FieldClassMatcher' ), - DataApiFactory = require( '../dapi/DataApiFactory' ), - DataApiManager = require( '../dapi/DataApiManager' ), + DataApiFactory = require( '../dapi/DataApiFactory' ), + DataApiManager = require( '../dapi/DataApiManager' ), RootDomContext = require( '../ui/context/RootDomContext' ), DomFieldFactory = require( '../ui/field/DomFieldFactory' ), @@ -345,8 +344,6 @@ module.exports = Class( 'ClientDependencyFactory', createNotifyBar: UiNotifyBar, - createFieldClassMatcher: FieldClassMatcher, - createClientEventHandler: function( client, data_validator, styler, data_proxy, jquery @@ -373,5 +370,5 @@ module.exports = Class( 'ClientDependencyFactory', 'action$cvv2Dialog': requireh( 'Cvv2DialogEventHandler' )( jquery ) } ); - } -}); + }, +} ); diff --git a/src/client/Cmatch.js b/src/client/Cmatch.js new file mode 100644 index 0000000..4705e0c --- /dev/null +++ b/src/client/Cmatch.js @@ -0,0 +1,515 @@ +/** + * Liza classification match (cmatch) handling + * + * Copyright (C) 2018 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * TODO: This is code directly extracted from Client, modified to maintain + * references to necessary objects. It is coupled with far too many things, + * and the code is a mess. Getting this clean and well-tested is important, + * as this is one of the core systems and is both complicated and complex. + */ + +"use strict"; + +const { Class } = require( 'easejs' ); + + +module.exports = Class( 'Cmatch', +{ + /** + * Contains classification match data per field + * + * @type {Object} + */ + 'private _cmatch': {}, + + /** + * Fields that were hidden (including indexes) since the last cmatch + * clear + * + * @type {Object} + */ + 'private _cmatchHidden': {}, + + /** + * Performs classification matching on fields + * + * A field will have a positive match for a given index if all of its + * classes match + * + * @type {FieldClassMatcher} + */ + 'private _classMatcher': null, + + /** + * Program client + * @type {Client} + */ + 'private _client': null, + + + /** + * Initialize match handler + * + * This relies on too many objects; see header. + * + * @param {FieldClassMatcher} class_matcher class/field matcher + * @param {Program} program active program + * @param {Client} client active client + */ + constructor( class_matcher, program, client ) + { + this._classMatcher = class_matcher; + this._program = program; + this._client = client; + }, + + + 'private _cmatchVisFromUi': function( field, all ) + { + var step = this._client.getUi().getCurrentStep(); + + if ( !step ) + { + return []; + } + + var group = step.getElementGroup( field ); + if ( !group ) + { + return []; + } + + var i = group.getCurrentIndexCount(), + ret = []; + + while ( i-- ) + { + ret.push( all ); + } + + return ret; + }, + + + 'public hookClassifier': function( data_validator ) + { + var _self = this, + program = this._program; + + // clear/initialize cmatches + this._cmatch = {}; + + var cmatchprot = false; + + // set classifier + this._client.getQuote() + .setClassifier( program.getClassifierKnownFields(), function() + { + return program.classify.apply( program, arguments ); + } ) + .on( 'classify', function( classes ) + { + if ( cmatchprot === true ) + { + _self._client.handleError( Error( 'cmatch recursion' ) ); + } + + cmatchprot = true; + + // handle field fixes + data_validator.validate( undefined, classes ) + .catch( e => _self.client._handleError( e ) ); + + _self._classMatcher.match( classes, function( cmatch ) + { + // it's important that we do this here so that everything + // that uses the cmatch data will consistently benefit + _self._postProcessCmatch( cmatch ); + + // if we're not on a current step, defer + if ( !( _self._client.getUi().getCurrentStep() ) ) + { + _self._cmatch = cmatch; + cmatchprot = false; + return; + } + + _self._handleClassMatch( cmatch ); + cmatchprot = false; + } ); + } ); + }, + + + 'private _postProcessCmatch': function( cmatch ) + { + // for any matches that are scalars (they will have no indexes), loop + // through each field and set the index to the value of 'all' + for ( var field in cmatch ) + { + if ( field === '__classes' ) + { + continue; + } + + var cfield = cmatch[ field ]; + + if ( cfield.indexes.length === 0 ) + { + var data = this._client.getQuote().getDataByName( field ), + i = data.length; + + // this will do nothing if there is no data found + while ( i-- ) + { + cfield.indexes[ i ] = cfield.all; + } + } + } + + return cmatch; + }, + + + 'private _mergeCmatchHidden': function( name, indexes, hidden ) + { + if ( !( this._cmatchHidden[ name ] ) ) + { + this._cmatchHidden[ name ] = {}; + } + + var cindexes = this._cmatchHidden[ name ]; + + for ( i in indexes ) + { + if ( hidden ) + { + cindexes[ indexes[ i ] ] = i; + } + else + { + delete cindexes[ indexes[ i ] ]; + } + } + + var some = false; + for ( var i in cindexes ) + { + some = true; + break; + } + + if ( !some ) + { + // v8 devs do not recomment delete as it progressively slows down + // property access on the object + this._cmatchHidden[ name ] = undefined; + } + }, + + + 'private _handleClassMatch': function( cmatch, force ) + { + force = !!force; + + this._client.getUi().setCmatch( cmatch ); + + var _self = this, + quote = this._client.getQuote(), + + // oh dear god...(Demeter, specifically..) + fields = this._client.getUi().getCurrentStep().getStep() + .getExclusiveFieldNames(); + + + var visq = {}; + for ( var field in cmatch ) + { + // ignore fields that are not on the current step + if ( !( fields[ field ] ) ) + { + continue; + } + + // if the match is still false, then we can rest assured + // that nothing has changed (and skip the overhead) + if ( !force + && ( cmatch[ field ] === false ) + && ( _self._cmatch[ field ] === false ) + ) + { + continue; + } + + var show = [], + hide = [], + + cfield = cmatch[ field ], + + vis = cfield.indexes, + cur = ( + ( _self._cmatch[ field ] || {} ).indexes + || [] + ); + + // TODO: Figure out something better here. This is currently + // needed for hiding statics---they are registered as exclusive + // fields (`fields', above), but aren't actually fields (they're + // not in the bucket). But we must show/hide them appropriately. + if ( vis.length === 0 ) + { + vis = this._cmatchVisFromUi( field, cfield.all ); + } + + // consider the number of indexes in the bucket first; + // otherwise, we might try to operate on fields that don't + // exist (bucket/class indexes not the same). the check for + // undefined in the first index is a workaround for the explicit + // setting of the length property of the bucket value when + // indexes are removed + var curdata = quote.getDataByName( field ), + fieldn = ( curdata.length > 0 && ( curdata[ 0 ] !== undefined ) ) + ? curdata.length + : vis.length; + + for ( var i = 0; i < fieldn; i++ ) + { + // do not record unchanged indexes as changed + // (avoiding the event overhead) + if ( !force && ( vis[ i ] === cur[ i ] ) ) + { + continue; + } + + ( ( vis[ i ] ) ? show : hide ).push( i ); + } + + this.markShowHide( field, visq, show, hide ); + } + + // it's important to do this before showing/hiding fields, since + // those might trigger events that check the current cmatches + this._cmatch = cmatch; + + + // allow DOM operations to complete before we trigger + // manipulations on it (TODO: this is a workaround for group + // show/hide issues; we need a better solution to guarantee + // order + setTimeout( () => + { + Object.keys( visq ).forEach( field => + { + const field_vis = visq[ field ]; + + Object.keys( field_vis ).forEach( event_id => + { + const indexes = field_vis[ event_id ]; + + this._client.handleEvent( event_id, { + elementName: field, + indexes: indexes, + } ); + } ); + + this._dapiTrigger( name ); + } ); + }, 25 ); + }, + + + /** + * Mark fields to be shown/hidden + * + * This also updates the cached visibility of field FIELD. + * + * @param {string} field field name + * @param {Array} show indexes to show + * @param {Array} hide indexes to hide + * + * @return {undefined} + */ + 'virtual protected markShowHide'( field, visq, show, hide ) + { + if ( !( show.length || hide.length ) ) + { + return visq; + } + + const { [field]: result = {} } = visq; + + if ( show.length ) + { + this._mergeCmatchHidden( field, show, false ); + result.show = show; + } + + if ( hide.length ) + { + this._mergeCmatchHidden( field, hide, true ); + result.hide = hide; + } + + visq[ field ] = result; + + return visq; + }, + + + /** + * Trigger DataApi event for field FIELD + * + * @param {string} field field name + * + * @return {undefined} + */ + 'private _dapiTrigger': function( field ) + { + const current_step_id = this._client.nav.getCurrentStepId(); + + this._client.getQuote().visitData( bucket => + { + this._program.dapi( + current_step_id, + field, + bucket, + {}, + this._cmatch, + null + ); + } ); + }, + + + 'public clearCmatchFields': function() + { + var step = this._client.getUi().getCurrentStep(), + program = this._program; + + // don't bother if we're not yet on a step + if ( !step ) + { + return; + } + + var reset = {}; + for ( var name in step.getStep().getExclusiveFieldNames() ) + { + var data = this._cmatchHidden[ name ]; + + // if there is no data or we have been asked to retain this field's + // value, then do not clear + if ( !data || program.cretain[ name ] ) + { + continue; + } + + // what state is the current data in? + var cur = this._client.getQuote().getDataByName( name ); + + // we could have done Array.join(',').split(','), but we're trying + // to keep performance sane here + var indexes = []; + for ( var i in data ) + { + // we do *not* want to reset fields that have been removed + if ( cur[ i ] === undefined ) + { + break; + } + + indexes.push( i ); + } + + reset[ name ] = indexes; + } + + // batch reset (limit the number of times events are kicked off) + this._resetFields( reset ); + + // we've done our deed; reset it for the next time around + this._cmatchHidden = {}; + }, + + + 'private _resetFields': function( fields ) + { + const quote = this._client.getQuote(); + const update = {}; + + for ( var field in fields ) + { + var cur = fields[ field ], + cdata = quote.getDataByName( field ), + val = this._client.elementStyler.getDefault( field ); + + var data = []; + for ( var i in cur ) + { + var index = cur[ i ]; + + if ( cdata[ index ] === val ) + { + continue; + } + + data[ index ] = val; + } + + update[ field ] = data; + } + + quote.setData( update ); + }, + + + /** + * Force handling of the most recent cmatch data + * + * This can be used to refresh the UI to ensure that it is consistent with + * the cmatch data. + * + * @return {Client} self + */ + 'public forceCmatchAction': function() + { + if ( !( this._cmatch ) ) + { + return this; + } + + this._handleClassMatch( this._cmatch, true ); + + return this; + }, + + + /** + * Get matches from last classifier application + * + * TODO: Remove me; breaks encapsulation. Intended for transition from + * mammoth Client. + * + * @return {Object} classification matches + */ + 'public getMatches'() + { + return this._cmatch; + }, +} ); diff --git a/src/system/client.js b/src/system/client.js index 18652b5..a1e109c 100644 --- a/src/system/client.js +++ b/src/system/client.js @@ -21,7 +21,9 @@ "use strict"; -const store = require( '../store' ); +const Cmatch = require( '../client/Cmatch' ); +const field = require( '../field' ); +const store = require( '../store' ); /** @@ -33,6 +35,12 @@ const store = require( '../store' ); * This is incomplete; it will be added to as code is ported to liza. */ module.exports = { + cmatch: ( program, client ) => Cmatch( + field.FieldClassMatcher( program.whens ), + program, + client + ), + data: { /** * Create a store suitable for comparing diffs diff --git a/test/client/CmatchTest.js b/test/client/CmatchTest.js new file mode 100644 index 0000000..cfbfb14 --- /dev/null +++ b/test/client/CmatchTest.js @@ -0,0 +1,87 @@ +/** + * Test case for Cmatch + * + * Copyright (C) 2018 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const { event } = require( '../../' ).client; +const { expect } = require( 'chai' ); + +const Sut = require( '../../src/client/Cmatch' ) + .extend( +{ + 'override constructor'( _, __, ___ ) {}, + + // make public + 'override public markShowHide'( field, visq, show, hide ) + { + return this.__super( field, visq, show, hide ); + } +} ); + + +// these tests aren't terribly effective right now +describe( "Cmatch", () => +{ + it( "marks hidden fields on class change to show", () => + { + expect( + Sut().markShowHide( 'foo', {}, [ 1, 2 ], [] ) + ).to.deep.equal( { foo: { show: [ 1, 2 ] } } ); + } ); + + + it( "marks shown fields on class change to hide", () => + { + expect( + Sut().markShowHide( 'foo', {}, [], [ 3, 4, 5 ] ) + ).to.deep.equal( { foo: { hide: [ 3, 4, 5 ] } } ); + } ); + + + it( "marks combination show/hide on class change", () => + { + expect( + Sut().markShowHide( 'foo', {}, [ 2, 3 ], [ 4, 5, 6 ] ) + ).to.deep.equal( { + foo: { + show: [ 2, 3 ], + hide: [ 4, 5, 6 ], + } + } ); + } ); + + + it( "marks no fields with no show or hide", () => + { + expect( + Sut().markShowHide( 'foo', {}, [], [] ) + ).to.deep.equal( {} ); + } ); + + + it( "does not affect marking of other fields", () => + { + const barval = {}; + const visq = { bar: barval }; + + Sut().markShowHide( 'foo', {}, [ 1 ], [ 0 ] ); + + expect( visq.bar ).to.equal( barval ); + } ); +} );