import Condition from './models/condition';
import ConditionsConfig from './services/conditions-config';
import BaseContext from './base-context';
import { TemplatesConditions, TemplatesConditionsConflicts } from '../data/commands';
export const Context = React.createContext();
export class ConditionsProvider extends BaseContext {
static propTypes = {
children: PropTypes.any.isRequired,
currentTemplate: PropTypes.object.isRequired,
onConditionsSaved: PropTypes.func.isRequired,
validateConflicts: PropTypes.bool,
};
static defaultProps = {
validateConflicts: true,
};
static actions = {
FETCH_CONFIG: 'fetch-config',
SAVE: 'save',
CHECK_CONFLICTS: 'check-conflicts',
};
/**
* Holds the conditions config object.
*
* @type {ConditionsConfig}
*/
conditionsConfig = null;
/**
* ConditionsProvider constructor.
*
* @param props
*/
constructor( props ) {
super( props );
this.state = {
...this.state,
conditions: {},
updateConditionItemState: this.updateConditionItemState.bind( this ),
removeConditionItemInState: this.removeConditionItemInState.bind( this ),
createConditionItemInState: this.createConditionItemInState.bind( this ),
findConditionItemInState: this.findConditionItemInState.bind( this ),
saveConditions: this.saveConditions.bind( this ),
};
}
/**
* Fetch the conditions config, then normalize the conditions and then setup titles for
* the subIds.
*/
componentDidMount() {
this.executeAction( ConditionsProvider.actions.FETCH_CONFIG, () => ConditionsConfig.create() )
.then( ( conditionsConfig ) => this.conditionsConfig = conditionsConfig )
.then( this.normalizeConditionsState.bind( this ) )
.then( this.setSubIdTitles.bind( this ) );
}
/**
* Execute a request to save the template conditions.
*
* @returns {*}
*/
saveConditions() {
const conditions = Object.values( this.state.conditions )
.map( ( condition ) => condition.forDb() );
return this.executeAction(
ConditionsProvider.actions.SAVE,
() => $e.data.update( TemplatesConditions.signature, { conditions }, { id: this.props.currentTemplate.id } )
).then( () => {
const contextConditions = Object.values( this.state.conditions )
.map( ( condition ) => condition.forContext() );
this.props.onConditionsSaved( this.props.currentTemplate.id, {
conditions: contextConditions,
instances: this.conditionsConfig.calculateInstances( Object.values( this.state.conditions ) ),
isActive: !! ( Object.keys( this.state.conditions ).length && 'publish' === this.props.currentTemplate.status ),
} );
} );
}
/**
* Check for conflicts in the server and mark the condition if there
* is a conflict.
*
* @param condition
*/
checkConflicts( condition ) {
return this.executeAction(
ConditionsProvider.actions.CHECK_CONFLICTS,
() => $e.data.get( TemplatesConditionsConflicts.signature, {
post_id: this.props.currentTemplate.id,
condition: condition.clone().toString(),
} )
).then( ( response ) =>
this.updateConditionItemState( condition.id, { conflictErrors: Object.values( response.data ) }, false )
);
}
/**
* Fetching subId titles.
*
* @param condition
* @returns {Promise<unknown>}
*/
fetchSubIdsTitles( condition ) {
return new Promise( ( resolve ) => {
return elementorCommon.ajax.loadObjects( {
action: 'query_control_value_titles',
ids: _.isArray( condition.subId ) ? condition.subId : [ condition.subId ],
data: {
get_titles: condition.subIdAutocomplete,
unique_id: elementorCommon.helpers.getUniqueId(),
},
success( response ) {
resolve( response );
},
} );
} );
}
/**
* Get the conditions from the template and normalize it to data structure
* that the components can work with.
*/
normalizeConditionsState() {
this.updateConditionsState( () => {
return this.props.currentTemplate.conditions.reduce( ( current, condition ) => {
const conditionObj = new Condition( {
...condition,
default: this.props.currentTemplate.defaultCondition,
options: this.conditionsConfig.getOptions(),
subOptions: this.conditionsConfig.getSubOptions( condition.name ),
subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( condition.sub ),
supIdOptions: condition.subId ? [ { value: condition.subId, label: condition.subId } ] : [],
} );
return {
...current,
[ conditionObj.id ]: conditionObj,
};
}, {} );
} ).then( () => {
Object.values( this.state.conditions ).forEach( ( condition ) => this.checkConflicts( condition ) );
} );
}
/**
* Set titles to the subIds,
* for the first render of the component.
*/
setSubIdTitles() {
return Object.values( this.state.conditions ).forEach( ( condition ) => {
if ( ! condition.subId ) {
return;
}
return this.fetchSubIdsTitles( condition )
.then( ( response ) =>
this.updateConditionItemState( condition.id, {
subIdOptions: [ {
label: Object.values( response )[ 0 ],
value: condition.subId,
} ],
}, false )
);
} );
}
/**
* Update state of specific condition item.
*
* @param id
* @param args
* @param shouldCheckConflicts
*/
updateConditionItemState( id, args, shouldCheckConflicts = true ) {
if ( args.name ) {
args.subOptions = this.conditionsConfig.getSubOptions( args.name );
}
if ( args.sub || args.name ) {
args.subIdAutocomplete = this.conditionsConfig.getSubIdAutocomplete( args.sub );
// In case that the condition has been changed, it will set the options of the subId
// to empty array to let select2 autocomplete handle the options.
args.subIdOptions = [];
}
this.updateConditionsState( ( prev ) => {
const condition = prev[ id ];
return {
...prev,
[ id ]: condition.clone().set( args ),
};
} ).then( () => {
if ( shouldCheckConflicts ) {
this.checkConflicts( this.findConditionItemInState( id ) );
}
} );
}
/**
* Remove a condition item from the state.
*
* @param id
*/
removeConditionItemInState( id ) {
this.updateConditionsState( ( prev ) => {
const newConditions = { ...prev };
delete newConditions[ id ];
return newConditions;
} );
}
/**
* Add a new condition item into the state.
*
* @param shouldCheckConflicts
*/
createConditionItemInState( shouldCheckConflicts = true ) {
const defaultCondition = this.props.currentTemplate.defaultCondition,
newCondition = new Condition( {
name: defaultCondition,
default: defaultCondition,
options: this.conditionsConfig.getOptions(),
subOptions: this.conditionsConfig.getSubOptions( defaultCondition ),
subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( '' ),
} );
this.updateConditionsState( ( prev ) => ( { ...prev, [ newCondition.id ]: newCondition } ) )
.then( () => {
if ( shouldCheckConflicts ) {
this.checkConflicts( newCondition );
}
} );
}
/**
* Find a condition item from the conditions state.
*
* @param id
* @returns {Condition|null}
*/
findConditionItemInState( id ) {
return Object.values( this.state.conditions ).find( ( c ) => c.id === id );
}
/**
* Update the whole conditions state.
*
* @param callback
* @returns {Promise<*>}
*/
updateConditionsState( callback ) {
return new Promise( ( resolve ) =>
this.setState( ( prev ) => ( { conditions: callback( prev.conditions ) } ), resolve )
);
}
/**
* Renders the provider.
*
* @returns {*}
*/
render() {
if ( this.state.action.current === ConditionsProvider.actions.FETCH_CONFIG ) {
if ( this.state.error ) {
return <h3>{ __( 'Error:', 'elementor-pro' ) } { this.state.error }</h3>;
}
if ( this.state.loading ) {
return <h3>{ __( 'Loading', 'elementor-pro' ) }...</h3>;
}
}
return (
<Context.Provider value={ this.state }>
{ this.props.children }
</Context.Provider>
);
}
}
export default ConditionsProvider;