%PDF- %PDF-
Direktori : /proc/985914/root/data/old/home/stash/stash/atlassian-stash/static/feature/compare/ |
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(); }; });