<commit_before>/*
 * Copyright 2013 Mirantis, Inc.
 *
 * 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
 *
 *     http://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.
**/
define(
[
    'coccyx',
    'js/coccyx_mixins',
    'models',
    'views/common',
    'views/cluster_page',
    'views/cluster_page_tabs/nodes_tab',
    'views/clusters_page',
    'views/releases_page',
    'views/notifications_page',
    'views/support_page',
    'views/capacity_page'
],
function(Coccyx, coccyxMixins, models, commonViews, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) {
    'use strict';

    var AppRouter = Backbone.Router.extend({
        routes: {
            'clusters': 'listClusters',
            'cluster/:id': 'showCluster',
            'cluster/:id/:tab(/:opt1)(/:opt2)': 'showClusterTab',
            'releases': 'listReleases',
            'notifications': 'showNotifications',
            'support': 'showSupportPage',
            'capacity': 'showCapacityPage',
            '*default': 'listClusters'
        },
        initialize: function() {
            this.content = $('#content');
            this.navbar = new commonViews.Navbar({elements: [
                ['environments', '#clusters'],
                ['releases', '#releases'],
                ['support', '#support']
            ]});
            this.content.before(this.navbar.render().el);
            this.breadcrumbs = new commonViews.Breadcrumbs();
            this.content.before(this.breadcrumbs.render().el);
            this.footer = new commonViews.Footer();
            $('#footer').html(this.footer.render().el);
            this.content.find('.loading').addClass('layout-loaded');
        },
        setPage: function(NewPage, options) {
            if (this.page) {
                this.page.tearDown();
            }
            this.page = new NewPage(options);
            this.page.updateNavbar();
            this.page.updateBreadcrumbs();
            this.page.updateTitle();
            this.content.html(this.page.render().el);
        },
        showCluster: function(id) {
            this.navigate('#cluster/' + id + '/nodes', {trigger: true, replace: true});
        },
        showClusterTab: function(id, activeTab) {
            if (!_.contains(_.pluck(_.result(ClusterPage.prototype, 'tabs'), 'url'), activeTab)) {
                this.showCluster(id);
                return;
            }

            var tabOptions = _.toArray(arguments).slice(2);

            if (activeTab == 'nodes') {
                // special case for nodes tab screen change
                try {
                    if (app.page.constructor == ClusterPage && app.page.model.id == id && app.page.tab.constructor == NodesTab) {
                        app.page.tab.routeScreen(tabOptions);
                        return;
                    }
                } catch (ignore) {}
            }

            var cluster, tasks;
            var render = function() {
                this.setPage(ClusterPage, {
                    model: cluster,
                    activeTab: activeTab,
                    tabOptions: tabOptions,
                    tasks: tasks
                });
            };

            if (app.page && app.page.constructor == ClusterPage && app.page.model.id == id) {
                // just another tab has been chosen, do not load cluster again
                cluster = app.page.model;
                tasks = app.page.tasks;
                render.call(this);
            } else {
                cluster = new models.Cluster({id: id});
                tasks = new models.Tasks();
                tasks.fetch = function(options) {
                    return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
                };
                $.when(cluster.fetch(), cluster.fetchRelated('nodes'), cluster.fetchRelated('tasks'), tasks.fetch())
                    .done(_.bind(render, this))
                    .fail(_.bind(this.listClusters, this));
            }
        },
        listClusters: function() {
            this.navigate('#clusters', {replace: true});
            var clusters = new models.Clusters();
            var nodes = new models.Nodes();
            var tasks = new models.Tasks();
            $.when(clusters.fetch(), nodes.deferred = nodes.fetch(), tasks.fetch()).always(_.bind(function() {
                clusters.each(function(cluster) {
                    cluster.set('nodes', new models.Nodes(nodes.where({cluster: cluster.id})));
                    cluster.get('nodes').deferred = nodes.deferred;
                    cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
                }, this);
                this.setPage(ClustersPage, {collection: clusters});
            }, this));
        },
        listReleases: function() {
            var releases = new models.Releases();
            var tasks = new models.Tasks();
            tasks.fetch = function(options) {
                return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
            };
            $.when(releases.fetch(), tasks.fetch()).done(_.bind(function() {
                this.setPage(ReleasesPage, {
                    collection: releases,
                    tasks: tasks
                });
            }, this));
        },
        showNotifications: function() {
            this.setPage(NotificationsPage, {notifications: app.navbar.notifications});
        },
        showSupportPage: function() {
            this.setPage(SupportPage);
        },
        showCapacityPage: function() {
            this.setPage(CapacityPage);
        }
    });

    return {
        initialize: function() {
            // our server doesn't support PATCH, so use PUT instead
            var originalSync = Backbone.sync;
            Backbone.sync = function() {
                var args = arguments;
                if (args[0] == 'patch') {
                    args[0] = 'update';
                }
                return originalSync.apply(this, args);
            };

            // add deferred-related mixins
            _.extend(Backbone.View.prototype, coccyxMixins);

            // reject deferreds on view teardown
            Coccyx.addTearDownCallback(function() {
                this.rejectRegisteredDeferreds();
            });

            // remove stickit bindings on teardown
            Coccyx.addTearDownCallback(function() {
                this.unstickit();
            });

            window.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') !== -1;

            var app = new AppRouter();
            window.app = app;

            Backbone.history.start();
        }
    };
});
<commit_msg>Fix OSTF in IE<commit_after>/*
 * Copyright 2013 Mirantis, Inc.
 *
 * 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
 *
 *     http://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.
**/
define(
[
    'coccyx',
    'js/coccyx_mixins',
    'models',
    'views/common',
    'views/cluster_page',
    'views/cluster_page_tabs/nodes_tab',
    'views/clusters_page',
    'views/releases_page',
    'views/notifications_page',
    'views/support_page',
    'views/capacity_page'
],
function(Coccyx, coccyxMixins, models, commonViews, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) {
    'use strict';

    var AppRouter = Backbone.Router.extend({
        routes: {
            'clusters': 'listClusters',
            'cluster/:id': 'showCluster',
            'cluster/:id/:tab(/:opt1)(/:opt2)': 'showClusterTab',
            'releases': 'listReleases',
            'notifications': 'showNotifications',
            'support': 'showSupportPage',
            'capacity': 'showCapacityPage',
            '*default': 'listClusters'
        },
        initialize: function() {
            this.content = $('#content');
            this.navbar = new commonViews.Navbar({elements: [
                ['environments', '#clusters'],
                ['releases', '#releases'],
                ['support', '#support']
            ]});
            this.content.before(this.navbar.render().el);
            this.breadcrumbs = new commonViews.Breadcrumbs();
            this.content.before(this.breadcrumbs.render().el);
            this.footer = new commonViews.Footer();
            $('#footer').html(this.footer.render().el);
            this.content.find('.loading').addClass('layout-loaded');
        },
        setPage: function(NewPage, options) {
            if (this.page) {
                this.page.tearDown();
            }
            this.page = new NewPage(options);
            this.page.updateNavbar();
            this.page.updateBreadcrumbs();
            this.page.updateTitle();
            this.content.html(this.page.render().el);
        },
        showCluster: function(id) {
            this.navigate('#cluster/' + id + '/nodes', {trigger: true, replace: true});
        },
        showClusterTab: function(id, activeTab) {
            if (!_.contains(_.pluck(_.result(ClusterPage.prototype, 'tabs'), 'url'), activeTab)) {
                this.showCluster(id);
                return;
            }

            var tabOptions = _.toArray(arguments).slice(2);

            if (activeTab == 'nodes') {
                // special case for nodes tab screen change
                try {
                    if (app.page.constructor == ClusterPage && app.page.model.id == id && app.page.tab.constructor == NodesTab) {
                        app.page.tab.routeScreen(tabOptions);
                        return;
                    }
                } catch (ignore) {}
            }

            var cluster, tasks;
            var render = function() {
                this.setPage(ClusterPage, {
                    model: cluster,
                    activeTab: activeTab,
                    tabOptions: tabOptions,
                    tasks: tasks
                });
            };

            if (app.page && app.page.constructor == ClusterPage && app.page.model.id == id) {
                // just another tab has been chosen, do not load cluster again
                cluster = app.page.model;
                tasks = app.page.tasks;
                render.call(this);
            } else {
                cluster = new models.Cluster({id: id});
                tasks = new models.Tasks();
                tasks.fetch = function(options) {
                    return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
                };
                $.when(cluster.fetch(), cluster.fetchRelated('nodes'), cluster.fetchRelated('tasks'), tasks.fetch())
                    .done(_.bind(render, this))
                    .fail(_.bind(this.listClusters, this));
            }
        },
        listClusters: function() {
            this.navigate('#clusters', {replace: true});
            var clusters = new models.Clusters();
            var nodes = new models.Nodes();
            var tasks = new models.Tasks();
            $.when(clusters.fetch(), nodes.deferred = nodes.fetch(), tasks.fetch()).always(_.bind(function() {
                clusters.each(function(cluster) {
                    cluster.set('nodes', new models.Nodes(nodes.where({cluster: cluster.id})));
                    cluster.get('nodes').deferred = nodes.deferred;
                    cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
                }, this);
                this.setPage(ClustersPage, {collection: clusters});
            }, this));
        },
        listReleases: function() {
            var releases = new models.Releases();
            var tasks = new models.Tasks();
            tasks.fetch = function(options) {
                return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
            };
            $.when(releases.fetch(), tasks.fetch()).done(_.bind(function() {
                this.setPage(ReleasesPage, {
                    collection: releases,
                    tasks: tasks
                });
            }, this));
        },
        showNotifications: function() {
            this.setPage(NotificationsPage, {notifications: app.navbar.notifications});
        },
        showSupportPage: function() {
            this.setPage(SupportPage);
        },
        showCapacityPage: function() {
            this.setPage(CapacityPage);
        }
    });

    return {
        initialize: function() {
            // our server doesn't support PATCH, so use PUT instead
            var originalSync = Backbone.sync;
            Backbone.sync = function() {
                var args = arguments;
                if (args[0] == 'patch') {
                    args[0] = 'update';
                }
                return originalSync.apply(this, args);
            };

            // add deferred-related mixins
            _.extend(Backbone.View.prototype, coccyxMixins);

            // reject deferreds on view teardown
            Coccyx.addTearDownCallback(function() {
                this.rejectRegisteredDeferreds();
            });

            // remove stickit bindings on teardown
            Coccyx.addTearDownCallback(function() {
                this.unstickit();
            });

            // this is needed for IE, which caches requests resulting in wrong results (e.g /ostf/testruns/last/1)
            $.ajaxSetup({ cache: false });

            window.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') !== -1;

            var app = new AppRouter();
            window.app = app;

            Backbone.history.start();
        }
    };
});
<|endoftext|>


<commit_before>/**
 * `modules/analytics-4` data store: api tests.
 *
 * Site Kit by Google, Copyright 2021 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.
 */

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import { STORE_NAME } from './constants';
import { createTestRegistry, unsubscribeFromAll } from 'tests/js/utils';

describe( 'modules/analytics-4 properties', () => {
	let registry;

	beforeAll( () => {
		API.setUsingCache( false );
	} );

	beforeEach( () => {
		registry = createTestRegistry();
		// Receive empty settings to prevent unexpected fetch by resolver.
		registry.dispatch( STORE_NAME ).receiveGetSettings( {} );
	} );

	afterAll( () => {
		API.setUsingCache( true );
	} );

	afterEach( () => {
		unsubscribeFromAll( registry );
	} );

	describe( 'selectors', () => {
		describe( 'isAdminAPIWorking', () => {
			test.each`
               property   | webData    | errorProperty | errorWebData | expected
               ${ true }  | ${ true }  |  ${ false }   | ${ false }   | ${ true }
               ${ true }  | ${ false } |  ${ false }   | ${ false }   | ${ undefined }
               ${ false } | ${ true }  |  ${ false }   | ${ false }   | ${ undefined }
               ${ false } | ${ false } |  ${ false }   | ${ false }   | ${ undefined }
               ${ true }  | ${ true }  |  ${ true }    | ${ false }   | ${ false }
               ${ true }  | ${ true }  |  ${ false }   | ${ true }    | ${ false }
               ${ true }  | ${ true }  |  ${ true }    | ${ true }    | ${ false }
               ${ false } | ${ true }  |  ${ true }    | ${ true }    | ${ false }
               ${ true }  | ${ false } |  ${ true }    | ${ true }    | ${ false }
               ${ false } | ${ false } |  ${ true }    | ${ true }    | ${ false }
            `( 'DOES $propertyValue | $webDataValue | $errorProperty | $errorWebData',
				( { property, webData, errorProperty, errorWebData, expected } ) => {
					if ( property ) {
						registry.dispatch( STORE_NAME ).receiveGetProperties( [
							{
								_id: '1000',
								_accountID: '100',
								name: 'properties/1000',
								createTime: '2014-10-02T15:01:23Z',
								updateTime: '2014-10-02T15:01:23Z',
								parent: 'accounts/100',
								displayName: 'Test GA4 Property',
								industryCategory: 'TECHNOLOGY',
								timeZone: 'America/Los_Angeles',
								currencyCode: 'USD',
								deleted: false,
							},
						],
						{ accountID: 'foo-bar' },
						);
					}
					if ( webData ) {
						registry.dispatch( STORE_NAME ).receiveGetWebDataStreams(
							[
								{
									_id: '2000',
									_propertyID: '1000',
									name: 'properties/1000/webDataStreams/2000',
									// eslint-disable-next-line sitekit/acronym-case
									measurementId: '1A2BCD345E',
									// eslint-disable-next-line sitekit/acronym-case
									firebaseAppId: '',
									createTime: '2014-10-02T15:01:23Z',
									updateTime: '2014-10-02T15:01:23Z',
									defaultUri: 'http://example.com',
									displayName: 'Test GA4 WebDataStream',
								},
							],
							{ propertyID: 'foo-bar' }
						);
					}
					if ( errorProperty ) {
						registry.dispatch( STORE_NAME ).receiveError(
							new Error( 'foo' ),	'getProperties', [ 'foo', 'bar' ]
						);
					}
					if ( errorWebData ) {
						registry.dispatch( STORE_NAME ).receiveError(
							new Error( 'foo' ),	'getWebDataStreams', [ 'foo', 'bar' ]
						);
					}

					const isAdminAPIWorking = registry.select( STORE_NAME ).isAdminAPIWorking( );

					expect( isAdminAPIWorking ).toBe( expected );
				} );
		} );
	} );
} );
<commit_msg>Update propertyID for new rules.<commit_after>/**
 * `modules/analytics-4` data store: api tests.
 *
 * Site Kit by Google, Copyright 2021 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.
 */

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import { STORE_NAME } from './constants';
import { createTestRegistry, unsubscribeFromAll } from 'tests/js/utils';

describe( 'modules/analytics-4 properties', () => {
	let registry;

	beforeAll( () => {
		API.setUsingCache( false );
	} );

	beforeEach( () => {
		registry = createTestRegistry();
		// Receive empty settings to prevent unexpected fetch by resolver.
		registry.dispatch( STORE_NAME ).receiveGetSettings( {} );
	} );

	afterAll( () => {
		API.setUsingCache( true );
	} );

	afterEach( () => {
		unsubscribeFromAll( registry );
	} );

	describe( 'selectors', () => {
		describe( 'isAdminAPIWorking', () => {
			test.each`
               property   | webData    | errorProperty | errorWebData | expected
               ${ true }  | ${ true }  |  ${ false }   | ${ false }   | ${ true }
               ${ true }  | ${ false } |  ${ false }   | ${ false }   | ${ undefined }
               ${ false } | ${ true }  |  ${ false }   | ${ false }   | ${ undefined }
               ${ false } | ${ false } |  ${ false }   | ${ false }   | ${ undefined }
               ${ true }  | ${ true }  |  ${ true }    | ${ false }   | ${ false }
               ${ true }  | ${ true }  |  ${ false }   | ${ true }    | ${ false }
               ${ true }  | ${ true }  |  ${ true }    | ${ true }    | ${ false }
               ${ false } | ${ true }  |  ${ true }    | ${ true }    | ${ false }
               ${ true }  | ${ false } |  ${ true }    | ${ true }    | ${ false }
               ${ false } | ${ false } |  ${ true }    | ${ true }    | ${ false }
            `( 'DOES $propertyValue | $webDataValue | $errorProperty | $errorWebData',
				( { property, webData, errorProperty, errorWebData, expected } ) => {
					if ( property ) {
						registry.dispatch( STORE_NAME ).receiveGetProperties( [
							{
								_id: '1000',
								_accountID: '100',
								name: 'properties/1000',
								createTime: '2014-10-02T15:01:23Z',
								updateTime: '2014-10-02T15:01:23Z',
								parent: 'accounts/100',
								displayName: 'Test GA4 Property',
								industryCategory: 'TECHNOLOGY',
								timeZone: 'America/Los_Angeles',
								currencyCode: 'USD',
								deleted: false,
							},
						],
						{ accountID: 'foo-bar' },
						);
					}
					if ( webData ) {
						registry.dispatch( STORE_NAME ).receiveGetWebDataStreams(
							[
								{
									_id: '2000',
									_propertyID: '1000',
									name: 'properties/1000/webDataStreams/2000',
									// eslint-disable-next-line sitekit/acronym-case
									measurementId: '1A2BCD345E',
									// eslint-disable-next-line sitekit/acronym-case
									firebaseAppId: '',
									createTime: '2014-10-02T15:01:23Z',
									updateTime: '2014-10-02T15:01:23Z',
									defaultUri: 'http://example.com',
									displayName: 'Test GA4 WebDataStream',
								},
							],
							{ propertyID: 'foobar' }
						);
					}
					if ( errorProperty ) {
						registry.dispatch( STORE_NAME ).receiveError(
							new Error( 'foo' ),	'getProperties', [ 'foo', 'bar' ]
						);
					}
					if ( errorWebData ) {
						registry.dispatch( STORE_NAME ).receiveError(
							new Error( 'foo' ),	'getWebDataStreams', [ 'foo', 'bar' ]
						);
					}

					const isAdminAPIWorking = registry.select( STORE_NAME ).isAdminAPIWorking( );

					expect( isAdminAPIWorking ).toBe( expected );
				} );
		} );
	} );
} );
<|endoftext|>


<commit_before>/**
 * @copyright Devexperts
 *
 * @requires DX
 * @requires DX.Measure
 * @requires DX.Dom
 * @requires DX.Bem
 * @requires DX.Event
 * @requires DX.String
 * @requires DX.Tmpl
 * @namespace
 */

var DropDown = (function(DX) {
	'use strict';

	var CN_DROPDOWN = 'dropDown',
			M_SHOWN = 'shown',
			M_HIDDEN = 'hidden',
			CN_CONTAINER = CN_DROPDOWN + '--container',
			CN_LIST_WRAP = CN_DROPDOWN + '--listWrap',
			CN_LIST = CN_DROPDOWN + '--list',
			CN_OPTION = CN_DROPDOWN + '--option',
			M_SELECTED = 'selected',
			M_HOVERED = 'hovered',
			CN_GROUP = CN_DROPDOWN + '--group',
			CN_GROUP_TITLE = CN_DROPDOWN + '--groupTitle',
			A_FOR = 'data-for',
			ESC_KEY_CODE = 27,
			defaults = {
				modifiers: [],
				width: 'control',
				optionTmpl: '
{%= optionInnerTmpl %}{%= currentMarkTmpl %}
',
				optionInnerTmpl: '{%= text %}',
				currentMarkTmpl: null,
				groupTmpl: [
					'
',
					'{%= label %}',
					'
{%= optionList %}
',
					'
'
				].join(''),
				innerTmpl: [
					'
',
					'
',
					'

',
					'
',
					'
'
				].join(''),
				hideOnClick: true,
				animationStepDelay: 0.03
			};

	/**
	 * Check is object variable
	 * @param {*} param
	 * @returns {boolean}
	 */
	function isObject(param) {
		var type = typeof param;
		return type === 'object' && !Array.isArray(param);
	}

	function rePosition(block, control) {
		var offset = DX.Measure.getPosition(control),
			controlPosition = control.getBoundingClientRect(),
			blockPosition,
			upDirectionTopCoordinates,
			leftDirectionLeftCoordinates;

		block.style.top = offset.y + controlPosition.height + 'px';
		block.style.left = offset.x + 'px';

		blockPosition = block.getBoundingClientRect();

		upDirectionTopCoordinates =  document.body.scrollTop + controlPosition.top - blockPosition.height + controlPosition.height;
		leftDirectionLeftCoordinates = document.body.scrollLeft + controlPosition.left - blockPosition.width + controlPosition.width;

		if (blockPosition.top + blockPosition.height > window.innerHeight && upDirectionTopCoordinates > 0) {
			block.style.top = upDirectionTopCoordinates + 'px';
		}

		if (blockPosition.left + blockPosition.width > window.innerWidth && leftDirectionLeftCoordinates > 0) {
			block.style.left = leftDirectionLeftCoordinates + 'px';
		}
	}

	function reCalculateWidth(block, control, config) {
		if (isNaN(config.width)) {
			block.style.minWidth = DX.Measure.getSize(control, true).width + 'px';
		} else {
			block.style.width = config.width + 'px';
		}
	}

	function reCalculateAnimationDelay(block, config) {
		var listChildren = block.querySelectorAll(`.${CN_OPTION}, .${CN_GROUP_TITLE}`);

		for(var i=0; i<listChildren.length; i++) {
			listChildren[i].style.animationDelay = config.animationStepDelay * i + 's';
		}
	}

	function reCalculateHeight(block) {
		block.style.display = 'block';
		var dropDownHeight = DX.Measure.getSize(block, true).height;
		block.style.display = '';
		block.style.height = dropDownHeight + 'px';
	}

	function getOptionListHTML(data, config) {
		return data.reduce(function(prevValue, item) {
			var result = '';
			if (isObject(item)) {
				var isItGroup = Array.isArray(item.options);
				result = prevValue + (isItGroup ? getOptgroupHTML(item, config) : getOptionHTML(item, config));
			}
			return result;
		}, '');
	}

	function getOptgroupHTML(data, config) {
		data = Object.assign({}, data);
		data.optionList = getOptionListHTML(data.options, config);

		return DX.Tmpl.process(config.groupTmpl, data);
	}

	function getOptionHTML(data, config) {
		var dataAttrs = '',
			template = config.optionTmpl;
		data = Object.assign({}, data);
		data.classNames = DX.Bem.createModifiedClassName(CN_OPTION, data.modifiers);

		if (data.data) {
			Object.keys(data.data).forEach(function(key) {
				var item = data.data[key];
				dataAttrs += 'data-' + DX.String.hyphenate(key) + '="' + item + '" ';
			});

			data.dataAttrs = dataAttrs;
		}

		data.currentMarkTmpl = config.currentMarkTmpl;
		data.optionInnerTmpl = DX.Tmpl.process(config.optionInnerTmpl, data);

		return DX.Tmpl.process(template, data);
	}

	function createElements(control, config) {
		var body = document.body,
			block,
			list;

		block = DX.Dom.createElement('div', {
			className: DX.Bem.createModifiedClassName(CN_DROPDOWN, config.modifiers),
			innerHTML: config.innerTmpl
		});

		if (control.id) {
			block.setAttribute(A_FOR, control.id);
		}

		list = DX.$$('.' + CN_LIST, block);

		body.appendChild(block);

		return {
			block: block,
			list: list
		};
	}

	function keyDownHandler(e) {

		var key;
		key = e.key || e.which;
		if (key === ESC_KEY_CODE || key === 'Escape') {
			setTimeout(hideAllDropDowns, 0);
		}
	}
	function hideAllDropDowns() {
		var dropDownsArray = DX.$$$('.' + CN_DROPDOWN);
		Array.prototype.forEach.call(dropDownsArray, function(dropdown) {
			DX.Event.trigger(dropdown, DropDown.E_HIDE);
		});

	}

	if (typeof document !== 'undefined') {
		document.addEventListener(DX.Event.KEY_DOWN, keyDownHandler);
	}
	/**
	 * Creates new dropdown
	 * @constructor DropDown
	 * @param {Node|Element} control
	 * @param {Object} config - {Array:modifiers, String|Number:width, String:optionTmpl, String:groupTmpl, String:innerTmpl, String:currentMarkTmpl}
	 */
	return function DropDown(control, config) {
		var elements,
			selectedIndex,
			hoveredIndex,
			optionElements,
			selectedOptionElement,
			hoveredOptionElement,
			isShownOnce;

		/**
		 * Dropdown is created
		 *
		 * @event dropdown:created
		 */
		function init() {
			config = Object.assign({}, defaults, (config || {}));
			elements = createElements(control, config);
			selectedIndex = 0;
			hoveredIndex = null;
			isShownOnce = false;

			initListeners();

			DX.Event.trigger(control, DropDown.E_CREATED, {
				detail: {
					block: elements.block,
					eventTarget: elements.block
				}
			});
		}
		function initListeners() {
			var block = getEventTarget();
			block.addEventListener(DropDown.E_HIDE, hide);
			document.addEventListener(DropDown.E_HIDE_ALL, hideAllDropDowns);

			if (elements.list) {
				elements.list.addEventListener('click', function(e) {
					var optionElement = DX.Dom.getAscendantByClassName(e.target, CN_OPTION),
						index;

					if (optionElement) {
						index = Array.prototype.indexOf.call(optionElements, optionElement);

						if (index !== selectedIndex) {
							selectedIndex = index;
							setSelectedIndex(index);
							DX.Event.trigger(elements.block, DropDown.E_CHANGED);
						}

						if (config.hideOnClick) {
							hide();
						}
					}
				}, true);
			}
		}
		/**
		 * Sets popup data list
		 * @method setDataList
		 * @param {Object} data
		 */
		function setDataList(data) {
			elements.list.innerHTML = getOptionListHTML(data, config);
			optionElements = DX.$$$('.' + CN_OPTION, elements.list);
		}
		/**
		 * Shows dropdown
		 * @method show
		 */
		/**
		 * Dropdown is shown
		 *
		 * @event dropdown:shown
		 */
		function show() {
			var block = elements.block;

			if (!isShownOnce) {
				isShownOnce = true;
				reCalculateWidth(block, control, config);
				reCalculateAnimationDelay(block, config);
				reCalculateHeight(block);
			}

			setHoveredIndex(0);

			DX.Bem.removeModifier(block, M_HIDDEN, CN_DROPDOWN);
			DX.Bem.addModifier(block, M_SHOWN, CN_DROPDOWN);
			rePosition(block, control);

			document.addEventListener('mousedown', function(e) {
				documentClickHandler(e);
			}, true);

			DX.Event.trigger(block, DropDown.E_SHOWN);
		}
		/**
		 * Hides dropdown
		 * @method hide
		 */
		/**
		 * Dropdown is hidden
		 *
		 * @event dropdown:hidden
		 */
		/**
		 * Hide dropdown
		 *
		 * @event dropdown:hide
		 */
		function hide() {
			var block = elements.block;

			clearHoveredIndex();
			DX.Bem.removeModifier(block, M_SHOWN, CN_DROPDOWN);
			DX.Bem.addModifier(block, M_HIDDEN, CN_DROPDOWN);

			document.removeEventListener(DX.Event.TOUCH_CLICK, documentClickHandler, true);
			DX.Event.trigger(block, DropDown.E_HIDDEN);
		}

		function normalizeIndex(index) {
			if (index <= 0) {
				index = 0;
			}
			if (optionElements && index >= optionElements.length) {
				index = optionElements.length - 1;
			}
			return index;
		}
		/**
		 * Sets popup selectedelement by index
		 * @method setSelectedIndex
		 * @param {Number} index
		 * @param {bool=} [triggerChangeEvent=false] whether or not trigger dropdown:changed event
		 */
		/**
		 * Dropdown has changed
		 *
		 * @event dropdown:changed
		 */
		function setSelectedIndex(index, triggerChangeEvent) {
			if (selectedOptionElement) {
				DX.Bem.removeModifier(selectedOptionElement, M_SELECTED, CN_OPTION);
			}

			selectedIndex = index;
			selectedOptionElement = optionElements[index];
			if (selectedOptionElement) {
				DX.Bem.addModifier(selectedOptionElement, M_SELECTED, CN_OPTION);
				if (triggerChangeEvent) {
					DX.Event.trigger(elements.block, DropDown.E_CHANGED);
				}
			}
		}
		/**
		 * Sets popup hoveredelement by index
		 * @method setHoveredIndex
		 * @param {Number} index
		 */
		function setHoveredIndex(index) {
			if (hoveredOptionElement) {
				DX.Bem.removeModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);
			}

			index = normalizeIndex(index);

			hoveredIndex = index;
			hoveredOptionElement = optionElements ? optionElements[index] : null;

			if (hoveredOptionElement) {
				DX.Bem.addModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);

			}
		}

		function clearHoveredIndex() {
			if (hoveredOptionElement) {
				DX.Bem.removeModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);
			}
			hoveredIndex = null;
			hoveredOptionElement = null;
		}
		/**
		 * Gets popup selectedelement by index
		 * @method getSelectedIndex
		 * @returns {Number} selectedIndex
		 */
		function getSelectedIndex() {
			return selectedIndex;
		}
		/**
		 * Gets popup hoveredelement by index
		 * @method getHoveredIndex
		 * @returns {Number} hoveredIndex
		 */
		function getHoveredIndex() {
			return hoveredIndex;
		}

		/**
		 * Gets HTMLNode containing dropdown
		 * @method getBlock
		 * @returns {Node}
		 */
		function getBlock() {
			return elements.block;
		}
		/**
		 * Gets element which listens to events
		 * @method getEventTarget
		 * @returns {Node}
		 */
		function getEventTarget() {
			return elements.block;
		}

		function documentClickHandler(e) {
			var target = e.target,
				closestDropDown = DX.Dom.getAscendantByClassName(target, CN_DROPDOWN),
				block = elements.block,
				id = block.getAttribute(A_FOR),
				forElement;

			if (id) {
				forElement = DX.Dom.getAscendantByAttribute(target, 'id', id);
			}

			if (closestDropDown !== block && !forElement) {
				hide();
			}
		}
		/**
		 * Gets whether dropdown is shown
		 * @method isShown
		 * @returns {bool}
		 */
		function isShown() {
			return DX.Bem.hasModifier(elements.block, M_SHOWN, CN_DROPDOWN);
		}

		init();

		this.setDataList = setDataList;
		this.setSelectedIndex = setSelectedIndex;
		this.getSelectedIndex = getSelectedIndex;
		this.setHoveredIndex = setHoveredIndex;
		this.getHoveredIndex = getHoveredIndex;
		this.show = show;
		this.hide = hide;
		this.isShown = isShown;
		this.getBlock = getBlock;
		this.getEventTarget = getEventTarget;
	};
})(DX);

/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_CREATED = 'dropdown:created';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_SHOWN = 'dropdown:shown';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDDEN = 'dropdown:hidden';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_CHANGED = 'dropdown:changed';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDE = 'dropdown:hide';

/**
 * @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDE_ALL = 'dropdown:hideAll';

/**
 * Gets if there is any shown dropdown
 * @method isAnyShown
 * @static
 * @memberof DropDown
 * @returns {Boolean}
 */
DropDown.isAnyShown = function() {
	'use strict';
	return document.querySelectorAll('.dropDown-shown').length > 0;
};

<commit_msg>Added destroy() and removeListeners() methods. Moved elementsListClickHandler() to separate function.<commit_after>/**
 * @copyright Devexperts
 *
 * @requires DX
 * @requires DX.Measure
 * @requires DX.Dom
 * @requires DX.Bem
 * @requires DX.Event
 * @requires DX.String
 * @requires DX.Tmpl
 * @namespace
 */

var DropDown = (function(DX) {
	'use strict';

	var CN_DROPDOWN = 'dropDown',
			M_SHOWN = 'shown',
			M_HIDDEN = 'hidden',
			CN_CONTAINER = CN_DROPDOWN + '--container',
			CN_LIST_WRAP = CN_DROPDOWN + '--listWrap',
			CN_LIST = CN_DROPDOWN + '--list',
			CN_OPTION = CN_DROPDOWN + '--option',
			M_SELECTED = 'selected',
			M_HOVERED = 'hovered',
			CN_GROUP = CN_DROPDOWN + '--group',
			CN_GROUP_TITLE = CN_DROPDOWN + '--groupTitle',
			A_FOR = 'data-for',
			ESC_KEY_CODE = 27,
			defaults = {
				modifiers: [],
				width: 'control',
				optionTmpl: '
{%= optionInnerTmpl %}{%= currentMarkTmpl %}
',
				optionInnerTmpl: '{%= text %}',
				currentMarkTmpl: null,
				groupTmpl: [
					'
',
					'{%= label %}',
					'
{%= optionList %}
',
					'
'
				].join(''),
				innerTmpl: [
					'
',
					'
',
					'

',
					'
',
					'
'
				].join(''),
				hideOnClick: true,
				animationStepDelay: 0.03
			};

	/**
	 * Check is object variable
	 * @param {*} param
	 * @returns {boolean}
	 */
	function isObject(param) {
		var type = typeof param;
		return type === 'object' && !Array.isArray(param);
	}

	function rePosition(block, control) {
		var offset = DX.Measure.getPosition(control),
			controlPosition = control.getBoundingClientRect(),
			blockPosition,
			upDirectionTopCoordinates,
			leftDirectionLeftCoordinates;

		block.style.top = offset.y + controlPosition.height + 'px';
		block.style.left = offset.x + 'px';

		blockPosition = block.getBoundingClientRect();

		upDirectionTopCoordinates =  document.body.scrollTop + controlPosition.top - blockPosition.height + controlPosition.height;
		leftDirectionLeftCoordinates = document.body.scrollLeft + controlPosition.left - blockPosition.width + controlPosition.width;

		if (blockPosition.top + blockPosition.height > window.innerHeight && upDirectionTopCoordinates > 0) {
			block.style.top = upDirectionTopCoordinates + 'px';
		}

		if (blockPosition.left + blockPosition.width > window.innerWidth && leftDirectionLeftCoordinates > 0) {
			block.style.left = leftDirectionLeftCoordinates + 'px';
		}
	}

	function reCalculateWidth(block, control, config) {
		if (isNaN(config.width)) {
			block.style.minWidth = DX.Measure.getSize(control, true).width + 'px';
		} else {
			block.style.width = config.width + 'px';
		}
	}

	function reCalculateAnimationDelay(block, config) {
		var listChildren = block.querySelectorAll(`.${CN_OPTION}, .${CN_GROUP_TITLE}`);

		for(var i=0; i<listChildren.length; i++) {
			listChildren[i].style.animationDelay = config.animationStepDelay * i + 's';
		}
	}

	function reCalculateHeight(block) {
		block.style.display = 'block';
		var dropDownHeight = DX.Measure.getSize(block, true).height;
		block.style.display = '';
		block.style.height = dropDownHeight + 'px';
	}

	function getOptionListHTML(data, config) {
		return data.reduce(function(prevValue, item) {
			var result = '';
			if (isObject(item)) {
				var isItGroup = Array.isArray(item.options);
				result = prevValue + (isItGroup ? getOptgroupHTML(item, config) : getOptionHTML(item, config));
			}
			return result;
		}, '');
	}

	function getOptgroupHTML(data, config) {
		data = Object.assign({}, data);
		data.optionList = getOptionListHTML(data.options, config);

		return DX.Tmpl.process(config.groupTmpl, data);
	}

	function getOptionHTML(data, config) {
		var dataAttrs = '',
			template = config.optionTmpl;
		data = Object.assign({}, data);
		data.classNames = DX.Bem.createModifiedClassName(CN_OPTION, data.modifiers);

		if (data.data) {
			Object.keys(data.data).forEach(function(key) {
				var item = data.data[key];
				dataAttrs += 'data-' + DX.String.hyphenate(key) + '="' + item + '" ';
			});

			data.dataAttrs = dataAttrs;
		}

		data.currentMarkTmpl = config.currentMarkTmpl;
		data.optionInnerTmpl = DX.Tmpl.process(config.optionInnerTmpl, data);

		return DX.Tmpl.process(template, data);
	}

	function createElements(control, config) {
		var body = document.body,
			block,
			list;

		block = DX.Dom.createElement('div', {
			className: DX.Bem.createModifiedClassName(CN_DROPDOWN, config.modifiers),
			innerHTML: config.innerTmpl
		});

		if (control.id) {
			block.setAttribute(A_FOR, control.id);
		}

		list = DX.$$('.' + CN_LIST, block);

		body.appendChild(block);

		return {
			block: block,
			list: list
		};
	}

	function keyDownHandler(e) {

		var key;
		key = e.key || e.which;
		if (key === ESC_KEY_CODE || key === 'Escape') {
			setTimeout(hideAllDropDowns, 0);
		}
	}
	function hideAllDropDowns() {
		var dropDownsArray = DX.$$$('.' + CN_DROPDOWN);
		Array.prototype.forEach.call(dropDownsArray, function(dropdown) {
			DX.Event.trigger(dropdown, DropDown.E_HIDE);
		});

	}

	if (typeof document !== 'undefined') {
		document.addEventListener(DX.Event.KEY_DOWN, keyDownHandler);
	}
	/**
	 * Creates new dropdown
	 * @constructor DropDown
	 * @param {Node|Element} control
	 * @param {Object} config - {Array:modifiers, String|Number:width, String:optionTmpl, String:groupTmpl, String:innerTmpl, String:currentMarkTmpl}
	 */
	return function DropDown(control, config) {
		var elements,
			selectedIndex,
			hoveredIndex,
			optionElements,
			selectedOptionElement,
			hoveredOptionElement,
			isShownOnce;

		/**
		 * Dropdown is created
		 *
		 * @event dropdown:created
		 */
		function init() {
			config = Object.assign({}, defaults, (config || {}));
			elements = createElements(control, config);
			selectedIndex = 0;
			hoveredIndex = null;
			isShownOnce = false;

			initListeners();

			DX.Event.trigger(control, DropDown.E_CREATED, {
				detail: {
					block: elements.block,
					eventTarget: elements.block
				}
			});
		}
		function initListeners() {
			var block = getEventTarget();
			block.addEventListener(DropDown.E_HIDE, hide);
			document.addEventListener(DropDown.E_HIDE_ALL, hideAllDropDowns);

			if (elements.list) {
				elements.list.addEventListener('click', elementsListClickHandler, true);
			}
		}

		/**
		 * Dropdown is destroyed
		 *
		 * @event dropdown:destroyed
		 */
		function destroy() {
			removeListeners();
			DX.Event.trigger(elements.block, DropDown.E_DESTROYED);
			elements.block.remove();
		}
		function removeListeners() {
			var block = getEventTarget();
			block.removeEventListener(DropDown.E_HIDE, hide);
			document.removeEventListener(DropDown.E_HIDE_ALL, hideAllDropDowns);

			if (elements.list) {
				elements.list.removeEventListener('click', elementsListClickHandler, true);
			}
		}

		/**
		 * Sets popup data list
		 * @method setDataList
		 * @param {Object} data
		 */
		function setDataList(data) {
			elements.list.innerHTML = getOptionListHTML(data, config);
			optionElements = DX.$$$('.' + CN_OPTION, elements.list);
		}
		/**
		 * Shows dropdown
		 * @method show
		 */
		/**
		 * Dropdown is shown
		 *
		 * @event dropdown:shown
		 */
		function show() {
			var block = elements.block;

			if (!isShownOnce) {
				isShownOnce = true;
				reCalculateWidth(block, control, config);
				reCalculateAnimationDelay(block, config);
				reCalculateHeight(block);
			}

			setHoveredIndex(0);

			DX.Bem.removeModifier(block, M_HIDDEN, CN_DROPDOWN);
			DX.Bem.addModifier(block, M_SHOWN, CN_DROPDOWN);
			rePosition(block, control);

			document.addEventListener('mousedown', function(e) {
				documentClickHandler(e);
			}, true);

			DX.Event.trigger(block, DropDown.E_SHOWN);
		}
		/**
		 * Hides dropdown
		 * @method hide
		 */
		/**
		 * Dropdown is hidden
		 *
		 * @event dropdown:hidden
		 */
		/**
		 * Hide dropdown
		 *
		 * @event dropdown:hide
		 */
		function hide() {
			var block = elements.block;

			clearHoveredIndex();
			DX.Bem.removeModifier(block, M_SHOWN, CN_DROPDOWN);
			DX.Bem.addModifier(block, M_HIDDEN, CN_DROPDOWN);

			document.removeEventListener(DX.Event.TOUCH_CLICK, documentClickHandler, true);
			DX.Event.trigger(block, DropDown.E_HIDDEN);
		}

		function normalizeIndex(index) {
			if (index <= 0) {
				index = 0;
			}
			if (optionElements && index >= optionElements.length) {
				index = optionElements.length - 1;
			}
			return index;
		}
		/**
		 * Sets popup selectedelement by index
		 * @method setSelectedIndex
		 * @param {Number} index
		 * @param {bool=} [triggerChangeEvent=false] whether or not trigger dropdown:changed event
		 */
		/**
		 * Dropdown has changed
		 *
		 * @event dropdown:changed
		 */
		function setSelectedIndex(index, triggerChangeEvent) {
			if (selectedOptionElement) {
				DX.Bem.removeModifier(selectedOptionElement, M_SELECTED, CN_OPTION);
			}

			selectedIndex = index;
			selectedOptionElement = optionElements[index];
			if (selectedOptionElement) {
				DX.Bem.addModifier(selectedOptionElement, M_SELECTED, CN_OPTION);
				if (triggerChangeEvent) {
					DX.Event.trigger(elements.block, DropDown.E_CHANGED);
				}
			}
		}
		/**
		 * Sets popup hoveredelement by index
		 * @method setHoveredIndex
		 * @param {Number} index
		 */
		function setHoveredIndex(index) {
			if (hoveredOptionElement) {
				DX.Bem.removeModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);
			}

			index = normalizeIndex(index);

			hoveredIndex = index;
			hoveredOptionElement = optionElements ? optionElements[index] : null;

			if (hoveredOptionElement) {
				DX.Bem.addModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);

			}
		}

		function clearHoveredIndex() {
			if (hoveredOptionElement) {
				DX.Bem.removeModifier(hoveredOptionElement, M_HOVERED, CN_OPTION);
			}
			hoveredIndex = null;
			hoveredOptionElement = null;
		}
		/**
		 * Gets popup selectedelement by index
		 * @method getSelectedIndex
		 * @returns {Number} selectedIndex
		 */
		function getSelectedIndex() {
			return selectedIndex;
		}
		/**
		 * Gets popup hoveredelement by index
		 * @method getHoveredIndex
		 * @returns {Number} hoveredIndex
		 */
		function getHoveredIndex() {
			return hoveredIndex;
		}

		/**
		 * Gets HTMLNode containing dropdown
		 * @method getBlock
		 * @returns {Node}
		 */
		function getBlock() {
			return elements.block;
		}
		/**
		 * Gets element which listens to events
		 * @method getEventTarget
		 * @returns {Node}
		 */
		function getEventTarget() {
			return elements.block;
		}

		function documentClickHandler(e) {
			var target = e.target,
				closestDropDown = DX.Dom.getAscendantByClassName(target, CN_DROPDOWN),
				block = elements.block,
				id = block.getAttribute(A_FOR),
				forElement;

			if (id) {
				forElement = DX.Dom.getAscendantByAttribute(target, 'id', id);
			}

			if (closestDropDown !== block && !forElement) {
				hide();
			}
		}
		function elementsListClickHandler(e) {
			var optionElement = DX.Dom.getAscendantByClassName(e.target, CN_OPTION),
				index;

			if (optionElement) {
				index = Array.prototype.indexOf.call(optionElements, optionElement);

				if (index !== selectedIndex) {
					selectedIndex = index;
					setSelectedIndex(index);
					DX.Event.trigger(elements.block, DropDown.E_CHANGED);
				}

				if (config.hideOnClick) {
					hide();
				}
			}
		}
		/**
		 * Gets whether dropdown is shown
		 * @method isShown
		 * @returns {bool}
		 */
		function isShown() {
			return DX.Bem.hasModifier(elements.block, M_SHOWN, CN_DROPDOWN);
		}

		init();

		this.setDataList = setDataList;
		this.setSelectedIndex = setSelectedIndex;
		this.getSelectedIndex = getSelectedIndex;
		this.setHoveredIndex = setHoveredIndex;
		this.getHoveredIndex = getHoveredIndex;
		this.show = show;
		this.hide = hide;
		this.isShown = isShown;
		this.getBlock = getBlock;
		this.getEventTarget = getEventTarget;
	};
})(DX);

/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_CREATED = 'dropdown:created';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_DESTROYED = 'dropdown:destroyed';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_SHOWN = 'dropdown:shown';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDDEN = 'dropdown:hidden';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_CHANGED = 'dropdown:changed';
/** @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDE = 'dropdown:hide';

/**
 * @constant
 * @type {string}
 * @default
 * @memberof DropDown
 */
DropDown.E_HIDE_ALL = 'dropdown:hideAll';

/**
 * Gets if there is any shown dropdown
 * @method isAnyShown
 * @static
 * @memberof DropDown
 * @returns {Boolean}
 */
DropDown.isAnyShown = function() {
	'use strict';
	return document.querySelectorAll('.dropDown-shown').length > 0;
};

<|endoftext|>


<commit_before>import getItemModelName from 'appkit/utils/model';
import SearchIndex from 'appkit/utils/search-index';

export default Ember.Route.extend({

  beforeModel: function (transition) {

    var EditRoute = this;
    var promises = [];

    var itemId = transition.params['wh.content.type.edit'] && transition.params['wh.content.type.edit'].item_id;

    var contentType = this.modelFor('wh.content.type');
    var modelName = getItemModelName(contentType);

    if (itemId) {
      var lockRef   = window.ENV.firebase.child('presence/locked').child(modelName).child(itemId);

      var userEmail = this.get('session.user.email');

      var lockCheck = new Ember.RSVP.Promise(function (resolve, reject) {
        lockRef.once('value', function (snapshot) {
          var lock = snapshot.val();
          if (lock && typeof lock === 'object' && lock.email !== userEmail) {
            // check for expired lock
            if (moment(lock.time).diff(moment()) > 0) {
              reject(new Ember.Error(lock.email + ' is already editing this item.'));
            } else {
              resolve();
            }
          } else {
            resolve();
          }
        });
      }).then(function () {

        // Unlock on disconnect
        lockRef.onDisconnect().remove();

        EditRoute.addObserver('lockUntil', EditRoute.updateLock);

        EditRoute.set('lockUntil', moment().add(2, 'minutes').format());

        return this.store.find(modelName, itemId).then(function (item) {

          // item found
          this.set('itemModel', item);

        }.bind(this), function (message) {

          // item does not exist

          // create the item if we're a one-off
          if (this.modelFor('wh.content.type').get('oneOff')) {

            // hack to overwrite empty state model that is being put in store from find method
            var item = this.store.getById(modelName, contentType.get('id'));
            item.loadedData();

            this.set('itemModel', item);

            return Ember.RSVP.resolve(item);

          } else {

            lockRef.remove();
            return Ember.RSVP.reject(new Ember.Error(itemId + ' does not exist.'));

          }

        }.bind(this));

      }.bind(this));

      promises.push(lockCheck);

      this.set('lockRef', lockRef);
      this.set('itemId', itemId);

    }

    // need to make sure all the content types are in the store
    // basically a hack
    promises.push(this.store.find('control-type'));

    // make sure `create_date`, `last_updated` and `publish_date` controls exist
    promises.push(this.fixControlType(this.modelFor('wh.content.type')));

    return Ember.RSVP.Promise.all(promises);
    // .catch(function (error) {
    //   window.alert(error.message);
    //   var contentType = this.modelFor('wh.content.type');
    //   if (contentType.get('oneOff')) {
    //     this.transitionTo('wh');
    //   } else {
    //     this.transitionTo('wh.content.type', contentType);
    //   }
    // }.bind(this));
  },

  updateLock: function () {

    var lockUntil = this.get('lockUntil');

    if (Ember.isEmpty(lockUntil)) {
      return;
    }

    this.get('lockRef').set({
      email: this.get('session.user.email'),
      time: lockUntil
    });

    var EditRoute = this;
    var incrementLockTime = Ember.run.later(function () {
      EditRoute.set('lockUntil', moment().add(2, 'minutes').format());
    }, 60000);

    this.set('incrementLockTime', incrementLockTime);

  },

  model: function (params) {
    return this.modelFor('wh.content.type');
  },

  searchName: function () {

    var route = this;
    var type = this.get('context');
    var control = type.get('controls').filterBy('name', 'name').get('firstObject');
    var itemName = control.get('value');

    if (!itemName) {
      return;
    }

    var item = this.get('itemModel');

    SearchIndex.search(itemName, 1, type.get('id')).then(function (results) {
      results.forEach(function (result) {
        if ((Ember.isNone(item) || (item && item.get('id') !== result.id)) && itemName.toLowerCase() === Ember.$('<span>').html(result.name).text().toLowerCase()) {
          control.set('widgetIsValid', false);
          control.get('widgetErrors').pushObject(route.get('dupeNameError'));
        }
      });
    });

  },

  dupeNameCheck: function () {

    var type = this.get('context');
    var control = type.get('controls').filterBy('name', 'name').get('firstObject');

    if (this.get('isObservingName')) {
      control.get('widgetErrors').removeObjects([
        'This field is required',
        this.get('dupeNameError')
      ]);
      if (!control.get('widgetErrors.length')) {
        control.set('widgetIsValid', true);
      }

      Ember.run.debounce(this, this.searchName, 1000);

    } else {

      this.set('isObservingName', true);

    }
  },

  setupController: function (controller, type) {

    this._super.apply(this, arguments);

    var route = this;

    this.set('dupeNameError', 'Name must be unique among ' + type.get('name') + ' entries.');

    controller.set('showSchedule', false);
    controller.set('itemModel', this.get('itemModel'));
    controller.set('initialRelations', Ember.Object.create());

    var data = this.getWithDefault('itemModel.itemData', {});

    type.get('controls').forEach(function (control) {

      control.set('widgetErrors', Ember.A([]));

      var value = data[control.get('name')];

      // Use search to check for duplicate names
      if (control.get('name') === 'name') {
        control.addObserver('value', route.dupeNameCheck.bind(route));
        controller.set('nameControl', control);
      }

      if (control.get('name') === 'slug') {
        controller.set('slugControl', control);
        controller.set('isEditingSlug', false);
      }

      if (control.get('controlType.widget') === 'checkbox') {
        control.get('meta.options').forEach(function (option) {
          if (value && value.findBy('label', option.label)) {
            option.value = value.findBy('label', option.label).value;
          }
        });
      }

      // remove offset so datetime input can display
      if (value && control.get('controlType.widget') === 'datetime') {
        value = moment(value).format('YYYY-MM-DDTHH:mm');
      }

      if (control.get('controlType.widget') === 'tabular') {
        if (!value) {
          value = Ember.A([]);
          var emptyRow = Ember.A([]);
          control.get('meta.options').forEach(function () {
            emptyRow.pushObject("");
          });
          value.pushObject(emptyRow);
        } else {
          // we must convert data into mutable form
          var mutableValue = Ember.A([]);
          value.forEach(function (row) {
            var mutableData = Ember.A([]);
            row.forEach(function (data) {
              mutableData.pushObject({
                value: data
              });
            });
            mutableValue.pushObject(mutableData);
          });
          value = mutableValue;
        }
      }

      if (value && control.get('controlType.widget') === 'relation') {
        if (value && !Ember.isArray(value)) {
          value = Ember.A([value]);
        }
        // Remember what the initial relations are so we can check for diffs on save.
        controller.get('initialRelations').set(control.get('name'), Ember.copy(value));
      }

      if (!value && control.get('controlType.valueType') === 'object') {
        value = {};
      }

      control.set('value', value);
    });

    controller.set('publishDate', type.get('controls').findBy('name', 'publish_date').get('value'));

    if (type.get('oneOff')) {
      controller.set('isDraft', null);
    } else {
      controller.set('isDraft', data.isDraft || !controller.get('publishDate'));
    }

    controller.set('type', type);

    controller.set('previewUrl', null);

    // watch for value changes so we can prevent user from accidentally leaving
    controller.set('initialValues', type.get('controls').getEach('value'));
  },

  fixControlType: function (contentType) {

    return this.store.find('control-type', 'datetime').then(function (controlType) {

      var datetimeDefaults = {
        controlType: controlType,
        locked     : true,
        showInCms  : true,
        required   : true,
        hidden     : true
      };

      var controls = contentType.get('controls'),
          save = false;

      var addControl = function (data) {
        var control = this.store.createRecord('control', Ember.$.extend({}, datetimeDefaults, data));
        control.set('widgetIsValid', true);
        controls.pushObject(control);
        save = true;
      }.bind(this);

      if (!controls.isAny('name', 'create_date')) {
        addControl({
          name : 'create_date',
          label: 'Create Date'
        });
      }

      if (!controls.isAny('name', 'last_updated')) {
        addControl({
          name : 'last_updated',
          label: 'Last Updated'
        });
      }

      if (!controls.isAny('name', 'publish_date')) {
        addControl({
          name : 'publish_date',
          label: 'Publish Date',
          required: false
        });
      }

      if (!controls.isAny('name', 'preview_url')) {
        addControl({
          controlType: this.store.getById('control-type', 'textfield'),
          name       : 'preview_url',
          label      : 'Preview URL',
          showInCms  : false
        });
      }

      if (!controls.isAny('name', 'slug')) {
        addControl({
          controlType: this.store.getById('control-type', 'textfield'),
          name       : 'slug',
          label      : 'Slug',
          showInCms  : false,
          required   : false
        });
      }

      if (save) {
        contentType.save();
      }

    }.bind(this));
  },

  actions: {
    willTransition: function (transition) {

      if (this.get('controller.isDirty') && !window.confirm('You have changes that have not been saved, are you sure you would like to leave?')) {
        transition.abort();
        return;
      }

      this.get('controller').removeObserver('type.controls.@each.value');
      this.set('controller.isDirty', false);

      this.get('controller.type.controls').findBy('name', 'name').removeObserver('value');
      this.set('isObservingName', false);

      // Unlock on transition
      this.removeObserver('lockUntil', this.updateLock);
      this.set('lockUntil', null);
      if (this.get('lockRef')) {
        this.get('lockRef').remove();
      }

      return true;
    }
  }
});
<commit_msg>re-enable transitioning on error<commit_after>import getItemModelName from 'appkit/utils/model';
import SearchIndex from 'appkit/utils/search-index';

export default Ember.Route.extend({

  beforeModel: function (transition) {

    var EditRoute = this;
    var promises = [];

    var itemId = transition.params['wh.content.type.edit'] && transition.params['wh.content.type.edit'].item_id;

    var contentType = this.modelFor('wh.content.type');
    var modelName = getItemModelName(contentType);

    if (itemId) {
      var lockRef   = window.ENV.firebase.child('presence/locked').child(modelName).child(itemId);

      var userEmail = this.get('session.user.email');

      var lockCheck = new Ember.RSVP.Promise(function (resolve, reject) {
        lockRef.once('value', function (snapshot) {
          var lock = snapshot.val();
          if (lock && typeof lock === 'object' && lock.email !== userEmail) {
            // check for expired lock
            if (moment(lock.time).diff(moment()) > 0) {
              reject(new Ember.Error(lock.email + ' is already editing this item.'));
            } else {
              resolve();
            }
          } else {
            resolve();
          }
        });
      }).then(function () {

        // Unlock on disconnect
        lockRef.onDisconnect().remove();

        EditRoute.addObserver('lockUntil', EditRoute.updateLock);

        EditRoute.set('lockUntil', moment().add(2, 'minutes').format());

        return this.store.find(modelName, itemId).then(function (item) {

          // item found
          this.set('itemModel', item);

        }.bind(this), function (message) {

          // item does not exist

          // create the item if we're a one-off
          if (this.modelFor('wh.content.type').get('oneOff')) {

            // hack to overwrite empty state model that is being put in store from find method
            var item = this.store.getById(modelName, contentType.get('id'));
            item.loadedData();

            this.set('itemModel', item);

            return Ember.RSVP.resolve(item);

          } else {

            lockRef.remove();
            return Ember.RSVP.reject(new Ember.Error(itemId + ' does not exist.'));

          }

        }.bind(this));

      }.bind(this));

      promises.push(lockCheck);

      this.set('lockRef', lockRef);
      this.set('itemId', itemId);

    }

    // need to make sure all the content types are in the store
    // basically a hack
    promises.push(this.store.find('control-type'));

    // make sure `create_date`, `last_updated` and `publish_date` controls exist
    promises.push(this.fixControlType(this.modelFor('wh.content.type')));

    return Ember.RSVP.Promise.all(promises).catch(function (error) {
      window.alert(error.message);
      var contentType = this.modelFor('wh.content.type');
      if (contentType.get('oneOff')) {
        this.transitionTo('wh');
      } else {
        this.transitionTo('wh.content.type', contentType);
      }
    }.bind(this));
  },

  updateLock: function () {

    var lockUntil = this.get('lockUntil');

    if (Ember.isEmpty(lockUntil)) {
      return;
    }

    this.get('lockRef').set({
      email: this.get('session.user.email'),
      time: lockUntil
    });

    var EditRoute = this;
    var incrementLockTime = Ember.run.later(function () {
      EditRoute.set('lockUntil', moment().add(2, 'minutes').format());
    }, 60000);

    this.set('incrementLockTime', incrementLockTime);

  },

  model: function (params) {
    return this.modelFor('wh.content.type');
  },

  searchName: function () {

    var route = this;
    var type = this.get('context');
    var control = type.get('controls').filterBy('name', 'name').get('firstObject');
    var itemName = control.get('value');

    if (!itemName) {
      return;
    }

    var item = this.get('itemModel');

    SearchIndex.search(itemName, 1, type.get('id')).then(function (results) {
      results.forEach(function (result) {
        if ((Ember.isNone(item) || (item && item.get('id') !== result.id)) && itemName.toLowerCase() === Ember.$('<span>').html(result.name).text().toLowerCase()) {
          control.set('widgetIsValid', false);
          control.get('widgetErrors').pushObject(route.get('dupeNameError'));
        }
      });
    });

  },

  dupeNameCheck: function () {

    var type = this.get('context');
    var control = type.get('controls').filterBy('name', 'name').get('firstObject');

    if (this.get('isObservingName')) {
      control.get('widgetErrors').removeObjects([
        'This field is required',
        this.get('dupeNameError')
      ]);
      if (!control.get('widgetErrors.length')) {
        control.set('widgetIsValid', true);
      }

      Ember.run.debounce(this, this.searchName, 1000);

    } else {

      this.set('isObservingName', true);

    }
  },

  setupController: function (controller, type) {

    this._super.apply(this, arguments);

    var route = this;

    this.set('dupeNameError', 'Name must be unique among ' + type.get('name') + ' entries.');

    controller.set('showSchedule', false);
    controller.set('itemModel', this.get('itemModel'));
    controller.set('initialRelations', Ember.Object.create());

    var data = this.getWithDefault('itemModel.itemData', {});

    type.get('controls').forEach(function (control) {

      control.set('widgetErrors', Ember.A([]));

      var value = data[control.get('name')];

      // Use search to check for duplicate names
      if (control.get('name') === 'name') {
        control.addObserver('value', route.dupeNameCheck.bind(route));
        controller.set('nameControl', control);
      }

      if (control.get('name') === 'slug') {
        controller.set('slugControl', control);
        controller.set('isEditingSlug', false);
      }

      if (control.get('controlType.widget') === 'checkbox') {
        control.get('meta.options').forEach(function (option) {
          if (value && value.findBy('label', option.label)) {
            option.value = value.findBy('label', option.label).value;
          }
        });
      }

      // remove offset so datetime input can display
      if (value && control.get('controlType.widget') === 'datetime') {
        value = moment(value).format('YYYY-MM-DDTHH:mm');
      }

      if (control.get('controlType.widget') === 'tabular') {
        if (!value) {
          value = Ember.A([]);
          var emptyRow = Ember.A([]);
          control.get('meta.options').forEach(function () {
            emptyRow.pushObject("");
          });
          value.pushObject(emptyRow);
        } else {
          // we must convert data into mutable form
          var mutableValue = Ember.A([]);
          value.forEach(function (row) {
            var mutableData = Ember.A([]);
            row.forEach(function (data) {
              mutableData.pushObject({
                value: data
              });
            });
            mutableValue.pushObject(mutableData);
          });
          value = mutableValue;
        }
      }

      if (value && control.get('controlType.widget') === 'relation') {
        if (value && !Ember.isArray(value)) {
          value = Ember.A([value]);
        }
        // Remember what the initial relations are so we can check for diffs on save.
        controller.get('initialRelations').set(control.get('name'), Ember.copy(value));
      }

      if (!value && control.get('controlType.valueType') === 'object') {
        value = {};
      }

      control.set('value', value);
    });

    controller.set('publishDate', type.get('controls').findBy('name', 'publish_date').get('value'));

    if (type.get('oneOff')) {
      controller.set('isDraft', null);
    } else {
      controller.set('isDraft', data.isDraft || !controller.get('publishDate'));
    }

    controller.set('type', type);

    controller.set('previewUrl', null);

    // watch for value changes so we can prevent user from accidentally leaving
    controller.set('initialValues', type.get('controls').getEach('value'));
  },

  fixControlType: function (contentType) {

    return this.store.find('control-type', 'datetime').then(function (controlType) {

      var datetimeDefaults = {
        controlType: controlType,
        locked     : true,
        showInCms  : true,
        required   : true,
        hidden     : true
      };

      var controls = contentType.get('controls'),
          save = false;

      var addControl = function (data) {
        var control = this.store.createRecord('control', Ember.$.extend({}, datetimeDefaults, data));
        control.set('widgetIsValid', true);
        controls.pushObject(control);
        save = true;
      }.bind(this);

      if (!controls.isAny('name', 'create_date')) {
        addControl({
          name : 'create_date',
          label: 'Create Date'
        });
      }

      if (!controls.isAny('name', 'last_updated')) {
        addControl({
          name : 'last_updated',
          label: 'Last Updated'
        });
      }

      if (!controls.isAny('name', 'publish_date')) {
        addControl({
          name : 'publish_date',
          label: 'Publish Date',
          required: false
        });
      }

      if (!controls.isAny('name', 'preview_url')) {
        addControl({
          controlType: this.store.getById('control-type', 'textfield'),
          name       : 'preview_url',
          label      : 'Preview URL',
          showInCms  : false
        });
      }

      if (!controls.isAny('name', 'slug')) {
        addControl({
          controlType: this.store.getById('control-type', 'textfield'),
          name       : 'slug',
          label      : 'Slug',
          showInCms  : false,
          required   : false
        });
      }

      if (save) {
        contentType.save();
      }

    }.bind(this));
  },

  actions: {
    willTransition: function (transition) {

      if (this.get('controller.isDirty') && !window.confirm('You have changes that have not been saved, are you sure you would like to leave?')) {
        transition.abort();
        return;
      }

      this.get('controller').removeObserver('type.controls.@each.value');
      this.set('controller.isDirty', false);

      this.get('controller.type.controls').findBy('name', 'name').removeObserver('value');
      this.set('isObservingName', false);

      // Unlock on transition
      this.removeObserver('lockUntil', this.updateLock);
      this.set('lockUntil', null);
      if (this.get('lockRef')) {
        this.get('lockRef').remove();
      }

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


<commit_before>/**
 * `modules/analytics` data store: properties.
 *
 * Site Kit by Google, Copyright 2021 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';
import { actions as errorStoreActions } from '../../../googlesitekit/data/create-error-store';

// Get access to error store action creators.
// If the parent store doesn't include the error store,
// yielded error actions will be a no-op.
const { clearError, receiveError } = errorStoreActions;
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/acronym-case
			}

			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/acronym-case
				registry.dispatch( STORE_NAME ).setProfileID( property.defaultProfileId ); // eslint-disable-line sitekit/acronym-case
				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/acronym-case
			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();
		yield clearError( 'getProperties', [ accountID ] );

		// Only fetch properties if there are none in the store for the given account.
		let properties = registry.select( STORE_NAME ).getProperties( accountID );
		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/acronym-case
				if ( response.profiles?.[ 0 ]?.webPropertyId ) {
					// eslint-disable-next-line sitekit/acronym-case
					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 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/acronym-case
		}
	},
};

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>Save previous item.<commit_after>/**
 * `modules/analytics` data store: properties.
 *
 * Site Kit by Google, Copyright 2021 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';
import { actions as errorStoreActions } from '../../../googlesitekit/data/create-error-store';

// Get access to error store action creators.
// If the parent store doesn't include the error store,
// yielded error actions will be a no-op.
const { clearError, receiveError } = errorStoreActions;
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/acronym-case
			}

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

			const existingProfileID = registry.dispatch( STORE_NAME ).getProfileID(); // eslint-disable-line @wordpress/no-unused-vars-before-return

			// 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/acronym-case
				registry.dispatch( STORE_NAME ).setProfileID( property.defaultProfileId ); // eslint-disable-line sitekit/acronym-case
				return;
			}

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

			let profile = profiles.find( ( { id: ID } ) => ID === existingProfileID );

			if ( ! profile ) {
				profile = profiles.find( ( { webPropertyId } ) => webPropertyId === propertyID ) || { id: PROFILE_CREATE }; // eslint-disable-line sitekit/acronym-case
			}

			registry.dispatch( STORE_NAME ).setProfileID( profile.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();
		yield clearError( 'getProperties', [ accountID ] );

		// Only fetch properties if there are none in the store for the given account.
		let properties = registry.select( STORE_NAME ).getProperties( accountID );
		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/acronym-case
				if ( response.profiles?.[ 0 ]?.webPropertyId ) {
					// eslint-disable-next-line sitekit/acronym-case
					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 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/acronym-case
		}
	},
};

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>(function (_, metrics) {
    'use strict';

    function DashboardController($scope, $interval, $http, metricsEndpoint, metricsService) {
        var updatesInterval;

        function startUpdates() {
            updatesInterval = $interval(updateMetrics, 500);
        }

        function stopUpdates() {
            if (updatesInterval) {
                $interval.cancel(updatesInterval);
            }
        }

        function updateMetrics() {
            $http.get(metricsEndpoint).success(function (data) {
                metricsService.update(data);
                updateDashboard();
            });
        }

        function updateDashboard() {
            $scope.metricNames = metricsService.getSeriesNames();

            _($scope.charts).each(function (c) {
                c.update(metricsService);
            });
        }

        $scope.createChart = function () {
            if ($scope.currentChart.hasMetrics()) {
                $scope.charts.splice(0, 0, $scope.currentChart);
                $scope.currentChart = new metrics.Chart();
            }
        };

        $scope.createChartFromMetric = function (name) {
            $scope.charts.splice(0, 0, new metrics.Chart(name, [name]));
        };

        $scope.addToChart = function () {
            $scope.currentChart.addMetric($scope.metricSearch);
            $scope.metricSearch = '';
        };

        $scope.removeChart = function (chart) {
            $scope.charts.splice($scope.charts.indexOf(chart), 1);
        };

        $scope.metricSearch = '';

        $scope.currentChart = new metrics.Chart();

        $scope.charts = [new metrics.Chart('CPU Usage', ['System.CPU Usage'])];

        $scope.$on('$destroy', function () {
            stopUpdates();
        });

        startUpdates();
    }

    $.extend(true, this, { metrics: { DashboardController: DashboardController } });

}).call(this, this._, this.metrics);<commit_msg>added alert if app is unable to get json data.<commit_after>(function (_, metrics) {
	'use strict';

	var alertShown = false;

	function DashboardController($scope, $interval, $http, metricsEndpoint, metricsService) {
		var updatesInterval;

		function startUpdates() {
			updatesInterval = $interval(updateMetrics, 500);
		}

		function stopUpdates() {
			if (updatesInterval) {
				$interval.cancel(updatesInterval);
			}
		}

		function updateMetrics() {
			$http.get(metricsEndpoint).success(function (data) {
				metricsService.update(data);
				updateDashboard();
			}).error(function (reason) {
				stopUpdates();
				if (!alertShown) {
					alertShown = true;
					alert("Error reading JSON data from [" + metricsEndpoint + "]. " + reason);
				}
			});
		}

		function updateDashboard() {
			$scope.metricNames = metricsService.getSeriesNames();

			_($scope.charts).each(function (c) {
				c.update(metricsService);
			});
		}

		$scope.createChart = function () {
			if ($scope.currentChart.hasMetrics()) {
				$scope.charts.splice(0, 0, $scope.currentChart);
				$scope.currentChart = new metrics.Chart();
			}
		};

		$scope.createChartFromMetric = function (name) {
			$scope.charts.splice(0, 0, new metrics.Chart(name, [name]));
		};

		$scope.addToChart = function () {
			$scope.currentChart.addMetric($scope.metricSearch);
			$scope.metricSearch = '';
		};

		$scope.removeChart = function (chart) {
			$scope.charts.splice($scope.charts.indexOf(chart), 1);
		};

		$scope.metricSearch = '';

		$scope.currentChart = new metrics.Chart();

		$scope.charts = [new metrics.Chart('CPU Usage', ['System.CPU Usage'])];

		$scope.$on('$destroy', function () {
			stopUpdates();
		});

		startUpdates();
	}

	$.extend(true, this, { metrics: { DashboardController: DashboardController } });

}).call(this, this._, this.metrics);<|endoftext|>