<commit_before>import Controller from '@ember/controller';
import config from 'ghost-admin/config/environment';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {A} from '@ember/array';
import {action} from '@ember/object';
import {capitalize} from '@ember/string';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const PAID_PARAMS = [{
    name: 'All members',
    value: null
}, {
    name: 'Free members',
    value: 'false'
}, {
    name: 'Paid members',
    value: 'true'
}];

export default class MembersController extends Controller {
    @service ajax;
    @service config;
    @service ellaSparse;
    @service feature;
    @service ghostPaths;
    @service membersStats;
    @service router;
    @service store;

    queryParams = [
        'label',
        {paidParam: 'paid'},
        {searchParam: 'search'},
        {orderParam: 'order'},
        {filterParam: 'filter'}
    ];

    @tracked members = A([]);
    @tracked searchText = '';
    @tracked searchParam = '';
    @tracked filterParam = null;
    @tracked paidParam = null;
    @tracked label = null;
    @tracked orderParam = null;
    @tracked modalLabel = null;
    @tracked showLabelModal = false;
    @tracked showDeleteMembersModal = false;
    @tracked showUnsubscribeMembersModal = false;
    @tracked showAddMembersLabelModal = false;
    @tracked showRemoveMembersLabelModal = false;
    @tracked filters = A([]);

    @tracked _availableLabels = A([]);

    paidParams = PAID_PARAMS;

    constructor() {
        super(...arguments);
        this._availableLabels = this.store.peekAll('label');

        if (this.isTesting === undefined) {
            this.isTesting = config.environment === 'test';
        }
    }

    // Computed properties -----------------------------------------------------

    get listHeader() {
        let {searchText, selectedLabel, members} = this;

        if (members.loading) {
            return 'Loading...';
        }

        if (searchText) {
            return 'Search result';
        }

        let count = ghPluralize(members.length, 'member');

        if (selectedLabel && selectedLabel.slug) {
            if (members.length > 1) {
                return `${count} match current filter`;
            } else {
                return `${count} matches current filter`;
            }
        }

        return count;
    }

    get showingAll() {
        return !this.searchParam && !this.paidParam && !this.label && !this.filterParam;
    }

    get availableOrders() {
        // don't return anything if email analytics is disabled because
        // we don't want to show an order dropdown with only a single option

        if (this.feature.get('emailAnalytics')) {
            return [{
                name: 'Newest',
                value: null
            }, {
                name: 'Open rate',
                value: 'email_open_rate'
            }];
        }

        return [];
    }

    get selectedOrder() {
        return this.availableOrders.find(order => order.value === this.orderParam);
    }

    get availableLabels() {
        let labels = this._availableLabels
            .filter(label => !label.isNew)
            .filter(label => label.id !== null)
            .sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true}));
        let options = labels.toArray();

        options.unshiftObject({name: 'All labels', slug: null});

        return options;
    }

    get selectedLabel() {
        let {label, availableLabels} = this;
        return availableLabels.findBy('slug', label);
    }

    get labelModalData() {
        let label = this.modalLabel;
        let labels = this.availableLabels;

        return {
            label,
            labels
        };
    }

    get selectedPaidParam() {
        return this.paidParams.findBy('value', this.paidParam) || {value: '!unknown'};
    }

    get isFiltered() {
        return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
    }

    get filterColumns() {
        const defaultColumns = ['name', 'email'];
        return this.filters.map((filter) => {
            return filter.type;
        }).filter((f, idx, arr) => {
            return arr.indexOf(f) === idx;
        }).filter(d => !defaultColumns.includes(d));
    }

    get filterColumnLabels() {
        const filterColumnLabelMap = {
            'subscriptions.plan_interval': 'Billing period',
            subscribed: 'Subscribed to email',
            'subscriptions.status': 'Subscription Status'
        };
        return this.filterColumns.map((d) => {
            return filterColumnLabelMap[d] ? filterColumnLabelMap[d] : capitalize(d.replace(/_/g, ' '));
        });
    }

    getApiQueryObject({params, extraFilters = []} = {}) {
        let {label, paidParam, searchParam, filterParam} = params ? params : this;

        let filters = [];

        filters = filters.concat(extraFilters);

        if (label) {
            filters.push(`label:'${label}'`);
        }

        if (paidParam !== null) {
            if (paidParam === 'true') {
                filters.push('status:-free');
            } else {
                filters.push('status:free');
            }
        }

        if (filterParam && this.feature.get('membersFiltering')) {
            filters.push(filterParam);
        }

        let searchQuery = searchParam ? {search: searchParam} : {};

        return Object.assign({}, {filter: filters.join('+')}, searchQuery);
    }

    // Actions -----------------------------------------------------------------

    @action
    refreshData() {
        this.fetchMembersTask.perform();
        this.fetchLabelsTask.perform();
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
    }

    @action
    changeOrder(order) {
        this.orderParam = order.value;
    }

    @action
    applyFilter(filterStr, filters) {
        this.filters = filters;
        this.filterParam = filterStr || null;
    }

    @action
    resetFilter() {
        this.filters = A([]);
        this.filterParam = null;
    }

    @action
    search(e) {
        this.searchTask.perform(e.target.value);
    }

    @action
    exportData() {
        let exportUrl = ghostPaths().url.api('members/upload');
        let downloadParams = new URLSearchParams(this.getApiQueryObject());
        downloadParams.set('limit', 'all');

        let iframe = document.getElementById('iframeDownload');

        if (!iframe) {
            iframe = document.createElement('iframe');
            iframe.id = 'iframeDownload';
            iframe.style.display = 'none';
            document.body.append(iframe);
        }
        iframe.setAttribute('src', `${exportUrl}?${downloadParams.toString()}`);
    }

    @action
    changeLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        this.label = label.slug;
    }

    @action
    addLabel(e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        const newLabel = this.store.createRecord('label');
        this.modalLabel = newLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    editLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        let modalLabel = this.availableLabels.findBy('slug', label);
        this.modalLabel = modalLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    toggleLabelModal() {
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    changePaidParam(paid) {
        this.paidParam = paid.value;
    }

    @action
    toggleDeleteMembersModal() {
        this.showDeleteMembersModal = !this.showDeleteMembersModal;
    }

    @action
    toggleUnsubscribeMembersModal() {
        this.showUnsubscribeMembersModal = !this.showUnsubscribeMembersModal;
    }

    @action
    toggleAddMembersLabelModal() {
        this.showAddMembersLabelModal = !this.showAddMembersLabelModal;
    }

    @action
    toggleRemoveMembersLabelModal() {
        this.showRemoveMembersLabelModal = !this.showRemoveMembersLabelModal;
    }

    @action
    deleteMembers() {
        return this.deleteMembersTask.perform();
    }

    @action
    unsubscribeMembers() {
        return this.unsubscribeMembersTask.perform();
    }

    @action
    addLabelToMembers(selectedLabel) {
        return this.addLabelToMembersTask.perform(selectedLabel);
    }

    @action
    removeLabelFromMembers(selectedLabel) {
        return this.removeLabelFromMembersTask.perform(selectedLabel);
    }

    // Tasks -------------------------------------------------------------------

    @task({restartable: true})
    *searchTask(query) {
        yield timeout(250); // debounce
        this.searchParam = query;
    }

    @task
    *fetchLabelsTask() {
        yield this.store.query('label', {limit: 'all'});
    }

    @task({restartable: true})
    *fetchMembersTask(params) {
        // params is undefined when called as a "refresh" of the model
        let {label, paidParam, searchParam, orderParam, filterParam} = typeof params === 'undefined' ? this : params;

        if (!searchParam) {
            this.resetSearch();
        }

        // use a fixed created_at date so that subsequent pages have a consistent index
        let startDate = new Date();

        // bypass the stale data shortcut if params change
        let forceReload = !params
            || label !== this._lastLabel
            || paidParam !== this._lastPaidParam
            || searchParam !== this._lastSearchParam
            || orderParam !== this._lastOrderParam
            || filterParam !== this._lastFilterParam;
        this._lastLabel = label;
        this._lastPaidParam = paidParam;
        this._lastSearchParam = searchParam;
        this._lastOrderParam = orderParam;
        this._lastFilterParam = filterParam;

        // unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
        // keeps navigation between list->details->list snappy
        if (!forceReload && this._startDate && !(this._startDate - startDate > 1 * 60 * 1000)) {
            return this.members;
        }

        this._startDate = startDate;

        this.members = yield this.ellaSparse.array((range = {}, query = {}) => {
            const searchQuery = this.getApiQueryObject({
                params,
                extraFilters: [`created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`]
            });
            const order = orderParam ? `${orderParam} desc` : `created_at desc`;

            query = Object.assign({
                order,
                limit: range.length,
                page: range.page
            }, searchQuery, query);

            return this.store.query('member', query).then((result) => {
                return {
                    data: result,
                    total: result.meta.pagination.total
                };
            });
        }, {
            limit: 50
        });
    }

    @task({drop: true})
    *deleteMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());

        // Trigger download before deleting. Uses the CSV export endpoint but
        // needs to fetch the file and trigger a download directly rather than
        // via an iframe. The iframe approach can't tell us when a download has
        // started/finished meaning we could end up deleting the data before exporting it
        const exportUrl = ghostPaths().url.api('members/upload');
        const exportParams = new URLSearchParams(this.getApiQueryObject());
        exportParams.set('limit', 'all');

        yield fetch(exportUrl, {method: 'GET'})
            .then(res => res.blob())
            .then((blob) => {
                const blobUrl = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = `members.${moment().format('YYYY-MM-DD')}.csv`;
                document.body.appendChild(a);
                if (!this.isTesting) {
                    a.click();
                }
                a.remove();
                URL.revokeObjectURL(blobUrl);
            });

        // backup downloaded, continue with deletion

        const deleteUrl = `${this.ghostPaths.url.api('members')}?${query}`;

        // response contains details of which members failed to be deleted
        const response = yield this.ajax.del(deleteUrl);

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *unsubscribeMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());
        const unsubscribeUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        // response contains details of which members failed to be unsubscribe
        const response = yield this.ajax.put(unsubscribeUrl, {
            data: {
                action: 'unsubscribe',
                meta: {}
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *addLabelToMembersTask(selectedLabel) {
        const query = new URLSearchParams(this.getApiQueryObject());
        const addLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        const response = yield this.ajax.put(addLabelUrl, {
            data: {
                action: 'addLabel',
                meta: {
                    label: {
                        id: selectedLabel
                    }
                }
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *removeLabelFromMembersTask(selectedLabel) {
        const query = new URLSearchParams(this.getApiQueryObject());
        const removeLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        const response = yield this.ajax.put(removeLabelUrl, {
            data: {
                action: 'removeLabel',
                meta: {
                    label: {
                        id: selectedLabel
                    }
                }
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }
    // Internal ----------------------------------------------------------------

    resetSearch() {
        this.searchText = '';
    }

    reset() {
        this.filterParam = null;
    }

    reload(params) {
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
        this.fetchMembersTask.perform(params);
    }
}
<commit_msg>Retained filter query on members list after bulk operation<commit_after>import Controller from '@ember/controller';
import config from 'ghost-admin/config/environment';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {A} from '@ember/array';
import {action} from '@ember/object';
import {capitalize} from '@ember/string';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const PAID_PARAMS = [{
    name: 'All members',
    value: null
}, {
    name: 'Free members',
    value: 'false'
}, {
    name: 'Paid members',
    value: 'true'
}];

export default class MembersController extends Controller {
    @service ajax;
    @service config;
    @service ellaSparse;
    @service feature;
    @service ghostPaths;
    @service membersStats;
    @service router;
    @service store;

    queryParams = [
        'label',
        {paidParam: 'paid'},
        {searchParam: 'search'},
        {orderParam: 'order'},
        {filterParam: 'filter'}
    ];

    @tracked members = A([]);
    @tracked searchText = '';
    @tracked searchParam = '';
    @tracked filterParam = null;
    @tracked paidParam = null;
    @tracked label = null;
    @tracked orderParam = null;
    @tracked modalLabel = null;
    @tracked showLabelModal = false;
    @tracked showDeleteMembersModal = false;
    @tracked showUnsubscribeMembersModal = false;
    @tracked showAddMembersLabelModal = false;
    @tracked showRemoveMembersLabelModal = false;
    @tracked filters = A([]);

    @tracked _availableLabels = A([]);

    paidParams = PAID_PARAMS;

    constructor() {
        super(...arguments);
        this._availableLabels = this.store.peekAll('label');

        if (this.isTesting === undefined) {
            this.isTesting = config.environment === 'test';
        }
    }

    // Computed properties -----------------------------------------------------

    get listHeader() {
        let {searchText, selectedLabel, members} = this;

        if (members.loading) {
            return 'Loading...';
        }

        if (searchText) {
            return 'Search result';
        }

        let count = ghPluralize(members.length, 'member');

        if (selectedLabel && selectedLabel.slug) {
            if (members.length > 1) {
                return `${count} match current filter`;
            } else {
                return `${count} matches current filter`;
            }
        }

        return count;
    }

    get showingAll() {
        return !this.searchParam && !this.paidParam && !this.label && !this.filterParam;
    }

    get availableOrders() {
        // don't return anything if email analytics is disabled because
        // we don't want to show an order dropdown with only a single option

        if (this.feature.get('emailAnalytics')) {
            return [{
                name: 'Newest',
                value: null
            }, {
                name: 'Open rate',
                value: 'email_open_rate'
            }];
        }

        return [];
    }

    get selectedOrder() {
        return this.availableOrders.find(order => order.value === this.orderParam);
    }

    get availableLabels() {
        let labels = this._availableLabels
            .filter(label => !label.isNew)
            .filter(label => label.id !== null)
            .sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true}));
        let options = labels.toArray();

        options.unshiftObject({name: 'All labels', slug: null});

        return options;
    }

    get selectedLabel() {
        let {label, availableLabels} = this;
        return availableLabels.findBy('slug', label);
    }

    get labelModalData() {
        let label = this.modalLabel;
        let labels = this.availableLabels;

        return {
            label,
            labels
        };
    }

    get selectedPaidParam() {
        return this.paidParams.findBy('value', this.paidParam) || {value: '!unknown'};
    }

    get isFiltered() {
        return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
    }

    get filterColumns() {
        const defaultColumns = ['name', 'email'];
        return this.filters.map((filter) => {
            return filter.type;
        }).filter((f, idx, arr) => {
            return arr.indexOf(f) === idx;
        }).filter(d => !defaultColumns.includes(d));
    }

    get filterColumnLabels() {
        const filterColumnLabelMap = {
            'subscriptions.plan_interval': 'Billing period',
            subscribed: 'Subscribed to email',
            'subscriptions.status': 'Subscription Status'
        };
        return this.filterColumns.map((d) => {
            return filterColumnLabelMap[d] ? filterColumnLabelMap[d] : capitalize(d.replace(/_/g, ' '));
        });
    }

    getApiQueryObject({params, extraFilters = []} = {}) {
        let {label, paidParam, searchParam, filterParam} = params ? params : this;

        let filters = [];

        filters = filters.concat(extraFilters);

        if (label) {
            filters.push(`label:'${label}'`);
        }

        if (paidParam !== null) {
            if (paidParam === 'true') {
                filters.push('status:-free');
            } else {
                filters.push('status:free');
            }
        }

        if (filterParam && this.feature.get('membersFiltering')) {
            filters.push(filterParam);
        }

        let searchQuery = searchParam ? {search: searchParam} : {};

        return Object.assign({}, {filter: filters.join('+')}, searchQuery);
    }

    // Actions -----------------------------------------------------------------

    @action
    refreshData() {
        this.fetchMembersTask.perform();
        this.fetchLabelsTask.perform();
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
    }

    @action
    changeOrder(order) {
        this.orderParam = order.value;
    }

    @action
    applyFilter(filterStr, filters) {
        this.filters = filters;
        this.filterParam = filterStr || null;
    }

    @action
    resetFilter() {
        this.filters = A([]);
        this.filterParam = null;
    }

    @action
    search(e) {
        this.searchTask.perform(e.target.value);
    }

    @action
    exportData() {
        let exportUrl = ghostPaths().url.api('members/upload');
        let downloadParams = new URLSearchParams(this.getApiQueryObject());
        downloadParams.set('limit', 'all');

        let iframe = document.getElementById('iframeDownload');

        if (!iframe) {
            iframe = document.createElement('iframe');
            iframe.id = 'iframeDownload';
            iframe.style.display = 'none';
            document.body.append(iframe);
        }
        iframe.setAttribute('src', `${exportUrl}?${downloadParams.toString()}`);
    }

    @action
    changeLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        this.label = label.slug;
    }

    @action
    addLabel(e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        const newLabel = this.store.createRecord('label');
        this.modalLabel = newLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    editLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        let modalLabel = this.availableLabels.findBy('slug', label);
        this.modalLabel = modalLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    toggleLabelModal() {
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    changePaidParam(paid) {
        this.paidParam = paid.value;
    }

    @action
    toggleDeleteMembersModal() {
        this.showDeleteMembersModal = !this.showDeleteMembersModal;
    }

    @action
    toggleUnsubscribeMembersModal() {
        this.showUnsubscribeMembersModal = !this.showUnsubscribeMembersModal;
    }

    @action
    toggleAddMembersLabelModal() {
        this.showAddMembersLabelModal = !this.showAddMembersLabelModal;
    }

    @action
    toggleRemoveMembersLabelModal() {
        this.showRemoveMembersLabelModal = !this.showRemoveMembersLabelModal;
    }

    @action
    deleteMembers() {
        return this.deleteMembersTask.perform();
    }

    @action
    unsubscribeMembers() {
        return this.unsubscribeMembersTask.perform();
    }

    @action
    addLabelToMembers(selectedLabel) {
        return this.addLabelToMembersTask.perform(selectedLabel);
    }

    @action
    removeLabelFromMembers(selectedLabel) {
        return this.removeLabelFromMembersTask.perform(selectedLabel);
    }

    // Tasks -------------------------------------------------------------------

    @task({restartable: true})
    *searchTask(query) {
        yield timeout(250); // debounce
        this.searchParam = query;
    }

    @task
    *fetchLabelsTask() {
        yield this.store.query('label', {limit: 'all'});
    }

    @task({restartable: true})
    *fetchMembersTask(params) {
        // params is undefined when called as a "refresh" of the model
        let {label, paidParam, searchParam, orderParam, filterParam} = typeof params === 'undefined' ? this : params;

        if (!searchParam) {
            this.resetSearch();
        }

        // use a fixed created_at date so that subsequent pages have a consistent index
        let startDate = new Date();

        // bypass the stale data shortcut if params change
        let forceReload = !params
            || label !== this._lastLabel
            || paidParam !== this._lastPaidParam
            || searchParam !== this._lastSearchParam
            || orderParam !== this._lastOrderParam
            || filterParam !== this._lastFilterParam;
        this._lastLabel = label;
        this._lastPaidParam = paidParam;
        this._lastSearchParam = searchParam;
        this._lastOrderParam = orderParam;
        this._lastFilterParam = filterParam;

        // unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
        // keeps navigation between list->details->list snappy
        if (!forceReload && this._startDate && !(this._startDate - startDate > 1 * 60 * 1000)) {
            return this.members;
        }

        this._startDate = startDate;

        this.members = yield this.ellaSparse.array((range = {}, query = {}) => {
            const searchQuery = this.getApiQueryObject({
                params,
                extraFilters: [`created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`]
            });
            const order = orderParam ? `${orderParam} desc` : `created_at desc`;

            query = Object.assign({
                order,
                limit: range.length,
                page: range.page
            }, searchQuery, query);

            return this.store.query('member', query).then((result) => {
                return {
                    data: result,
                    total: result.meta.pagination.total
                };
            });
        }, {
            limit: 50
        });
    }

    @task({drop: true})
    *deleteMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());

        // Trigger download before deleting. Uses the CSV export endpoint but
        // needs to fetch the file and trigger a download directly rather than
        // via an iframe. The iframe approach can't tell us when a download has
        // started/finished meaning we could end up deleting the data before exporting it
        const exportUrl = ghostPaths().url.api('members/upload');
        const exportParams = new URLSearchParams(this.getApiQueryObject());
        exportParams.set('limit', 'all');

        yield fetch(exportUrl, {method: 'GET'})
            .then(res => res.blob())
            .then((blob) => {
                const blobUrl = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = `members.${moment().format('YYYY-MM-DD')}.csv`;
                document.body.appendChild(a);
                if (!this.isTesting) {
                    a.click();
                }
                a.remove();
                URL.revokeObjectURL(blobUrl);
            });

        // backup downloaded, continue with deletion

        const deleteUrl = `${this.ghostPaths.url.api('members')}?${query}`;

        // response contains details of which members failed to be deleted
        const response = yield this.ajax.del(deleteUrl);

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *unsubscribeMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());
        const unsubscribeUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        // response contains details of which members failed to be unsubscribe
        const response = yield this.ajax.put(unsubscribeUrl, {
            data: {
                action: 'unsubscribe',
                meta: {}
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index', {filter: this.filterParam, search: null}))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *addLabelToMembersTask(selectedLabel) {
        const query = new URLSearchParams(this.getApiQueryObject());
        const addLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        const response = yield this.ajax.put(addLabelUrl, {
            data: {
                action: 'addLabel',
                meta: {
                    label: {
                        id: selectedLabel
                    }
                }
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index', {filter: this.filterParam, search: null}))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *removeLabelFromMembersTask(selectedLabel) {
        const query = new URLSearchParams(this.getApiQueryObject());
        const removeLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
        const response = yield this.ajax.put(removeLabelUrl, {
            data: {
                action: 'removeLabel',
                meta: {
                    label: {
                        id: selectedLabel
                    }
                }
            }
        });

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index', {filter: this.filterParam, search: null}))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }
    // Internal ----------------------------------------------------------------

    resetSearch() {
        this.searchText = '';
    }

    reset() {
        this.filterParam = null;
    }

    reload(params) {
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
        this.fetchMembersTask.perform(params);
    }
}
<|endoftext|>


<commit_before>var args = arguments[0] || {};
var raceCardInfo = Alloy.createCollection('raceCardInfo');
var raceCardDetails = Alloy.createCollection('raceCardDetails');
var infoValue = raceCardInfo.getRaceCardInfo();
var detailsValue = raceCardDetails.getRaceCardDetails("1");
var favourite = Alloy.createCollection('favourite');
var param_runner_id = args.runner || "";
Ti.App.Properties.setString('module',"member");
Ti.App.Properties.setString('root',"0");
var raceNo;
COMMON.construct($);
COMMON.showLoading();
setPicker1();

function refresh(index){
	if($.picker2.columns[0]) {
	    var _col = $.picker2.columns[0];
	        var len = _col.rowCount;
	        for(var x = len-1; x >= 0; x-- ){
	                var _row = _col.rows[x];
	                _col.removeRow(_row);
	        }
	}
	detailsValue = raceCardDetails.getRaceCardDetails(index);
	console.log(detailsValue);
	setPicker2();
	if(Ti.Platform.osname == "android")
	{
		$.picker2.setSelectedRow(0,false);
	}
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad")
	{
		//$.picker2.setSelectedRow(0,(detailsValue.length-1),false);
		var selectedRunner = 0;
		if(param_runner_id != ""){
			selectedRunner = param_runner_id;
		}
		$.picker2.setSelectedRow(0,selectedRunner,false);
	}
}

function setPicker1(){
	for(var i = 0 ; i < infoValue.length; i++){
		var venue = infoValue[i].venue;
		var race_id = infoValue[i].id;
		var data = Ti.UI.createPickerRow({title:venue.toString(),race_id:race_id.toString()});
		//$.pickerColumn1.addRow(data);
		$.picker1.add(data);
	}
}

function setPicker2(){

	for(var i=0; i < detailsValue.length; i++){
		var rec = detailsValue[i].runner_id;
	  	var row = Ti.UI.createPickerRow({
	   		title: rec.toString()
	  	});
	  	$.picker2.add(row);
		/*var favouriteInfo = favourite.getFavouriteInfoByVenueAndRaceNo(venue,detailsValue[i].runner_id);

		if(favouriteInfo.length > 0){
			var rec = detailsValue[i].runner_id;
		  	var row = Ti.UI.createPickerRow({
		   	 title: rec.toString()
		  	});
		  	$.picker2.add(row);
		}*/

	}
}

if(Ti.Platform.osname == "android"){

	var selectedRunner = 0;
	if(param_runner_id != ""){
		selectedRunner = param_runner_id;
	}
	$.picker1.setSelectedRow(0,false);
	$.picker2.setSelectedRow(selectedRunner,false);
}

if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
	// $.picker1.setSelectedRow(0,3,false);
	// $.picker2.setSelectedRow(0,3,false);
	$.picker1.setSelectedRow(0,0,false);
	//$.picker2.setSelectedRow(0,0,false);
}

function back(){
	Ti.App.Properties.setString('module',"");
	DRAWER.navigation("member",1);
}

function venue(e){
	venue = e.row.title;
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
		$.venueView.height = 50;
		$.venueContentView.height = 50;
		$.pickerView1.height = 50;
		$.pickerView1.setVisible(false);
		$.done1.setVisible(false);
		$.picker1.setVisible(false);
		$.venueLabel.text = venue;
	}
	refresh(e.row.race_id);
}

function changeRaceNo(e){
	raceNo = e.row.title;
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
		$.raceNoView.height = 50;
		$.raceNoContentView.height = 50;
		$.pickerView2.height = 50;
		$.pickerView2.setVisible(false);
		$.done2.setVisible(false);
		$.picker2.setVisible(false);
		$.raceNoLabel.text = raceNo;
	}
	//raceOdd(venue,raceNo);
	API.futureRace({
		raceNo: raceNo,
		venue: venue
	});
}

function raceOdd(data){
	COMMON.showLoading();
	removeAllChildren($.scrollView);
	if(data == "") {
		$.mtr.text = "Min to Race: -";
		var win = "-";
		var run = "-";
		var pla = "-";
	} else {
		$.mtr.text = "Min to Race: " + data[0].min_to_race;

		var win_odd = data[0].win_odd;
		var win = win_odd.split("$");
		var runner = data[0].runner;
		var run = runner.split("$");
		var pla_odd = data[0].pla_odd;
		var pla = pla_odd.split("$");
	}

	for(var i=0; i < win.length; i++) {
		var contentView = Titanium.UI.createView({
			layout: "horizontal",
			width:"100%",
			height:60
		});

		var leftView = Titanium.UI.createView({
			width:"33%"
		});

		var leftLabel = Ti.UI.createLabel({
			color: "black",
			text: win[i]
		});

		var centerView = Titanium.UI.createView({
			width:"33%"
		});

		var centerLabel = Ti.UI.createLabel({
			color: "black",
			text: run[i]
		});

		var rightView = Titanium.UI.createView({
			width:"33%"
		});

		var rightLabel = Ti.UI.createLabel({
			color: "black",
			text: pla[i]
		});

		var lineView = Titanium.UI.createView({
			backgroundColor: "#A5A5A5",
			width:"90%",
			height:1
		});

		var centerLineView = Titanium.UI.createView({
			layout: "composite",
			width:"100%",
			height: 1,
			bottom: 2
		});

		leftView.add(leftLabel);
		centerView.add(centerLabel);
		rightView.add(rightLabel);
		contentView.add(leftView);
		contentView.add(centerView);
		contentView.add(rightView);
		centerLineView.add(lineView);
		tableBetEvent(contentView,run[i], raceNo);
		$.scrollView.add(contentView);
		$.scrollView.add(centerLineView);
	}
	COMMON.hideLoading();
}

function tableBetEvent(contentView,runner,race_id){
	contentView.addEventListener('click', function(e){
		if(runner != "" && runner != '-'){
			Ti.App.Properties.setString('module','raceOdd');
			Ti.App.Properties.setString('presetRunner', parseInt(runner));
			Ti.App.Properties.setString('presetBet', race_id);
			DRAWER.navigation("play",1,{runner: runner, race_id: race_id});
		}
	});
}

function showVenue() {
	console.log("showVenue");
	$.venueView.height = 250;
	$.venueContentView.height = 250;
	$.pickerView1.height = 250;
	$.pickerView1.setVisible(true);
	$.done1.setVisible(true);
	$.picker1.setVisible(true);
	console.log("console: "+venue);
	$.venueLabel.text = venue;
	return false;
}

function showRaceNo() {
	$.raceNoView.height = 250;
	$.raceNoContentView.height = 250;
	$.pickerView2.height = 250;
	$.pickerView2.setVisible(true);
	$.done2.setVisible(true);
	$.picker2.setVisible(true);
	return false;
}

function done1()
{
	$.venueView.height = 50;
	$.venueContentView.height = 50;
	$.pickerView1.height = 50;
	$.pickerView1.setVisible(false);
	$.done1.setVisible(false);
	$.picker1.setVisible(false);
}

function done2()
{
	$.raceNoView.height = 50;
	$.raceNoContentView.height = 50;
	$.pickerView2.height = 50;
	$.pickerView2.setVisible(false);
	$.done2.setVisible(false);
	$.picker2.setVisible(false);
}

Ti.App.addEventListener("futureRace", function(e) {
	raceOdd(e.returnData);
});<commit_msg>16/2/2015 RaceCard Bug Fixed<commit_after>var args = arguments[0] || {};
var raceCardInfo = Alloy.createCollection('raceCardInfo');
var raceCardDetails = Alloy.createCollection('raceCardDetails');
var infoValue = raceCardInfo.getRaceCardInfo();
var detailsValue = raceCardDetails.getRaceCardDetails("1");
var favourite = Alloy.createCollection('favourite');
var param_runner_id = args.runner || "";
Ti.App.Properties.setString('module',"member");
Ti.App.Properties.setString('root',"0");
var raceNo;
COMMON.construct($);
COMMON.showLoading();
setPicker1();

function refresh(index){
	if($.picker2.columns[0]) {
	    var _col = $.picker2.columns[0];
	        var len = _col.rowCount;
	        for(var x = len-1; x >= 0; x-- ){
	                var _row = _col.rows[x];
	                _col.removeRow(_row);
	        }
	}
	detailsValue = raceCardDetails.getRaceCardDetails(index);
	console.log(detailsValue);
	setPicker2();
	if(Ti.Platform.osname == "android")
	{
		var selectedRunner = 0;
		if(param_runner_id != ""){
			selectedRunner = param_runner_id;
		}
		else
		{
			API.futureRace({
				raceNo: $.picker2.getSelectedRow(0).title,
				venue: venue
			});
		}
		$.picker2.setSelectedRow(0,selectedRunner,false);
	}
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad")
	{
		//$.picker2.setSelectedRow(0,(detailsValue.length-1),false);
		var selectedRunner = 0;
		if(param_runner_id != ""){
			console.log("param_runner_id: " +param_runner_id);
			selectedRunner = param_runner_id;
		}
		$.picker2.setSelectedRow(0,selectedRunner,false);
	}
}

function setPicker1(){
	for(var i = 0 ; i < infoValue.length; i++){
		var venue = infoValue[i].venue;
		var race_id = infoValue[i].id;
		var data = Ti.UI.createPickerRow({title:venue.toString(),race_id:race_id.toString()});
		//$.pickerColumn1.addRow(data);
		$.picker1.add(data);
	}
}

function setPicker2(){
	for(var i=0; i < detailsValue.length; i++){
		var rec = detailsValue[i].runner_id;
	  	var row = Ti.UI.createPickerRow({
	   		title: rec.toString()
	  	});
	  	$.picker2.add(row);
		/*var favouriteInfo = favourite.getFavouriteInfoByVenueAndRaceNo(venue,detailsValue[i].runner_id);

		if(favouriteInfo.length > 0){
			var rec = detailsValue[i].runner_id;
		  	var row = Ti.UI.createPickerRow({
		   	 title: rec.toString()
		  	});
		  	$.picker2.add(row);
		}*/

	}
}

if(Ti.Platform.osname == "android"){
	/* console.log("outer setter");
	var selectedRunner = 0;
	if(param_runner_id != ""){
		selectedRunner = param_runner_id;
	}*/
	$.picker1.setSelectedRow(0,0,false);
	//$.picker2.setSelectedRow(selectedRunner,false);
}

if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
	// $.picker1.setSelectedRow(0,3,false);
	// $.picker2.setSelectedRow(0,3,false);
	$.picker1.setSelectedRow(0,0,false);
	//$.picker2.setSelectedRow(0,0,false);
}

function back(){
	Ti.App.Properties.setString('module',"");
	DRAWER.navigation("member",1);
}

function venue(e){
	venue = e.row.title;
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
		$.venueView.height = 50;
		$.venueContentView.height = 50;
		$.pickerView1.height = 50;
		$.pickerView1.setVisible(false);
		$.done1.setVisible(false);
		$.picker1.setVisible(false);
		$.venueLabel.text = venue;
	}
	refresh(e.row.race_id);
}

function changeRaceNo(e){
	raceNo = e.row.title;
	if(Ti.Platform.osname == "iphone" || Ti.Platform.osname == "ipad"){
		$.raceNoView.height = 50;
		$.raceNoContentView.height = 50;
		$.pickerView2.height = 50;
		$.pickerView2.setVisible(false);
		$.done2.setVisible(false);
		$.picker2.setVisible(false);
		$.raceNoLabel.text = raceNo;
	}
	//raceOdd(venue,raceNo);
	API.futureRace({
		raceNo: raceNo,
		venue: venue
	});
}

function raceOdd(data){
	COMMON.showLoading();
	removeAllChildren($.scrollView);
	if(data == "") {
		$.mtr.text = "Min to Race: -";
		var win = "-";
		var run = "-";
		var pla = "-";
	} else {
		$.mtr.text = "Min to Race: " + data[0].min_to_race;

		var win_odd = data[0].win_odd;
		var win = win_odd.split("$");
		var runner = data[0].runner;
		var run = runner.split("$");
		var pla_odd = data[0].pla_odd;
		var pla = pla_odd.split("$");
	}

	for(var i=0; i < win.length; i++) {
		var contentView = Titanium.UI.createView({
			layout: "horizontal",
			width:"100%",
			height:60
		});

		var leftView = Titanium.UI.createView({
			width:"33%"
		});

		var leftLabel = Ti.UI.createLabel({
			color: "black",
			text: win[i]
		});

		var centerView = Titanium.UI.createView({
			width:"33%"
		});

		var centerLabel = Ti.UI.createLabel({
			color: "black",
			text: run[i]
		});

		var rightView = Titanium.UI.createView({
			width:"33%"
		});

		var rightLabel = Ti.UI.createLabel({
			color: "black",
			text: pla[i]
		});

		var lineView = Titanium.UI.createView({
			backgroundColor: "#A5A5A5",
			width:"90%",
			height:1
		});

		var centerLineView = Titanium.UI.createView({
			layout: "composite",
			width:"100%",
			height: 1,
			bottom: 2
		});

		leftView.add(leftLabel);
		centerView.add(centerLabel);
		rightView.add(rightLabel);
		contentView.add(leftView);
		contentView.add(centerView);
		contentView.add(rightView);
		centerLineView.add(lineView);
		tableBetEvent(contentView,run[i], raceNo);
		$.scrollView.add(contentView);
		$.scrollView.add(centerLineView);
	}
	COMMON.hideLoading();
}

function tableBetEvent(contentView,runner,race_id){
	contentView.addEventListener('click', function(e){
		if(runner != "" && runner != '-'){
			Ti.App.Properties.setString('module','raceOdd');
			Ti.App.Properties.setString('presetRunner', parseInt(runner));
			Ti.App.Properties.setString('presetBet', race_id);
			DRAWER.navigation("play",1,{runner: runner, race_id: race_id});
		}
	});
}

function showVenue() {
	$.venueView.height = 250;
	$.venueContentView.height = 250;
	$.pickerView1.height = 250;
	$.pickerView1.setVisible(true);
	$.done1.setVisible(true);
	$.picker1.setVisible(true);
	$.venueLabel.text = venue;
	return false;
}

function showRaceNo() {
	$.raceNoView.height = 250;
	$.raceNoContentView.height = 250;
	$.pickerView2.height = 250;
	$.pickerView2.setVisible(true);
	$.done2.setVisible(true);
	$.picker2.setVisible(true);
	return false;
}

function done1()
{
	$.venueView.height = 50;
	$.venueContentView.height = 50;
	$.pickerView1.height = 50;
	$.pickerView1.setVisible(false);
	$.done1.setVisible(false);
	$.picker1.setVisible(false);
}

function done2()
{
	$.raceNoView.height = 50;
	$.raceNoContentView.height = 50;
	$.pickerView2.height = 50;
	$.pickerView2.setVisible(false);
	$.done2.setVisible(false);
	$.picker2.setVisible(false);
}

Ti.App.addEventListener("futureRace", function(e) {
	raceOdd(e.returnData);
});<|endoftext|>


<commit_before>/* global ININ */
import Ember from 'ember';
import Chance from 'npm:chance';
import {purecloudEnvironmentTld} from '../utils/purecloud-environment';
import config from '../config/environment';

var computed = Ember.computed;

export default Ember.Controller.extend({
	purecloud: Ember.inject.service(),
	storageService: Ember.inject.service(),
	queueService: Ember.inject.service(),
	webChatService: Ember.inject.service(),
	openInNewWindow: true,
	isInChat: false,
	org: null,
	firstName: '',
	lastName: '',
	address: '',
	city:'',
	state: '',
	zip: '',
	phone: '',
	email: '',
	queue: '',
	locale: 'en',
	field1name: '',
	field1value: '',
	field2name: '',
	field2value: '',
	field3name: '',
	field3value: '',
	customAttributes: [],
	error: '',
	errorVisibility: computed('error', function() {
		const error = this.get('error');
		return !error || error == '' ? 'hidden' :  '';
	}),
	welcomeMessage: 'Thanks for chatting using the dev tools chat page.',
	chatEnvironment: computed('purecloud.environment', function() {
		return this.get('purecloud').get('environment');
	}),
	chatRegion: computed('purecloud.environment', function() {
		let environment = this.get('purecloud').get('environment');
		if (!environment || environment === '') return;

		if (environment.endsWith('.jp'))
			return 'ap-northeast-1';
		else if (environment.endsWith('.com.au'))
			this.set('chatRegion', 'ap-southeast-2');
		else if (environment.endsWith('.ie'))
			return 'eu-west-1';
		else if (environment.endsWith('.de'))
			return 'eu-central-1';
		else if (environment.endsWith('mypurecloud.com'))
			return 'us-east-1';
		else if (environment.includes('tca')) {
			this.set('chatEnv', 'test');
			this.set('chatEnvTag', '\n  env="test"');
			return 'us-east-1';
		} else if (environment.includes('dca')) {
			this.set('chatEnv', 'dev');
			this.set('chatEnvTag', '\n  env="dev"');
			return 'us-east-1';
		} else {
			console.warn(`Failed to identify environment: ${environment}`);
			return 'us-east-1';
		}
	}),
	chatEnv: '',
	chatEnvTag: '',
	init(){
		this._super(...arguments);

		let orgApi = this.get('purecloud').orgApi();

		orgApi.getOrganizationsMe()
			.then(result => {
				this.set('org',result);

				let storage = this.get('storageService');
				let savedData = storage.localStorageGet('webChatParams');

				if(savedData){
					this.set('firstName', savedData.firstName);
					this.set('lastName', savedData.lastName);
					this.set('address', savedData.address);
					this.set('city', savedData.city);
					this.set('state', savedData.state);
					this.set('zip', savedData.zip);
					this.set('phone', savedData.phone);
					this.set('email', savedData.email);
					this.set('queue', savedData.queue);
					this.set('locale', savedData.locale);
					this.set('welcomeMessage', savedData.welcomeMessage);
					this.set('deployment', savedData.deployment);
					this.set('field1name', savedData.field1name);
					this.set('field1value', savedData.field1value);
					this.set('field2name', savedData.field2name);
					this.set('field2value', savedData.field2value);
					this.set('field3name', savedData.field3name);
					this.set('field3value', savedData.field3value);
					this.set('customAttributes', savedData.customAttributes || []);

					let storage = this.get('storageService');
					if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
						this.set('email', savedData.email);
						this.set('showEmailField', true);
					}

					if(typeof 'savedData.openInNewWindow' !== 'undefined'){
						this.set('openInNewWindow', savedData.openInNewWindow);
					}

				}

				// Get the user's authorization. purecloud.me isn't populated yet.
				// Not using the Authorization API here because that will return a 404 if the user doesn't have permission to view their permissions.
				return this.get('purecloud').usersApi().getUsersMe({ expand: [ 'authorization' ]});
			})
			.then((me) => {
				// Be sure we got permissions
				let permissionsErr = '';
				if (!me.authorization) {
					this.setError('Unable to validate permissions. You may not see deployments or queues in the dropdowns. You\'re probably missing the permission "authorization:grant:view".');
					return;
				}

				// Check for permissions
				let hasReadDeployment = false;
				let hasViewQueues = false;
				me.authorization.permissions.forEach((p) => {
					// Must check for startswith because of how permissions look with divisions. Can't use exact match here.
					if (p.startsWith('webchat:deployment:read')) hasReadDeployment = true;
					if (p.startsWith('routing:queue:view')) hasViewQueues = true;
				});

				// Set error messages
				if (!hasReadDeployment)
					permissionsErr += 'Unable to list deployments. Missing permission: webchat:deployment:read
';
				if (!hasViewQueues)
					permissionsErr += 'Unable to list queues. Missing permission: routing:queue:view
';

				this.setError(permissionsErr);
			})
			.catch((err) => this.setError(err));
	},
	deployments: computed('webChatService.deployments', function() {
		return this.get('webChatService').get('deployments');
	}),
	needsDeployment: computed('webChatService.deploymentCount', function() {
		return this.get('webChatService').get('deploymentCount') == 0;
	}),
	queues: computed('queueService.queues', function() {
		return this.get('queueService').get('queues');
	}),
	chatConfig: computed('openInNewWindow', 'queue', 'firstName', 'lastName', 'address', 'city', 'zip', 'state', 'phone', 'email', 'locale', 'welcomeMessage', 'field1name', 'field1value', 'field2name', 'field2value', 'field3name', 'field3value', 'customAttributes.@each.name', 'customAttributes.@each.value', function() {
		try{
			let environment = purecloudEnvironmentTld();
			let companyLogo = $('#companyLogo').attr('src');
			let companyLogoSmall = $('#companyLogoSmall').attr('src');
			let agentAvatar = $('#agentAvatar').attr('src');

			let resourcePrefix = window.location.origin;
			if(config.APP.urlprefix){
				resourcePrefix = config.APP.urlprefix;
			}

			if(typeof companyLogo !== 'undefined' && companyLogo[0] !== 'h'){
				companyLogo = resourcePrefix + companyLogo;
				companyLogoSmall = resourcePrefix + companyLogoSmall;
				agentAvatar = resourcePrefix + agentAvatar;
			}

			let org = this.get('org');
			if(org === null){
				return '{}';
			}

			let chatData = {
				'firstName': this.get('firstName'),
				'lastName': this.get('lastName'),
				'addressStreet': this.get('address'),
				'addressCity': this.get('city'),
				'addressPostalCode': this.get('zip'),
				'addressState': this.get('state'),
				'phoneNumber': this.get('phone'),
				'customField1Label': this.get('field1name'),
				'customField1': this.get('field1value'),
				'customField2Label': this.get('field2name'),
				'customField2': this.get('field2value'),
				'customField3Label': this.get('field3name'),
				'customField3': this.get('field3value')
			};

			let storage = this.get('storageService');
			if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
				chatData.email = this.get('email');
			}

			const customAttributes = this.get('customAttributes');
			for (let attribute of customAttributes) {
				if (attribute.name != '') {
					chatData[attribute.name] = attribute.value;
				}
			}

			let chatConfig = {
				// Web chat application URL
				'webchatAppUrl': 'https://apps.'+ environment +'/webchat',

				// Web chat service URL
				'webchatServiceUrl': 'https://realtime.'+ environment +':443',

				'orgId': this.get('org').thirdPartyOrgId,

				// Organization name
				'orgName': this.get('org').thirdPartyOrgName,

				'queueName': this.get('queue'),

				// Log level
				'logLevel': 'DEBUG',

				// Locale code
				'locale': this.get('locale'),

				// Data that will be included with interaction
				'data': chatData,

				// Logo used at the top of the chat window
				'companyLogo': {
					'width': 600,
					'height': 149,
					'url': companyLogo
				},

				// Logo used within the chat window
				'companyLogoSmall': {
					'width': 25,
					'height': 25,
					'url': companyLogoSmall
				},

				// Image used for agent
				'agentAvatar': {
					'width': 462,
					'height': 462,
					'url': agentAvatar
				},

				// Text displayed with chat window is displayed
				'welcomeMessage': this.get('welcomeMessage'),

				// CSS class applied to the chat window
				'cssClass': 'webchat-frame',

				// Custom style applied to the chat window
				'css': {
					'width': '100%',
					'height': '100%'
				}
			};

			return JSON.stringify(chatConfig, null, '  ');
		}catch(ex){
			console.error(ex);
		}
		return '{}';
	}),
	setError(err) {
		if (!err) {
			this.set('error', '');
			return;
		}

		console.error(err);
		if (err.body && err.body.message)
			this.set('error', err.body.message);
		else if (err.message)
			this.set('error', err.message);
		else if (typeof(err) === 'string')
			this.set('error', err.htmlSafe());
		else
			this.set('error', 'An error has occurred');
	},
	openChatWindow() {
		try {
			let chatConfig = JSON.parse(this.get('chatConfig'));

			ININ.webchat.create(chatConfig, (err, webchat) => {
				if (err) {
					this.setError(err);
					return;
				}

				if(this.get("openInNewWindow")){
					webchat.renderPopup({
						width: 400,
						height: 400,
						title: 'PureCloud Developer Tools Web Chat'
					});
				}else{
					this.set("isInChat", true);
					webchat.renderFrame({
						containerEl: 'chat-container'
					});

					let self = this;
					webchat.chatEnded = function () {
						self.set("isInChat", false);
					};
				}

			});

			let savedData = {
				firstName: this.get('firstName'),
				lastName: this.get('lastName'),
				address: this.get('address'),
				city: this.get('city'),
				state: this.get('state'),
				zip: this.get('zip'),
				phone: this.get('phone'),
				queue: this.get('queue'),
				locale: this.get('locale'),
				welcomeMessage: this.get('welcomeMessage'),
				deployment: this.get('deployment'),
				field1name: this.get('field1name'),
				field1value: this.get('field1value'),
				field2name: this.get('field2name'),
				field2value: this.get('field2value'),
				field3name: this.get('field3name'),
				field3value: this.get('field3value'),
				customAttributes: this.get('customAttributes'),
				openInNewWindow: this.get('openInNewWindow'),
			};

			let storage = this.get('storageService');
			if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
				savedData.email = this.get('email');
			}

			storage.localStorageSet('webChatParams', savedData);
		} catch(err) {
			this.setError(err);
		}
	},
	actions:{
		startChat() {
			try {
				this.setError();

				// Validation
				let chatConfig = JSON.parse(this.get('chatConfig'));
				if (!chatConfig.queueName || chatConfig.queueName == '')
					throw new Error('You must select a queue before starting the chat');
				if (!this.deployment || this.deployment == '')
					throw new Error('You must select a deployment before starting the chat');

				const chatScript = document.getElementById('purecloud-webchat-js');

				// Skip injecting script tag if it's already been injected.
				if (chatScript.getAttribute('src') != null) {
					this.openChatWindow();
					return;
				} else {
					// Once a script has been loaded, it cannot be unloaded.
					// Therefore, we must prevent the user from choosing another deployment until the page is reloaded.
					$('#deployment-select-container select').prop('disabled', true);
				}

				// Inject deployment script
				chatScript.async = true;
				chatScript.setAttribute('type', 'text/javascript');
				chatScript.setAttribute('region', this.get('chatRegion'));
				if (this.get('chatEnv') !== '')
					chatScript.setAttribute('env', this.get('chatEnv'));
				chatScript.setAttribute('org-guid', this.get('org').id);
				chatScript.setAttribute('deployment-key', this.get('deployment'));
				chatScript.setAttribute('src', `https://apps.${this.get('chatEnvironment')}/webchat/jsapi-v1.js`);
				chatScript.addEventListener('error', () => this.setError('Error loading script.'));
				chatScript.addEventListener('abort', () => this.setError('Script loading aborted.'));
				chatScript.addEventListener('load', this.openChatWindow.bind(this));
			} catch(err) {
				this.setError(err);
			}
		},
		populate() {
			var chance = new Chance();

			this.set('firstName', chance.first());
			this.set('lastName', chance.last());
			this.set('address', chance.address());
			this.set('city', chance.city());
			this.set('state', chance.state());
			this.set('zip', chance.zip());
			this.set('phone', chance.phone());
			this.set('email', chance.email());
			this.set('locale', 'en');
			this.set('welcomeMessage', 'Thanks for chatting using the dev tools chat page.');
		},
		createDeployment() {
			this.setError();
			this.get('webChatService').createDeployment()
				.then((newDeploymentId) => {
					let storage = this.get('storageService');
					let savedData = storage.localStorageGet('webChatParams');
					if (savedData && savedData.deployment)
						this.set('deployment', savedData.deployment);
					else
						this.set('deployment', newDeploymentId);
				})
				.catch((err) => {
					this.setError(err);
				});
		},
		addCustomAttribute() {
			let customAttributes = this.get('customAttributes');
			customAttributes.pushObject({
				name: '',
				value: ''
			});
			this.set('customAttributes', customAttributes);
		},
		deleteCustomAttribute(attribute) {
			this.get('customAttributes').removeObject(attribute);
		}
	}
});
<commit_msg>clear out old webchat conversation after it completest<commit_after>/* global ININ */
/* global $ */
import Ember from 'ember';
import Chance from 'npm:chance';
import {purecloudEnvironmentTld} from '../utils/purecloud-environment';
import config from '../config/environment';

var computed = Ember.computed;

export default Ember.Controller.extend({
	purecloud: Ember.inject.service(),
	storageService: Ember.inject.service(),
	queueService: Ember.inject.service(),
	webChatService: Ember.inject.service(),
	openInNewWindow: true,
	isInChat: false,
	org: null,
	firstName: '',
	lastName: '',
	address: '',
	city:'',
	state: '',
	zip: '',
	phone: '',
	email: '',
	queue: '',
	locale: 'en',
	field1name: '',
	field1value: '',
	field2name: '',
	field2value: '',
	field3name: '',
	field3value: '',
	customAttributes: [],
	error: '',
	errorVisibility: computed('error', function() {
		const error = this.get('error');
		return !error || error == '' ? 'hidden' :  '';
	}),
	welcomeMessage: 'Thanks for chatting using the dev tools chat page.',
	chatEnvironment: computed('purecloud.environment', function() {
		return this.get('purecloud').get('environment');
	}),
	chatRegion: computed('purecloud.environment', function() {
		let environment = this.get('purecloud').get('environment');
		if (!environment || environment === '') return;

		if (environment.endsWith('.jp'))
			return 'ap-northeast-1';
		else if (environment.endsWith('.com.au'))
			this.set('chatRegion', 'ap-southeast-2');
		else if (environment.endsWith('.ie'))
			return 'eu-west-1';
		else if (environment.endsWith('.de'))
			return 'eu-central-1';
		else if (environment.endsWith('mypurecloud.com'))
			return 'us-east-1';
		else if (environment.includes('tca')) {
			this.set('chatEnv', 'test');
			this.set('chatEnvTag', '\n  env="test"');
			return 'us-east-1';
		} else if (environment.includes('dca')) {
			this.set('chatEnv', 'dev');
			this.set('chatEnvTag', '\n  env="dev"');
			return 'us-east-1';
		} else {
			console.warn(`Failed to identify environment: ${environment}`);
			return 'us-east-1';
		}
	}),
	chatEnv: '',
	chatEnvTag: '',
	init(){
		this._super(...arguments);

		let orgApi = this.get('purecloud').orgApi();

		orgApi.getOrganizationsMe()
			.then(result => {
				this.set('org',result);

				let storage = this.get('storageService');
				let savedData = storage.localStorageGet('webChatParams');

				if(savedData){
					this.set('firstName', savedData.firstName);
					this.set('lastName', savedData.lastName);
					this.set('address', savedData.address);
					this.set('city', savedData.city);
					this.set('state', savedData.state);
					this.set('zip', savedData.zip);
					this.set('phone', savedData.phone);
					this.set('email', savedData.email);
					this.set('queue', savedData.queue);
					this.set('locale', savedData.locale);
					this.set('welcomeMessage', savedData.welcomeMessage);
					this.set('deployment', savedData.deployment);
					this.set('field1name', savedData.field1name);
					this.set('field1value', savedData.field1value);
					this.set('field2name', savedData.field2name);
					this.set('field2value', savedData.field2value);
					this.set('field3name', savedData.field3name);
					this.set('field3value', savedData.field3value);
					this.set('customAttributes', savedData.customAttributes || []);

					let storage = this.get('storageService');
					if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
						this.set('email', savedData.email);
						this.set('showEmailField', true);
					}

					if(typeof 'savedData.openInNewWindow' !== 'undefined'){
						this.set('openInNewWindow', savedData.openInNewWindow);
					}

				}

				// Get the user's authorization. purecloud.me isn't populated yet.
				// Not using the Authorization API here because that will return a 404 if the user doesn't have permission to view their permissions.
				return this.get('purecloud').usersApi().getUsersMe({ expand: [ 'authorization' ]});
			})
			.then((me) => {
				// Be sure we got permissions
				let permissionsErr = '';
				if (!me.authorization) {
					this.setError('Unable to validate permissions. You may not see deployments or queues in the dropdowns. You\'re probably missing the permission "authorization:grant:view".');
					return;
				}

				// Check for permissions
				let hasReadDeployment = false;
				let hasViewQueues = false;
				me.authorization.permissions.forEach((p) => {
					// Must check for startswith because of how permissions look with divisions. Can't use exact match here.
					if (p.startsWith('webchat:deployment:read')) hasReadDeployment = true;
					if (p.startsWith('routing:queue:view')) hasViewQueues = true;
				});

				// Set error messages
				if (!hasReadDeployment)
					permissionsErr += 'Unable to list deployments. Missing permission: webchat:deployment:read
';
				if (!hasViewQueues)
					permissionsErr += 'Unable to list queues. Missing permission: routing:queue:view
';

				this.setError(permissionsErr);
			})
			.catch((err) => this.setError(err));
	},
	deployments: computed('webChatService.deployments', function() {
		return this.get('webChatService').get('deployments');
	}),
	needsDeployment: computed('webChatService.deploymentCount', function() {
		return this.get('webChatService').get('deploymentCount') == 0;
	}),
	queues: computed('queueService.queues', function() {
		return this.get('queueService').get('queues');
	}),
	chatConfig: computed('openInNewWindow', 'queue', 'firstName', 'lastName', 'address', 'city', 'zip', 'state', 'phone', 'email', 'locale', 'welcomeMessage', 'field1name', 'field1value', 'field2name', 'field2value', 'field3name', 'field3value', 'customAttributes.@each.name', 'customAttributes.@each.value', function() {
		try{
			let environment = purecloudEnvironmentTld();
			let companyLogo = $('#companyLogo').attr('src');
			let companyLogoSmall = $('#companyLogoSmall').attr('src');
			let agentAvatar = $('#agentAvatar').attr('src');

			let resourcePrefix = window.location.origin;
			if(config.APP.urlprefix){
				resourcePrefix = config.APP.urlprefix;
			}

			if(typeof companyLogo !== 'undefined' && companyLogo[0] !== 'h'){
				companyLogo = resourcePrefix + companyLogo;
				companyLogoSmall = resourcePrefix + companyLogoSmall;
				agentAvatar = resourcePrefix + agentAvatar;
			}

			let org = this.get('org');
			if(org === null){
				return '{}';
			}

			let chatData = {
				'firstName': this.get('firstName'),
				'lastName': this.get('lastName'),
				'addressStreet': this.get('address'),
				'addressCity': this.get('city'),
				'addressPostalCode': this.get('zip'),
				'addressState': this.get('state'),
				'phoneNumber': this.get('phone'),
				'customField1Label': this.get('field1name'),
				'customField1': this.get('field1value'),
				'customField2Label': this.get('field2name'),
				'customField2': this.get('field2value'),
				'customField3Label': this.get('field3name'),
				'customField3': this.get('field3value')
			};

			let storage = this.get('storageService');
			if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
				chatData.email = this.get('email');
			}

			const customAttributes = this.get('customAttributes');
			for (let attribute of customAttributes) {
				if (attribute.name != '') {
					chatData[attribute.name] = attribute.value;
				}
			}

			let chatConfig = {
				// Web chat application URL
				'webchatAppUrl': 'https://apps.'+ environment +'/webchat',

				// Web chat service URL
				'webchatServiceUrl': 'https://realtime.'+ environment +':443',

				'orgId': this.get('org').thirdPartyOrgId,

				// Organization name
				'orgName': this.get('org').thirdPartyOrgName,

				'queueName': this.get('queue'),

				// Log level
				'logLevel': 'DEBUG',

				// Locale code
				'locale': this.get('locale'),

				// Data that will be included with interaction
				'data': chatData,

				// Logo used at the top of the chat window
				'companyLogo': {
					'width': 600,
					'height': 149,
					'url': companyLogo
				},

				// Logo used within the chat window
				'companyLogoSmall': {
					'width': 25,
					'height': 25,
					'url': companyLogoSmall
				},

				// Image used for agent
				'agentAvatar': {
					'width': 462,
					'height': 462,
					'url': agentAvatar
				},

				// Text displayed with chat window is displayed
				'welcomeMessage': this.get('welcomeMessage'),

				// CSS class applied to the chat window
				'cssClass': 'webchat-frame',

				// Custom style applied to the chat window
				'css': {
					'width': '100%',
					'height': '100%'
				}
			};

			return JSON.stringify(chatConfig, null, '  ');
		}catch(ex){
			console.error(ex);
		}
		return '{}';
	}),
	setError(err) {
		if (!err) {
			this.set('error', '');
			return;
		}

		console.error(err);
		if (err.body && err.body.message)
			this.set('error', err.body.message);
		else if (err.message)
			this.set('error', err.message);
		else if (typeof(err) === 'string')
			this.set('error', err.htmlSafe());
		else
			this.set('error', 'An error has occurred');
	},
	openChatWindow() {
		try {
			let chatConfig = JSON.parse(this.get('chatConfig'));

			ININ.webchat.create(chatConfig, (err, webchat) => {
				if (err) {
					this.setError(err);
					return;
				}

				if(this.get("openInNewWindow")){
					webchat.renderPopup({
						width: 400,
						height: 400,
						title: 'PureCloud Developer Tools Web Chat'
					});
				}else{
					this.set("isInChat", true);
					webchat.renderFrame({
						containerEl: 'chat-container'
					});

					let self = this;
					webchat.chatEnded = function () {
						self.set("isInChat", false);
						$('#chat-container').html("");
					};
				}

			});

			let savedData = {
				firstName: this.get('firstName'),
				lastName: this.get('lastName'),
				address: this.get('address'),
				city: this.get('city'),
				state: this.get('state'),
				zip: this.get('zip'),
				phone: this.get('phone'),
				queue: this.get('queue'),
				locale: this.get('locale'),
				welcomeMessage: this.get('welcomeMessage'),
				deployment: this.get('deployment'),
				field1name: this.get('field1name'),
				field1value: this.get('field1value'),
				field2name: this.get('field2name'),
				field2value: this.get('field2value'),
				field3name: this.get('field3name'),
				field3value: this.get('field3value'),
				customAttributes: this.get('customAttributes'),
				openInNewWindow: this.get('openInNewWindow'),
			};

			let storage = this.get('storageService');
			if(storage.localStorageGet('relate.ui.useEmailAndPhoneForRWPLookupInWebChat')) {
				savedData.email = this.get('email');
			}

			storage.localStorageSet('webChatParams', savedData);
		} catch(err) {
			this.setError(err);
		}
	},
	actions:{
		startChat() {
			try {
				this.setError();

				// Validation
				let chatConfig = JSON.parse(this.get('chatConfig'));
				if (!chatConfig.queueName || chatConfig.queueName == '')
					throw new Error('You must select a queue before starting the chat');
				if (!this.deployment || this.deployment == '')
					throw new Error('You must select a deployment before starting the chat');

				const chatScript = document.getElementById('purecloud-webchat-js');

				// Skip injecting script tag if it's already been injected.
				if (chatScript.getAttribute('src') != null) {
					this.openChatWindow();
					return;
				} else {
					// Once a script has been loaded, it cannot be unloaded.
					// Therefore, we must prevent the user from choosing another deployment until the page is reloaded.
					$('#deployment-select-container select').prop('disabled', true);
				}

				// Inject deployment script
				chatScript.async = true;
				chatScript.setAttribute('type', 'text/javascript');
				chatScript.setAttribute('region', this.get('chatRegion'));
				if (this.get('chatEnv') !== '')
					chatScript.setAttribute('env', this.get('chatEnv'));
				chatScript.setAttribute('org-guid', this.get('org').id);
				chatScript.setAttribute('deployment-key', this.get('deployment'));
				chatScript.setAttribute('src', `https://apps.${this.get('chatEnvironment')}/webchat/jsapi-v1.js`);
				chatScript.addEventListener('error', () => this.setError('Error loading script.'));
				chatScript.addEventListener('abort', () => this.setError('Script loading aborted.'));
				chatScript.addEventListener('load', this.openChatWindow.bind(this));
			} catch(err) {
				this.setError(err);
			}
		},
		populate() {
			var chance = new Chance();

			this.set('firstName', chance.first());
			this.set('lastName', chance.last());
			this.set('address', chance.address());
			this.set('city', chance.city());
			this.set('state', chance.state());
			this.set('zip', chance.zip());
			this.set('phone', chance.phone());
			this.set('email', chance.email());
			this.set('locale', 'en');
			this.set('welcomeMessage', 'Thanks for chatting using the dev tools chat page.');
		},
		createDeployment() {
			this.setError();
			this.get('webChatService').createDeployment()
				.then((newDeploymentId) => {
					let storage = this.get('storageService');
					let savedData = storage.localStorageGet('webChatParams');
					if (savedData && savedData.deployment)
						this.set('deployment', savedData.deployment);
					else
						this.set('deployment', newDeploymentId);
				})
				.catch((err) => {
					this.setError(err);
				});
		},
		addCustomAttribute() {
			let customAttributes = this.get('customAttributes');
			customAttributes.pushObject({
				name: '',
				value: ''
			});
			this.set('customAttributes', customAttributes);
		},
		deleteCustomAttribute(attribute) {
			this.get('customAttributes').removeObject(attribute);
		}
	}
});
<|endoftext|>


<commit_before>import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'

import { canMoveLeft, canMoveRight, canMoveUp } from './move'
import { watchImagesForUpload } from './imageswap'
import { queryPage } from './search'
import {
  metaKey, htmlStringToDom, createClassname,
  isOffBounds, getStyles, deepElementFromPoint
} from '../utilities/'

export function Selectable() {
  const [body]            = $('body')
  const shadows           = $('*')
                              .filter(node => !isOffBounds(node))
                              .filter(node => node.shadowRoot)
                              .map(node => node)
  const elements          = $([body, ...shadows])
  let selected            = []
  let selectedCallbacks   = []
  let labels              = []
  let handles             = []

  const listen = () => {
    elements.forEach(el => el.addEventListener('click', on_click, true))
    elements.forEach(el => el.addEventListener('dblclick', on_dblclick, true))
    elements.on('selectstart', on_selection)
    elements.on('mousemove', ({clientX, clientY}) => {
      document
        .elementFromPoint(clientX, clientY)
        .setAttribute('data-hover', true)
    })
    // elements.on('mouseover', on_hover)
    elements.on('mouseout', on_hoverout)

    document.addEventListener('copy', on_copy)
    document.addEventListener('cut', on_cut)
    document.addEventListener('paste', on_paste)

    watchCommandKey()

    hotkeys(`${metaKey}+alt+c`, on_copy_styles)
    hotkeys(`${metaKey}+alt+v`, e => on_paste_styles())
    hotkeys('esc', on_esc)
    hotkeys(`${metaKey}+d`, on_duplicate)
    hotkeys('backspace,del,delete', on_delete)
    hotkeys('alt+del,alt+backspace', on_clearstyles)
    hotkeys(`${metaKey}+e,${metaKey}+shift+e`, on_expand_selection)
    hotkeys(`${metaKey}+g,${metaKey}+shift+g`, on_group)
    hotkeys('tab,shift+tab,enter,shift+enter', on_keyboard_traversal)
  }

  const unlisten = () => {
    elements.forEach(el => el.removeEventListener('click', on_click, true))
    elements.forEach(el => el.removeEventListener('dblclick', on_dblclick, true))
    elements.off('selectstart', on_selection)
    elements.off('mouseover', on_hover)
    elements.off('mouseout', on_hoverout)

    document.removeEventListener('copy', on_copy)
    document.removeEventListener('cut', on_cut)
    document.removeEventListener('paste', on_paste)

    hotkeys.unbind(`esc,${metaKey}+d,backspace,del,delete,alt+del,alt+backspace,${metaKey}+e,${metaKey}+shift+e,${metaKey}+g,${metaKey}+shift+g,tab,shift+tab,enter,shift+enter`)
  }

  const on_click = e => {
    const target = deepElementFromPoint(e.clientX, e.clientY)

    if (isOffBounds(target) && !selected.filter(el => el == target).length)
      return

    e.preventDefault()
    if (!e.altKey) e.stopPropagation()
    if (!e.shiftKey) unselect_all()

    if(e.shiftKey && target.hasAttribute('data-label-id'))
      unselect(target.getAttribute('data-label-id'))
    else
    select(target)
  }

  const unselect = id => {

    [...labels, ...handles]
      .filter(node =>
          node.getAttribute('data-label-id') === id)
        .forEach(node =>
          node.remove())

    selected.filter(node =>
      node.getAttribute('data-label-id') === id)
      .forEach(node =>
        $(node).attr({
          'data-selected':      null,
          'data-selected-hide': null,
          'data-label-id':      null,
          'data-hover': null
      }))

    selected = selected.filter(node => node.getAttribute('data-label-id') !== id)

    tellWatchers()
  }

  const on_dblclick = e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    $('tool-pallete')[0].toolSelected('text')
  }

  const watchCommandKey = e => {
    let did_hide = false

    document.onkeydown = function(e) {
      if (hotkeys.ctrl && selected.length) {
        $('pb-handles, pb-label').forEach(el =>
          el.style.display = 'none')

        did_hide = true
      }
    }

    document.onkeyup = function(e) {
      if (did_hide) {
        $('pb-handles, pb-label').forEach(el =>
          el.style.display = null)

        did_hide = false
      }
    }
  }

  const on_esc = _ =>
    selected.length && unselect_all()

  const on_duplicate = e => {
    const root_node = selected[0]
    if (!root_node) return

    const deep_clone = root_node.cloneNode(true)
    deep_clone.removeAttribute('data-selected')
    root_node.parentNode.insertBefore(deep_clone, root_node.nextSibling)
    e.preventDefault()
  }

  const on_delete = e =>
    selected.length && delete_all()

  const on_clearstyles = e =>
    selected.forEach(el =>
      el.attr('style', null))

  const on_copy = e => {
    // if user has selected text, dont try to copy an element
    if (window.getSelection().toString().length)
      return

    if (selected[0] && this.node_clipboard !== selected[0]) {
      e.preventDefault()
      let $node = selected[0].cloneNode(true)
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
    }
  }

  const on_cut = e => {
    if (selected[0] && this.node_clipboard !== selected[0]) {
      let $node = selected[0].cloneNode(true)
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
      selected[0].remove()
    }
  }

  const on_paste = e => {
    const clipData = e.clipboardData.getData('text/html')
    const potentialHTML = clipData || this.copy_backup
    if (selected[0] && potentialHTML) {
      e.preventDefault()
      selected[0].appendChild(
        htmlStringToDom(potentialHTML))
    }
  }

  const on_copy_styles = e => {
    e.preventDefault()
    this.copied_styles = selected.map(el =>
      getStyles(el))
  }

  const on_paste_styles = (index = 0) =>
    selected.forEach(el => {
      this.copied_styles[index]
        .map(({prop, value}) =>
          el.style[prop] = value)

      index >= this.copied_styles.length - 1
        ? index = 0
        : index++
    })

  const on_expand_selection = (e, {key}) => {
    e.preventDefault()

    expandSelection({
      query:  combineNodeNameAndClass(selected[0]),
      all:    key.includes('shift'),
    })
  }

  const on_group = (e, {key}) => {
    e.preventDefault()

    if (key.split('+').includes('shift')) {
      let $selected = [...selected]
      unselect_all()
      $selected.reverse().forEach(el => {
        let l = el.children.length
        while (el.children.length > 0) {
          var node = el.childNodes[el.children.length - 1]
          if (node.nodeName !== '#text')
            select(node)
          el.parentNode.prepend(node)
        }
        el.parentNode.removeChild(el)
      })
    }
    else {
      let div = document.createElement('div')
      selected[0].parentNode.prepend(
        selected.reverse().reduce((div, el) => {
          div.appendChild(el)
          return div
        }, div)
      )
      unselect_all()
      select(div)
    }
  }

  const on_selection = e =>
    !isOffBounds(e.target)
    && selected.length
    && selected[0].textContent != e.target.textContent
    && e.preventDefault()

  const on_keyboard_traversal = (e, {key}) => {
    if (selected.length !== 1) return

    e.preventDefault()
    e.stopPropagation()

    const current = selected[0]

    if (key.includes('shift')) {
      if (key.includes('tab') && canMoveLeft(current)) {
        unselect_all()
        select(canMoveLeft(current))
      }
      if (key.includes('enter') && canMoveUp(current)) {
        unselect_all()
        select(current.parentNode)
      }
    }
    else {
      if (key.includes('tab') && canMoveRight(current)) {
        unselect_all()
        select(canMoveRight(current))
      }
      if (key.includes('enter') && current.children.length) {
        unselect_all()
        select(current.children[0])
      }
    }
  }

  const on_hover = ({target}) => {
    if (isOffBounds(target)) return
    target.setAttribute('data-hover', true)
  }

  const on_hoverout = ({target}) => {
    target.removeAttribute('data-hover')
  }

  const select = el => {
    el.setAttribute('data-selected', true)
    overlayMetaUI(el)
    selected.unshift(el)
    tellWatchers()
  }

  const unselect_all = () => {
    selected
      .forEach(el =>
        $(el).attr({
          'data-selected':      null,
          'data-selected-hide': null,
          'data-label-id':      null,
          'data-hover':         null,
        }))

    handles.forEach(el =>
      el.remove())

    labels.forEach(el =>
      el.remove())

    labels    = []
    handles   = []
    selected  = []
  }

  const delete_all = () => {
    const selected_after_delete = selected.map(el => {
      if (canMoveRight(el))     return canMoveRight(el)
      else if (canMoveLeft(el)) return canMoveLeft(el)
      else if (el.parentNode)   return el.parentNode
    })

    Array.from([...selected, ...labels, ...handles]).forEach(el =>
      el.remove())

    labels    = []
    handles   = []
    selected  = []

    selected_after_delete.forEach(el =>
      select(el))
  }

  const expandSelection = ({query, all = false}) => {
    if (all) {
      const unselecteds = $(query + ':not([data-selected])')
      unselecteds.forEach(select)
    }
    else {
      const potentials = $(query)
      if (!potentials) return

      const root_node_index = potentials.reduce((index, node, i) =>
        combineNodeNameAndClass(node) == query
          ? index = i
          : index
      , null)

      if (root_node_index !== null) {
        if (!potentials[root_node_index + 1]) {
          const potential = potentials.filter(el => !el.attr('data-selected'))[0]
          if (potential) select(potential)
        }
        else {
          select(potentials[root_node_index + 1])
        }
      }
    }
  }

  const combineNodeNameAndClass = node =>
    `${node.nodeName.toLowerCase()}${createClassname(node)}`

  const overlayMetaUI = el => {
    let handle = createHandle(el)
    let label  = createLabel(el, `
      ${el.nodeName.toLowerCase()}
      ${el.id && '#' + el.id}
      ${createClassname(el).split('.')
        .filter(name => name != '')
        .reduce((links, name) => `
          ${links}
          .${name}
        `, '')
      }
    `)

    let observer        = createObserver(el, {handle,label})
    let parentObserver  = createObserver(el, {handle,label})

    observer.observe(el, { attributes: true })
    parentObserver.observe(el.parentNode, { childList:true, subtree:true })

    $(label).on('DOMNodeRemoved', _ => {
      observer.disconnect()
      parentObserver.disconnect()
    })
  }

  const setLabel = (el, label) =>
    label.update = el.getBoundingClientRect()

  const createLabel = (el, text) => {
    if (!labels[parseInt(el.getAttribute('data-label-id'))]) {
      const label = document.createElement('pb-label')

      label.text = text
      label.position = {
        boundingRect:   el.getBoundingClientRect(),
        node_label_id:  labels.length,
      }
      el.setAttribute('data-label-id', labels.length)

      document.body.appendChild(label)

      $(label).on('query', ({detail}) => {
        if (!detail.text) return
        this.query_text = detail.text

        queryPage('[data-hover]', el =>
          el.setAttribute('data-hover', null))

        queryPage(this.query_text + ':not([data-selected])', el =>
          detail.activator === 'mouseenter'
            ? el.setAttribute('data-hover', true)
            : select(el))
      })

      $(label).on('mouseleave', e => {
        e.preventDefault()
        e.stopPropagation()
        queryPage('[data-hover]', el =>
          el.setAttribute('data-hover', null))
      })

      labels[labels.length] = label
      return label
    }
  }

  const createHandle = el => {
    if (!handles[parseInt(el.getAttribute('data-label-id'))]) {
      const handle = document.createElement('pb-handles')

      handle.position = {
        boundingRect:   el.getBoundingClientRect(),
        node_label_id:  handles.length,
      }

      document.body.appendChild(handle)

      handles[handles.length] = handle
      return handle
    }
  }

  const setHandle = (node, handle) => {
    handle.position = {
      boundingRect:   node.getBoundingClientRect(),
      node_label_id:  node.getAttribute('data-label-id'),
    }
  }

  const createObserver = (node, {label,handle}) =>
    new MutationObserver(list => {
      setLabel(node, label)
      setHandle(node, handle)
    })

  const onSelectedUpdate = (cb, immediateCallback = true) => {
    selectedCallbacks.push(cb)
    if (immediateCallback) cb(selected)
  }

  const removeSelectedCallback = cb =>
    selectedCallbacks = selectedCallbacks.filter(callback => callback != cb)

  const tellWatchers = () =>
    selectedCallbacks.forEach(cb => cb(selected))

  const disconnect = () => {
    unselect_all()
    unlisten()
  }

  watchImagesForUpload()
  listen()

  return {
    select,
    unselect_all,
    onSelectedUpdate,
    removeSelectedCallback,
    disconnect
  }
}
<commit_msg>clean up hover borders<commit_after>import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'

import { canMoveLeft, canMoveRight, canMoveUp } from './move'
import { watchImagesForUpload } from './imageswap'
import { queryPage } from './search'
import {
  metaKey, htmlStringToDom, createClassname,
  isOffBounds, getStyles, deepElementFromPoint
} from '../utilities/'

export function Selectable() {
  const elements          = $('body')
  let selected            = []
  let selectedCallbacks   = []
  let labels              = []
  let handles             = []

  const listen = () => {
    elements.forEach(el => el.addEventListener('click', on_click, true))
    elements.forEach(el => el.addEventListener('dblclick', on_dblclick, true))
    elements.on('selectstart', on_selection)
    elements.on('mousemove', on_hover)
    elements.on('mouseout', on_hoverout)

    document.addEventListener('copy', on_copy)
    document.addEventListener('cut', on_cut)
    document.addEventListener('paste', on_paste)

    watchCommandKey()

    hotkeys(`${metaKey}+alt+c`, on_copy_styles)
    hotkeys(`${metaKey}+alt+v`, e => on_paste_styles())
    hotkeys('esc', on_esc)
    hotkeys(`${metaKey}+d`, on_duplicate)
    hotkeys('backspace,del,delete', on_delete)
    hotkeys('alt+del,alt+backspace', on_clearstyles)
    hotkeys(`${metaKey}+e,${metaKey}+shift+e`, on_expand_selection)
    hotkeys(`${metaKey}+g,${metaKey}+shift+g`, on_group)
    hotkeys('tab,shift+tab,enter,shift+enter', on_keyboard_traversal)
  }

  const unlisten = () => {
    elements.forEach(el => el.removeEventListener('click', on_click, true))
    elements.forEach(el => el.removeEventListener('dblclick', on_dblclick, true))
    elements.off('selectstart', on_selection)
    elements.off('mousemove', on_hover)
    elements.off('mouseout', on_hoverout)

    document.removeEventListener('copy', on_copy)
    document.removeEventListener('cut', on_cut)
    document.removeEventListener('paste', on_paste)

    hotkeys.unbind(`esc,${metaKey}+d,backspace,del,delete,alt+del,alt+backspace,${metaKey}+e,${metaKey}+shift+e,${metaKey}+g,${metaKey}+shift+g,tab,shift+tab,enter,shift+enter`)
  }

  const on_click = e => {
    const target = deepElementFromPoint(e.clientX, e.clientY)

    if (isOffBounds(target) && !selected.filter(el => el == target).length)
      return

    e.preventDefault()
    if (!e.altKey) e.stopPropagation()
    if (!e.shiftKey) unselect_all()

    if(e.shiftKey && target.hasAttribute('data-label-id'))
      unselect(target.getAttribute('data-label-id'))
    else
    select(target)
  }

  const unselect = id => {

    [...labels, ...handles]
      .filter(node =>
          node.getAttribute('data-label-id') === id)
        .forEach(node =>
          node.remove())

    selected.filter(node =>
      node.getAttribute('data-label-id') === id)
      .forEach(node =>
        $(node).attr({
          'data-selected':      null,
          'data-selected-hide': null,
          'data-label-id':      null,
          'data-hover': null
      }))

    selected = selected.filter(node => node.getAttribute('data-label-id') !== id)

    tellWatchers()
  }

  const on_dblclick = e => {
    e.preventDefault()
    e.stopPropagation()
    if (isOffBounds(e.target)) return
    $('tool-pallete')[0].toolSelected('text')
  }

  const watchCommandKey = e => {
    let did_hide = false

    document.onkeydown = function(e) {
      if (hotkeys.ctrl && selected.length) {
        $('pb-handles, pb-label').forEach(el =>
          el.style.display = 'none')

        did_hide = true
      }
    }

    document.onkeyup = function(e) {
      if (did_hide) {
        $('pb-handles, pb-label').forEach(el =>
          el.style.display = null)

        did_hide = false
      }
    }
  }

  const on_esc = _ =>
    selected.length && unselect_all()

  const on_duplicate = e => {
    const root_node = selected[0]
    if (!root_node) return

    const deep_clone = root_node.cloneNode(true)
    deep_clone.removeAttribute('data-selected')
    root_node.parentNode.insertBefore(deep_clone, root_node.nextSibling)
    e.preventDefault()
  }

  const on_delete = e =>
    selected.length && delete_all()

  const on_clearstyles = e =>
    selected.forEach(el =>
      el.attr('style', null))

  const on_copy = e => {
    // if user has selected text, dont try to copy an element
    if (window.getSelection().toString().length)
      return

    if (selected[0] && this.node_clipboard !== selected[0]) {
      e.preventDefault()
      let $node = selected[0].cloneNode(true)
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
    }
  }

  const on_cut = e => {
    if (selected[0] && this.node_clipboard !== selected[0]) {
      let $node = selected[0].cloneNode(true)
      $node.removeAttribute('data-selected')
      this.copy_backup = $node.outerHTML
      e.clipboardData.setData('text/html', this.copy_backup)
      selected[0].remove()
    }
  }

  const on_paste = e => {
    const clipData = e.clipboardData.getData('text/html')
    const potentialHTML = clipData || this.copy_backup
    if (selected[0] && potentialHTML) {
      e.preventDefault()
      selected[0].appendChild(
        htmlStringToDom(potentialHTML))
    }
  }

  const on_copy_styles = e => {
    e.preventDefault()
    this.copied_styles = selected.map(el =>
      getStyles(el))
  }

  const on_paste_styles = (index = 0) =>
    selected.forEach(el => {
      this.copied_styles[index]
        .map(({prop, value}) =>
          el.style[prop] = value)

      index >= this.copied_styles.length - 1
        ? index = 0
        : index++
    })

  const on_expand_selection = (e, {key}) => {
    e.preventDefault()

    expandSelection({
      query:  combineNodeNameAndClass(selected[0]),
      all:    key.includes('shift'),
    })
  }

  const on_group = (e, {key}) => {
    e.preventDefault()

    if (key.split('+').includes('shift')) {
      let $selected = [...selected]
      unselect_all()
      $selected.reverse().forEach(el => {
        let l = el.children.length
        while (el.children.length > 0) {
          var node = el.childNodes[el.children.length - 1]
          if (node.nodeName !== '#text')
            select(node)
          el.parentNode.prepend(node)
        }
        el.parentNode.removeChild(el)
      })
    }
    else {
      let div = document.createElement('div')
      selected[0].parentNode.prepend(
        selected.reverse().reduce((div, el) => {
          div.appendChild(el)
          return div
        }, div)
      )
      unselect_all()
      select(div)
    }
  }

  const on_selection = e =>
    !isOffBounds(e.target)
    && selected.length
    && selected[0].textContent != e.target.textContent
    && e.preventDefault()

  const on_keyboard_traversal = (e, {key}) => {
    if (selected.length !== 1) return

    e.preventDefault()
    e.stopPropagation()

    const current = selected[0]

    if (key.includes('shift')) {
      if (key.includes('tab') && canMoveLeft(current)) {
        unselect_all()
        select(canMoveLeft(current))
      }
      if (key.includes('enter') && canMoveUp(current)) {
        unselect_all()
        select(current.parentNode)
      }
    }
    else {
      if (key.includes('tab') && canMoveRight(current)) {
        unselect_all()
        select(canMoveRight(current))
      }
      if (key.includes('enter') && current.children.length) {
        unselect_all()
        select(current.children[0])
      }
    }
  }

  const on_hover = e => {
    const target = deepElementFromPoint(e.clientX, e.clientY)
    if (isOffBounds(target)) return
    target.setAttribute('data-hover', true)
  }

  const on_hoverout = ({target}) => {
    target.removeAttribute('data-hover')
  }

  const select = el => {
    el.setAttribute('data-selected', true)
    overlayMetaUI(el)
    selected.unshift(el)
    tellWatchers()
  }

  const unselect_all = () => {
    selected
      .forEach(el =>
        $(el).attr({
          'data-selected':      null,
          'data-selected-hide': null,
          'data-label-id':      null,
          'data-hover':         null,
        }))

    handles.forEach(el =>
      el.remove())

    labels.forEach(el =>
      el.remove())

    labels    = []
    handles   = []
    selected  = []
  }

  const delete_all = () => {
    const selected_after_delete = selected.map(el => {
      if (canMoveRight(el))     return canMoveRight(el)
      else if (canMoveLeft(el)) return canMoveLeft(el)
      else if (el.parentNode)   return el.parentNode
    })

    Array.from([...selected, ...labels, ...handles]).forEach(el =>
      el.remove())

    labels    = []
    handles   = []
    selected  = []

    selected_after_delete.forEach(el =>
      select(el))
  }

  const expandSelection = ({query, all = false}) => {
    if (all) {
      const unselecteds = $(query + ':not([data-selected])')
      unselecteds.forEach(select)
    }
    else {
      const potentials = $(query)
      if (!potentials) return

      const root_node_index = potentials.reduce((index, node, i) =>
        combineNodeNameAndClass(node) == query
          ? index = i
          : index
      , null)

      if (root_node_index !== null) {
        if (!potentials[root_node_index + 1]) {
          const potential = potentials.filter(el => !el.attr('data-selected'))[0]
          if (potential) select(potential)
        }
        else {
          select(potentials[root_node_index + 1])
        }
      }
    }
  }

  const combineNodeNameAndClass = node =>
    `${node.nodeName.toLowerCase()}${createClassname(node)}`

  const overlayMetaUI = el => {
    let handle = createHandle(el)
    let label  = createLabel(el, `
      ${el.nodeName.toLowerCase()}
      ${el.id && '#' + el.id}
      ${createClassname(el).split('.')
        .filter(name => name != '')
        .reduce((links, name) => `
          ${links}
          .${name}
        `, '')
      }
    `)

    let observer        = createObserver(el, {handle,label})
    let parentObserver  = createObserver(el, {handle,label})

    observer.observe(el, { attributes: true })
    parentObserver.observe(el.parentNode, { childList:true, subtree:true })

    $(label).on('DOMNodeRemoved', _ => {
      observer.disconnect()
      parentObserver.disconnect()
    })
  }

  const setLabel = (el, label) =>
    label.update = el.getBoundingClientRect()

  const createLabel = (el, text) => {
    if (!labels[parseInt(el.getAttribute('data-label-id'))]) {
      const label = document.createElement('pb-label')

      label.text = text
      label.position = {
        boundingRect:   el.getBoundingClientRect(),
        node_label_id:  labels.length,
      }
      el.setAttribute('data-label-id', labels.length)

      document.body.appendChild(label)

      $(label).on('query', ({detail}) => {
        if (!detail.text) return
        this.query_text = detail.text

        queryPage('[data-hover]', el =>
          el.setAttribute('data-hover', null))

        queryPage(this.query_text + ':not([data-selected])', el =>
          detail.activator === 'mouseenter'
            ? el.setAttribute('data-hover', true)
            : select(el))
      })

      $(label).on('mouseleave', e => {
        e.preventDefault()
        e.stopPropagation()
        queryPage('[data-hover]', el =>
          el.setAttribute('data-hover', null))
      })

      labels[labels.length] = label
      return label
    }
  }

  const createHandle = el => {
    if (!handles[parseInt(el.getAttribute('data-label-id'))]) {
      const handle = document.createElement('pb-handles')

      handle.position = {
        boundingRect:   el.getBoundingClientRect(),
        node_label_id:  handles.length,
      }

      document.body.appendChild(handle)

      handles[handles.length] = handle
      return handle
    }
  }

  const setHandle = (node, handle) => {
    handle.position = {
      boundingRect:   node.getBoundingClientRect(),
      node_label_id:  node.getAttribute('data-label-id'),
    }
  }

  const createObserver = (node, {label,handle}) =>
    new MutationObserver(list => {
      setLabel(node, label)
      setHandle(node, handle)
    })

  const onSelectedUpdate = (cb, immediateCallback = true) => {
    selectedCallbacks.push(cb)
    if (immediateCallback) cb(selected)
  }

  const removeSelectedCallback = cb =>
    selectedCallbacks = selectedCallbacks.filter(callback => callback != cb)

  const tellWatchers = () =>
    selectedCallbacks.forEach(cb => cb(selected))

  const disconnect = () => {
    unselect_all()
    unlisten()
  }

  watchImagesForUpload()
  listen()

  return {
    select,
    unselect_all,
    onSelectedUpdate,
    removeSelectedCallback,
    disconnect
  }
}
<|endoftext|>