<commit_before>import $ from 'jquery';
import Ember from 'ember';
import Controller from 'ember-controller';
import computed from 'ember-computed';
import {guidFor} from 'ember-metal/utils';
import injectService from 'ember-service/inject';
import injectController from 'ember-controller/inject';
import {htmlSafe} from 'ember-string';
import observer from 'ember-metal/observer';

import {parseDateString} from 'ghost-admin/utils/date-formatting';
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-controller';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import isNumber from 'ghost-admin/utils/isNumber';

const {ArrayProxy, Handlebars, PromiseProxyMixin} = Ember;

export default Controller.extend(SettingsMenuMixin, {
    selectedAuthor: null,

    application: injectController(),
    config: injectService(),
    ghostPaths: injectService(),
    notifications: injectService(),
    session: injectService(),
    timeZone: injectService(),

    initializeSelectedAuthor: observer('model', function () {
        return this.get('model.author').then((author) => {
            this.set('selectedAuthor', author);
            return author;
        });
    }),

    authors: computed(function () {
        // Loaded asynchronously, so must use promise proxies.
        let deferred = {};

        deferred.promise = this.store.query('user', {limit: 'all'}).then((users) => {
            return users.rejectBy('id', 'me').sortBy('name');
        }).then((users) => {
            return users.filter((user) => {
                return user.get('active');
            });
        });

        return ArrayProxy
            .extend(PromiseProxyMixin)
            .create(deferred);
    }),

    slugValue: boundOneWay('model.slug'),
    metaTitleScratch: boundOneWay('model.metaTitle'),
    metaDescriptionScratch: boundOneWay('model.metaDescription'),

    seoTitle: computed('model.titleScratch', 'metaTitleScratch', function () {
        let metaTitle = this.get('metaTitleScratch') || '';

        metaTitle = metaTitle.length > 0 ? metaTitle : this.get('model.titleScratch');

        if (metaTitle.length > 70) {
            metaTitle = metaTitle.substring(0, 70).trim();
            metaTitle = Handlebars.Utils.escapeExpression(metaTitle);
            metaTitle = htmlSafe(`${metaTitle}…`);
        }

        return metaTitle;
    }),

    seoDescription: computed('model.scratch', 'metaDescriptionScratch', function () {
        let metaDescription = this.get('metaDescriptionScratch') || '';
        let html = '';
        let el, placeholder;

        if (metaDescription.length > 0) {
            placeholder = metaDescription;
        } else {
            el = $('.rendered-markdown');

            // Get rendered markdown
            if (el !== undefined && el.length > 0) {
                html = el.clone();
                html.find('.js-drop-zone').remove();
                html = html[0].innerHTML;
            }

            // Strip HTML
            placeholder = $('<div />', {html}).text();
            // Replace new lines and trim
            placeholder = placeholder.replace(/\n+/g, ' ').trim();
        }

        if (placeholder.length > 156) {
            // Limit to 156 characters
            placeholder = placeholder.substring(0, 156).trim();
            placeholder = Handlebars.Utils.escapeExpression(placeholder);
            placeholder = htmlSafe(`${placeholder}…`);
        }

        return placeholder;
    }),

    seoURL: computed('model.slug', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let seoSlug = this.get('model.slug') ? this.get('model.slug') : '';
        let seoURL = `${blogUrl}/${seoSlug}`;

        // only append a slash to the URL if the slug exists
        if (seoSlug) {
            seoURL += '/';
        }

        if (seoURL.length > 70) {
            seoURL = seoURL.substring(0, 70).trim();
            seoURL = htmlSafe(`${seoURL}…`);
        }

        return seoURL;
    }),

    // live-query of all tags for tag input autocomplete
    availableTags: computed(function () {
        return this.get('store').filter('tag', {limit: 'all'}, () => {
            return true;
        });
    }),

    showError(error) {
        // TODO: remove null check once ValidationEngine has been removed
        if (error) {
            this.get('notifications').showAPIError(error);
        }
    },

    actions: {
        discardEnter() {
            return false;
        },

        togglePage() {
            this.toggleProperty('model.page');

            // If this is a new post.  Don't save the model.  Defer the save
            // to the user pressing the save button
            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        toggleFeatured() {
            this.toggleProperty('model.featured');

            // If this is a new post.  Don't save the model.  Defer the save
            // to the user pressing the save button
            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save(this.get('saveOptions')).catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        /**
         * triggered by user manually changing slug
         */
        updateSlug(newSlug) {
            let slug = this.get('model.slug');

            newSlug = newSlug || slug;
            newSlug = newSlug && newSlug.trim();

            // Ignore unchanged slugs or candidate slugs that are empty
            if (!newSlug || slug === newSlug) {
                // reset the input to its previous state
                this.set('slugValue', slug);

                return;
            }

            this.get('slugGenerator').generateSlug('post', newSlug).then((serverSlug) => {
                // If after getting the sanitized and unique slug back from the API
                // we end up with a slug that matches the existing slug, abort the change
                if (serverSlug === slug) {
                    return;
                }

                // Because the server transforms the candidate slug by stripping
                // certain characters and appending a number onto the end of slugs
                // to enforce uniqueness, there are cases where we can get back a
                // candidate slug that is a duplicate of the original except for
                // the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)

                // get the last token out of the slug candidate and see if it's a number
                let slugTokens = serverSlug.split('-');
                let check = Number(slugTokens.pop());

                // if the candidate slug is the same as the existing slug except
                // for the incrementor then the existing slug should be used
                if (isNumber(check) && check > 0) {
                    if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
                        this.set('slugValue', slug);

                        return;
                    }
                }

                this.set('model.slug', serverSlug);

                if (this.hasObserverFor('model.titleScratch')) {
                    this.removeObserver('model.titleScratch', this, 'titleObserver');
                }

                // If this is a new post.  Don't save the model.  Defer the save
                // to the user pressing the save button
                if (this.get('model.isNew')) {
                    return;
                }

                return this.get('model').save();
            }).catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        /**
         * Parse user's set published date.
         * Action sent by post settings menu view.
         * (#1351)
         */
        setPublishedAtUTC(userInput) {
            if (!userInput) {
                // Clear out the publishedAtUTC field for a draft
                if (this.get('model.isDraft')) {
                    this.set('model.publishedAtUTC', null);
                }
                return;
            }

            // The user inputs a date which he expects to be in his timezone. Therefore, from now on
            // we have to work with the timezone offset which we get from the timeZone Service.
            this.get('timeZone.blogTimezone').then((blogTimezone) => {
                let newPublishedAt = parseDateString(userInput, blogTimezone);
                let publishedAtUTC = moment.utc(this.get('model.publishedAtUTC'));
                let errMessage = '';
                let newPublishedAtUTC;

                // Clear previous errors
                this.get('model.errors').remove('post-setting-date');

                // Validate new Published date
                if (!newPublishedAt.isValid()) {
                    errMessage = 'Published Date must be a valid date with format: ' +
                        'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
                }

                // Date is a valid date, so now make it UTC
                newPublishedAtUTC = moment.utc(newPublishedAt);

                if (newPublishedAtUTC.diff(moment.utc(new Date()), 'hours', true) > 0) {

                    // We have to check that the time from now is not shorter than 2 minutes,
                    // otherwise we'll have issues with the serverside scheduling procedure
                    if (newPublishedAtUTC.diff(moment.utc(new Date()), 'minutes', true) < 2) {
                        errMessage = 'Must be at least 2 minutes from now.';
                    } else {
                        // in case the post is already published and the user sets the date
                        // afterwards to a future time, we stop here, and he has to unpublish
                        // his post first
                        if (this.get('model.isPublished')) {
                            errMessage = 'Your post is already published.';
                            // this is the indicator for the different save button layout
                            this.set('timeScheduled', false);
                        }
                        // everything fine, we can start the schedule workflow and change
                        // the save buttons according to it
                        this.set('timeScheduled', true);
                    }
                    // if the post is already scheduled and the user changes the date back into the
                    // past, we'll set the status of the post back to draft, so he can start all over
                    // again
                } else if (this.get('model.isScheduled')) {
                    this.set('model.status', 'draft');
                }

                // If errors, notify and exit.
                if (errMessage) {
                    this.get('model.errors').add('post-setting-date', errMessage);
                    return;
                }

                // Do nothing if the user didn't actually change the date
                if (publishedAtUTC && publishedAtUTC.isSame(newPublishedAtUTC)) {
                    return;
                }

                // Validation complete
                this.set('model.publishedAtUTC', newPublishedAtUTC);

                // If this is a new post.  Don't save the model.  Defer the save
                // to the user pressing the save button
                if (this.get('model.isNew')) {
                    return;
                }

                this.get('model').save().catch((error) => {
                    this.showError(error);
                    this.get('model').rollbackAttributes();
                });
            });
        },

        setMetaTitle(metaTitle) {
            // Grab the model and current stored meta title
            let model = this.get('model');
            let currentTitle = model.get('metaTitle');

            // If the title entered matches the stored meta title, do nothing
            if (currentTitle === metaTitle) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            model.set('metaTitle', metaTitle);

            // Make sure the meta title is valid and if so, save it into the model
            return model.validate({property: 'metaTitle'}).then(() => {
                if (model.get('isNew')) {
                    return;
                }

                return model.save();
            });
        },

        setMetaDescription(metaDescription) {
            // Grab the model and current stored meta description
            let model = this.get('model');
            let currentDescription = model.get('metaDescription');

            // If the title entered matches the stored meta title, do nothing
            if (currentDescription === metaDescription) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            model.set('metaDescription', metaDescription);

            // Make sure the meta title is valid and if so, save it into the model
            return model.validate({property: 'metaDescription'}).then(() => {
                if (model.get('isNew')) {
                    return;
                }

                return model.save();
            });
        },

        setCoverImage(image) {
            this.set('model.image', image);

            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        clearCoverImage() {
            this.set('model.image', '');

            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        resetPubDate() {
            this.set('publishedAtUTCValue', '');
        },

        closeNavMenu() {
            this.get('application').send('closeNavMenu');
        },

        changeAuthor(newAuthor) {
            let author = this.get('model.author');
            let model = this.get('model');

            // return if nothing changed
            if (newAuthor.get('id') === author.get('id')) {
                return;
            }

            model.set('author', newAuthor);

            // if this is a new post (never been saved before), don't try to save it
            if (this.get('model.isNew')) {
                return;
            }

            model.save().catch((error) => {
                this.showError(error);
                this.set('selectedAuthor', author);
                model.rollbackAttributes();
            });
        },

        addTag(tagName, index) {
            let currentTags = this.get('model.tags');
            let currentTagNames = currentTags.map((tag) => {
                return tag.get('name').toLowerCase();
            });
            let availableTagNames,
                tagToAdd;

            tagName = tagName.trim();

            // abort if tag is already selected
            if (currentTagNames.contains(tagName.toLowerCase())) {
                return;
            }

            this.get('availableTags').then((availableTags) => {
                availableTagNames = availableTags.map((tag) => {
                    return tag.get('name').toLowerCase();
                });

                // find existing tag or create new
                if (availableTagNames.contains(tagName.toLowerCase())) {
                    tagToAdd = availableTags.find((tag) => {
                        return tag.get('name').toLowerCase() === tagName.toLowerCase();
                    });
                } else {
                    tagToAdd = this.get('store').createRecord('tag', {
                        name: tagName
                    });

                    // we need to set a UUID so that selectize has a unique value
                    // it will be ignored when sent to the server
                    tagToAdd.set('uuid', guidFor(tagToAdd));
                }

                // push tag onto post relationship
                if (tagToAdd) {
                    this.get('model.tags').insertAt(index, tagToAdd);
                }
            });
        },

        removeTag(tag) {
            this.get('model.tags').removeObject(tag);

            if (tag.get('isNew')) {
                tag.destroyRecord();
            }
        }
    }
});
<commit_msg>🐛 fix post-settings-menu slug input (#223)<commit_after>import $ from 'jquery';
import Ember from 'ember';
import Controller from 'ember-controller';
import computed from 'ember-computed';
import {guidFor} from 'ember-metal/utils';
import injectService from 'ember-service/inject';
import injectController from 'ember-controller/inject';
import {htmlSafe} from 'ember-string';
import observer from 'ember-metal/observer';

import {parseDateString} from 'ghost-admin/utils/date-formatting';
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-controller';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import isNumber from 'ghost-admin/utils/isNumber';

const {ArrayProxy, Handlebars, PromiseProxyMixin} = Ember;

export default Controller.extend(SettingsMenuMixin, {
    selectedAuthor: null,

    application: injectController(),
    config: injectService(),
    ghostPaths: injectService(),
    notifications: injectService(),
    session: injectService(),
    slugGenerator: injectService(),
    timeZone: injectService(),

    initializeSelectedAuthor: observer('model', function () {
        return this.get('model.author').then((author) => {
            this.set('selectedAuthor', author);
            return author;
        });
    }),

    authors: computed(function () {
        // Loaded asynchronously, so must use promise proxies.
        let deferred = {};

        deferred.promise = this.store.query('user', {limit: 'all'}).then((users) => {
            return users.rejectBy('id', 'me').sortBy('name');
        }).then((users) => {
            return users.filter((user) => {
                return user.get('active');
            });
        });

        return ArrayProxy
            .extend(PromiseProxyMixin)
            .create(deferred);
    }),

    slugValue: boundOneWay('model.slug'),
    metaTitleScratch: boundOneWay('model.metaTitle'),
    metaDescriptionScratch: boundOneWay('model.metaDescription'),

    seoTitle: computed('model.titleScratch', 'metaTitleScratch', function () {
        let metaTitle = this.get('metaTitleScratch') || '';

        metaTitle = metaTitle.length > 0 ? metaTitle : this.get('model.titleScratch');

        if (metaTitle.length > 70) {
            metaTitle = metaTitle.substring(0, 70).trim();
            metaTitle = Handlebars.Utils.escapeExpression(metaTitle);
            metaTitle = htmlSafe(`${metaTitle}…`);
        }

        return metaTitle;
    }),

    seoDescription: computed('model.scratch', 'metaDescriptionScratch', function () {
        let metaDescription = this.get('metaDescriptionScratch') || '';
        let html = '';
        let el, placeholder;

        if (metaDescription.length > 0) {
            placeholder = metaDescription;
        } else {
            el = $('.rendered-markdown');

            // Get rendered markdown
            if (el !== undefined && el.length > 0) {
                html = el.clone();
                html.find('.js-drop-zone').remove();
                html = html[0].innerHTML;
            }

            // Strip HTML
            placeholder = $('<div />', {html}).text();
            // Replace new lines and trim
            placeholder = placeholder.replace(/\n+/g, ' ').trim();
        }

        if (placeholder.length > 156) {
            // Limit to 156 characters
            placeholder = placeholder.substring(0, 156).trim();
            placeholder = Handlebars.Utils.escapeExpression(placeholder);
            placeholder = htmlSafe(`${placeholder}…`);
        }

        return placeholder;
    }),

    seoURL: computed('model.slug', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let seoSlug = this.get('model.slug') ? this.get('model.slug') : '';
        let seoURL = `${blogUrl}/${seoSlug}`;

        // only append a slash to the URL if the slug exists
        if (seoSlug) {
            seoURL += '/';
        }

        if (seoURL.length > 70) {
            seoURL = seoURL.substring(0, 70).trim();
            seoURL = htmlSafe(`${seoURL}…`);
        }

        return seoURL;
    }),

    // live-query of all tags for tag input autocomplete
    availableTags: computed(function () {
        return this.get('store').filter('tag', {limit: 'all'}, () => {
            return true;
        });
    }),

    showError(error) {
        // TODO: remove null check once ValidationEngine has been removed
        if (error) {
            this.get('notifications').showAPIError(error);
        }
    },

    actions: {
        discardEnter() {
            return false;
        },

        togglePage() {
            this.toggleProperty('model.page');

            // If this is a new post.  Don't save the model.  Defer the save
            // to the user pressing the save button
            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        toggleFeatured() {
            this.toggleProperty('model.featured');

            // If this is a new post.  Don't save the model.  Defer the save
            // to the user pressing the save button
            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save(this.get('saveOptions')).catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        /**
         * triggered by user manually changing slug
         */
        updateSlug(newSlug) {
            let slug = this.get('model.slug');

            newSlug = newSlug || slug;
            newSlug = newSlug && newSlug.trim();

            // Ignore unchanged slugs or candidate slugs that are empty
            if (!newSlug || slug === newSlug) {
                // reset the input to its previous state
                this.set('slugValue', slug);

                return;
            }

            this.get('slugGenerator').generateSlug('post', newSlug).then((serverSlug) => {
                // If after getting the sanitized and unique slug back from the API
                // we end up with a slug that matches the existing slug, abort the change
                if (serverSlug === slug) {
                    return;
                }

                // Because the server transforms the candidate slug by stripping
                // certain characters and appending a number onto the end of slugs
                // to enforce uniqueness, there are cases where we can get back a
                // candidate slug that is a duplicate of the original except for
                // the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)

                // get the last token out of the slug candidate and see if it's a number
                let slugTokens = serverSlug.split('-');
                let check = Number(slugTokens.pop());

                // if the candidate slug is the same as the existing slug except
                // for the incrementor then the existing slug should be used
                if (isNumber(check) && check > 0) {
                    if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
                        this.set('slugValue', slug);

                        return;
                    }
                }

                this.set('model.slug', serverSlug);

                if (this.hasObserverFor('model.titleScratch')) {
                    this.removeObserver('model.titleScratch', this, 'titleObserver');
                }

                // If this is a new post.  Don't save the model.  Defer the save
                // to the user pressing the save button
                if (this.get('model.isNew')) {
                    return;
                }

                return this.get('model').save();
            }).catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        /**
         * Parse user's set published date.
         * Action sent by post settings menu view.
         * (#1351)
         */
        setPublishedAtUTC(userInput) {
            if (!userInput) {
                // Clear out the publishedAtUTC field for a draft
                if (this.get('model.isDraft')) {
                    this.set('model.publishedAtUTC', null);
                }
                return;
            }

            // The user inputs a date which he expects to be in his timezone. Therefore, from now on
            // we have to work with the timezone offset which we get from the timeZone Service.
            this.get('timeZone.blogTimezone').then((blogTimezone) => {
                let newPublishedAt = parseDateString(userInput, blogTimezone);
                let publishedAtUTC = moment.utc(this.get('model.publishedAtUTC'));
                let errMessage = '';
                let newPublishedAtUTC;

                // Clear previous errors
                this.get('model.errors').remove('post-setting-date');

                // Validate new Published date
                if (!newPublishedAt.isValid()) {
                    errMessage = 'Published Date must be a valid date with format: ' +
                        'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
                }

                // Date is a valid date, so now make it UTC
                newPublishedAtUTC = moment.utc(newPublishedAt);

                if (newPublishedAtUTC.diff(moment.utc(new Date()), 'hours', true) > 0) {

                    // We have to check that the time from now is not shorter than 2 minutes,
                    // otherwise we'll have issues with the serverside scheduling procedure
                    if (newPublishedAtUTC.diff(moment.utc(new Date()), 'minutes', true) < 2) {
                        errMessage = 'Must be at least 2 minutes from now.';
                    } else {
                        // in case the post is already published and the user sets the date
                        // afterwards to a future time, we stop here, and he has to unpublish
                        // his post first
                        if (this.get('model.isPublished')) {
                            errMessage = 'Your post is already published.';
                            // this is the indicator for the different save button layout
                            this.set('timeScheduled', false);
                        }
                        // everything fine, we can start the schedule workflow and change
                        // the save buttons according to it
                        this.set('timeScheduled', true);
                    }
                    // if the post is already scheduled and the user changes the date back into the
                    // past, we'll set the status of the post back to draft, so he can start all over
                    // again
                } else if (this.get('model.isScheduled')) {
                    this.set('model.status', 'draft');
                }

                // If errors, notify and exit.
                if (errMessage) {
                    this.get('model.errors').add('post-setting-date', errMessage);
                    return;
                }

                // Do nothing if the user didn't actually change the date
                if (publishedAtUTC && publishedAtUTC.isSame(newPublishedAtUTC)) {
                    return;
                }

                // Validation complete
                this.set('model.publishedAtUTC', newPublishedAtUTC);

                // If this is a new post.  Don't save the model.  Defer the save
                // to the user pressing the save button
                if (this.get('model.isNew')) {
                    return;
                }

                this.get('model').save().catch((error) => {
                    this.showError(error);
                    this.get('model').rollbackAttributes();
                });
            });
        },

        setMetaTitle(metaTitle) {
            // Grab the model and current stored meta title
            let model = this.get('model');
            let currentTitle = model.get('metaTitle');

            // If the title entered matches the stored meta title, do nothing
            if (currentTitle === metaTitle) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            model.set('metaTitle', metaTitle);

            // Make sure the meta title is valid and if so, save it into the model
            return model.validate({property: 'metaTitle'}).then(() => {
                if (model.get('isNew')) {
                    return;
                }

                return model.save();
            });
        },

        setMetaDescription(metaDescription) {
            // Grab the model and current stored meta description
            let model = this.get('model');
            let currentDescription = model.get('metaDescription');

            // If the title entered matches the stored meta title, do nothing
            if (currentDescription === metaDescription) {
                return;
            }

            // If the title entered is different, set it as the new meta title
            model.set('metaDescription', metaDescription);

            // Make sure the meta title is valid and if so, save it into the model
            return model.validate({property: 'metaDescription'}).then(() => {
                if (model.get('isNew')) {
                    return;
                }

                return model.save();
            });
        },

        setCoverImage(image) {
            this.set('model.image', image);

            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        clearCoverImage() {
            this.set('model.image', '');

            if (this.get('model.isNew')) {
                return;
            }

            this.get('model').save().catch((error) => {
                this.showError(error);
                this.get('model').rollbackAttributes();
            });
        },

        resetPubDate() {
            this.set('publishedAtUTCValue', '');
        },

        closeNavMenu() {
            this.get('application').send('closeNavMenu');
        },

        changeAuthor(newAuthor) {
            let author = this.get('model.author');
            let model = this.get('model');

            // return if nothing changed
            if (newAuthor.get('id') === author.get('id')) {
                return;
            }

            model.set('author', newAuthor);

            // if this is a new post (never been saved before), don't try to save it
            if (this.get('model.isNew')) {
                return;
            }

            model.save().catch((error) => {
                this.showError(error);
                this.set('selectedAuthor', author);
                model.rollbackAttributes();
            });
        },

        addTag(tagName, index) {
            let currentTags = this.get('model.tags');
            let currentTagNames = currentTags.map((tag) => {
                return tag.get('name').toLowerCase();
            });
            let availableTagNames,
                tagToAdd;

            tagName = tagName.trim();

            // abort if tag is already selected
            if (currentTagNames.contains(tagName.toLowerCase())) {
                return;
            }

            this.get('availableTags').then((availableTags) => {
                availableTagNames = availableTags.map((tag) => {
                    return tag.get('name').toLowerCase();
                });

                // find existing tag or create new
                if (availableTagNames.contains(tagName.toLowerCase())) {
                    tagToAdd = availableTags.find((tag) => {
                        return tag.get('name').toLowerCase() === tagName.toLowerCase();
                    });
                } else {
                    tagToAdd = this.get('store').createRecord('tag', {
                        name: tagName
                    });

                    // we need to set a UUID so that selectize has a unique value
                    // it will be ignored when sent to the server
                    tagToAdd.set('uuid', guidFor(tagToAdd));
                }

                // push tag onto post relationship
                if (tagToAdd) {
                    this.get('model.tags').insertAt(index, tagToAdd);
                }
            });
        },

        removeTag(tag) {
            this.get('model.tags').removeObject(tag);

            if (tag.get('isNew')) {
                tag.destroyRecord();
            }
        }
    }
});
<|endoftext|>


<commit_before>// DoB means Days of Buffering

function ynabEnhancedDoB() {
    var YNABheader = document.getElementsByClassName("budget-header-flexbox")[0];
    var elementForAoM = document.getElementsByClassName("budget-header-days")[0];
    var elementForDoB = elementForAoM.cloneNode(true);

    elementForDoB.className = elementForDoB.className + " days-of-buffering";
    elementForDoB.children[1].textContent = "Days of Buffering";
    elementForDoB.children[1].title = "Don't like AoM? Try this out instead!";

    var calculation = ynabEnhancedDoBCalculate();
    if (!calculation){
        elementForDoB.children[0].textContent = "???";
        elementForDoB.children[0].title = "Your budget history is less than 15 days. Go on with YNAB a while.";
    }
    else {
        elementForDoB.children[0].textContent = calculation["DoB"] + " day" + (calculation["DoB"] == 1 ? "" : "s");
        elementForDoB.children[0].title = "Total outflow: " + ynab.YNABSharedLib.currencyFormatter.format(calculation["totalOutflow"]) +
            "\nTotal days of budgeting: " + calculation["totalDays"] +
            "\nAverage daily outflow: ~" + ynab.YNABSharedLib.currencyFormatter.format(calculation["averageDailyOutflow"]) +
            "\nAverage daily transactions: " + calculation["averageDailyTransactions"].toFixed(1);
    }

    YNABheader.appendChild(elementForDoB);
}

function onlyUnique(value, index, self) {
    return self.indexOf(value) === index;
}

function ynabEnhancedCheckTransactionTypes(transactions) {
    // Describe all handled transaction types and check that no other got.
    var handeledTransactionTypes = ["subTransaction", "transaction", "scheduledTransaction", "scheduledSubTransaction"];
    var uniqueTransactionTypes = Array.from(transactions, (el) => el.displayItemType).filter(onlyUnique);
    var allTypesHandeled = uniqueTransactionTypes.every(el => uniqueTransactionTypes.includes(el));
    if (!allTypesHandeled) {
        throw "Found unhandeled transaction type. " + uniqueTransactionTypes;
    }
}

function ynabEnhancedDoBCalculate() {
    // Get outflow transactions.
    var entityManager = ynab.YNABSharedLib.defaultInstance.entityManager;
    var transactions = entityManager.getAllTransactions();
    var outflowTransactions = transactions.filter((el) => !el.isTombstone
                                                        && el.transferAccountId == null
                                                        && el.amount < 0
                                                        && el.getAccount().onBudget);

    // Filter outflow transactions by Date for history lookup option.
    if (ynabToolKit.options.daysOfBufferingHistoryLookup > 0) {
        dateNow = Date.now();
        var outflowTransactions = outflowTransactions.filter((el) =>
            (dateNow - el.getDate().getUTCTime()) / 3600/24/1000/(365/12) < ynabToolKit.options.daysOfBufferingHistoryLookup)
    }

    // Get outflow transactions period
    outflowTransactionsDates = Array.from(outflowTransactions, (el) => el.getDate().getUTCTime());
    var firstTransactionDate = Math.min.apply(null, outflowTransactionsDates);
    var lastTransactionDate = Math.max.apply(null, outflowTransactionsDates);
    var totalDays = (lastTransactionDate - firstTransactionDate)/3600/24/1000;
    if (totalDays < 15){
        return false;
    }

    var totalOutflow = Array.from(outflowTransactions, (i) => -i.amount).reduce((a, b) => a + b, 0);
    var averageDailyOutflow = totalOutflow / totalDays;
    var budgetAccountsTotal = ynab.YNABSharedLib.getBudgetViewModel_SidebarViewModel()._result.getOnBudgetAccountsBalance();
    return {
        DoB: Math.floor(budgetAccountsTotal/averageDailyOutflow),
        totalOutflow: totalOutflow,
        totalDays: totalDays,
        averageDailyOutflow: averageDailyOutflow,
        averageDailyTransactions: transactions.length/totalDays,
    }
}

function ynabEnhancedDoBInit() {
    var elementForAoM = document.getElementsByClassName("budget-header-days");
    var elementForDoB = document.getElementsByClassName('days-of-buffering');
    if (elementForAoM.length == 1 && elementForDoB.length == 0) {
        ynabEnhancedDoB();
    }

    setTimeout(ynabEnhancedDoBInit, 250);
}

setTimeout(ynabEnhancedDoBInit, 250);
<commit_msg>Fix average daily transactions showing wrong for most of periods<commit_after>// DoB means Days of Buffering

function ynabEnhancedDoB() {
    var YNABheader = document.getElementsByClassName("budget-header-flexbox")[0];
    var elementForAoM = document.getElementsByClassName("budget-header-days")[0];
    var elementForDoB = elementForAoM.cloneNode(true);

    elementForDoB.className = elementForDoB.className + " days-of-buffering";
    elementForDoB.children[1].textContent = "Days of Buffering";
    elementForDoB.children[1].title = "Don't like AoM? Try this out instead!";

    var calculation = ynabEnhancedDoBCalculate();
    if (!calculation){
        elementForDoB.children[0].textContent = "???";
        elementForDoB.children[0].title = "Your budget history is less than 15 days. Go on with YNAB a while.";
    }
    else {
        elementForDoB.children[0].textContent = calculation["DoB"] + " day" + (calculation["DoB"] == 1 ? "" : "s");
        elementForDoB.children[0].title = "Total outflow: " + ynab.YNABSharedLib.currencyFormatter.format(calculation["totalOutflow"]) +
            "\nTotal days of budgeting: " + calculation["totalDays"] +
            "\nAverage daily outflow: ~" + ynab.YNABSharedLib.currencyFormatter.format(calculation["averageDailyOutflow"]) +
            "\nAverage daily transactions: " + calculation["averageDailyTransactions"].toFixed(1);
    }

    YNABheader.appendChild(elementForDoB);
}

function onlyUnique(value, index, self) {
    return self.indexOf(value) === index;
}

function ynabEnhancedCheckTransactionTypes(transactions) {
    // Describe all handled transaction types and check that no other got.
    var handeledTransactionTypes = ["subTransaction", "transaction", "scheduledTransaction", "scheduledSubTransaction"];
    var uniqueTransactionTypes = Array.from(transactions, (el) => el.displayItemType).filter(onlyUnique);
    var allTypesHandeled = uniqueTransactionTypes.every(el => uniqueTransactionTypes.includes(el));
    if (!allTypesHandeled) {
        throw "Found unhandeled transaction type. " + uniqueTransactionTypes;
    }
}

function ynabEnhancedDoBCalculate() {
    // Get outflow transactions.
    var entityManager = ynab.YNABSharedLib.defaultInstance.entityManager;
    var transactions = entityManager.getAllTransactions();
    var outflowTransactions = transactions.filter((el) => !el.isTombstone
                                                        && el.transferAccountId == null
                                                        && el.amount < 0
                                                        && el.getAccount().onBudget);

    // Filter outflow transactions by Date for history lookup option.
    if (ynabToolKit.options.daysOfBufferingHistoryLookup > 0) {
        dateNow = Date.now();
        var outflowTransactions = outflowTransactions.filter((el) =>
            (dateNow - el.getDate().getUTCTime()) / 3600/24/1000/(365/12) < ynabToolKit.options.daysOfBufferingHistoryLookup)
    }

    // Get outflow transactions period
    outflowTransactionsDates = Array.from(outflowTransactions, (el) => el.getDate().getUTCTime());
    var firstTransactionDate = Math.min.apply(null, outflowTransactionsDates);
    var lastTransactionDate = Math.max.apply(null, outflowTransactionsDates);
    var totalDays = (lastTransactionDate - firstTransactionDate)/3600/24/1000;
    if (totalDays < 15){
        return false;
    }

    var totalOutflow = Array.from(outflowTransactions, (i) => -i.amount).reduce((a, b) => a + b, 0);
    var averageDailyOutflow = totalOutflow / totalDays;
    var budgetAccountsTotal = ynab.YNABSharedLib.getBudgetViewModel_SidebarViewModel()._result.getOnBudgetAccountsBalance();
    return {
        DoB: Math.floor(budgetAccountsTotal/averageDailyOutflow),
        totalOutflow: totalOutflow,
        totalDays: totalDays,
        averageDailyOutflow: averageDailyOutflow,
        averageDailyTransactions: outflowTransactions.length/totalDays,
    }
}

function ynabEnhancedDoBInit() {
    var elementForAoM = document.getElementsByClassName("budget-header-days");
    var elementForDoB = document.getElementsByClassName('days-of-buffering');
    if (elementForAoM.length == 1 && elementForDoB.length == 0) {
        ynabEnhancedDoB();
    }

    setTimeout(ynabEnhancedDoBInit, 250);
}

setTimeout(ynabEnhancedDoBInit, 250);
<|endoftext|>


<commit_before>import _ from 'underscore';
import util from 'util';
import main from '../main.js';
import { Console } from '../console.js';
import buildmessage from '../buildmessage.js';
import files from '../files.js';
import bundler from '../bundler.js';
import archinfo from '../archinfo.js';
import release from '../release.js';
import isopackets from '../isopackets.js'

import { createCordovaProjectIfNecessary } from './project.js';
import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized } from './platforms.js';
import { ensureCordovaPluginsAreSynchronized } from './plugins.js';
import { processMobileControlFile } from './mobile-control-file.js';

const WEB_ARCH_NAME = "web.cordova";

// Returns the cordovaDependencies of the Cordova arch from a star json.
export function getCordovaDependenciesFromStar(star) {
  var cordovaProgram = _.findWhere(star.programs, { arch: WEB_ARCH_NAME });
  if (cordovaProgram) {
    return cordovaProgram.cordovaDependencies;
  } else {
    return {};
  }
}

// Build a Cordova project, creating it if necessary.
export function buildCordovaProject(projectContext, platforms, options) {
  if (_.isEmpty(platforms)) return;

  Console.debug('Building the Cordova project');

  // Make sure there is a project, as all other operations depend on that
  const cordovaProject = createCordovaProjectIfNecessary(projectContext);

  buildmessage.enterJob({ title: 'building for mobile devices' }, function () {
    const bundlePath =
      projectContext.getProjectLocalDirectory('build-cordova-temp');

    Console.debug('Bundling the web.cordova program of the app');
    const bundle = getBundle(projectContext, bundlePath, options);

    // Check and consume the control file
    const controlFilePath =
      files.pathJoin(projectContext.projectDir, 'mobile-config.js');

    processMobileControlFile(
      controlFilePath,
      projectContext,
      cordovaProject,
      options.host);

    ensureCordovaPlatformsAreSynchronized(cordovaProject, projectContext);

    ensureCordovaPluginsAreSynchronized(cordovaProject, getCordovaDependenciesFromStar(
      bundle.starManifest));

    const wwwPath = files.pathJoin(cordovaProject.projectRoot, 'www');

    Console.debug('Removing the www folder');
    files.rm_recursive(wwwPath);

    const applicationPath = files.pathJoin(wwwPath, 'application');
    const programPath = files.pathJoin(bundlePath, 'programs', WEB_ARCH_NAME);

    Console.debug('Writing www/application folder');
    files.mkdir_p(applicationPath);
    files.cp_r(programPath, applicationPath);

    // Clean up the temporary bundle directory
    files.rm_recursive(bundlePath);

    Console.debug('Writing index.html');

    // Generate index.html
    var indexHtml = generateCordovaBoilerplate(
      projectContext, applicationPath, options);
    files.writeFile(files.pathJoin(applicationPath, 'index.html'), indexHtml, 'utf8');

    // Write the cordova loader
    Console.debug('Writing meteor_cordova_loader');
    var loaderPath = files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js');
    var loaderCode = files.readFile(loaderPath);
    files.writeFile(files.pathJoin(wwwPath, 'meteor_cordova_loader.js'), loaderCode);

    Console.debug('Writing a default index.html for cordova app');
    var indexPath = files.pathJoin(__dirname, 'client', 'cordova_index.html');
    var indexContent = files.readFile(indexPath);
    files.writeFile(files.pathJoin(wwwPath, 'index.html'), indexContent);

    // Cordova Build Override feature (c)
    var buildOverridePath =
      files.pathJoin(projectContext.projectDir, 'cordova-build-override');

    if (files.exists(buildOverridePath) &&
      files.stat(buildOverridePath).isDirectory()) {
      Console.debug('Copying over the cordova-build-override');
      files.cp_r(buildOverridePath, cordovaProject.projectRoot);
    }

    // Run the actual build
    Console.debug('Running the build command');

    // Depending on the debug mode build the android part in different modes
    if (_.contains(projectContext.platformList.getPlatforms(), 'android') &&
        _.contains(platforms, 'android')) {
      var androidBuildPath = files.pathJoin(cordovaProject.projectRoot, 'platforms', 'android');
      var manifestPath = files.pathJoin(androidBuildPath, 'AndroidManifest.xml');

      // XXX a hack to reset the debuggable mode
      var manifest = files.readFile(manifestPath, 'utf8');
      manifest = manifest.replace(/android:debuggable=.(true|false)./g, '');
      if (options.debug)
        manifest = manifest.replace(/<application /g, '<application android:debuggable="' + !!options.debug + '" ');
      files.writeFile(manifestPath, manifest, 'utf8');

      // XXX workaround the problem of cached apk invalidation
      files.rm_recursive(files.pathJoin(androidBuildPath, 'build'));
    }

    buildmessage.enterJob({ title: 'building mobile project' }, () => {
      const buildOptions = options.debug ? [] : ['release'];
      Promise.await(cordovaProject.build({ platforms: platforms, options: buildOptions }));
    });
  });

  Console.debug('Done building the cordova build project');

  return cordovaProject;
};

// options
//  - debug
function getBundle(projectContext, bundlePath, options) {
  var bundleResult = bundler.bundle({
    projectContext: projectContext,
    outputPath: bundlePath,
    buildOptions: {
      minifyMode: options.debug ? 'development' : 'production',
      // XXX can we ask it not to create the server arch?
      serverArch: archinfo.host(),
      webArchs: [WEB_ARCH_NAME],
      includeDebug: !!options.debug
    }
  });

  if (bundleResult.errors) {
    // XXX better error handling?
    throw new Error("Errors prevented bundling:\n" +
                    bundleResult.errors.formatMessages());
  }

  return bundleResult;
};

function generateCordovaBoilerplate(projectContext, clientDir, options) {
  var clientJsonPath = files.pathJoin(clientDir, 'program.json');
  var clientJson = JSON.parse(files.readFile(clientJsonPath, 'utf8'));
  var manifest = clientJson.manifest;
  var settings = options.settings ?
    JSON.parse(files.readFile(options.settings, 'utf8')) : {};
  var publicSettings = settings['public'];

  var meteorRelease =
    release.current.isCheckout() ? "none" : release.current.name;

  var configDummy = {};
  if (publicSettings) configDummy.PUBLIC_SETTINGS = publicSettings;

  const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing'];
  var calculatedHash =
    WebAppHashing.calculateClientHash(manifest, null, configDummy);

  // XXX partially copied from autoupdate package
  var version = process.env.AUTOUPDATE_VERSION || calculatedHash;

  var mobileServer = options.protocol + options.host;
  if (options.port) {
    mobileServer = mobileServer + ":" + options.port;
  }

  var runtimeConfig = {
    meteorRelease: meteorRelease,
    ROOT_URL: mobileServer + "/",
    // XXX propagate it from options?
    ROOT_URL_PATH_PREFIX: '',
    DDP_DEFAULT_CONNECTION_URL: mobileServer,
    autoupdateVersionCordova: version,
    cleanCache: options.clean,
    httpProxyPort: options.httpProxyPort,
    appId: projectContext.appIdentifier
  };

  if (publicSettings)
    runtimeConfig.PUBLIC_SETTINGS = publicSettings;

  const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator'];
  var boilerplate = new Boilerplate(WEB_ARCH_NAME, manifest, {
    urlMapper: _.identity,
    pathMapper: function (p) { return files.pathJoin(clientDir, p); },
    baseDataExtension: {
      meteorRuntimeConfig: JSON.stringify(
        encodeURIComponent(JSON.stringify(runtimeConfig)))
    }
  });
  return boilerplate.toHTML();
};
<commit_msg>Change bundler import<commit_after>import _ from 'underscore';
import util from 'util';
import main from '../main.js';
import { Console } from '../console.js';
import buildmessage from '../buildmessage.js';
import files from '../files.js';
import bundler from '../isobuild/bundler.js';
import archinfo from '../archinfo.js';
import release from '../release.js';
import isopackets from '../isopackets.js'

import { createCordovaProjectIfNecessary } from './project.js';
import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized } from './platforms.js';
import { ensureCordovaPluginsAreSynchronized } from './plugins.js';
import { processMobileControlFile } from './mobile-control-file.js';

const WEB_ARCH_NAME = "web.cordova";

// Returns the cordovaDependencies of the Cordova arch from a star json.
export function getCordovaDependenciesFromStar(star) {
  var cordovaProgram = _.findWhere(star.programs, { arch: WEB_ARCH_NAME });
  if (cordovaProgram) {
    return cordovaProgram.cordovaDependencies;
  } else {
    return {};
  }
}

// Build a Cordova project, creating it if necessary.
export function buildCordovaProject(projectContext, platforms, options) {
  if (_.isEmpty(platforms)) return;

  Console.debug('Building the Cordova project');

  // Make sure there is a project, as all other operations depend on that
  const cordovaProject = createCordovaProjectIfNecessary(projectContext);

  buildmessage.enterJob({ title: 'building for mobile devices' }, function () {
    const bundlePath =
      projectContext.getProjectLocalDirectory('build-cordova-temp');

    Console.debug('Bundling the web.cordova program of the app');
    const bundle = getBundle(projectContext, bundlePath, options);

    // Check and consume the control file
    const controlFilePath =
      files.pathJoin(projectContext.projectDir, 'mobile-config.js');

    processMobileControlFile(
      controlFilePath,
      projectContext,
      cordovaProject,
      options.host);

    ensureCordovaPlatformsAreSynchronized(cordovaProject, projectContext);

    ensureCordovaPluginsAreSynchronized(cordovaProject, getCordovaDependenciesFromStar(
      bundle.starManifest));

    const wwwPath = files.pathJoin(cordovaProject.projectRoot, 'www');

    Console.debug('Removing the www folder');
    files.rm_recursive(wwwPath);

    const applicationPath = files.pathJoin(wwwPath, 'application');
    const programPath = files.pathJoin(bundlePath, 'programs', WEB_ARCH_NAME);

    Console.debug('Writing www/application folder');
    files.mkdir_p(applicationPath);
    files.cp_r(programPath, applicationPath);

    // Clean up the temporary bundle directory
    files.rm_recursive(bundlePath);

    Console.debug('Writing index.html');

    // Generate index.html
    var indexHtml = generateCordovaBoilerplate(
      projectContext, applicationPath, options);
    files.writeFile(files.pathJoin(applicationPath, 'index.html'), indexHtml, 'utf8');

    // Write the cordova loader
    Console.debug('Writing meteor_cordova_loader');
    var loaderPath = files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js');
    var loaderCode = files.readFile(loaderPath);
    files.writeFile(files.pathJoin(wwwPath, 'meteor_cordova_loader.js'), loaderCode);

    Console.debug('Writing a default index.html for cordova app');
    var indexPath = files.pathJoin(__dirname, 'client', 'cordova_index.html');
    var indexContent = files.readFile(indexPath);
    files.writeFile(files.pathJoin(wwwPath, 'index.html'), indexContent);

    // Cordova Build Override feature (c)
    var buildOverridePath =
      files.pathJoin(projectContext.projectDir, 'cordova-build-override');

    if (files.exists(buildOverridePath) &&
      files.stat(buildOverridePath).isDirectory()) {
      Console.debug('Copying over the cordova-build-override');
      files.cp_r(buildOverridePath, cordovaProject.projectRoot);
    }

    // Run the actual build
    Console.debug('Running the build command');

    // Depending on the debug mode build the android part in different modes
    if (_.contains(projectContext.platformList.getPlatforms(), 'android') &&
        _.contains(platforms, 'android')) {
      var androidBuildPath = files.pathJoin(cordovaProject.projectRoot, 'platforms', 'android');
      var manifestPath = files.pathJoin(androidBuildPath, 'AndroidManifest.xml');

      // XXX a hack to reset the debuggable mode
      var manifest = files.readFile(manifestPath, 'utf8');
      manifest = manifest.replace(/android:debuggable=.(true|false)./g, '');
      if (options.debug)
        manifest = manifest.replace(/<application /g, '<application android:debuggable="' + !!options.debug + '" ');
      files.writeFile(manifestPath, manifest, 'utf8');

      // XXX workaround the problem of cached apk invalidation
      files.rm_recursive(files.pathJoin(androidBuildPath, 'build'));
    }

    buildmessage.enterJob({ title: 'building mobile project' }, () => {
      const buildOptions = options.debug ? [] : ['release'];
      Promise.await(cordovaProject.build({ platforms: platforms, options: buildOptions }));
    });
  });

  Console.debug('Done building the cordova build project');

  return cordovaProject;
};

// options
//  - debug
function getBundle(projectContext, bundlePath, options) {
  var bundleResult = bundler.bundle({
    projectContext: projectContext,
    outputPath: bundlePath,
    buildOptions: {
      minifyMode: options.debug ? 'development' : 'production',
      // XXX can we ask it not to create the server arch?
      serverArch: archinfo.host(),
      webArchs: [WEB_ARCH_NAME],
      includeDebug: !!options.debug
    }
  });

  if (bundleResult.errors) {
    // XXX better error handling?
    throw new Error("Errors prevented bundling:\n" +
                    bundleResult.errors.formatMessages());
  }

  return bundleResult;
};

function generateCordovaBoilerplate(projectContext, clientDir, options) {
  var clientJsonPath = files.pathJoin(clientDir, 'program.json');
  var clientJson = JSON.parse(files.readFile(clientJsonPath, 'utf8'));
  var manifest = clientJson.manifest;
  var settings = options.settings ?
    JSON.parse(files.readFile(options.settings, 'utf8')) : {};
  var publicSettings = settings['public'];

  var meteorRelease =
    release.current.isCheckout() ? "none" : release.current.name;

  var configDummy = {};
  if (publicSettings) configDummy.PUBLIC_SETTINGS = publicSettings;

  const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing'];
  var calculatedHash =
    WebAppHashing.calculateClientHash(manifest, null, configDummy);

  // XXX partially copied from autoupdate package
  var version = process.env.AUTOUPDATE_VERSION || calculatedHash;

  var mobileServer = options.protocol + options.host;
  if (options.port) {
    mobileServer = mobileServer + ":" + options.port;
  }

  var runtimeConfig = {
    meteorRelease: meteorRelease,
    ROOT_URL: mobileServer + "/",
    // XXX propagate it from options?
    ROOT_URL_PATH_PREFIX: '',
    DDP_DEFAULT_CONNECTION_URL: mobileServer,
    autoupdateVersionCordova: version,
    cleanCache: options.clean,
    httpProxyPort: options.httpProxyPort,
    appId: projectContext.appIdentifier
  };

  if (publicSettings)
    runtimeConfig.PUBLIC_SETTINGS = publicSettings;

  const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator'];
  var boilerplate = new Boilerplate(WEB_ARCH_NAME, manifest, {
    urlMapper: _.identity,
    pathMapper: function (p) { return files.pathJoin(clientDir, p); },
    baseDataExtension: {
      meteorRuntimeConfig: JSON.stringify(
        encodeURIComponent(JSON.stringify(runtimeConfig)))
    }
  });
  return boilerplate.toHTML();
};
<|endoftext|>


<commit_before>'use strict';
import _ from 'lodash';
import Field from '../field/Field';
import Components from '../../Components';
import NativePromise from 'native-promise-only';

export default class NestedComponent extends Field {
  static schema(...extend) {
    return Field.schema({
      tree: false
    }, ...extend);
  }

  constructor(component, options, data) {
    super(component, options, data);
    this.type = 'components';
    this._collapsed = !!this.component.collapsed;
  }

  get defaultSchema() {
    return NestedComponent.schema();
  }

  get schema() {
    const schema = super.schema;
    const components = _.uniqBy(this.getComponents(), 'component.key');
    schema.components = _.map(components, 'schema');
    return schema;
  }

  get collapsed() {
    return this._collapsed;
  }

  set collapsed(value) {
    this._collapsed = value;
    this.redraw();
  }

  set visible(value) {
    super.visible = value;
    const isVisible = this.visible;
    const forceShow = this.options.show && this.options.show[this.component.key];
    const forceHide = this.options.hide && this.options.hide[this.component.key];
    this.components.forEach(component => {
      const conditionallyVisible = component.conditionallyVisible();
      if (forceShow || (!isVisible && conditionallyVisible)) {
        component.visible = true;
      }
      else if (forceHide || (isVisible && !conditionallyVisible)) {
        component.visible = false;
      }
      // If hiding a nested component, clear all errors below.
      if (!component.visible) {
        component.error = '';
      }
      component.parentVisible = isVisible;
    });
  }

  get visible() {
    return super.visible;
  }

  set parentVisible(value) {
    super.parentVisible = value;
    this.components.forEach(component => {
      component.parentVisible = this.visible;
    });
  }

  get parentVisible() {
    return super.parentVisible;
  }

  get disabled() {
    return super.disabled;
  }

  set disabled(disabled) {
    super.disabled = disabled;
    this.components.forEach((component) => component.parentDisabled = disabled);
  }

  set parentDisabled(value) {
    super.parentDisabled = value;
    this.components.forEach(component => {
      component.parentDisabled = this.disabled;
    });
  }

  get parentDisabled() {
    return super.parentDisabled;
  }

  get ready() {
    return NativePromise.all(this.getComponents().map(component => component.ready));
  }

  get currentForm() {
    return super.currentForm;
  }

  set currentForm(instance) {
    super.currentForm = instance;
    this.getComponents().forEach(component => {
      component.currentForm = instance;
    });
  }

  get data() {
    return this._data;
  }

  set data(value) {
    this._data = value;
    this.eachComponent(component => {
      component.data = this._data;
    });
  }

  getComponents() {
    return this.components || [];
  }

  getAllComponents() {
    return this.getComponents().reduce((components, component) => {
      let result = component;

      if (component && component.getAllComponents) {
        result = component.getAllComponents();
      }

      return components.concat(result);
    }, []);
  }

  /**
   * Perform a deep iteration over every component, including those
   * within other container based components.
   *
   * @param {function} fn - Called for every component.
   */
  everyComponent(fn) {
    const components = this.getComponents();
    _.each(components, (component, index) => {
      if (fn(component, components, index) === false) {
        return false;
      }

      if (typeof component.everyComponent === 'function') {
        if (component.everyComponent(fn) === false) {
          return false;
        }
      }
    });
  }

  flattenComponents() {
    const result = {};

    this.everyComponent((component) => {
      result[component.key] = component;
    });

    return result;
  }

  /**
   * Perform an iteration over each component within this container component.
   *
   * @param {function} fn - Called for each component
   */
  eachComponent(fn) {
    _.each(this.getComponents(), (component, index) => {
      if (fn(component, index) === false) {
        return false;
      }
    });
  }

  /**
   * Returns a component provided a key. This performs a deep search within the
   * component tree.
   *
   * @param {string} key - The key of the component to retrieve.
   * @param {function} fn - Called with the component once found.
   * @return {Object} - The component that is located.
   */
  getComponent(path, fn) {
    path = Array.isArray(path) ? path : [path];
    const [key, ...remainingPath] = path;
    let comp = null;

    if (!_.isString(key)) {
      return comp;
    }

    this.everyComponent((component, components) => {
      if (component.component.key === key) {
        comp = component;
        if (remainingPath.length > 0 && 'getComponent' in component) {
          comp = component.getComponent(remainingPath, fn);
        }
        else if (fn) {
          fn(component, components);
        }
        return false;
      }
    });

    return comp;
  }

  /**
   * Return a component provided the Id of the component.
   *
   * @param {string} id - The Id of the component.
   * @param {function} fn - Called with the component once it is retrieved.
   * @return {Object} - The component retrieved.
   */
  getComponentById(id, fn) {
    let comp = null;
    this.everyComponent((component, components) => {
      if (component.id === id) {
        comp = component;
        if (fn) {
          fn(component, components);
        }
        return false;
      }
    });
    return comp;
  }

  /**
   * Create a new component and add it to the components array.
   *
   * @param component
   * @param data
   */
  createComponent(component, options, data, before) {
    options = options || this.options;
    data = data || this.data;
    options.parent = this;
    options.root = this.root || this;
    const comp = Components.create(component, options, data, true);
    comp.parentVisible = this.visible;
    comp.isBuilt = true;
    if (component.internal) {
      return comp;
    }

    if (before) {
      const index = _.findIndex(this.components, { id: before.id });
      if (index !== -1) {
        this.components.splice(index, 0, comp);
      }
      else {
        this.components.push(comp);
      }
    }
    else {
      this.components.push(comp);
    }
    return comp;
  }

  getContainer() {
    return this.element;
  }

  get componentComponents() {
    return this.component.components || [];
  }

  get nestedKey() {
    return `nested-${this.key}`;
  }

  get templateName() {
    return 'container';
  }

  init() {
    this.components = this.components || [];
    this.addComponents();
    return super.init();
  }

  /**
   *
   * @param element
   * @param data
   */
  addComponents(data, options) {
    data = data || this.data;
    options = options || this.options;
    if (options.components) {
      this.components = options.components;
    }
    else {
      const components = this.hook('addComponents', this.componentComponents, this) || [];
      components.forEach((component) => this.addComponent(component, data));
    }
  }

  /**
   * Add a new component to the components array.
   *
   * @param {Object} component - The component JSON schema to add.
   * @param {Object} data - The submission data object to house the data for this component.
   * @param {HTMLElement} before - A DOM element to insert this element before.
   * @return {Component} - The created component instance.
   */
  addComponent(component, data, before, noAdd) {
    data = data || this.data;
    const comp = this.createComponent(component, this.options, data, before ? before : null);
    if (noAdd) {
      return comp;
    }
    return comp;
  }

  render(children) {
    // If already rendering, don't re-render.
    return super.render(children || this.renderTemplate(this.templateName, {
      children: this.renderComponents(),
      nestedKey: this.nestedKey,
      collapsed: this.collapsed,
    }));
  }

  renderComponents(components) {
    components = components || this.getComponents();
    const children = components.map(component => component.render());
    return this.renderTemplate('components', {
      children,
      components,
    });
  }

  attach(element) {
    const superPromise = super.attach(element);

    this.loadRefs(element, {
      header: 'single',
      collapsed: this.collapsed,
      [this.nestedKey]: 'single',
    });

    let childPromise = NativePromise.resolve();
    if (this.refs[this.nestedKey]) {
      childPromise = this.attachComponents(this.refs[this.nestedKey]);
    }

    if (this.component.collapsible && this.refs.header) {
      this.addEventListener(this.refs.header, 'click', () => {
        this.collapsed = !this.collapsed;
      });
    }

    return NativePromise.all([
      superPromise,
      childPromise,
    ]);
  }

  attachComponents(element, components, container) {
    components = components || this.components;
    container = container || this.component.components;

    element = this.hook('attachComponents', element, components, container, this);
    if (!element) {
      return;
    }

    let index = 0;
    const promises = [];
    Array.prototype.slice.call(element.children).forEach(child => {
      if (!child.getAttribute('data-noattach') && components[index]) {
        promises.push(components[index].attach(child));
        index++;
      }
    });
    return NativePromise.all(promises);
  }

  /**
   * Remove a component from the components array.
   *
   * @param {Component} component - The component to remove from the components.
   * @param {Array<Component>} components - An array of components to remove this component from.
   */
  removeComponent(component, components) {
    component.destroy();
    _.remove(components, { id: component.id });
  }

  /**
   * Removes a component provided the API key of that component.
   *
   * @param {string} key - The API key of the component to remove.
   * @param {function} fn - Called once the component is removed.
   * @return {null}
   */
  removeComponentByKey(key, fn) {
    const comp = this.getComponent(key, (component, components) => {
      this.removeComponent(component, components);
      if (fn) {
        fn(component, components);
      }
    });
    if (!comp) {
      if (fn) {
        fn(null);
      }
      return null;
    }
  }

  /**
   * Removes a component provided the Id of the component.
   *
   * @param {string} id - The Id of the component to remove.
   * @param {function} fn - Called when the component is removed.
   * @return {null}
   */
  removeComponentById(id, fn) {
    const comp = this.getComponentById(id, (component, components) => {
      this.removeComponent(component, components);
      if (fn) {
        fn(component, components);
      }
    });
    if (!comp) {
      if (fn) {
        fn(null);
      }
      return null;
    }
  }

  updateValue(value, flags, source) {
    return this.components.reduce((changed, comp) => {
      // Skip over the source if it is provided.
      if (source && source.id === comp.id) {
        return changed;
      }
      return comp.updateValue(null, flags) || changed;
    }, false);
  }

  hasChanged() {
    return false;
  }

  /**
   * A more performant way to check the conditions, calculations, and validity of
   * a submission once it has been changed.
   *
   * @param data
   * @param flags
   */
  checkData(data, flags, source) {
    flags = flags || {};
    let valid = true;
    if (flags.noCheck) {
      return;
    }

    // Update the value.
    let changed = this.updateValue(null, {
      noUpdateEvent: true
    }, source);

    // Iterate through all components and check conditions, and calculate values.
    this.getComponents().forEach((comp) => {
      if (comp.checkData) {
        valid &= comp.checkData(data, flags);
      }
      changed |= comp.calculateValue(data, {
        noUpdateEvent: true
      });
      comp.checkConditions(data);
      if (!flags.noValidate) {
        valid &= comp.checkValidity(data);
      }
    });

    // Trigger the change if the values changed.
    if (changed) {
      this.triggerChange(flags, changed);
    }

    // Return if the value is valid.
    return !!valid;
  }

  checkConditions(data) {
    this.getComponents().forEach(comp => comp.checkConditions(data));
    return super.checkConditions(data);
  }

  clearOnHide(show) {
    super.clearOnHide(show);
    if (this.component.clearOnHide && this.hasValue()) {
      this.restoreComponentsContext();
    }
    this.getComponents().forEach(component => component.clearOnHide(show));
  }

  restoreComponentsContext() {
    this.getComponents().forEach((component) => component.data = this.dataValue);
  }

  /**
   * Allow components to hook into the next page trigger to perform their own logic.
   *
   * @return {*}
   */
  beforeNext() {
    return NativePromise.all(this.getComponents().map((comp) => comp.beforeNext()));
  }

  /**
   * Allow components to hook into the submission to provide their own async data.
   *
   * @return {*}
   */
  beforeSubmit() {
    return NativePromise.all(this.getComponents().map((comp) => comp.beforeSubmit()));
  }

  calculateValue(data, flags) {
    // Do not iterate into children and calculateValues if this nested component is conditionally hidden.
    if (!this.conditionallyVisible()) {
      return false;
    }
    return this.getComponents().reduce(
      (changed, comp) => comp.calculateValue(data, flags) || changed,
      super.calculateValue(data, flags)
    );
  }

  isLastPage() {
    return this.pages.length - 1 === this.page;
  }

  isValid(data, dirty) {
    return this.getComponents().reduce(
      (valid, comp) => comp.isValid(data, dirty) && valid,
      super.isValid(data, dirty)
    );
  }

  checkValidity(data, dirty) {
    if (!this.checkCondition(null, data)) {
      this.setCustomValidity('');
      return true;
    }

    const components = this.wizard && !this.isLastPage()
      ? this.pages[this.page]
      : this.getComponents();

    return components.reduce(
      (check, comp) => comp.checkValidity(data, dirty) && check,
      super.checkValidity(data, dirty)
    );
  }

  setPristine(pristine) {
    super.setPristine(pristine);
    this.getComponents().forEach((comp) => comp.setPristine(pristine));
  }

  detach() {
    this.components.forEach(component => {
      component.detach();
    });
    super.detach();
  }

  destroy() {
    this.destroyComponents();
    super.destroy();
  }

  destroyComponents() {
    const components = this.getComponents().slice();
    components.forEach((comp) => this.removeComponent(comp, this.components));
    this.components = [];
  }

  get errors() {
    return this.getAllComponents().reduce((errors, comp) => errors.concat(comp.errors || []), []);
  }

  getValue() {
    return this.data;
  }

  resetValue() {
    this.getComponents().forEach((comp) => comp.resetValue());
    _.unset(this.data, this.key);
    this.setPristine(true);
  }

  get dataReady() {
    return NativePromise.all(this.getComponents().map((component) => component.dataReady));
  }

  setNestedValue(component, value, flags, changed) {
    if (component.type === 'button') {
      return false;
    }
    if (component.type === 'components') {
      return component.setValue(value, flags) || changed;
    }
    else if (value && component.hasValue(value)) {
      return component.setValue(_.get(value, component.key), flags) || changed;
    }
    else {
      flags.noValidate = true;
      return component.setValue(component.defaultValue, flags) || changed;
    }
  }

  setValue(value, flags) {
    if (!value) {
      return false;
    }
    flags = flags || {};
    return this.getComponents().reduce((changed, component) => {
      return this.setNestedValue(component, value, flags, changed);
    }, false);
  }
}
<commit_msg>Ensure building with nested forms still works.<commit_after>'use strict';
import _ from 'lodash';
import Field from '../field/Field';
import Components from '../../Components';
import NativePromise from 'native-promise-only';

export default class NestedComponent extends Field {
  static schema(...extend) {
    return Field.schema({
      tree: false
    }, ...extend);
  }

  constructor(component, options, data) {
    super(component, options, data);
    this.type = 'components';
    if (this.component && this.componentRef) {
      if (!this.componentRef.components) {
        this.componentRef.components = [];
      }
      // Ensure the components point to the same reference as the componentRef.components.
      this.component.components = this.componentRef.components;
    }
    this._collapsed = !!this.component.collapsed;
  }

  get defaultSchema() {
    return NestedComponent.schema();
  }

  get schema() {
    const schema = super.schema;
    const components = _.uniqBy(this.getComponents(), 'component.key');
    schema.components = _.map(components, 'schema');
    return schema;
  }

  get collapsed() {
    return this._collapsed;
  }

  set collapsed(value) {
    this._collapsed = value;
    this.redraw();
  }

  set visible(value) {
    super.visible = value;
    const isVisible = this.visible;
    const forceShow = this.options.show && this.options.show[this.component.key];
    const forceHide = this.options.hide && this.options.hide[this.component.key];
    this.components.forEach(component => {
      const conditionallyVisible = component.conditionallyVisible();
      if (forceShow || (!isVisible && conditionallyVisible)) {
        component.visible = true;
      }
      else if (forceHide || (isVisible && !conditionallyVisible)) {
        component.visible = false;
      }
      // If hiding a nested component, clear all errors below.
      if (!component.visible) {
        component.error = '';
      }
      component.parentVisible = isVisible;
    });
  }

  get visible() {
    return super.visible;
  }

  set parentVisible(value) {
    super.parentVisible = value;
    this.components.forEach(component => {
      component.parentVisible = this.visible;
    });
  }

  get parentVisible() {
    return super.parentVisible;
  }

  get disabled() {
    return super.disabled;
  }

  set disabled(disabled) {
    super.disabled = disabled;
    this.components.forEach((component) => component.parentDisabled = disabled);
  }

  set parentDisabled(value) {
    super.parentDisabled = value;
    this.components.forEach(component => {
      component.parentDisabled = this.disabled;
    });
  }

  get parentDisabled() {
    return super.parentDisabled;
  }

  get ready() {
    return NativePromise.all(this.getComponents().map(component => component.ready));
  }

  get currentForm() {
    return super.currentForm;
  }

  set currentForm(instance) {
    super.currentForm = instance;
    this.getComponents().forEach(component => {
      component.currentForm = instance;
    });
  }

  get data() {
    return this._data;
  }

  set data(value) {
    this._data = value;
    this.eachComponent(component => {
      component.data = this._data;
    });
  }

  getComponents() {
    return this.components || [];
  }

  getAllComponents() {
    return this.getComponents().reduce((components, component) => {
      let result = component;

      if (component && component.getAllComponents) {
        result = component.getAllComponents();
      }

      return components.concat(result);
    }, []);
  }

  /**
   * Perform a deep iteration over every component, including those
   * within other container based components.
   *
   * @param {function} fn - Called for every component.
   */
  everyComponent(fn) {
    const components = this.getComponents();
    _.each(components, (component, index) => {
      if (fn(component, components, index) === false) {
        return false;
      }

      if (typeof component.everyComponent === 'function') {
        if (component.everyComponent(fn) === false) {
          return false;
        }
      }
    });
  }

  flattenComponents() {
    const result = {};

    this.everyComponent((component) => {
      result[component.key] = component;
    });

    return result;
  }

  /**
   * Perform an iteration over each component within this container component.
   *
   * @param {function} fn - Called for each component
   */
  eachComponent(fn) {
    _.each(this.getComponents(), (component, index) => {
      if (fn(component, index) === false) {
        return false;
      }
    });
  }

  /**
   * Returns a component provided a key. This performs a deep search within the
   * component tree.
   *
   * @param {string} key - The key of the component to retrieve.
   * @param {function} fn - Called with the component once found.
   * @return {Object} - The component that is located.
   */
  getComponent(path, fn) {
    path = Array.isArray(path) ? path : [path];
    const [key, ...remainingPath] = path;
    let comp = null;

    if (!_.isString(key)) {
      return comp;
    }

    this.everyComponent((component, components) => {
      if (component.component.key === key) {
        comp = component;
        if (remainingPath.length > 0 && 'getComponent' in component) {
          comp = component.getComponent(remainingPath, fn);
        }
        else if (fn) {
          fn(component, components);
        }
        return false;
      }
    });

    return comp;
  }

  /**
   * Return a component provided the Id of the component.
   *
   * @param {string} id - The Id of the component.
   * @param {function} fn - Called with the component once it is retrieved.
   * @return {Object} - The component retrieved.
   */
  getComponentById(id, fn) {
    let comp = null;
    this.everyComponent((component, components) => {
      if (component.id === id) {
        comp = component;
        if (fn) {
          fn(component, components);
        }
        return false;
      }
    });
    return comp;
  }

  /**
   * Create a new component and add it to the components array.
   *
   * @param component
   * @param data
   */
  createComponent(component, options, data, before) {
    options = options || this.options;
    data = data || this.data;
    options.parent = this;
    options.root = this.root || this;
    const comp = Components.create(component, options, data, true);
    comp.parentVisible = this.visible;
    comp.isBuilt = true;
    if (component.internal) {
      return comp;
    }

    if (before) {
      const index = _.findIndex(this.components, { id: before.id });
      if (index !== -1) {
        this.components.splice(index, 0, comp);
      }
      else {
        this.components.push(comp);
      }
    }
    else {
      this.components.push(comp);
    }
    return comp;
  }

  getContainer() {
    return this.element;
  }

  get componentComponents() {
    return this.component.components || [];
  }

  get nestedKey() {
    return `nested-${this.key}`;
  }

  get templateName() {
    return 'container';
  }

  init() {
    this.components = this.components || [];
    this.addComponents();
    return super.init();
  }

  /**
   *
   * @param element
   * @param data
   */
  addComponents(data, options) {
    data = data || this.data;
    options = options || this.options;
    if (options.components) {
      this.components = options.components;
    }
    else {
      const components = this.hook('addComponents', this.componentComponents, this) || [];
      components.forEach((component) => this.addComponent(component, data));
    }
  }

  /**
   * Add a new component to the components array.
   *
   * @param {Object} component - The component JSON schema to add.
   * @param {Object} data - The submission data object to house the data for this component.
   * @param {HTMLElement} before - A DOM element to insert this element before.
   * @return {Component} - The created component instance.
   */
  addComponent(component, data, before, noAdd) {
    data = data || this.data;
    const comp = this.createComponent(component, this.options, data, before ? before : null);
    if (noAdd) {
      return comp;
    }
    return comp;
  }

  render(children) {
    // If already rendering, don't re-render.
    return super.render(children || this.renderTemplate(this.templateName, {
      children: this.renderComponents(),
      nestedKey: this.nestedKey,
      collapsed: this.collapsed,
    }));
  }

  renderComponents(components) {
    components = components || this.getComponents();
    const children = components.map(component => component.render());
    return this.renderTemplate('components', {
      children,
      components,
    });
  }

  attach(element) {
    const superPromise = super.attach(element);

    this.loadRefs(element, {
      header: 'single',
      collapsed: this.collapsed,
      [this.nestedKey]: 'single',
    });

    let childPromise = NativePromise.resolve();
    if (this.refs[this.nestedKey]) {
      childPromise = this.attachComponents(this.refs[this.nestedKey]);
    }

    if (this.component.collapsible && this.refs.header) {
      this.addEventListener(this.refs.header, 'click', () => {
        this.collapsed = !this.collapsed;
      });
    }

    return NativePromise.all([
      superPromise,
      childPromise,
    ]);
  }

  attachComponents(element, components, container) {
    components = components || this.components;
    container = container || this.component.components;

    element = this.hook('attachComponents', element, components, container, this);
    if (!element) {
      return;
    }

    let index = 0;
    const promises = [];
    Array.prototype.slice.call(element.children).forEach(child => {
      if (!child.getAttribute('data-noattach') && components[index]) {
        promises.push(components[index].attach(child));
        index++;
      }
    });
    return NativePromise.all(promises);
  }

  /**
   * Remove a component from the components array.
   *
   * @param {Component} component - The component to remove from the components.
   * @param {Array<Component>} components - An array of components to remove this component from.
   */
  removeComponent(component, components) {
    component.destroy();
    _.remove(components, { id: component.id });
  }

  /**
   * Removes a component provided the API key of that component.
   *
   * @param {string} key - The API key of the component to remove.
   * @param {function} fn - Called once the component is removed.
   * @return {null}
   */
  removeComponentByKey(key, fn) {
    const comp = this.getComponent(key, (component, components) => {
      this.removeComponent(component, components);
      if (fn) {
        fn(component, components);
      }
    });
    if (!comp) {
      if (fn) {
        fn(null);
      }
      return null;
    }
  }

  /**
   * Removes a component provided the Id of the component.
   *
   * @param {string} id - The Id of the component to remove.
   * @param {function} fn - Called when the component is removed.
   * @return {null}
   */
  removeComponentById(id, fn) {
    const comp = this.getComponentById(id, (component, components) => {
      this.removeComponent(component, components);
      if (fn) {
        fn(component, components);
      }
    });
    if (!comp) {
      if (fn) {
        fn(null);
      }
      return null;
    }
  }

  updateValue(value, flags, source) {
    return this.components.reduce((changed, comp) => {
      // Skip over the source if it is provided.
      if (source && source.id === comp.id) {
        return changed;
      }
      return comp.updateValue(null, flags) || changed;
    }, false);
  }

  hasChanged() {
    return false;
  }

  /**
   * A more performant way to check the conditions, calculations, and validity of
   * a submission once it has been changed.
   *
   * @param data
   * @param flags
   */
  checkData(data, flags, source) {
    flags = flags || {};
    let valid = true;
    if (flags.noCheck) {
      return;
    }

    // Update the value.
    let changed = this.updateValue(null, {
      noUpdateEvent: true
    }, source);

    // Iterate through all components and check conditions, and calculate values.
    this.getComponents().forEach((comp) => {
      if (comp.checkData) {
        valid &= comp.checkData(data, flags);
      }
      changed |= comp.calculateValue(data, {
        noUpdateEvent: true
      });
      comp.checkConditions(data);
      if (!flags.noValidate) {
        valid &= comp.checkValidity(data);
      }
    });

    // Trigger the change if the values changed.
    if (changed) {
      this.triggerChange(flags, changed);
    }

    // Return if the value is valid.
    return !!valid;
  }

  checkConditions(data) {
    this.getComponents().forEach(comp => comp.checkConditions(data));
    return super.checkConditions(data);
  }

  clearOnHide(show) {
    super.clearOnHide(show);
    if (this.component.clearOnHide && this.hasValue()) {
      this.restoreComponentsContext();
    }
    this.getComponents().forEach(component => component.clearOnHide(show));
  }

  restoreComponentsContext() {
    this.getComponents().forEach((component) => component.data = this.dataValue);
  }

  /**
   * Allow components to hook into the next page trigger to perform their own logic.
   *
   * @return {*}
   */
  beforeNext() {
    return NativePromise.all(this.getComponents().map((comp) => comp.beforeNext()));
  }

  /**
   * Allow components to hook into the submission to provide their own async data.
   *
   * @return {*}
   */
  beforeSubmit() {
    return NativePromise.all(this.getComponents().map((comp) => comp.beforeSubmit()));
  }

  calculateValue(data, flags) {
    // Do not iterate into children and calculateValues if this nested component is conditionally hidden.
    if (!this.conditionallyVisible()) {
      return false;
    }
    return this.getComponents().reduce(
      (changed, comp) => comp.calculateValue(data, flags) || changed,
      super.calculateValue(data, flags)
    );
  }

  isLastPage() {
    return this.pages.length - 1 === this.page;
  }

  isValid(data, dirty) {
    return this.getComponents().reduce(
      (valid, comp) => comp.isValid(data, dirty) && valid,
      super.isValid(data, dirty)
    );
  }

  checkValidity(data, dirty) {
    if (!this.checkCondition(null, data)) {
      this.setCustomValidity('');
      return true;
    }

    const components = this.wizard && !this.isLastPage()
      ? this.pages[this.page]
      : this.getComponents();

    return components.reduce(
      (check, comp) => comp.checkValidity(data, dirty) && check,
      super.checkValidity(data, dirty)
    );
  }

  setPristine(pristine) {
    super.setPristine(pristine);
    this.getComponents().forEach((comp) => comp.setPristine(pristine));
  }

  detach() {
    this.components.forEach(component => {
      component.detach();
    });
    super.detach();
  }

  destroy() {
    this.destroyComponents();
    super.destroy();
  }

  destroyComponents() {
    const components = this.getComponents().slice();
    components.forEach((comp) => this.removeComponent(comp, this.components));
    this.components = [];
  }

  get errors() {
    return this.getAllComponents().reduce((errors, comp) => errors.concat(comp.errors || []), []);
  }

  getValue() {
    return this.data;
  }

  resetValue() {
    this.getComponents().forEach((comp) => comp.resetValue());
    _.unset(this.data, this.key);
    this.setPristine(true);
  }

  get dataReady() {
    return NativePromise.all(this.getComponents().map((component) => component.dataReady));
  }

  setNestedValue(component, value, flags, changed) {
    if (component.type === 'button') {
      return false;
    }
    if (component.type === 'components') {
      return component.setValue(value, flags) || changed;
    }
    else if (value && component.hasValue(value)) {
      return component.setValue(_.get(value, component.key), flags) || changed;
    }
    else {
      flags.noValidate = true;
      return component.setValue(component.defaultValue, flags) || changed;
    }
  }

  setValue(value, flags) {
    if (!value) {
      return false;
    }
    flags = flags || {};
    return this.getComponents().reduce((changed, component) => {
      return this.setNestedValue(component, value, flags, changed);
    }, false);
  }
}
<|endoftext|>


<commit_before>import React, { PropTypes } from 'react';
import Input from '../../common/Input';
import ModalAnimalButtons from '../../common/ModalAnimalButtons';

const AdopterFormEdit = ({ adopter, onSave, onChange, onCancel, errors }) => {

  const errorsFullName = errors.firstName || errors.lastName;

  return (

 * campos necesarios

        <Input styleClass="animal-input"
                name="fullName"
                label="Nombre y Apellido *"
                type="text"
                value={adopter.fullName}
                onChange={onChange}
                error={errorsFullName}
                 />
        <Input styleClass="animal-input disable"
                name="ci"
                label="CI"
                type="text"
                disabled={true}
                value={adopter.ci}
                 />
        <Input styleClass="animal-input"
                name="email"
                label="Email"
                type="text"
                value={adopter.email}
                onChange={onChange}
                error={errors.email}
                 />
        <Input styleClass="animal-input"
                name="phone"
                label="Teléfono *"
                type="tel"
                value={adopter.phone}
                onChange={onChange}
                error={errors.phone}
                 />
        <Input styleClass="animal-input"
                name="homeAddress"
                label="Dirección *"
                type="text"
                value={adopter.homeAddress}
                onChange={onChange}
                error={errors.homeAddress}
                 />
        <Input styleClass="animal-input"
                name="houseDescription"
                label="Descripción de la casa"
                type="text"
                value={adopter.houseDescription}
                onChange={onChange}
                error={errors.houseDescription}
                 />

      <ModalAnimalButtons onSubmit={onSave} onClose={onCancel} />

  );
};

const { object, func } = PropTypes;

AdopterFormEdit.propTypes = {
  adopter: object.isRequired,
  onSave: func.isRequired,
  onChange: func.isRequired,
  onCancel: func.isRequired,
  errors: object.isRequired
};

export default AdopterFormEdit;
<commit_msg>Cambiar LLabel<commit_after>import React, { PropTypes } from 'react';
import Input from '../../common/Input';
import ModalAnimalButtons from '../../common/ModalAnimalButtons';

const AdopterFormEdit = ({ adopter, onSave, onChange, onCancel, errors }) => {

  const errorsFullName = errors.firstName || errors.lastName;

  return (

 * campos necesarios

        <Input styleClass="animal-input"
                name="fullName"
                label="Nombre y Apellido *"
                type="text"
                value={adopter.fullName}
                onChange={onChange}
                error={errorsFullName}
                 />
        <Input styleClass="animal-input disable"
                name="ci"
                label="CI"
                type="text"
                disabled={true}
                value={adopter.ci}
                 />
        <Input styleClass="animal-input"
                name="email"
                label="Email"
                type="text"
                value={adopter.email}
                onChange={onChange}
                error={errors.email}
                 />
        <Input styleClass="animal-input"
                name="phone"
                label="Teléfono *"
                type="tel"
                value={adopter.phone}
                onChange={onChange}
                error={errors.phone}
                 />
        <Input styleClass="animal-input"
                name="homeAddress"
                label="Dirección *"
                type="text"
                value={adopter.homeAddress}
                onChange={onChange}
                error={errors.homeAddress}
                 />
        <Input styleClass="animal-input"
                name="houseDescription"
                label="Descripción de la casa"
                type="text"
                value={adopter.houseDescription}
                onChange={onChange}
                error={errors.houseDescription}
                 />

      <ModalAnimalButtons title="GUARDAR" onSubmit={onSave} onClose={onCancel} />

  );
};

const { object, func } = PropTypes;

AdopterFormEdit.propTypes = {
  adopter: object.isRequired,
  onSave: func.isRequired,
  onChange: func.isRequired,
  onCancel: func.isRequired,
  errors: object.isRequired
};

export default AdopterFormEdit;
<|endoftext|>


<commit_before>// To draw the coloured underline beneath the input field value

const searchInput = document.querySelector('.js-search-input');
const searchUnderscore = document.querySelector('.js-search-underscore');

function doSearchUnderscore () {
  searchUnderscore.innerHTML = searchInput.value.replace(/\s/g, ' ');
}

if (searchInput) {
  window.addEventListener('load', doSearchUnderscore);
  searchInput.addEventListener('input', doSearchUnderscore);
}
<commit_msg>search-form auto focus<commit_after>// To draw the coloured underline beneath the input field value

const searchInput = document.querySelector('.js-search-input');
const searchUnderscore = document.querySelector('.js-search-underscore');

function doSearchUnderscore () {
  searchUnderscore.innerHTML = searchInput.value.replace(/\s/g, ' ');
}

if (searchInput) {
  searchInput.focus();
  doSearchUnderscore();
  searchInput.addEventListener('input', doSearchUnderscore);
}
<|endoftext|>