%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/985914/root/data/old/home/stash/stash/atlassian-stash/static/feature/compare/
Upload File :
Create Path :
Current File : //proc/985914/root/data/old/home/stash/stash/atlassian-stash/static/feature/compare/compare.js

define('feature/compare', [
    'bacon',
    'jquery',
    'memoir',
    'underscore',
    'stash/api/util/navbuilder',
    'util/bacon',
    'util/dom-event',
    'util/events',
    'util/function',
    'model/repository',
    'model/revision-reference',
    'widget/keyboard-shortcuts',
    'feature/pull-request/pull-request-create',
    'feature/repository/source-target-selector',
    'exports'
], function(
    Bacon,
    $,
    memoir,
    _,
    nav,
    bacon,
    domEventUtil,
    events,
    fn,
    Repository,
    RevisionReference,
    keyboardShortcuts,
    PullRequestCreate,
    SourceTargetSelector,
    exports
) {

    "use strict";

    var $tabMenu = $('.tabs-menu');

    /**
     * Generates the URL for the current state (selected tab, source branch, target branch).
     *
     * @param {boolean} prCreateMode               - If the page state should stay in a create PR context
     * @param {boolean} prShowing                  - If the page is in Pull Request create mode
     * @param {string} tab                         - currently selected tab, which should correspond to the name of a
     *                                               method on navbuild compare().
     * @param {SourceTargetSelectorState} selector - state of the repository/branch selector
     * @returns {string}          - url that represents the current state of `selector`
     */
    function calculateUrlForState(prCreateMode, prShowing, tab, selector) {
        var builder = nav
            .project(selector.sourceRepo.getProject())
            .repo(selector.sourceRepo);
        if (prShowing || prCreateMode) {
            builder = builder.createPullRequest();
        } else {
            builder = builder.compare()[tab]();
        }
        if (selector.target && !selector.target.isDefault()) {
            builder = builder.targetBranch(selector.target.getId());
        }
        if (selector.source && !selector.source.isDefault()) {
            builder = builder.sourceBranch(selector.source.getId());
        }
        if (!selector.sourceRepo.isEqual(selector.targetRepo)) {
            builder = builder.targetRepo(selector.targetRepo.getId());
        }
        return builder.build();
    }

    /**
     * Generates the URL that the form should POST to.
     *
     * @param {SourceTargetSelectorState} selector - state of the repository/branch selector
     * @returns {string} the URL the form should use, "" if no valid URL can be found.
     */
    function caculateFormURL(selector) {
        if (selector.targetRepo) {
            return nav.project(selector.targetRepo.getProject()).repo(selector.targetRepo).createPullRequest().build();
        }
        return "";
    }

    /**
     * Calculates the new page url and title
     *
     * @param {boolean} prCreateMode               - If the page state should stay in a create PR context
     * @param {boolean} prShowing                  - If the page is in Pull Request create mode
     * @param {string} tab                         - currently selected tab, which should correspond to the name of a
     *                                               method on navbuild compare().
     * @param {SourceTargetSelectorState} selector - state of the repository/branch selector
     * @returns {{url: string, title: string, selector: SourceTargetSelectorState, prShowing: boolean, tab: string}}
     */
    function calculateNewPageState(prCreateMode, prShowing, tab, selector) {
        var source = selector.sourceRepo;
        return {
            url: calculateUrlForState(prCreateMode, prShowing, tab, selector),
            formURL: caculateFormURL(selector),
            title: prShowing || prCreateMode ?
                AJS.I18n.getText('stash.web.pullrequest.create.windowtitle', source.getProject().getName(), source.getName()) :
                AJS.I18n.getText('stash.web.repository.compare.page.title', source.getProject().getName(), source.getName()),
            selector: selector,
            prShowing: prShowing,
            tab: tab
        };
    }

    /**
     * @returns {boolean} true if both refs are selected, otherwise false
     */
    function allRefsSelected(sourceTargetSelector) {
        return sourceTargetSelector.branchesSelected();
    }

    /**
     * The state for a `SourceTargetSelector`
     *
     * @typedef {object} SourceTargetSelectorState
     * @property {?object} source     - The currently selected source repository.
     * @property {object} sourceRepo - The currently selected source branch.
     * @property {?object} target     - The currently selected target repository.
     * @property {object} targetRepo - The currently selected target branch.
     */

    /**
     * @param {SourceTargetSelector} sourceTargetSelector
     * @returns {Bacon.Property<SourceTargetSelectorState>} a property of the state representing branch or
     *                                                      repository selectors, seeded with the current state
     */
    function branchStateProperty(sourceTargetSelector) {
        var branchState = function() {
            return {
                source: sourceTargetSelector.getSourceBranch(),
                sourceRepo: sourceTargetSelector.getSourceRepository(),
                target: sourceTargetSelector.getTargetBranch(),
                targetRepo: sourceTargetSelector.getTargetRepository()
            };
        };

        var eventStreams = ['source.repositoryChanged', 'target.repositoryChanged',
                            'source.revisionRefChanged', 'target.revisionRefChanged'].map(function(name) {
            return bacon.events('stash.feature.repository.sourceTargetSelector.' + name);
        });
        return Bacon.mergeAll.apply(Bacon, eventStreams)
            .map(branchState).toProperty(branchState());
    }

    /**
     * @param {Array<{pathSegment: string, init: function}>} tabs - an object where the key is the name of the tab and
     *                                                              value a function used to create the view..
     * @returns {Bacon<string>} return a stream of tab events (seeded with the initial value)
     */
    function initTabs(tabs) {

        var tabLookup = _.reduce(tabs, function(obj, value) {
            obj['compare-' + value.pathSegment + '-tab'] = value;
            return obj;
        }, {});

        var selectedTabDestroy = $.noop;

        function setTabActive($tab) {
            $tab.addClass('active-tab').siblings().removeClass('active-tab');
            selectedTabDestroy();
            var selectedTab = tabLookup[$tab.attr('data-module-key')];
            selectedTabDestroy = selectedTab.init();
            keyboardShortcuts.resetContexts();
            return selectedTab.pathSegment;
        }

        var tab = setTabActive($tabMenu.find('.active-tab'));

        // Create a event stream from the tab header clicks
        var tabChanges = $tabMenu.asEventStream('click', 'a')
            .filter(domEventUtil.openInSameTab)
            // Stop the click only if someone subscribes - dirty but works
            .doAction(domEventUtil.preventDefault())
            // Discard the click if the tab is already active
            .flatMap(function(e) {
                var $tab = $(e.currentTarget).parent();
                if (!$tab.is('.active-tab')) {
                    // Activate the tab and return the name
                    return Bacon.constant(setTabActive($tab));
                }
                // Don't return anything, could have also done this as a filter
                return Bacon.never();
            });

        // Return a stream of the tab changes, starting with the current tab
        return tabChanges.toProperty(tab);
    }

    /**
     * Handles the render/destroy lifecycle of a tab, destroying the content when finished.
     *
     * @param {SourceTargetSelector} sourceTargetSelector -
     * @param {Function} createView                       - function that creates a single instance of a view and should return a destroy function
     * @returns {Function} function that destroys the current view
     */
    function renderTab(sourceTargetSelector, createView) {
        var $el = $('#compare-content');
        // TODO Yuck - would like to do this in a cleaner way
        var destroy = $.noop;
        var events = bacon.events('stash.feature.repository.sourceTargetSelector.source.revisionRefChanged')
            .merge(bacon.events('stash.feature.repository.sourceTargetSelector.target.revisionRefChanged'))
            .merge(Bacon.fromArray([0]))
            .map(fn.constant(sourceTargetSelector))
            .filter(allRefsSelected)
            .onValue(function() {
                destroy();
                $el.empty();
                // Ideally we get a jQuery object back and add it ourselves
                destroy = createView($el);
            });
        return function() {
            destroy();
            events();
        };
    }

    function bindKeyboardShortcuts() {
        bacon.keyboardEvents('requestBranchCompareSectionHandler').onValue(function(e) {
            var number = parseInt(String.fromCharCode(e.which), 10);
            var $tabLink = $tabMenu.children().eq(number - 1).children('a');
            // use the pushstate if available
            if (memoir.nativeSupport()) {
                $tabLink.click();
            } else {
                window.location.href = $tabLink.prop('href');
            }
        });

        bacon.events('stash.widget.keyboard-shortcuts.register-contexts').onValue(function(keyboardShortcuts) {
            keyboardShortcuts.enableContext('branch-compare');
        });
    }

    /**
     * Sets up the transitions for going between the ref selector form and the PR details form.
     *
     * @param {Bacon.Property<boolean>} prShowingProperty - The current state of the form.
     * @param {Bacon.Property<SourceTargetSelectorState>} selectorProperty - The current state of the branch selectors.
     * @param {boolean} prCreateMode - If the form should always be in a PR create context.
     * @param {jQuery} $showPRButton - The button on the page that continues to the next stage.
     * @param {jQuery} $prFormParent - The parent that can be unhidden to revel the PR details form.
     * @param {jQuery} $compareEl    - The element that contains all of the compare feature elements.
     */
    function initFormTransitions(prShowingProperty, selectorProperty, prCreateMode,
                                 $showPRButton, $prFormParent,  $compareEl) {
        var $title = $('.aui-page-header-main h2');
        var $expandedBranches = $compareEl.find('.expanded-branches');
        var $firstRefSelector = $compareEl.find('#sourceRepo');
        var $descriptionBox = $compareEl.find('#pull-request-description');

        prShowingProperty
            .doAction($showPRButton, 'toggleClass', 'hidden')
            .doAction($expandedBranches, 'toggleClass', 'hidden')
            .not().doAction($prFormParent.toggleClass.bind($prFormParent, 'hidden'))
            .doAction(function(prNotShowing) {
                (prNotShowing ? $firstRefSelector : $descriptionBox).focus();
            }).map(function(prNotShowing) {
                return prNotShowing && !prCreateMode ?
                    AJS.I18n.getText('stash.web.repository.compare.header.title') :
                    AJS.I18n.getText('stash.web.pullrequest.create.title');
            }).onValue($title.text.bind($title));

        prShowingProperty
            .combine(selectorProperty, function(prShowing, selector) {
                return {prShowing: prShowing, selector: selector};
            })
            .onValue(function(state) {
                if (state.prShowing) {
                    var HTML = stash.feature.compare.collapsedBranches({
                        sourceBranch: state.selector.source.toJSON(),
                        targetBranch: state.selector.target.toJSON()
                    });
                    $(HTML).insertBefore($expandedBranches);
                } else {
                    $compareEl.find('.collapsed-branches').remove();
                }
            });
    }

    /**
     *
     * @param {jQuery} $showPRButton      - The button on the page that continues to the next stage.
     * @returns {Bacon.Property<boolean>} - True => The PR details form is showing, False => The ref selectors are showing
     */
    function initPRShowingProperty($showPRButton) {
        return $(document).asEventStream('click', '.show-hide-button')
                .map(function(e) {
                    return $(e.target).is($showPRButton);
                }).toProperty($showPRButton.hasClass('hidden'));
    }

    /**
     * Sets up a stream of events for when the branches changes
     *
     * @param {Bacon.Property<SourceTargetSelectorState>} selectorProperty - The current state of the branch selectors.
     * @param {jQuery} $container                                          - The element containing the compare feature.
     * @returns {Bacon.EventStream}                                        - True => the selected refs are not the same, False => The selected refs are the same
     */
    function initCanCreatePR(selectorProperty, $container) {
        var creationErrorType = {
            REF_UNSELECTED : 'REF_UNSELECTED',
            TAG_SELECTED: 'TAG_SELECTED',
            REFS_EQUAL: 'REFS_EQUAL'
        };
        var prFromProperty = selectorProperty
            .debounce(0) // debounce for when the swap button is pressed
            .map(function(state) {
                var output = {
                    canCreate: false
                };
                if (!state.source || !state.target) {
                    output.reason = creationErrorType.REF_UNSELECTED;
                } else if (state.source.isTag() || state.target.isTag()) {
                    output.reason = creationErrorType.TAG_SELECTED;
                } else if (state.source.isEqual(state.target)) {
                    output.reason = creationErrorType.REFS_EQUAL;
                } else {
                    output.canCreate = true;
                }
                return output;
            }).skipDuplicates()
            .toProperty();

        var $createPrButton = $container.find('#show-create-pr-button');
        var $refsEqualWarning = $container.find('.refs-equal-warning');
        var $tagsWarning = $container.find('.tags-warning');
        var $tabs = $container.find('.aui-tabs');

        var canCreatePRProperty = prFromProperty
            .doAction(function(state) {
                $refsEqualWarning.toggleClass("hidden", state.reason !== creationErrorType.REFS_EQUAL);
                $tagsWarning.toggleClass("hidden", state.reason !== creationErrorType.TAG_SELECTED);
                $tabs.toggleClass("hidden", state.reason === creationErrorType.REF_UNSELECTED);
           })
            .map(fn.dot('canCreate'));

        canCreatePRProperty
            .doAction($createPrButton, 'enable')
            .not()
            .onValue($createPrButton, 'attr', 'aria-disabled');

        return canCreatePRProperty;
    }

    /**
     * Sets up what will happen to revert the page when a popstate event happens
     *
     * Because all of the transitions are controlled by bacon, and events firing on elements this function has to
     * invoke the events on each HTML level element.
     *
     * @param {Bacon.Property<Array>} rawStateProperty    - Array of <prShowing, tab, selector>.
     * @param {jQuery} $showPRButton                      - The show PR button to take the page to the next screen.
     * @param {jQuery} $compareEl                         - The element to find compare features in.
     * @param {SourceTargetSelector} sourceTargetSelector - Selector of set the state of when the page is changed.
     * @returns {Function} a destroy function that will unbind the bacon stream.
     */
    function initPopstate(rawStateProperty, $showPRButton, $compareEl, sourceTargetSelector) {
        var $cancelButton = $compareEl.find('#cancel-button');

        // filter out events that are caused by the user clicking #blah links.
        var memoirEvents = bacon.events('memoir.popstate').filter(_.compose(fn.not(fn.eq(null)), fn.dot('state')));
        return Bacon.combineAsArray(rawStateProperty, memoirEvents)
            .sampledBy(memoirEvents)
            .filter(function(state) { // check for popstate of {}
                return _.keys(state[1].state).length !== 0;
            })
            .map(function(state) {
                // clean up state object
                var rawState = state[0];
                var memoirEvent = state[1];
                return {
                    prShowing: rawState[0],
                    tab: rawState[1],
                    selector: rawState[2],
                    oldState: memoirEvent.state
                };
            })
            .onValue(function(state) {
                if (state.prShowing !== state.oldState.prShowing) {
                    if (state.prShowing) {
                        $cancelButton.click();
                    } else {
                        $showPRButton.click();
                    }
                }

                if (state.tab !== state.oldState.tab) {
                    $compareEl.find('li.menu-item[data-module-key=compare-' + state.oldState.tab + '-tab] > a').click();
                }

                // Check for changes in the branch or reference.
                // We cannot revert back to having nothing selected as that isn't supported by the ref selectors

                var oldSelector = state.oldState.selector;

                sourceTargetSelector.refSelectors.source.setSelection({
                    repository: oldSelector.sourceRepo ? new Repository(oldSelector.sourceRepo) : null,
                    branch: oldSelector.source ? new RevisionReference(oldSelector.source) : null
                });

                sourceTargetSelector.refSelectors.target.setSelection({
                    repository: oldSelector.targetRepo ? new Repository(oldSelector.targetRepo) : null,
                    branch: oldSelector.target ? new RevisionReference(oldSelector.target) : null
                });

            });
    }

    /**
     * Sanitizes a state object for pushing to memoir
     *
     * @param {object} state                             - A object representing the state of the page.
     * @param {string} state.url                         - URL that the page should be set to.
     * @param {string} state.title                       - A title that the page should have.
     * @param {boolean} state.prShowing                  - Is the PR details form showing.
     * @param {SourceTargetSelectorState} state.selector - The state of the selectors
     */
    function sanitizedState(state) {
        // clone the state object rather than changing it
        var safeState = _.clone(state);
        safeState.selector = {};

        // sanitize the selectors so they can be stored by the browser.
        _.each(state.selector, function(value, key){
            safeState.selector[key] = value ? value.toJSON() : null;
        });

        return safeState;
    }

    /**
     * @param {HTMLElement} compareEl The element containing the compare feature.
     * @param {object} opts.targetRepositoryJson target branch's repository
     * @param {object} opts.sourceRepositoryJson source branch's repository
     * @param {object} opts.tabs tab notification callbacks
     * @param {Array<object>} opts.submittedReviewers reviewers to auto fill the suggestion with
     * @param {Array<object>} opts.additionalPreloadRepositories a list of repos to preload to speed up the selector
     * @param {boolean} opts.prCreateMode If the page state should stay in the PR creation context
     * @param {object} opts extra option options.
     */
    exports.onReady = function(compareEl, opts) {
        var $compareEl = $(compareEl);
        opts.prCreateMode = !!opts.prCreateMode;

        var additionalPreloadRepositories = _.map(opts.additionalPreloadRepositories, fn.create(Repository));

        var sourceTargetSelector = new SourceTargetSelector(
            $compareEl.find('.compare-selector'),
            new Repository(opts.sourceRepositoryJson), new Repository(opts.targetRepositoryJson),
            additionalPreloadRepositories, { showTags: true }
        );

        var tabProperty = initTabs(_.map(opts.tabs, function(callback, key) {
            return {
                pathSegment: key,
                init: _.partial(renderTab, sourceTargetSelector, _.partial(callback, sourceTargetSelector))
            };
        }));

        var selectorProperty = branchStateProperty(sourceTargetSelector);
        var canCreatePRProperty = initCanCreatePR(selectorProperty, $compareEl);

        var $prFormParent = $compareEl.find('.pull-request-create-form');
        var $showPRButton = $compareEl.find('#show-create-pr-button');
        var $prForm = $compareEl.find('form.aui');

        PullRequestCreate.init($prFormParent, opts.submittedReviewers || [], selectorProperty, tabProperty, canCreatePRProperty);
        var prShowingProperty = initPRShowingProperty($showPRButton);
        initFormTransitions(prShowingProperty, selectorProperty, opts.prCreateMode,
            $showPRButton, $prFormParent, $compareEl);

        var needsInitialState = true;

        var rawStateProperty = Bacon.combineAsArray(prShowingProperty, tabProperty, selectorProperty);
        initPopstate(rawStateProperty, $showPRButton, $compareEl, sourceTargetSelector);

        // Combine the _latest_ from either the tab events or changes to the branch selections
        // What's neat about this is we keep the last 'tab' so we don't need to track our own state
        // One sourceTargetSelector is merged in to give its stream an initial value
        rawStateProperty
            .map(Function.apply.bind(_.partial(calculateNewPageState, opts.prCreateMode)), null)
            .onValue(function(state) {
                $prForm.attr('action', state.formURL);
                if (memoir.nativeSupport()) {
                    if (needsInitialState) {
                        memoir.initialState(sanitizedState(state));
                        needsInitialState = false;
                    } else if (window.location.href.indexOf(state.url) === -1) {
                        memoir.pushState(sanitizedState(state), state.title, state.url);
                    }
                }
            });

        bindKeyboardShortcuts();
    };
});

Zerion Mini Shell 1.0