<commit_before>/**
 * `modules/analytics` data store: properties.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { isValidAccountID, isValidPropertyID, parsePropertyID, isValidPropertySelection } from '../util';
import { STORE_NAME, PROPERTY_CREATE, PROFILE_CREATE } from './constants';
import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store';
const { createRegistrySelector, createRegistryControl } = Data;

const fetchGetPropertiesProfilesStore = createFetchStore( {
	baseName: 'getPropertiesProfiles',
	controlCallback: ( { accountID } ) => {
		return API.get( 'modules', 'analytics', 'properties-profiles', { accountID }, {
			useCache: false,
		} );
	},
	reducerCallback: ( state, response, { accountID } ) => {
		// Actual properties, profiles are set by resolver with custom logic,
		// hence here we just set a flag.
		return {
			...state,
			isAwaitingPropertiesProfilesCompletion: {
				...state.isAwaitingPropertiesProfilesCompletion,
				[ accountID ]: true,
			},
		};
	},
	argsToParams: ( accountID ) => {
		return { accountID };
	},
	validateParams: ( { accountID } = {} ) => {
		invariant( accountID, 'accountID is required.' );
	},
} );

const fetchCreatePropertyStore = createFetchStore( {
	baseName: 'createProperty',
	controlCallback: ( { accountID } ) => {
		return API.set( 'modules', 'analytics', 'create-property', { accountID } );
	},
	reducerCallback: ( state, property, { accountID } ) => {
		return {
			...state,
			properties: {
				...state.properties,
				[ accountID ]: [
					...( state.properties[ accountID ] || [] ),
					property,
				],
			},
		};
	},
	argsToParams: ( accountID ) => {
		return { accountID };
	},
	validateParams: ( { accountID } = {} ) => {
		invariant( accountID, 'accountID is required.' );
	},
} );

// Actions
const RECEIVE_MATCHED_PROPERTY = 'RECEIVE_MATCHED_PROPERTY';
const RECEIVE_GET_PROPERTIES = 'RECEIVE_GET_PROPERTIES';
const RECEIVE_PROPERTIES_PROFILES_COMPLETION = 'RECEIVE_PROPERTIES_PROFILES_COMPLETION';
const WAIT_FOR_PROPERTIES = 'WAIT_FOR_PROPERTIES';

const baseInitialState = {
	properties: {},
	isAwaitingPropertiesProfilesCompletion: {},
	matchedProperty: undefined,
};

const baseActions = {
	/**
	 * Creates a new Analytics property.
	 *
	 * Creates a new Analytics property for an existing Google Analytics account.
	 *
	 * @since 1.8.0
	 *
	 * @param {string} accountID Google Analytics account ID.
	 * @return {Object} Object with `response` and `error`.
	 */
	*createProperty( accountID ) {
		invariant( accountID, 'accountID is required.' );

		const { response, error } = yield fetchCreatePropertyStore.actions.fetchCreateProperty( accountID );
		return { response, error };
	},

	/**
	 * Adds a matchedProperty to the store.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} matchedProperty Property object.
	 * @return {Object} Redux-style action.
	 */
	receiveMatchedProperty( matchedProperty ) {
		invariant( matchedProperty, 'matchedProperty is required.' );

		return {
			payload: { matchedProperty },
			type: RECEIVE_MATCHED_PROPERTY,
		};
	},

	/**
	 * Sets the given property and related fields in the store.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {string} propertyID           Property ID to select.
	 * @param {string} [internalPropertyID] Internal property ID (if available).
	 * @return {Object} A Generator function.
	 */
	selectProperty( propertyID, internalPropertyID = '' ) {
		invariant( isValidPropertySelection( propertyID ), 'A valid propertyID selection is required.' );

		return ( function* () {
			const registry = yield Data.commonActions.getRegistry();

			const accountID = registry.select( STORE_NAME ).getAccountID();
			if ( ! isValidAccountID( accountID ) ) {
				return;
			}

			if ( PROPERTY_CREATE === propertyID ) {
				registry.dispatch( STORE_NAME ).setSettings( {
					propertyID,
					internalWebPropertyID: '',
					profileID: PROFILE_CREATE,
				} );

				return;
			}

			yield baseActions.waitForProperties( accountID );
			const property = registry.select( STORE_NAME ).getPropertyByID( propertyID ) || {};

			if ( ! internalPropertyID ) {
				internalPropertyID = property.internalWebPropertyId; // eslint-disable-line sitekit/camelcase-acronyms
			}

			registry.dispatch( STORE_NAME ).setSettings( {
				propertyID,
				internalWebPropertyID: internalPropertyID || '',
				profileID: '',
			} );

			const profiles = registry.select( STORE_NAME ).getProfiles( accountID, propertyID );
			if ( property.defaultProfileId && profiles?.some( ( profile ) => profile.id === property.defaultProfileId ) ) { // eslint-disable-line sitekit/camelcase-acronyms
				registry.dispatch( STORE_NAME ).setProfileID( property.defaultProfileId ); // eslint-disable-line sitekit/camelcase-acronyms
				return;
			}

			if ( profiles === undefined ) {
				return; // Selection will happen in in getProfiles resolver.
			}

			const matchedProfile = profiles.find( ( { webPropertyId } ) => webPropertyId === propertyID ) || { id: PROFILE_CREATE }; // eslint-disable-line sitekit/camelcase-acronyms
			registry.dispatch( STORE_NAME ).setProfileID( matchedProfile.id );
		}() );
	},

	receiveGetProperties( properties, { accountID } ) {
		invariant( Array.isArray( properties ), 'properties must be an array.' );
		invariant( accountID, 'accountID is required.' );

		return {
			payload: { properties, accountID },
			type: RECEIVE_GET_PROPERTIES,
		};
	},

	receivePropertiesProfilesCompletion( accountID ) {
		invariant( accountID, 'accountID is required.' );

		return {
			payload: { accountID },
			type: RECEIVE_PROPERTIES_PROFILES_COMPLETION,
		};
	},

	waitForProperties( accountID ) {
		return {
			payload: { accountID },
			type: WAIT_FOR_PROPERTIES,
		};
	},
};

const baseControls = {
	[ WAIT_FOR_PROPERTIES ]: createRegistryControl( ( registry ) => ( { payload: { accountID } } ) => {
		const arePropertiesLoaded = () => registry.select( STORE_NAME ).getProperties( accountID ) !== undefined;

		if ( arePropertiesLoaded() ) {
			return true;
		}

		return new Promise( ( resolve ) => {
			const unsubscribe = registry.subscribe( () => {
				if ( arePropertiesLoaded() ) {
					unsubscribe();
					resolve();
				}
			} );
		} );
	} ),
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case RECEIVE_MATCHED_PROPERTY: {
			const { matchedProperty } = payload;

			return {
				...state,
				matchedProperty,
			};
		}

		case RECEIVE_GET_PROPERTIES: {
			const { properties, accountID } = payload;

			return {
				...state,
				properties: {
					...state.properties,
					[ accountID ]: [ ...properties ],
				},
			};
		}

		case RECEIVE_PROPERTIES_PROFILES_COMPLETION: {
			const { accountID } = payload;

			return {
				...state,
				isAwaitingPropertiesProfilesCompletion: {
					...state.isAwaitingPropertiesProfilesCompletion,
					[ accountID ]: false,
				},
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getProperties( accountID ) {
		if ( ! isValidAccountID( accountID ) ) {
			return;
		}

		const registry = yield Data.commonActions.getRegistry();
		let properties = registry.select( STORE_NAME ).getProperties( accountID );

		// Only fetch properties if there are none in the store for the given account.
		if ( properties === undefined ) {
			const { response, error } = yield fetchGetPropertiesProfilesStore.actions.fetchGetPropertiesProfiles( accountID );
			const { dispatch } = registry;
			if ( response ) {
				dispatch( STORE_NAME ).receiveGetProperties( response.properties, { accountID } );

				// eslint-disable-next-line sitekit/camelcase-acronyms
				if ( response.profiles?.[ 0 ]?.webPropertyId ) {
					// eslint-disable-next-line sitekit/camelcase-acronyms
					const propertyID = response.profiles[ 0 ].webPropertyId;
					dispatch( STORE_NAME ).receiveGetProfiles( response.profiles, { accountID, propertyID } );
				}

				if ( response.matchedProperty ) {
					dispatch( STORE_NAME ).receiveMatchedProperty( response.matchedProperty );
				}

				( { properties } = response );
			}

			dispatch( STORE_NAME ).receivePropertiesProfilesCompletion( accountID );
			if ( error ) {
				// Store error manually since getProperties signature differs from fetchGetPropertiesProfiles.
				yield dispatch( STORE_NAME ).receiveError( error, 'getProperties', [ accountID ] );
				return;
			}
		}

		const propertyID = registry.select( STORE_NAME ).getPropertyID();
		if ( ! propertyID ) {
			const property = properties[ 0 ] || { id: PROPERTY_CREATE };
			yield baseActions.selectProperty( property.id, property.internalWebPropertyId ); // eslint-disable-line sitekit/camelcase-acronyms
		}
	},
};

const baseSelectors = {
	/**
	 * Gets the property object by the property ID.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} propertyID Property ID.
	 * @return {(Object|undefined)} Property object, or undefined if not present in store.
	 */
	getPropertyByID( state, propertyID ) {
		if ( ! isValidPropertyID( propertyID ) ) {
			return undefined;
		}
		const { accountID } = parsePropertyID( propertyID );

		return ( state.properties[ accountID ] || [] ).find( ( { id } ) => id === propertyID );
	},

	/**
	 * Gets the matched property, if any.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Matched property if set, otherwise `undefined`.
	 */
	getMatchedProperty( state ) {
		return state.matchedProperty;
	},

	/**
	 * Gets all Google Analytics properties this account can access.
	 *
	 * Returns an array of all analytics properties.
	 *
	 * Returns `undefined` if accounts have not yet loaded.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to fetch properties for.
	 * @return {(Array.<Object>|undefined)} An array of Analytics properties; `undefined` if not loaded.
	 */
	getProperties( state, accountID ) {
		const { properties } = state;

		return properties[ accountID ];
	},

	/**
	 * Checks if a property is being created for an account.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if creating a property, `false` if not.
	 */
	isDoingCreateProperty: createRegistrySelector( ( select ) => ( state, accountID ) => {
		return select( STORE_NAME ).isFetchingCreateProperty( accountID );
	} ),

	/**
	 * Checks if properties are being fetched for the given account.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if fetching a properties, `false` if not.
	 */
	isDoingGetProperties: createRegistrySelector( ( select ) => ( state, accountID ) => {
		// Check if dispatch calls right after fetching are still awaiting.
		if ( accountID && state.isAwaitingPropertiesProfilesCompletion[ accountID ] ) {
			return true;
		}

		return select( STORE_NAME ).isFetchingGetPropertiesProfiles( accountID );
	} ),
};

const store = Data.combineStores(
	fetchGetPropertiesProfilesStore,
	fetchCreatePropertyStore,
	{
		initialState: baseInitialState,
		actions: baseActions,
		controls: baseControls,
		reducer: baseReducer,
		resolvers: baseResolvers,
		selectors: baseSelectors,
	}
);

export const initialState = store.initialState;
export const actions = store.actions;
export const controls = store.controls;
export const reducer = store.reducer;
export const resolvers = store.resolvers;
export const selectors = store.selectors;

export default store;
<commit_msg>Revert changes in properties store.<commit_after>/**
 * `modules/analytics` data store: properties.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { isValidAccountID, isValidPropertyID, parsePropertyID, isValidPropertySelection } from '../util';
import { STORE_NAME, PROPERTY_CREATE, PROFILE_CREATE } from './constants';
import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store';
const { createRegistrySelector, createRegistryControl } = Data;

const fetchGetPropertiesProfilesStore = createFetchStore( {
	baseName: 'getPropertiesProfiles',
	controlCallback: ( { accountID } ) => {
		return API.get( 'modules', 'analytics', 'properties-profiles', { accountID }, {
			useCache: false,
		} );
	},
	reducerCallback: ( state, response, { accountID } ) => {
		// Actual properties, profiles are set by resolver with custom logic,
		// hence here we just set a flag.
		return {
			...state,
			isAwaitingPropertiesProfilesCompletion: {
				...state.isAwaitingPropertiesProfilesCompletion,
				[ accountID ]: true,
			},
		};
	},
	argsToParams: ( accountID ) => {
		return { accountID };
	},
	validateParams: ( { accountID } = {} ) => {
		invariant( accountID, 'accountID is required.' );
	},
} );

const fetchCreatePropertyStore = createFetchStore( {
	baseName: 'createProperty',
	controlCallback: ( { accountID } ) => {
		return API.set( 'modules', 'analytics', 'create-property', { accountID } );
	},
	reducerCallback: ( state, property, { accountID } ) => {
		return {
			...state,
			properties: {
				...state.properties,
				[ accountID ]: [
					...( state.properties[ accountID ] || [] ),
					property,
				],
			},
		};
	},
	argsToParams: ( accountID ) => {
		return { accountID };
	},
	validateParams: ( { accountID } = {} ) => {
		invariant( accountID, 'accountID is required.' );
	},
} );

// Actions
const RECEIVE_MATCHED_PROPERTY = 'RECEIVE_MATCHED_PROPERTY';
const RECEIVE_GET_PROPERTIES = 'RECEIVE_GET_PROPERTIES';
const RECEIVE_PROPERTIES_PROFILES_COMPLETION = 'RECEIVE_PROPERTIES_PROFILES_COMPLETION';
const WAIT_FOR_PROPERTIES = 'WAIT_FOR_PROPERTIES';

const baseInitialState = {
	properties: {},
	isAwaitingPropertiesProfilesCompletion: {},
	matchedProperty: undefined,
};

const baseActions = {
	/**
	 * Creates a new Analytics property.
	 *
	 * Creates a new Analytics property for an existing Google Analytics account.
	 *
	 * @since 1.8.0
	 *
	 * @param {string} accountID Google Analytics account ID.
	 * @return {Object} Object with `response` and `error`.
	 */
	*createProperty( accountID ) {
		invariant( accountID, 'accountID is required.' );

		const { response, error } = yield fetchCreatePropertyStore.actions.fetchCreateProperty( accountID );
		return { response, error };
	},

	/**
	 * Adds a matchedProperty to the store.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} matchedProperty Property object.
	 * @return {Object} Redux-style action.
	 */
	receiveMatchedProperty( matchedProperty ) {
		invariant( matchedProperty, 'matchedProperty is required.' );

		return {
			payload: { matchedProperty },
			type: RECEIVE_MATCHED_PROPERTY,
		};
	},

	/**
	 * Sets the given property and related fields in the store.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {string} propertyID           Property ID to select.
	 * @param {string} [internalPropertyID] Internal property ID (if available).
	 * @return {Object} A Generator function.
	 */
	selectProperty( propertyID, internalPropertyID = '' ) {
		invariant( isValidPropertySelection( propertyID ), 'A valid propertyID selection is required.' );

		return ( function* () {
			const registry = yield Data.commonActions.getRegistry();

			const accountID = registry.select( STORE_NAME ).getAccountID();
			if ( ! isValidAccountID( accountID ) ) {
				return;
			}

			registry.dispatch( STORE_NAME ).setPropertyID( propertyID );

			if ( PROPERTY_CREATE === propertyID ) {
				registry.dispatch( STORE_NAME ).setProfileID( PROFILE_CREATE );
				return;
			}

			yield baseActions.waitForProperties( accountID );
			const property = registry.select( STORE_NAME ).getPropertyByID( propertyID ) || {};

			if ( ! internalPropertyID ) {
				internalPropertyID = property.internalWebPropertyId; // eslint-disable-line sitekit/camelcase-acronyms
			}

			registry.dispatch( STORE_NAME ).setInternalWebPropertyID( internalPropertyID || '' );

			// Clear any profile ID selection in the case that selection falls to the getProfiles resolver.
			registry.dispatch( STORE_NAME ).setProfileID( '' );

			const profiles = registry.select( STORE_NAME ).getProfiles( accountID, propertyID );
			if ( property.defaultProfileId && profiles?.some( ( profile ) => profile.id === property.defaultProfileId ) ) { // eslint-disable-line sitekit/camelcase-acronyms
				registry.dispatch( STORE_NAME ).setProfileID( property.defaultProfileId ); // eslint-disable-line sitekit/camelcase-acronyms
				return;
			}

			if ( profiles === undefined ) {
				return; // Selection will happen in in getProfiles resolver.
			}

			const matchedProfile = profiles.find( ( { webPropertyId } ) => webPropertyId === propertyID ) || { id: PROFILE_CREATE }; // eslint-disable-line sitekit/camelcase-acronyms
			registry.dispatch( STORE_NAME ).setProfileID( matchedProfile.id );
		}() );
	},

	receiveGetProperties( properties, { accountID } ) {
		invariant( Array.isArray( properties ), 'properties must be an array.' );
		invariant( accountID, 'accountID is required.' );

		return {
			payload: { properties, accountID },
			type: RECEIVE_GET_PROPERTIES,
		};
	},

	receivePropertiesProfilesCompletion( accountID ) {
		invariant( accountID, 'accountID is required.' );

		return {
			payload: { accountID },
			type: RECEIVE_PROPERTIES_PROFILES_COMPLETION,
		};
	},

	waitForProperties( accountID ) {
		return {
			payload: { accountID },
			type: WAIT_FOR_PROPERTIES,
		};
	},
};

const baseControls = {
	[ WAIT_FOR_PROPERTIES ]: createRegistryControl( ( registry ) => ( { payload: { accountID } } ) => {
		const arePropertiesLoaded = () => registry.select( STORE_NAME ).getProperties( accountID ) !== undefined;

		if ( arePropertiesLoaded() ) {
			return true;
		}

		return new Promise( ( resolve ) => {
			const unsubscribe = registry.subscribe( () => {
				if ( arePropertiesLoaded() ) {
					unsubscribe();
					resolve();
				}
			} );
		} );
	} ),
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case RECEIVE_MATCHED_PROPERTY: {
			const { matchedProperty } = payload;

			return {
				...state,
				matchedProperty,
			};
		}

		case RECEIVE_GET_PROPERTIES: {
			const { properties, accountID } = payload;

			return {
				...state,
				properties: {
					...state.properties,
					[ accountID ]: [ ...properties ],
				},
			};
		}

		case RECEIVE_PROPERTIES_PROFILES_COMPLETION: {
			const { accountID } = payload;

			return {
				...state,
				isAwaitingPropertiesProfilesCompletion: {
					...state.isAwaitingPropertiesProfilesCompletion,
					[ accountID ]: false,
				},
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getProperties( accountID ) {
		if ( ! isValidAccountID( accountID ) ) {
			return;
		}

		const registry = yield Data.commonActions.getRegistry();
		let properties = registry.select( STORE_NAME ).getProperties( accountID );

		// Only fetch properties if there are none in the store for the given account.
		if ( properties === undefined ) {
			const { response, error } = yield fetchGetPropertiesProfilesStore.actions.fetchGetPropertiesProfiles( accountID );
			const { dispatch } = registry;
			if ( response ) {
				dispatch( STORE_NAME ).receiveGetProperties( response.properties, { accountID } );

				// eslint-disable-next-line sitekit/camelcase-acronyms
				if ( response.profiles?.[ 0 ]?.webPropertyId ) {
					// eslint-disable-next-line sitekit/camelcase-acronyms
					const propertyID = response.profiles[ 0 ].webPropertyId;
					dispatch( STORE_NAME ).receiveGetProfiles( response.profiles, { accountID, propertyID } );
				}

				if ( response.matchedProperty ) {
					dispatch( STORE_NAME ).receiveMatchedProperty( response.matchedProperty );
				}

				( { properties } = response );
			}

			dispatch( STORE_NAME ).receivePropertiesProfilesCompletion( accountID );
			if ( error ) {
				// Store error manually since getProperties signature differs from fetchGetPropertiesProfiles.
				yield dispatch( STORE_NAME ).receiveError( error, 'getProperties', [ accountID ] );
				return;
			}
		}

		const propertyID = registry.select( STORE_NAME ).getPropertyID();
		if ( ! propertyID ) {
			const property = properties[ 0 ] || { id: PROPERTY_CREATE };
			yield baseActions.selectProperty( property.id, property.internalWebPropertyId ); // eslint-disable-line sitekit/camelcase-acronyms
		}
	},
};

const baseSelectors = {
	/**
	 * Gets the property object by the property ID.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} propertyID Property ID.
	 * @return {(Object|undefined)} Property object, or undefined if not present in store.
	 */
	getPropertyByID( state, propertyID ) {
		if ( ! isValidPropertyID( propertyID ) ) {
			return undefined;
		}
		const { accountID } = parsePropertyID( propertyID );

		return ( state.properties[ accountID ] || [] ).find( ( { id } ) => id === propertyID );
	},

	/**
	 * Gets the matched property, if any.
	 *
	 * @since 1.8.0
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Matched property if set, otherwise `undefined`.
	 */
	getMatchedProperty( state ) {
		return state.matchedProperty;
	},

	/**
	 * Gets all Google Analytics properties this account can access.
	 *
	 * Returns an array of all analytics properties.
	 *
	 * Returns `undefined` if accounts have not yet loaded.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to fetch properties for.
	 * @return {(Array.<Object>|undefined)} An array of Analytics properties; `undefined` if not loaded.
	 */
	getProperties( state, accountID ) {
		const { properties } = state;

		return properties[ accountID ];
	},

	/**
	 * Checks if a property is being created for an account.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if creating a property, `false` if not.
	 */
	isDoingCreateProperty: createRegistrySelector( ( select ) => ( state, accountID ) => {
		return select( STORE_NAME ).isFetchingCreateProperty( accountID );
	} ),

	/**
	 * Checks if properties are being fetched for the given account.
	 *
	 * @since 1.8.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if fetching a properties, `false` if not.
	 */
	isDoingGetProperties: createRegistrySelector( ( select ) => ( state, accountID ) => {
		// Check if dispatch calls right after fetching are still awaiting.
		if ( accountID && state.isAwaitingPropertiesProfilesCompletion[ accountID ] ) {
			return true;
		}

		return select( STORE_NAME ).isFetchingGetPropertiesProfiles( accountID );
	} ),
};

const store = Data.combineStores(
	fetchGetPropertiesProfilesStore,
	fetchCreatePropertyStore,
	{
		initialState: baseInitialState,
		actions: baseActions,
		controls: baseControls,
		reducer: baseReducer,
		resolvers: baseResolvers,
		selectors: baseSelectors,
	}
);

export const initialState = store.initialState;
export const actions = store.actions;
export const controls = store.controls;
export const reducer = store.reducer;
export const resolvers = store.resolvers;
export const selectors = store.selectors;

export default store;
<|endoftext|>


<commit_before>angular.module('zoomableApp').controller('statisticController', function($scope, $timeout, $filter, moment, servicesAPI, $q){
  // VARIABLES
  $scope.location = location.pathname.split('/');           // location array contains path name in array[1]
  $scope.criteria = 'DAY';                                  // default set to day for date criteria
  $scope.endDate = new Date();                              // default end date is today's date
  $scope.maxDate = new Date();                              // default end date is today's date
  $scope.noStatisticsYet = false;                           // scope to track if statistics should be shown
  $scope.viewSessions = [];                                 // scope to store viewSessions to empty array
  $scope.viewsCount = [];                                   // scope to store processed viewSessions into date and count
  $scope.userVideoLength = 0;                               // scope to store user uploaded video length

  // CHART VARIABLES
  $scope.series = ['Views'];                                // default series to show for graph

  // HEATMAP VARIABLES
  var sessions = {};
  var compiledSessions = {};
  var videoTotalTime = 0;
  var totalCanvas = [];
  var video = new Whammy.Video(1);

  servicesAPI.getHeatMapStats(152).success(function (data) {
    // mock view sessions data
    var mockData = [
      {coordinates: [0,0], videoTime: 0, videoTotalTime: 5},
      {coordinates: [100,100], videoTime: 0, videoTotalTime: 5},
      {coordinates: [200,200], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,10], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,50], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,80], videoTime: 1, videoTotalTime: 5},
      {coordinates: [250,100], videoTime: 2, videoTotalTime: 5},
      {coordinates: [430,270], videoTime: 5, videoTotalTime: 5}
    ];
    sessions = mockData;

    videoTotalTime = 8;

    // group all coordinates belonging to a second into key value array
    sessions.forEach(function(session){
      compiledSessions[session.videoTime] = compiledSessions[session.videoTime] || [];
      compiledSessions[session.videoTime].push({x: session.coordinates[0], y: session.coordinates[1], radius: 300});
    });

    generateHeatmapVideo();
  });

  var currentTime = 0
  function generateHeatmapVideo() {
    if (currentTime > videoTotalTime) {
      compileHeatmapVideo();
    } else {
      // generate heatmap for a second if session exists
      if (compiledSessions.hasOwnProperty(currentTime)){
        coordinatesPerSecond( compiledSessions[currentTime]).then(function(context){
          video.add(context);
          currentTime++;
          generateHeatmapVideo();
        });;
      }
      // generate empty canvas for seconds without sessions
      else {
        document.getElementsByClassName("heatmap-canvas")[0].setAttribute("id","heatmap-image");
        var context = document.getElementById("heatmap-image").getContext('2d');
        context.clearRect(0,0,1920,1080);
        context.fillStyle = "#FFFFFF";
        context.fillRect(0,0,1920,1080);
        video.add(context);
        currentTime++;
        generateHeatmapVideo();
      }
    }
  }

  var heatmapInstance = h337.create({
    container: document.getElementById('heatmap-canvas'),
    radius: 10,
    maxOpacity: .7,
    minOpacity: 0,
    blur: .8,
    backgroundColor: '#FFFFFF'
  });

  function coordinatesPerSecond(coordinates){
    var deferred = $q.defer();
    var aggregatedCoordinates = {
      max: coordinates.length,
      min: 0,
      data: coordinates
    };

    generateCanvas(aggregatedCoordinates).then(function() {
      document.getElementsByClassName("heatmap-canvas")[0].setAttribute("id","heatmap-image");
      var canvas = document.getElementById("heatmap-image");
      var context = document.getElementById("heatmap-image").getContext('2d');
      // change canvas transparent background to white
      var data = context.getImageData(0,0,1920,1080);
  		var compositeOperation = context.globalCompositeOperation;
  		context.globalCompositeOperation = "destination-over";
  		context.fillStyle = '#FFFFFF';
  		context.fillRect(0,0,1920,1080);
      deferred.resolve(context);
    });

    return deferred.promise;
  }

  function generateCanvas(aggregatedCoordinates) {
    var deferred = $q.defer();
    deferred.resolve(heatmapInstance.setData(aggregatedCoordinates));
    return deferred.promise;
  }

  function compileHeatmapVideo() {
  	video.compile(false, function(output){
  		var url = window.URL.createObjectURL(output);
  		document.getElementById('video').src = url;
  		document.getElementById('download').style.display = '';
  		document.getElementById('download').href = url;
  	});
  }

  /* Get all user view data required for stats */
  var init = function() {
    if ($scope.location[1] === 'statistics') {
      // get user account created date
      servicesAPI.getUserAccountDate().success(function (data) {
        $scope.accountCreatedDate = data.createdDate;

        // call stats api to get all of user's videos with stats
        servicesAPI.getVideoStats().success(function(data) {
          if (data.length === 0) {
            $scope.noStatisticsYet = true;
            $scope.userVideoLength = 0;
          }
          else {
            $scope.viewSessions = data.viewSessions;
            $scope.userVideoLength = data.videoLength;
            setStartAndMinDate($scope.accountCreatedDate);
            processViewSessions();
            updateLabelsAndData();
          }
        });
      });
    }
    else if ($scope.location[1] === 'edit') {
      // call stats api for selected video with id = $scope.location[2]
      var uid = $scope.location[2];
      servicesAPI.getVideoStat(uid).success(function(data) {
        if (data.length === 0 || data.viewSessions.length === 0) {
          $scope.noStatisticsYet = true;
        }
        else {
          $scope.viewSessions = data.viewSessions;
          $scope.videoCreatedDate = data.createdDate;
          setStartAndMinDate($scope.videoCreatedDate);
          processViewSessions();
          updateLabelsAndData();
        }
      });
    }
  };
  init();

  /* Function to set start and minimum date for statistics */
  function setStartAndMinDate(createdDate) {
    var oneMonthAgo = moment().subtract(1, 'months').toDate();

    // default start date is previous month if created date is more than a month ago
    if (moment(createdDate).isBefore(oneMonthAgo)) {
      $scope.startDate = moment(oneMonthAgo).toDate();
    }
    else {
      $scope.startDate = moment(createdDate).toDate();
    }
    // default min date is creation date
    $scope.minDate = moment(createdDate).toDate();
  };

  /* Function to order view sessions and count by date */
  function processViewSessions() {
    // order view sessions in ascending order
    var orderByDate = $filter('orderBy')($scope.viewSessions, 'createdAt', false);
    // track viewsCount array index
    var vc_index = 0;
    // convert time to local time zone
    var localTime  = moment.utc(orderByDate[0].createdAt).toDate();

    // create a session obj and push to viewsCount array
    var sessionObj = {
      date: moment(localTime).format('YYYY-MM-DD HH:mm:ss'),
      count: 1
    }
    $scope.viewsCount.push(sessionObj);

    for (var i=1; i<orderByDate.length; i++) {
      if (moment(orderByDate[i].createdAt).isSame(moment(orderByDate[i-1].createdAt), 'day')) {
        // if same date, add to count of the current viewsCount[vc_index]
        $scope.viewsCount[vc_index].count++;
      }
      else {
        localTime  = moment.utc(orderByDate[i].createdAt).toDate();
        // push new sessionObj into viewsCount array and increment vc_index
        sessionObj = {
          date: moment(localTime).format('YYYY-MM-DD HH:mm:ss'),
          count: 1
        }
        $scope.viewsCount.push(sessionObj);
        vc_index++;
      }
    }
  };

  /* Function to set date criteria */
  $scope.updateCriteria = function(event) {
    $scope.criteria = event.target.id.toUpperCase();
    updateLabelsAndData();
  };

  $scope.updateDate = function(start, end) {
    // pass start and end date again to make sure scope from view is updated correctly
    $scope.startDate = start;
    $scope.endDate = end;
    updateLabelsAndData();
  };

  /* Function to update chart data and labels */
  function updateLabelsAndData() {
    var prevDate = 0;

    // initialse scope label, data and other variables
    $scope.labels = [];
    $scope.data = [[]];
    $scope.totalViewsForPeriod = 0;
    var vc_index = 0;
    var count = 0;

    // convert Javascript Date object to Moment object
    var startMoment = moment($scope.startDate).startOf('day');
    var endMoment = moment($scope.endDate).add(1, 'days').startOf('day');  // add an additional day to include end date

    // update the labels and data to use for chart
    if ($scope.criteria === 'DAY') {
      // populate the labels
      for (var dd = startMoment; dd.isBefore(endMoment); dd.add(1, 'days')) {
        $scope.labels.push(dd.format('D/M'));
      }

      // update the data to show
      for (var idx = 0; idx < $scope.labels.length; idx++) {

        // add all view counts from the array
        if (vc_index < $scope.viewsCount.length) {
          // format view count date to same format as labels
          var formatedVCDate = moment($scope.viewsCount[vc_index].date).format('D/M');

          if ($scope.labels[idx] === formatedVCDate) {
            // assign count to data[0] scope if date is the same
            $scope.data[0][idx] = $scope.viewsCount[vc_index].count;
            // add to total view count for selected period
            $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + $scope.viewsCount[vc_index].count;
            // increase vc_index
            vc_index++;
          }
          else {
            // add count to be zero if date is not the same
            $scope.data[0][idx] = 0;
          }
        }
        else {
          // add count of zero for rest of the remaining days not including in viewsCount array
          $scope.data[0][idx] = 0;
        }
      }
    }
    else if ($scope.criteria === 'WEEK') {
      // create date clone   for manipulation (moments are mutable)
      var startMomentCopy = startMoment.clone();
      // set start moment copy to 1 week after start date
      startMomentCopy.subtract(1,'days').add(1, 'weeks');
      // set end moment to one week after end date to include extra days that don't add up to a week
      endMoment.add(1, 'weeks');
      // populate the labels
      for (var wk = startMomentCopy; wk.isBefore(endMoment); wk.add(1, 'weeks')) {
        $scope.labels.push(wk.format('D/M'));
        count++;
      }

      // update the data to show
      for (var idx = 0; idx < count; idx++) {
        // for each count add up the sum of the next 7 days for subsequent week
        var sum = 0;
        for (var day = 0; day < 7; day++) {
          // add all view counts from the array
          if (vc_index < $scope.viewsCount.length) {
            // format view count date to same format as labels
            var formatedVCDate = moment($scope.viewsCount[vc_index].date).format('D/M');

            if (startMoment.format('D/M') === formatedVCDate) {
              // add to sum if date is the same
              sum = sum + $scope.viewsCount[vc_index].count;
              // add to total view count for selected period
              $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + $scope.viewsCount[vc_index].count;
              // increase vc_index
              vc_index++;
            }
          }
          // increment start date by one for each day
          startMoment.add(1, 'days');
        }
        // update scope data for the week
        $scope.data[0][idx] = sum;
      }
    }
    else if ($scope.criteria === 'MONTH') {
      // populate the labels
      for (var mth = moment(startMoment).startOf('month'); mth.isBefore(endMoment); mth.add(1, 'months')) {
        $scope.labels.push(mth.format('MMM/YY'));
        count++;
      }

      // get month of startMoment
      var currentMonth = moment(startMoment).month();
      // create a copy of $scope.viewsCount
      var vcArr = $scope.viewsCount.slice(0);

      // update the data to show
      for (var idx = 0; idx < count; idx++) {
        var sum = 0;

        // add all view counts from the array
        for (var i = 0; i < vcArr.length; i++) {
          if (moment(vcArr[i].date).month() === currentMonth) {
            // add to sum if month is the same
            sum = sum + vcArr[i].count;
            // add to total view count for selected period
            $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + vcArr[i].count;
          }
          else {
            // splice added count from vcArray
            vcArr.splice(0, i);
            break;
          }
        }
        // update scope data for the month
        $scope.data[0][idx] = sum;
        // increment month
        currentMonth++;
      }
    }
  };

});
<commit_msg>Make video id a scope for functions to use<commit_after>angular.module('zoomableApp').controller('statisticController', function($scope, $timeout, $filter, moment, servicesAPI, $q){
  // VARIABLES
  $scope.location = location.pathname.split('/');           // location array contains path name in array[1]
  $scope.criteria = 'DAY';                                  // default set to day for date criteria
  $scope.endDate = new Date();                              // default end date is today's date
  $scope.maxDate = new Date();                              // default end date is today's date
  $scope.noStatisticsYet = false;                           // scope to track if statistics should be shown
  $scope.viewSessions = [];                                 // scope to store viewSessions to empty array
  $scope.viewsCount = [];                                   // scope to store processed viewSessions into date and count
  $scope.userVideoLength = 0;                               // scope to store user uploaded video length

  // Get video id if is on edit page
  if ($scope.location[1] === 'edit') {
    $scope.videoId = $scope.location[2];
  }

  // CHART VARIABLES
  $scope.series = ['Views'];                                // default series to show for graph

  // HEATMAP VARIABLES
  var sessions = {};
  var compiledSessions = {};
  var videoTotalTime = 0;
  var totalCanvas = [];
  var video = new Whammy.Video(1);

  servicesAPI.getHeatMapStats($scope.videoId).success(function (data) {
    // mock view sessions data
    var mockData = [
      {coordinates: [0,0], videoTime: 0, videoTotalTime: 5},
      {coordinates: [100,100], videoTime: 0, videoTotalTime: 5},
      {coordinates: [200,200], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,10], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,50], videoTime: 1, videoTotalTime: 5},
      {coordinates: [10,80], videoTime: 1, videoTotalTime: 5},
      {coordinates: [250,100], videoTime: 2, videoTotalTime: 5},
      {coordinates: [430,270], videoTime: 5, videoTotalTime: 5}
    ];
    sessions = mockData;

    videoTotalTime = 8;

    // group all coordinates belonging to a second into key value array
    sessions.forEach(function(session){
      compiledSessions[session.videoTime] = compiledSessions[session.videoTime] || [];
      compiledSessions[session.videoTime].push({x: session.coordinates[0], y: session.coordinates[1], radius: 300});
    });

    generateHeatmapVideo();
  });

  var currentTime = 0
  function generateHeatmapVideo() {
    if (currentTime > videoTotalTime) {
      compileHeatmapVideo();
    } else {
      // generate heatmap for a second if session exists
      if (compiledSessions.hasOwnProperty(currentTime)){
        coordinatesPerSecond( compiledSessions[currentTime]).then(function(context){
          video.add(context);
          currentTime++;
          generateHeatmapVideo();
        });;
      }
      // generate empty canvas for seconds without sessions
      else {
        document.getElementsByClassName("heatmap-canvas")[0].setAttribute("id","heatmap-image");
        var context = document.getElementById("heatmap-image").getContext('2d');
        context.clearRect(0,0,1920,1080);
        context.fillStyle = "#FFFFFF";
        context.fillRect(0,0,1920,1080);
        video.add(context);
        currentTime++;
        generateHeatmapVideo();
      }
    }
  }

  var heatmapInstance = h337.create({
    container: document.getElementById('heatmap-canvas'),
    radius: 10,
    maxOpacity: .7,
    minOpacity: 0,
    blur: .8,
    backgroundColor: '#FFFFFF'
  });

  function coordinatesPerSecond(coordinates){
    var deferred = $q.defer();
    var aggregatedCoordinates = {
      max: coordinates.length,
      min: 0,
      data: coordinates
    };

    generateCanvas(aggregatedCoordinates).then(function() {
      document.getElementsByClassName("heatmap-canvas")[0].setAttribute("id","heatmap-image");
      var canvas = document.getElementById("heatmap-image");
      var context = document.getElementById("heatmap-image").getContext('2d');
      // change canvas transparent background to white
      var data = context.getImageData(0,0,1920,1080);
  		var compositeOperation = context.globalCompositeOperation;
  		context.globalCompositeOperation = "destination-over";
  		context.fillStyle = '#FFFFFF';
  		context.fillRect(0,0,1920,1080);
      deferred.resolve(context);
    });

    return deferred.promise;
  }

  function generateCanvas(aggregatedCoordinates) {
    var deferred = $q.defer();
    deferred.resolve(heatmapInstance.setData(aggregatedCoordinates));
    return deferred.promise;
  }

  function compileHeatmapVideo() {
  	video.compile(false, function(output){
  		var url = window.URL.createObjectURL(output);
  		document.getElementById('video').src = url;
  		document.getElementById('download').style.display = '';
  		document.getElementById('download').href = url;
  	});
  }

  /* Get all user view data required for stats */
  var init = function() {
    if ($scope.location[1] === 'statistics') {
      // get user account created date
      servicesAPI.getUserAccountDate().success(function (data) {
        $scope.accountCreatedDate = data.createdDate;

        // call stats api to get all of user's videos with stats
        servicesAPI.getVideoStats().success(function(data) {
          if (data.length === 0) {
            $scope.noStatisticsYet = true;
            $scope.userVideoLength = 0;
          }
          else {
            $scope.viewSessions = data.viewSessions;
            $scope.userVideoLength = data.videoLength;
            setStartAndMinDate($scope.accountCreatedDate);
            processViewSessions();
            updateLabelsAndData();
          }
        });
      });
    }
    else if ($scope.location[1] === 'edit') {
      // call stats api for selected video
      servicesAPI.getVideoStat($scope.videoId).success(function(data) {
        if (data.length === 0 || data.viewSessions.length === 0) {
          $scope.noStatisticsYet = true;
        }
        else {
          $scope.viewSessions = data.viewSessions;
          $scope.videoCreatedDate = data.createdDate;
          setStartAndMinDate($scope.videoCreatedDate);
          processViewSessions();
          updateLabelsAndData();
        }
      });
    }
  };
  init();

  /* Function to set start and minimum date for statistics */
  function setStartAndMinDate(createdDate) {
    var oneMonthAgo = moment().subtract(1, 'months').toDate();

    // default start date is previous month if created date is more than a month ago
    if (moment(createdDate).isBefore(oneMonthAgo)) {
      $scope.startDate = moment(oneMonthAgo).toDate();
    }
    else {
      $scope.startDate = moment(createdDate).toDate();
    }
    // default min date is creation date
    $scope.minDate = moment(createdDate).toDate();
  };

  /* Function to order view sessions and count by date */
  function processViewSessions() {
    // order view sessions in ascending order
    var orderByDate = $filter('orderBy')($scope.viewSessions, 'createdAt', false);
    // track viewsCount array index
    var vc_index = 0;
    // convert time to local time zone
    var localTime  = moment.utc(orderByDate[0].createdAt).toDate();

    // create a session obj and push to viewsCount array
    var sessionObj = {
      date: moment(localTime).format('YYYY-MM-DD HH:mm:ss'),
      count: 1
    }
    $scope.viewsCount.push(sessionObj);

    for (var i=1; i<orderByDate.length; i++) {
      if (moment(orderByDate[i].createdAt).isSame(moment(orderByDate[i-1].createdAt), 'day')) {
        // if same date, add to count of the current viewsCount[vc_index]
        $scope.viewsCount[vc_index].count++;
      }
      else {
        localTime  = moment.utc(orderByDate[i].createdAt).toDate();
        // push new sessionObj into viewsCount array and increment vc_index
        sessionObj = {
          date: moment(localTime).format('YYYY-MM-DD HH:mm:ss'),
          count: 1
        }
        $scope.viewsCount.push(sessionObj);
        vc_index++;
      }
    }
  };

  /* Function to set date criteria */
  $scope.updateCriteria = function(event) {
    $scope.criteria = event.target.id.toUpperCase();
    updateLabelsAndData();
  };

  $scope.updateDate = function(start, end) {
    // pass start and end date again to make sure scope from view is updated correctly
    $scope.startDate = start;
    $scope.endDate = end;
    updateLabelsAndData();
  };

  /* Function to update chart data and labels */
  function updateLabelsAndData() {
    var prevDate = 0;

    // initialse scope label, data and other variables
    $scope.labels = [];
    $scope.data = [[]];
    $scope.totalViewsForPeriod = 0;
    var vc_index = 0;
    var count = 0;

    // convert Javascript Date object to Moment object
    var startMoment = moment($scope.startDate).startOf('day');
    var endMoment = moment($scope.endDate).add(1, 'days').startOf('day');  // add an additional day to include end date

    // update the labels and data to use for chart
    if ($scope.criteria === 'DAY') {
      // populate the labels
      for (var dd = startMoment; dd.isBefore(endMoment); dd.add(1, 'days')) {
        $scope.labels.push(dd.format('D/M'));
      }

      // update the data to show
      for (var idx = 0; idx < $scope.labels.length; idx++) {

        // add all view counts from the array
        if (vc_index < $scope.viewsCount.length) {
          // format view count date to same format as labels
          var formatedVCDate = moment($scope.viewsCount[vc_index].date).format('D/M');

          if ($scope.labels[idx] === formatedVCDate) {
            // assign count to data[0] scope if date is the same
            $scope.data[0][idx] = $scope.viewsCount[vc_index].count;
            // add to total view count for selected period
            $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + $scope.viewsCount[vc_index].count;
            // increase vc_index
            vc_index++;
          }
          else {
            // add count to be zero if date is not the same
            $scope.data[0][idx] = 0;
          }
        }
        else {
          // add count of zero for rest of the remaining days not including in viewsCount array
          $scope.data[0][idx] = 0;
        }
      }
    }
    else if ($scope.criteria === 'WEEK') {
      // create date clone   for manipulation (moments are mutable)
      var startMomentCopy = startMoment.clone();
      // set start moment copy to 1 week after start date
      startMomentCopy.subtract(1,'days').add(1, 'weeks');
      // set end moment to one week after end date to include extra days that don't add up to a week
      endMoment.add(1, 'weeks');
      // populate the labels
      for (var wk = startMomentCopy; wk.isBefore(endMoment); wk.add(1, 'weeks')) {
        $scope.labels.push(wk.format('D/M'));
        count++;
      }

      // update the data to show
      for (var idx = 0; idx < count; idx++) {
        // for each count add up the sum of the next 7 days for subsequent week
        var sum = 0;
        for (var day = 0; day < 7; day++) {
          // add all view counts from the array
          if (vc_index < $scope.viewsCount.length) {
            // format view count date to same format as labels
            var formatedVCDate = moment($scope.viewsCount[vc_index].date).format('D/M');

            if (startMoment.format('D/M') === formatedVCDate) {
              // add to sum if date is the same
              sum = sum + $scope.viewsCount[vc_index].count;
              // add to total view count for selected period
              $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + $scope.viewsCount[vc_index].count;
              // increase vc_index
              vc_index++;
            }
          }
          // increment start date by one for each day
          startMoment.add(1, 'days');
        }
        // update scope data for the week
        $scope.data[0][idx] = sum;
      }
    }
    else if ($scope.criteria === 'MONTH') {
      // populate the labels
      for (var mth = moment(startMoment).startOf('month'); mth.isBefore(endMoment); mth.add(1, 'months')) {
        $scope.labels.push(mth.format('MMM/YY'));
        count++;
      }

      // get month of startMoment
      var currentMonth = moment(startMoment).month();
      // create a copy of $scope.viewsCount
      var vcArr = $scope.viewsCount.slice(0);

      // update the data to show
      for (var idx = 0; idx < count; idx++) {
        var sum = 0;

        // add all view counts from the array
        for (var i = 0; i < vcArr.length; i++) {
          if (moment(vcArr[i].date).month() === currentMonth) {
            // add to sum if month is the same
            sum = sum + vcArr[i].count;
            // add to total view count for selected period
            $scope.totalViewsForPeriod = $scope.totalViewsForPeriod + vcArr[i].count;
          }
          else {
            // splice added count from vcArray
            vcArr.splice(0, i);
            break;
          }
        }
        // update scope data for the month
        $scope.data[0][idx] = sum;
        // increment month
        currentMonth++;
      }
    }
  };

});
<|endoftext|>


<commit_before>var Trinary = require('./example');

describe('Trinary', function () {

  it('1 is decimal 1', function() {
    expect(1).toEqual(new Trinary('1').toDecimal());
  });

  xit('2 is decimal 2', function() {
    expect(2).toEqual(new Trinary('2').toDecimal());
  });

  xit('10 is decimal 3', function() {
    expect(3).toEqual(new Trinary('10').toDecimal());
  });

  xit('11 is decimal 4', function() {
    expect(4).toEqual(new Trinary('11').toDecimal());
  });

  xit('100 is decimal 9', function() {
    expect(9).toEqual(new Trinary('100').toDecimal());
  });

  xit('112 is decimal 14', function() {
    expect(14).toEqual(new Trinary('112').toDecimal());
  });

  xit('222 is 26', function() {
    expect(26).toEqual(new Trinary('222').toDecimal());
  });

  xit('1122000120 is 32091', function() {
    expect(32091).toEqual(new Trinary('1122000120').toDecimal());
  });

  xit('invalid trinary is decimal 0', function() {
    expect(0).toEqual(new Trinary('carrot').toDecimal());
  });

});
<commit_msg>js/trinary rename exercise file<commit_after>var Trinary = require('./trinary');

describe('Trinary', function () {

  it('1 is decimal 1', function() {
    expect(1).toEqual(new Trinary('1').toDecimal());
  });

  xit('2 is decimal 2', function() {
    expect(2).toEqual(new Trinary('2').toDecimal());
  });

  xit('10 is decimal 3', function() {
    expect(3).toEqual(new Trinary('10').toDecimal());
  });

  xit('11 is decimal 4', function() {
    expect(4).toEqual(new Trinary('11').toDecimal());
  });

  xit('100 is decimal 9', function() {
    expect(9).toEqual(new Trinary('100').toDecimal());
  });

  xit('112 is decimal 14', function() {
    expect(14).toEqual(new Trinary('112').toDecimal());
  });

  xit('222 is 26', function() {
    expect(26).toEqual(new Trinary('222').toDecimal());
  });

  xit('1122000120 is 32091', function() {
    expect(32091).toEqual(new Trinary('1122000120').toDecimal());
  });

  xit('invalid trinary is decimal 0', function() {
    expect(0).toEqual(new Trinary('carrot').toDecimal());
  });

});
<|endoftext|>


<commit_before>$(document).on('page:load', init_default_data_block);
$(document).ready(init_default_data_block);

function init_default_data_block() {
    $('.default_data_block .title').click(function () {
        toggle_default_data_bloc(this, 400);
    });

    $('.default_data_block.default_visible').each(function() {
        toggle_default_data_bloc($(this).find('.title'), 0);
    });

    function toggle_default_data_bloc(element, duration){
        var block = $(element).parents('.show-block');
        if (block.attr("id") == "messages") {
          block.children(".last-message").toggle();
        }

        block.children(".body").slideToggle(duration);

        block.find(".carret-right").toggle();
        block.find(".carret-down").toggle();
    }
}
<commit_msg>Add new-action button to toggle default_block for messages<commit_after>$(document).on('page:load', init_default_data_block);
$(document).ready(init_default_data_block);

function init_default_data_block() {
    $('.default_data_block .title').click(function () {
        toggle_default_data_bloc(this, 400);
    });

    $('.new-action').click(function () {
        var messages_block = $(this).parents().closest(".default_data_block").find(".title")
        toggle_default_data_bloc(messages_block, 400);
    });

    $('.default_data_block.default_visible').each(function() {
        toggle_default_data_bloc($(this).find('.title'), 0);
    });

    function toggle_default_data_bloc(element, duration){
        var block = $(element).parents('.show-block');
        if (block.attr("id") == "messages") {
          block.children(".last-message").toggle();
        }

        block.children(".body").slideToggle(duration);

        block.find(".carret-right").toggle();
        block.find(".carret-down").toggle();
    }
}
<|endoftext|>


<commit_before>var $SP = $SP || {};

(function() {
  var idPattern         = /^#([\w\d\-_]+)/,
      idPatternGroup    = /^#\[([\w\d\s\-_,]+)\]/,
      classPattern      = /(\..+)/,
      classPatternGroup = /\[(.+)\]/,
      groupReplacePattern = /\[.+\]/;

  var LoadHook = function(selectors) {
    var selectorLength = selectors.length;

    for (var k = selectorLength - 1; k >= 0; k--) {
      var selector = selectors[k];

      var ids = [], classes = [], idMatch, classesMatch;

      // Collect the IDs
      if (idMatch = selector.match(idPattern)) {
        ids.push("#" + idMatch[1]);
      }
      else if (idMatch = selector.match(idPatternGroup)) {
        ids = _.map(idMatch[1].split(', '), function(cls) {
          return '#' + cls;
        });
      }

      // Collect the classes
      if (classesMatch = selector.match(classPattern)) {
        var groupedClasses = classesMatch[1].match(classPatternGroup);
        if (groupedClasses) {
          var original = classesMatch[1],
              entries = groupedClasses[1].split(', '),
              length = entries.length;

          for (var i = length - 1; i >= 0; i--) {
            classes.push(original.replace(groupReplacePattern, entries[i]));
          };
        }
        else {
          classes.push(classesMatch[1]);
        }
      }

      // Check to see if we have to concatenate the IDs and classes or just
      // process one of them.
      var hasIds = ids.length > 0,
          hasClasses = classes.length > 0;

      if (hasIds && hasClasses) {
        this.selectors = [];

        for (var i = ids.length - 1; i >= 0; i--) {
          for (var j = classes.length - 1; j >= 0; j--) {
            this.selectors.push(ids[i] + classes[j]);
          };
        };
      }
      else if (hasIds) {
        this.selectors.concat(ids);
      }
      else if (hasClasses) {
        this.selectors.concat(classes);
      }
    }
  };

  LoadHook.prototype = {
    fireOnMatch: function(body) {
      var length = this.selectors.length;
      for (var i = length - 1; i >= 0; i--) {
        if (body.is(this.selectors[i])) {
          this._fire();
          return;
        }
      };
    },

    _fire: function() {
      var arg;
      if (this.selector) arg = $(this.selector);

      if (this.obj) {
        this.instance = new this.obj(arg)
      }
      else if (this.run) {
        this.fn.apply(this.scope, [arg]);
      }
    },

    select: function(selector) {
      this.selector = selector;
      return this;
    },

    make: function(obj) {
      this.obj = obj;
      return this;
    },

    run: function(fn, scope) {
      this.fn = fn;
      this.scope = scope || this;
      return this;
    }
  };

  var hooks = []

  window.$SP.where = function() {
    var hook = new LoadHook(_.toArray(arguments));
    hooks.push(hook);
    return hook;
  };

  window.$SP.init = function() {
    var body = $(document.body);
    _.each(hooks, function(hook) { hook.fireOnMatch(body) });
  };
})();
<commit_msg>Fix loader to correctly add selectors that are just IDs or classes.<commit_after>var $SP = $SP || {};

(function() {
  var idPattern         = /^#([\w\d\-_]+)/,
      idPatternGroup    = /^#\[([\w\d\s\-_,]+)\]/,
      classPattern      = /(\..+)/,
      classPatternGroup = /\[(.+)\]/,
      groupReplacePattern = /\[.+\]/;

  var LoadHook = function(selectors) {
    var selectorLength = selectors.length;
    this.selectors = [];

    for (var k = selectorLength - 1; k >= 0; k--) {
      var selector = selectors[k];

      var ids = [], classes = [], idMatch, classesMatch;

      // Collect the IDs
      if (idMatch = selector.match(idPattern)) {
        ids.push("#" + idMatch[1]);
      }
      else if (idMatch = selector.match(idPatternGroup)) {
        ids = _.map(idMatch[1].split(', '), function(cls) {
          return '#' + cls;
        });
      }

      // Collect the classes
      if (classesMatch = selector.match(classPattern)) {
        var groupedClasses = classesMatch[1].match(classPatternGroup);
        if (groupedClasses) {
          var original = classesMatch[1],
              entries = groupedClasses[1].split(', '),
              length = entries.length;

          for (var i = length - 1; i >= 0; i--) {
            classes.push(original.replace(groupReplacePattern, entries[i]));
          };
        }
        else {
          classes.push(classesMatch[1]);
        }
      }

      // Check to see if we have to concatenate the IDs and classes or just
      // process one of them.
      var hasIds = ids.length > 0,
          hasClasses = classes.length > 0;

      if (hasIds && hasClasses) {
        for (var i = ids.length - 1; i >= 0; i--) {
          for (var j = classes.length - 1; j >= 0; j--) {
            this.selectors.push(ids[i] + classes[j]);
          };
        };
      }
      else if (hasIds) {
        this.selectors = this.selectors.concat(ids);
      }
      else if (hasClasses) {
        this.selectors = this.selectors.concat(classes);
      }
    }
  };

  LoadHook.prototype = {
    fireOnMatch: function(body) {
      var length = this.selectors.length;
      for (var i = length - 1; i >= 0; i--) {
        if (body.is(this.selectors[i])) {
          this._fire();
          return;
        }
      };
    },

    _fire: function() {
      var arg;
      if (this.selector) arg = $(this.selector);

      if (this.obj) {
        this.instance = new this.obj(arg)
      }
      else if (this.run) {
        this.fn.apply(this.scope, [arg]);
      }
    },

    select: function(selector) {
      this.selector = selector;
      return this;
    },

    make: function(obj) {
      this.obj = obj;
      return this;
    },

    run: function(fn, scope) {
      this.fn = fn;
      this.scope = scope || this;
      return this;
    }
  };

  var hooks = []

  window.$SP.where = function() {
    var hook = new LoadHook(_.toArray(arguments));
    hooks.push(hook);
    return hook;
  };

  window.$SP.init = function() {
    var body = $(document.body);
    _.each(hooks, function(hook) { hook.fireOnMatch(body) });
  };
})();
<|endoftext|>


<commit_before>$(function() {

  if ($('body').hasClass('homerooms') && $('body').hasClass('show')) {
    var Tablesort = window.Tablesort;
    var Cookies = window.Cookies;

    // track user for mixpanel
    var currentEducator = $('#current-educator-data').data().currentEducator;
    var homeroom = $('#homeroom-data').data().homeroom;
    var MixpanelUtils = window.shared.MixpanelUtils;
    MixpanelUtils.registerUser(currentEducator);
    MixpanelUtils.track('PAGE_VISIT', {
      page_key: 'ROSTER_PAGE',
      homeroom_id: homeroom.id,
      homeroom_slug: homeroom.slug,
      homeroom_grade: homeroom.grade
    });

    // Initialize table sort on roster table
    var table = document.getElementById('roster-table');
    new Tablesort(table, { descending: false });

    // Initialize table sort on roster table
    $('#homeroom-select').bind('change', function() {
      window.location.pathname = '/homerooms/' + $(this).val();
    });

    function updateColumns () {
      columns_selected_inputs = $("#column-listing").find("input:checked");
      columns_selected = $.map(columns_selected_inputs, function(c) {
        return c.name;
      });
      for (var column in roster_columns) {
        if (columns_selected.indexOf(column) === -1) {
          $('.' + column).hide();
        } else {
          $('.' + column).show();
        }
      }
    }
    updateColumns();

    function updateCookies () {
      Cookies.set("columns_selected", columns_selected);
    }

    // Show/hide column groups
    var roster_columns = {
      'name': 'Name',
      'risk': 'Risk',
      'program': 'Program',
      'sped': 'SPED & Disability',
      'language': 'Language',
      'free-reduced': 'Free/Reduced Lunch',
      'star_math': 'STAR Math',
      'star_reading': 'STAR Reading',
      'mcas_math': 'MCAS Math',
      'mcas_ela': 'MCAS ELA',
        // 'access': 'Access',
        // 'dibels': 'DIBELS',
        // 'attendance': 'Attendance',
        // 'discipline': 'Discipline',
    };

    var columns_selected = Cookies.getJSON("columns_selected");
    var columnTemplate = $("#column-template").remove();

    $.each(roster_columns, function(key, column){
      var newColumnTemplate = columnTemplate.clone(),
        isselected = columns_selected.indexOf(key) !== -1;
      newColumnTemplate
        .find("input")
          .attr("name", key)
          .attr("checked", isselected)
          .on("click", function(event) {
            event.stopPropagation();
            updateColumns();
          }).on("change", updateCookies)
        .end()
        .find("label")
          .text(column)
        .end()
        .appendTo("#column-listing");
    });

    // "Click off" for column select
    $("body").click(function() {
      $('#column-picker').hide();
    });

    $("#column-picker").click(function(event) {
      event.stopPropagation();
    });

    $('#column-picker-toggle').click(function(event) {
      event.stopPropagation();
      $('#column-picker').show();
    });

    // Turn table rows into links to student profiles
    $('tbody tr').click(function () {
      location.href = $(this).attr('href');
    });

    // Make Risk Level summary chart
    var chartData = $('#chart-data');

    // Replace blank cells with em dashes
    $('#roster-table tbody td').each(function() {
      var content = $.trim($(this).html());
      if (content.length === 0) {
        $(this).html('—');
      }
    });

  }

});
<commit_msg>Add missing variable delcaration<commit_after>$(function() {

  if ($('body').hasClass('homerooms') && $('body').hasClass('show')) {
    var Tablesort = window.Tablesort;
    var Cookies = window.Cookies;

    // track user for mixpanel
    var currentEducator = $('#current-educator-data').data().currentEducator;
    var homeroom = $('#homeroom-data').data().homeroom;
    var MixpanelUtils = window.shared.MixpanelUtils;
    MixpanelUtils.registerUser(currentEducator);
    MixpanelUtils.track('PAGE_VISIT', {
      page_key: 'ROSTER_PAGE',
      homeroom_id: homeroom.id,
      homeroom_slug: homeroom.slug,
      homeroom_grade: homeroom.grade
    });

    // Initialize table sort on roster table
    var table = document.getElementById('roster-table');
    new Tablesort(table, { descending: false });

    // Initialize table sort on roster table
    $('#homeroom-select').bind('change', function() {
      window.location.pathname = '/homerooms/' + $(this).val();
    });

    function updateColumns () {
      const columns_selected_inputs = $("#column-listing").find("input:checked");
      columns_selected = $.map(columns_selected_inputs, function(c) {
        return c.name;
      });
      for (var column in roster_columns) {
        if (columns_selected.indexOf(column) === -1) {
          $('.' + column).hide();
        } else {
          $('.' + column).show();
        }
      }
    }

    updateColumns();

    function updateCookies () {
      Cookies.set("columns_selected", columns_selected);
    }

    // Show/hide column groups
    var roster_columns = {
      'name': 'Name',
      'risk': 'Risk',
      'program': 'Program',
      'sped': 'SPED & Disability',
      'language': 'Language',
      'free-reduced': 'Free/Reduced Lunch',
      'star_math': 'STAR Math',
      'star_reading': 'STAR Reading',
      'mcas_math': 'MCAS Math',
      'mcas_ela': 'MCAS ELA',
        // 'access': 'Access',
        // 'dibels': 'DIBELS',
        // 'attendance': 'Attendance',
        // 'discipline': 'Discipline',
    };

    var columns_selected = Cookies.getJSON("columns_selected");
    var columnTemplate = $("#column-template").remove();

    $.each(roster_columns, function(key, column){
      var newColumnTemplate = columnTemplate.clone(),
        isselected = columns_selected.indexOf(key) !== -1;
      newColumnTemplate
        .find("input")
          .attr("name", key)
          .attr("checked", isselected)
          .on("click", function(event) {
            event.stopPropagation();
            updateColumns();
          }).on("change", updateCookies)
        .end()
        .find("label")
          .text(column)
        .end()
        .appendTo("#column-listing");
    });

    // "Click off" for column select
    $("body").click(function() {
      $('#column-picker').hide();
    });

    $("#column-picker").click(function(event) {
      event.stopPropagation();
    });

    $('#column-picker-toggle').click(function(event) {
      event.stopPropagation();
      $('#column-picker').show();
    });

    // Turn table rows into links to student profiles
    $('tbody tr').click(function () {
      location.href = $(this).attr('href');
    });

    // Make Risk Level summary chart
    var chartData = $('#chart-data');

    // Replace blank cells with em dashes
    $('#roster-table tbody td').each(function() {
      var content = $.trim($(this).html());
      if (content.length === 0) {
        $(this).html('—');
      }
    });

  }

});
<|endoftext|>


<commit_before>(function(Modules) {
  "use strict";

  Modules.TemplateFolderForm = function() {

    this.start = function(templateFolderForm) {
      this.$form = $(templateFolderForm);

      // remove the hidden unknown button - if you've got JS enabled then the action you want to do is implied by
      // which field is visible.
      this.$form.find('button[value=unknown]').remove();

      this.$stickyBottom = this.$form.find('#sticky_template_forms');

      this.$stickyBottom.append(this.nothingSelectedButtons);
      this.$stickyBottom.append(this.itemsSelectedButtons);

      // all the diff states that we want to show or hide
      this.states = [
        {key: 'nothing-selected-buttons', $el: this.$form.find('#nothing_selected'), cancellable: false},
        {key: 'items-selected-buttons', $el: this.$form.find('#items_selected'), cancellable: false},
        {key: 'move-to-existing-folder', $el: this.$form.find('#move_to_folder_radios'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_folder_radios legend', true)},
        {key: 'move-to-new-folder', $el: this.$form.find('#move_to_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_new_folder_name', false)},
        {key: 'add-new-folder', $el: this.$form.find('#add_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_folder_name', false)},
        {key: 'add-new-template', $el: this.$form.find('#add_new_template_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_template_form legend', true)}
      ];

      // cancel/clear buttons only relevant if JS enabled, so
      this.states.filter(state => state.cancellable).forEach((x) => this.addCancelButton(x));
      this.states.filter(state => state.key === 'items-selected-buttons').forEach(x => this.addClearButton(x));

      // activate stickiness of elements in each state
      this.activateStickyElements();

      // first off show the new template / new folder buttons
      this.currentState = this.$form.data('prev-state') || 'unknown';
      if (this.currentState === 'unknown') {
        this.selectActionButtons(false);
      } else {
        this.render();
      }

      this.$form.on('click', 'button.button-secondary', (event) => this.actionButtonClicked(event));
      this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged());
    };

    this.getFocusRoutine = function (selector, setTabindex) {
      return function () {
        let $el = $(selector);
        let removeTabindex = (e) => {
          $(e.target)
            .removeAttr('tabindex')
            .off('blur', removeTabindex);
        };

        if (setTabindex) {
          $el.attr('tabindex', '-1');
          $el.on('blur', removeTabindex);
        }

        $el.focus();
      };
    };

    this.activateStickyElements = function() {
      var oldClass = 'js-will-stick-at-bottom-when-scrolling';
      var newClass = 'js-stick-at-bottom-when-scrolling';

      this.states.forEach(state => {
        state.$el
          .find('.' + oldClass)
          .removeClass(oldClass)
          .addClass(newClass);
      });
    };

    this.addCancelButton = function(state) {
      let selector = `[value=${state.key}]`;
      let $cancel = this.makeButton('Cancel', () => {

        // clear existing data
        state.$el.find('input:radio').prop('checked', false);
        state.$el.find('input:text').val('');

        // go back to action buttons
        this.selectActionButtons(selector);
      }, selector);

      state.$el.find('[type=submit]').after($cancel);
    };

    this.addClearButton = function(state) {

      let $clear = this.makeButton('Clear', () => {
        // uncheck all templates and folders
        this.$form.find('input:checkbox').prop('checked', false);

        // go back to action buttons
        this.selectActionButtons();
      });

      state.$el.find('.template-list-selected-counter').append($clear);
    };

    this.makeButton = (text, fn, cancelSelector) => $('')
      .html(text)
      .addClass('js-cancel')
      .data('target', cancelSelector) // isn't set if cancelSelector is undefined
      .attr('tabindex', '0')
      .on('click keydown', event => {
        // space, enter or no keyCode (must be mouse input)
        if ([13, 32, undefined].indexOf(event.keyCode) > -1) {
          event.preventDefault();
          fn();
        }
      });

    this.selectActionButtons = function (targetSelector) {
      // If we want to show one of the grey choose actions state, we can pretend we're in the choose actions state,
      // and then pretend a checkbox was clicked to work out whether to show zero or non-zero options.
      // This calls a render at the end
      this.currentState = 'nothing-selected-buttons';
      this.templateFolderCheckboxChanged();
      if (targetSelector) {
        let setFocus = this.getFocusRoutine(targetSelector, false);
        setFocus();
      }
    };

    this.actionButtonClicked = function(event) {
      event.preventDefault();
      this.currentState = $(event.currentTarget).val();

      this.render();
    };

    this.templateFolderCheckboxChanged = function() {
      let numSelected = this.countSelectedCheckboxes();

      if (this.currentState === 'nothing-selected-buttons' && numSelected !== 0) {
        // user has just selected first item
        this.currentState = 'items-selected-buttons';
      } else if (this.currentState === 'items-selected-buttons' && numSelected === 0) {
        // user has just deselected last item
        this.currentState = 'nothing-selected-buttons';
      }

      this.render();

      $('.template-list-selected-counter-count').html(numSelected);

      $('.template-list-selected-counter').toggle(this.hasCheckboxes());

    };

    this.hasCheckboxes = function() {
      return !!this.$form.find('input:checkbox').length;
    };

    this.countSelectedCheckboxes = function() {
      return this.$form.find('input:checkbox:checked').length;
    };

    this.render = function() {
      let mode = 'default';
      let currentStateObj = this.states.filter(state => { return (state.key === this.currentState); })[0];

      // detach everything, unless they are the currentState
      this.states.forEach(
        state => (state.key === this.currentState ? this.$stickyBottom.append(state.$el) : state.$el.detach())
      );

      // use dialog mode for states which contain more than one form control
      if (['move-to-existing-folder', 'add-new-template'].indexOf(this.currentState) !== -1) {
        mode = 'dialog';
      }
      GOVUK.stickAtBottomWhenScrolling.setMode(mode);
      // make sticky JS recalculate its cache of the element's position
      GOVUK.stickAtBottomWhenScrolling.recalculate();

      if (currentStateObj && ('setFocus' in currentStateObj)) { currentStateObj.setFocus(); }
    };

    this.nothingSelectedButtons = $(`

          <button class="button-secondary" value="add-new-template">New template</button>
          <button class="button-secondary" value="add-new-folder">New folder</button>

            Nothing selected

    `).get(0);

    this.itemsSelectedButtons = $(`

          <button class="button-secondary" value="move-to-existing-folder">Move</button>
          <button class="button-secondary" value="move-to-new-folder">Add to a new folder</button>

            1 selected

    `).get(0);
  };

})(window.GOVUK.Modules);
<commit_msg>Corrected: Add to new folder button<commit_after>(function(Modules) {
  "use strict";

  Modules.TemplateFolderForm = function() {

    this.start = function(templateFolderForm) {
      this.$form = $(templateFolderForm);

      // remove the hidden unknown button - if you've got JS enabled then the action you want to do is implied by
      // which field is visible.
      this.$form.find('button[value=unknown]').remove();

      this.$stickyBottom = this.$form.find('#sticky_template_forms');

      this.$stickyBottom.append(this.nothingSelectedButtons);
      this.$stickyBottom.append(this.itemsSelectedButtons);

      // all the diff states that we want to show or hide
      this.states = [
        {key: 'nothing-selected-buttons', $el: this.$form.find('#nothing_selected'), cancellable: false},
        {key: 'items-selected-buttons', $el: this.$form.find('#items_selected'), cancellable: false},
        {key: 'move-to-existing-folder', $el: this.$form.find('#move_to_folder_radios'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_folder_radios legend', true)},
        {key: 'move-to-new-folder', $el: this.$form.find('#move_to_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_new_folder_name', false)},
        {key: 'add-new-folder', $el: this.$form.find('#add_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_folder_name', false)},
        {key: 'add-new-template', $el: this.$form.find('#add_new_template_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_template_form legend', true)}
      ];

      // cancel/clear buttons only relevant if JS enabled, so
      this.states.filter(state => state.cancellable).forEach((x) => this.addCancelButton(x));
      this.states.filter(state => state.key === 'items-selected-buttons').forEach(x => this.addClearButton(x));

      // activate stickiness of elements in each state
      this.activateStickyElements();

      // first off show the new template / new folder buttons
      this.currentState = this.$form.data('prev-state') || 'unknown';
      if (this.currentState === 'unknown') {
        this.selectActionButtons(false);
      } else {
        this.render();
      }

      this.$form.on('click', 'button.button-secondary', (event) => this.actionButtonClicked(event));
      this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged());
    };

    this.getFocusRoutine = function (selector, setTabindex) {
      return function () {
        let $el = $(selector);
        let removeTabindex = (e) => {
          $(e.target)
            .removeAttr('tabindex')
            .off('blur', removeTabindex);
        };

        if (setTabindex) {
          $el.attr('tabindex', '-1');
          $el.on('blur', removeTabindex);
        }

        $el.focus();
      };
    };

    this.activateStickyElements = function() {
      var oldClass = 'js-will-stick-at-bottom-when-scrolling';
      var newClass = 'js-stick-at-bottom-when-scrolling';

      this.states.forEach(state => {
        state.$el
          .find('.' + oldClass)
          .removeClass(oldClass)
          .addClass(newClass);
      });
    };

    this.addCancelButton = function(state) {
      let selector = `[value=${state.key}]`;
      let $cancel = this.makeButton('Cancel', () => {

        // clear existing data
        state.$el.find('input:radio').prop('checked', false);
        state.$el.find('input:text').val('');

        // go back to action buttons
        this.selectActionButtons(selector);
      }, selector);

      state.$el.find('[type=submit]').after($cancel);
    };

    this.addClearButton = function(state) {

      let $clear = this.makeButton('Clear', () => {
        // uncheck all templates and folders
        this.$form.find('input:checkbox').prop('checked', false);

        // go back to action buttons
        this.selectActionButtons();
      });

      state.$el.find('.template-list-selected-counter').append($clear);
    };

    this.makeButton = (text, fn, cancelSelector) => $('')
      .html(text)
      .addClass('js-cancel')
      .data('target', cancelSelector) // isn't set if cancelSelector is undefined
      .attr('tabindex', '0')
      .on('click keydown', event => {
        // space, enter or no keyCode (must be mouse input)
        if ([13, 32, undefined].indexOf(event.keyCode) > -1) {
          event.preventDefault();
          fn();
        }
      });

    this.selectActionButtons = function (targetSelector) {
      // If we want to show one of the grey choose actions state, we can pretend we're in the choose actions state,
      // and then pretend a checkbox was clicked to work out whether to show zero or non-zero options.
      // This calls a render at the end
      this.currentState = 'nothing-selected-buttons';
      this.templateFolderCheckboxChanged();
      if (targetSelector) {
        let setFocus = this.getFocusRoutine(targetSelector, false);
        setFocus();
      }
    };

    this.actionButtonClicked = function(event) {
      event.preventDefault();
      this.currentState = $(event.currentTarget).val();

      this.render();
    };

    this.templateFolderCheckboxChanged = function() {
      let numSelected = this.countSelectedCheckboxes();

      if (this.currentState === 'nothing-selected-buttons' && numSelected !== 0) {
        // user has just selected first item
        this.currentState = 'items-selected-buttons';
      } else if (this.currentState === 'items-selected-buttons' && numSelected === 0) {
        // user has just deselected last item
        this.currentState = 'nothing-selected-buttons';
      }

      this.render();

      $('.template-list-selected-counter-count').html(numSelected);

      $('.template-list-selected-counter').toggle(this.hasCheckboxes());

    };

    this.hasCheckboxes = function() {
      return !!this.$form.find('input:checkbox').length;
    };

    this.countSelectedCheckboxes = function() {
      return this.$form.find('input:checkbox:checked').length;
    };

    this.render = function() {
      let mode = 'default';
      let currentStateObj = this.states.filter(state => { return (state.key === this.currentState); })[0];

      // detach everything, unless they are the currentState
      this.states.forEach(
        state => (state.key === this.currentState ? this.$stickyBottom.append(state.$el) : state.$el.detach())
      );

      // use dialog mode for states which contain more than one form control
      if (['move-to-existing-folder', 'add-new-template'].indexOf(this.currentState) !== -1) {
        mode = 'dialog';
      }
      GOVUK.stickAtBottomWhenScrolling.setMode(mode);
      // make sticky JS recalculate its cache of the element's position
      GOVUK.stickAtBottomWhenScrolling.recalculate();

      if (currentStateObj && ('setFocus' in currentStateObj)) { currentStateObj.setFocus(); }
    };

    this.nothingSelectedButtons = $(`

          <button class="button-secondary" value="add-new-template">New template</button>
          <button class="button-secondary" value="add-new-folder">New folder</button>

            Nothing selected

    `).get(0);

    this.itemsSelectedButtons = $(`

          <button class="button-secondary" value="move-to-existing-folder">Move</button>
          <button class="button-secondary" value="move-to-new-folder">Add to new folder</button>

            1 selected

    `).get(0);
  };

})(window.GOVUK.Modules);
<|endoftext|>


<commit_before>/*
 * jQuery UI Autocomplete HTML Extension
 *
 * Copyright 2010, Scott González (http://scottgonzalez.com)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 *
 * http://github.com/scottgonzalez/jquery-ui-extensions
 */
(function(a){function d(b,c){var d=new RegExp(a.ui.autocomplete.escapeRegex(c),"i");return a.grep(b,function(b){return d.test(a("
").html(b.label||b.value||b).text())})}var b=a.ui.autocomplete.prototype,c=b._initSource;a.extend(b,{_initSource:function(){this.options.html&&a.isArray(this.options.source)?this.source=function(a,b){b(d(this.options.source,a.term))}:c.call(this)},_renderItem:function(b,c){return a("

").data("item.autocomplete",c).append(a("")[this.options.html?"html":"text"](c.label)).appendTo(b)}})})(jQuery)<commit_msg>Delete jquery.ui.autocomplete.html.js<commit_after><|endoftext|>


<commit_before>import moment from 'moment';

Template.dateCell.helpers({
  'formattedDate': function () {
    // Load the Finnish locale
    moment.locale('fi');

    // Create resident latest activity variable
    var activity = this;

    // Convert latest activity date to 'from now' format
    var activityDateFormatted = moment(activity.activityDate).fromNow();

    return activityDateFormatted;
  }
});
<commit_msg>Use client locale for date format<commit_after>import moment from 'moment';

Template.dateCell.helpers({
  'formattedDate': function () {
    // Get client locale
    const clientLocale = TAPi18n.getLanguage();

    // Load the client locale into Moment
    moment.locale(clientLocale);

    // Create resident latest activity variable
    var activity = this;

    // Convert latest activity date to 'from now' format
    var activityDateFormatted = moment(activity.activityDate).fromNow();

    return activityDateFormatted;
  }
});
<|endoftext|>