| /** |
| * @license |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Navigation parameters object format: |
| // |
| // Each object has a `view` property with a value from GerritNav.View. The |
| // remaining properties depend on the value used for view. |
| // |
| // - GerritNav.View.CHANGE: |
| // - `changeNum`, required, String: the numeric ID of the change. |
| // - `project`, optional, String: the project name. |
| // - `patchNum`, optional, Number: the patch for the right-hand-side of |
| // the diff. |
| // - `basePatchNum`, optional, Number: the patch for the left-hand-side |
| // of the diff. If `basePatchNum` is provided, then `patchNum` must |
| // also be provided. |
| // - `edit`, optional, Boolean: whether or not to load the file list with |
| // edit controls. |
| // - `messageHash`, optional, String: the hash of the change message to |
| // scroll to. |
| // |
| // - GerritNav.View.SEARCH: |
| // - `query`, optional, String: the literal search query. If provided, |
| // the string will be used as the query, and all other params will be |
| // ignored. |
| // - `owner`, optional, String: the owner name. |
| // - `project`, optional, String: the project name. |
| // - `branch`, optional, String: the branch name. |
| // - `topic`, optional, String: the topic name. |
| // - `hashtag`, optional, String: the hashtag name. |
| // - `statuses`, optional, Array<String>: the list of change statuses to |
| // search for. If more than one is provided, the search will OR them |
| // together. |
| // - `offset`, optional, Number: the offset for the query. |
| // |
| // - GerritNav.View.DIFF: |
| // - `changeNum`, required, String: the numeric ID of the change. |
| // - `path`, required, String: the filepath of the diff. |
| // - `patchNum`, required, Number: the patch for the right-hand-side of |
| // the diff. |
| // - `basePatchNum`, optional, Number: the patch for the left-hand-side |
| // of the diff. If `basePatchNum` is provided, then `patchNum` must |
| // also be provided. |
| // - `lineNum`, optional, Number: the line number to be selected on load. |
| // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value |
| // of true selects the line from base of the patch range. False by |
| // default. |
| // |
| // - GerritNav.View.GROUP: |
| // - `groupId`, required, String: the ID of the group. |
| // - `detail`, optional, String: the name of the group detail view. |
| // Takes any value from GerritNav.GroupDetailView. |
| // |
| // - GerritNav.View.REPO: |
| // - `repoName`, required, String: the name of the repo |
| // - `detail`, optional, String: the name of the repo detail view. |
| // Takes any value from GerritNav.RepoDetailView. |
| // |
| // - GerritNav.View.DASHBOARD |
| // - `repo`, optional, String. |
| // - `sections`, optional, Array of objects with `title` and `query` |
| // strings. |
| // - `user`, optional, String. |
| // |
| // - GerritNav.View.ROOT: |
| // - no possible parameters. |
| |
| const uninitialized = () => { |
| console.warn('Use of uninitialized routing'); |
| }; |
| |
| const EDIT_PATCHNUM = 'edit'; |
| const PARENT_PATCHNUM = 'PARENT'; |
| |
| const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g; |
| |
| // NOTE: These queries are tested in Java. Any changes made to definitions |
| // here require corresponding changes to: |
| // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java |
| const DEFAULT_SECTIONS = [ |
| { |
| // Changes with unpublished draft comments. This section is omitted when |
| // viewing other users, so we don't need to filter anything out. |
| name: 'Has draft comments', |
| query: 'has:draft', |
| selfOnly: true, |
| hideIfEmpty: true, |
| suffixForDashboard: 'limit:10', |
| }, |
| { |
| // Changes that are assigned to the viewed user. |
| name: 'Assigned reviews', |
| query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' + |
| 'is:open -is:ignored', |
| hideIfEmpty: true, |
| suffixForDashboard: 'limit:25', |
| }, |
| { |
| // WIP open changes owned by viewing user. This section is omitted when |
| // viewing other users, so we don't need to filter anything out. |
| name: 'Work in progress', |
| query: 'is:open owner:${user} is:wip', |
| selfOnly: true, |
| hideIfEmpty: true, |
| suffixForDashboard: 'limit:25', |
| }, |
| { |
| // Non-WIP open changes owned by viewed user. Filter out changes ignored |
| // by the viewing user. |
| name: 'Outgoing reviews', |
| query: 'is:open owner:${user} -is:wip -is:ignored', |
| isOutgoing: true, |
| suffixForDashboard: 'limit:25', |
| }, |
| { |
| // Non-WIP open changes not owned by the viewed user, that the viewed user |
| // is associated with (as either a reviewer or the assignee). Changes |
| // ignored by the viewing user are filtered out. |
| name: 'Incoming reviews', |
| query: 'is:open -owner:${user} -is:wip -is:ignored ' + |
| '(reviewer:${user} OR assignee:${user})', |
| suffixForDashboard: 'limit:25', |
| }, |
| { |
| // Open changes the viewed user is CCed on. Changes ignored by the viewing |
| // user are filtered out. |
| name: 'CCed on', |
| query: 'is:open -is:ignored cc:${user}', |
| suffixForDashboard: 'limit:10', |
| }, |
| { |
| name: 'Recently closed', |
| // Closed changes where viewed user is owner, reviewer, or assignee. |
| // Changes ignored by the viewing user are filtered out, and so are WIP |
| // changes not owned by the viewing user (the one instance of |
| // 'owner:self' is intentional and implements this logic). |
| query: 'is:closed -is:ignored (-is:wip OR owner:self) ' + |
| '(owner:${user} OR reviewer:${user} OR assignee:${user} ' + |
| 'OR cc:${user})', |
| suffixForDashboard: '-age:4w limit:10', |
| }, |
| ]; |
| |
| // TODO(dmfilippov) Convert to class, extract consts, give better name and |
| // expose as a service from appContext |
| export const GerritNav = { |
| |
| View: { |
| ADMIN: 'admin', |
| AGREEMENTS: 'agreements', |
| CHANGE: 'change', |
| DASHBOARD: 'dashboard', |
| DIFF: 'diff', |
| DOCUMENTATION_SEARCH: 'documentation-search', |
| EDIT: 'edit', |
| GROUP: 'group', |
| PLUGIN_SCREEN: 'plugin-screen', |
| REPO: 'repo', |
| ROOT: 'root', |
| SEARCH: 'search', |
| SETTINGS: 'settings', |
| }, |
| |
| GroupDetailView: { |
| MEMBERS: 'members', |
| LOG: 'log', |
| }, |
| |
| RepoDetailView: { |
| ACCESS: 'access', |
| BRANCHES: 'branches', |
| COMMANDS: 'commands', |
| DASHBOARDS: 'dashboards', |
| TAGS: 'tags', |
| }, |
| |
| WeblinkType: { |
| CHANGE: 'change', |
| FILE: 'file', |
| PATCHSET: 'patchset', |
| }, |
| |
| /** @type {Function} */ |
| _navigate: uninitialized, |
| |
| /** @type {Function} */ |
| _generateUrl: uninitialized, |
| |
| /** @type {Function} */ |
| _generateWeblinks: uninitialized, |
| |
| /** @type {Function} */ |
| mapCommentlinks: uninitialized, |
| |
| /** |
| * @param {number=} patchNum |
| * @param {number|string=} basePatchNum |
| */ |
| _checkPatchRange(patchNum, basePatchNum) { |
| if (basePatchNum && !patchNum) { |
| throw new Error('Cannot use base patch number without patch number.'); |
| } |
| }, |
| |
| /** |
| * Setup router implementation. |
| * |
| * @param {function(!string)} navigate the router-abstracted equivalent of |
| * `window.location.href = ...`. Takes a string. |
| * @param {function(!Object): string} generateUrl generates a URL given |
| * navigation parameters, detailed in the file header. |
| * @param {function(!Object): string} generateWeblinks weblinks generator |
| * function takes single payload parameter with type property that |
| * determines which |
| * part of the UI is the consumer of the weblinks. type property can |
| * be one of file, change, or patchset. |
| * - For file type, payload will also contain string properties: repo, |
| * commit, file. |
| * - For patchset type, payload will also contain string properties: |
| * repo, commit. |
| * - For change type, payload will also contain string properties: |
| * repo, commit. If server provides weblinks, those will be passed |
| * as options.weblinks property on the main payload object. |
| * @param {function(!Object): Object} mapCommentlinks provides an escape |
| * hatch to modify the commentlinks object, e.g. if it contains any |
| * relative URLs. |
| */ |
| setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) { |
| this._navigate = navigate; |
| this._generateUrl = generateUrl; |
| this._generateWeblinks = generateWeblinks; |
| this.mapCommentlinks = mapCommentlinks; |
| }, |
| |
| destroy() { |
| this._navigate = uninitialized; |
| this._generateUrl = uninitialized; |
| this._generateWeblinks = uninitialized; |
| this.mapCommentlinks = uninitialized; |
| }, |
| |
| /** |
| * Generate a URL for the given route parameters. |
| * |
| * @param {Object} params |
| * @return {string} |
| */ |
| _getUrlFor(params) { |
| return this._generateUrl(params); |
| }, |
| |
| getUrlForSearchQuery(query, opt_offset) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| query, |
| offset: opt_offset, |
| }); |
| }, |
| |
| /** |
| * @param {!string} project The name of the project. |
| * @param {boolean=} opt_openOnly When true, only search open changes in |
| * the project. |
| * @param {string=} opt_host The host in which to search. |
| * @return {string} |
| */ |
| getUrlForProjectChanges(project, opt_openOnly, opt_host) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| project, |
| statuses: opt_openOnly ? ['open'] : [], |
| host: opt_host, |
| }); |
| }, |
| |
| /** |
| * @param {string} branch The name of the branch. |
| * @param {string} project The name of the project. |
| * @param {string=} opt_status The status to search. |
| * @param {string=} opt_host The host in which to search. |
| * @return {string} |
| */ |
| getUrlForBranch(branch, project, opt_status, opt_host) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| branch, |
| project, |
| statuses: opt_status ? [opt_status] : undefined, |
| host: opt_host, |
| }); |
| }, |
| |
| /** |
| * @param {string} topic The name of the topic. |
| * @param {string=} opt_host The host in which to search. |
| * @return {string} |
| */ |
| getUrlForTopic(topic, opt_host) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| topic, |
| statuses: ['open', 'merged'], |
| host: opt_host, |
| }); |
| }, |
| |
| /** |
| * @param {string} hashtag The name of the hashtag. |
| * @return {string} |
| */ |
| getUrlForHashtag(hashtag) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| hashtag, |
| statuses: ['open', 'merged'], |
| }); |
| }, |
| |
| /** |
| * Navigate to a search for changes with the given status. |
| * |
| * @param {string} status |
| */ |
| navigateToStatusSearch(status) { |
| this._navigate(this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| statuses: [status], |
| })); |
| }, |
| |
| /** |
| * Navigate to a search query |
| * |
| * @param {string} query |
| * @param {number=} opt_offset |
| */ |
| navigateToSearchQuery(query, opt_offset) { |
| return this._navigate(this.getUrlForSearchQuery(query, opt_offset)); |
| }, |
| |
| /** |
| * Navigate to the user's dashboard |
| */ |
| navigateToUserDashboard() { |
| return this._navigate(this.getUrlForUserDashboard('self')); |
| }, |
| |
| /** |
| * @param {!Object} change The change object. |
| * @param {number=} opt_patchNum |
| * @param {number|string=} opt_basePatchNum The string 'PARENT' can be |
| * used for none. |
| * @param {boolean=} opt_isEdit |
| * @param {string=} opt_messageHash |
| * @return {string} |
| */ |
| getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit, |
| opt_messageHash) { |
| if (opt_basePatchNum === PARENT_PATCHNUM) { |
| opt_basePatchNum = undefined; |
| } |
| |
| this._checkPatchRange(opt_patchNum, opt_basePatchNum); |
| return this._getUrlFor({ |
| view: GerritNav.View.CHANGE, |
| changeNum: change._number, |
| project: change.project, |
| patchNum: opt_patchNum, |
| basePatchNum: opt_basePatchNum, |
| edit: opt_isEdit, |
| host: change.internalHost || undefined, |
| messageHash: opt_messageHash, |
| }); |
| }, |
| |
| /** |
| * @param {number} changeNum |
| * @param {string} project The name of the project. |
| * @param {number=} opt_patchNum |
| * @return {string} |
| */ |
| getUrlForChangeById(changeNum, project, opt_patchNum) { |
| return this._getUrlFor({ |
| view: GerritNav.View.CHANGE, |
| changeNum, |
| project, |
| patchNum: opt_patchNum, |
| }); |
| }, |
| |
| /** |
| * @param {!Object} change The change object. |
| * @param {number=} opt_patchNum |
| * @param {number|string=} opt_basePatchNum The string 'PARENT' can be |
| * used for none. |
| * @param {boolean=} opt_isEdit |
| */ |
| navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) { |
| this._navigate(this.getUrlForChange(change, opt_patchNum, |
| opt_basePatchNum, opt_isEdit)); |
| }, |
| |
| /** |
| * @param {{ _number: number, project: string }} change The change object. |
| * @param {string} path The file path. |
| * @param {number=} opt_patchNum |
| * @param {number|string=} opt_basePatchNum The string 'PARENT' can be |
| * used for none. |
| * @param {number|string=} opt_lineNum |
| * @return {string} |
| */ |
| getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) { |
| return this.getUrlForDiffById(change._number, change.project, path, |
| opt_patchNum, opt_basePatchNum, opt_lineNum); |
| }, |
| |
| /** |
| * @param {number} changeNum |
| * @param {string} project The name of the project. |
| * @param {string} path The file path. |
| * @param {number=} opt_patchNum |
| * @param {number|string=} opt_basePatchNum The string 'PARENT' can be |
| * used for none. |
| * @param {number=} opt_lineNum |
| * @param {boolean=} opt_leftSide |
| * @return {string} |
| */ |
| getUrlForDiffById(changeNum, project, path, opt_patchNum, |
| opt_basePatchNum, opt_lineNum, opt_leftSide) { |
| if (opt_basePatchNum === PARENT_PATCHNUM) { |
| opt_basePatchNum = undefined; |
| } |
| |
| this._checkPatchRange(opt_patchNum, opt_basePatchNum); |
| return this._getUrlFor({ |
| view: GerritNav.View.DIFF, |
| changeNum, |
| project, |
| path, |
| patchNum: opt_patchNum, |
| basePatchNum: opt_basePatchNum, |
| lineNum: opt_lineNum, |
| leftSide: opt_leftSide, |
| }); |
| }, |
| |
| /** |
| * @param {{ _number: number, project: string }} change The change object. |
| * @param {string} path The file path. |
| * @param {number=} opt_patchNum |
| * @return {string} |
| */ |
| getEditUrlForDiff(change, path, opt_patchNum) { |
| return this.getEditUrlForDiffById(change._number, change.project, path, |
| opt_patchNum); |
| }, |
| |
| /** |
| * @param {number} changeNum |
| * @param {string} project The name of the project. |
| * @param {string} path The file path. |
| * @param {number|string=} opt_patchNum The patchNum the file content |
| * should be based on, or ${EDIT_PATCHNUM} if left undefined. |
| * @return {string} |
| */ |
| getEditUrlForDiffById(changeNum, project, path, opt_patchNum) { |
| return this._getUrlFor({ |
| view: GerritNav.View.EDIT, |
| changeNum, |
| project, |
| path, |
| patchNum: opt_patchNum || EDIT_PATCHNUM, |
| }); |
| }, |
| |
| /** |
| * @param {!Object} change The change object. |
| * @param {string} path The file path. |
| * @param {number=} opt_patchNum |
| * @param {number|string=} opt_basePatchNum The string 'PARENT' can be |
| * used for none. |
| */ |
| navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { |
| this._navigate(this.getUrlForDiff(change, path, opt_patchNum, |
| opt_basePatchNum)); |
| }, |
| |
| /** |
| * @param {string} owner The name of the owner. |
| * @return {string} |
| */ |
| getUrlForOwner(owner) { |
| return this._getUrlFor({ |
| view: GerritNav.View.SEARCH, |
| owner, |
| }); |
| }, |
| |
| /** |
| * @param {string} user The name of the user. |
| * @return {string} |
| */ |
| getUrlForUserDashboard(user) { |
| return this._getUrlFor({ |
| view: GerritNav.View.DASHBOARD, |
| user, |
| }); |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| getUrlForRoot() { |
| return this._getUrlFor({ |
| view: GerritNav.View.ROOT, |
| }); |
| }, |
| |
| /** |
| * @param {string} repo The name of the repo. |
| * @param {string} dashboard The ID of the dashboard, in the form of |
| * '<ref>:<path>'. |
| * @return {string} |
| */ |
| getUrlForRepoDashboard(repo, dashboard) { |
| return this._getUrlFor({ |
| view: GerritNav.View.DASHBOARD, |
| repo, |
| dashboard, |
| }); |
| }, |
| |
| /** |
| * Navigate to an arbitrary relative URL. |
| * |
| * @param {string} relativeUrl |
| */ |
| navigateToRelativeUrl(relativeUrl) { |
| if (!relativeUrl.startsWith('/')) { |
| throw new Error('navigateToRelativeUrl with non-relative URL'); |
| } |
| this._navigate(relativeUrl); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepo(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| }); |
| }, |
| |
| /** |
| * Navigate to a repo settings page. |
| * |
| * @param {string} repoName |
| */ |
| navigateToRepo(repoName) { |
| this._navigate(this.getUrlForRepo(repoName)); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepoTags(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| detail: GerritNav.RepoDetailView.TAGS, |
| }); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepoBranches(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| detail: GerritNav.RepoDetailView.BRANCHES, |
| }); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepoAccess(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| detail: GerritNav.RepoDetailView.ACCESS, |
| }); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepoCommands(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| detail: GerritNav.RepoDetailView.COMMANDS, |
| }); |
| }, |
| |
| /** |
| * @param {string} repoName |
| * @return {string} |
| */ |
| getUrlForRepoDashboards(repoName) { |
| return this._getUrlFor({ |
| view: GerritNav.View.REPO, |
| repoName, |
| detail: GerritNav.RepoDetailView.DASHBOARDS, |
| }); |
| }, |
| |
| /** |
| * @param {string} groupId |
| * @return {string} |
| */ |
| getUrlForGroup(groupId) { |
| return this._getUrlFor({ |
| view: GerritNav.View.GROUP, |
| groupId, |
| }); |
| }, |
| |
| /** |
| * @param {string} groupId |
| * @return {string} |
| */ |
| getUrlForGroupLog(groupId) { |
| return this._getUrlFor({ |
| view: GerritNav.View.GROUP, |
| groupId, |
| detail: GerritNav.GroupDetailView.LOG, |
| }); |
| }, |
| |
| /** |
| * @param {string} groupId |
| * @return {string} |
| */ |
| getUrlForGroupMembers(groupId) { |
| return this._getUrlFor({ |
| view: GerritNav.View.GROUP, |
| groupId, |
| detail: GerritNav.GroupDetailView.MEMBERS, |
| }); |
| }, |
| |
| getUrlForSettings() { |
| return this._getUrlFor({view: GerritNav.View.SETTINGS}); |
| }, |
| |
| /** |
| * @param {string} repo |
| * @param {string} commit |
| * @param {string} file |
| * @param {Object=} opt_options |
| * @return { |
| * Array<{label: string, url: string}>| |
| * {label: string, url: string} |
| * } |
| */ |
| getFileWebLinks(repo, commit, file, opt_options) { |
| const params = {type: GerritNav.WeblinkType.FILE, repo, commit, file}; |
| if (opt_options) { |
| params.options = opt_options; |
| } |
| return [].concat(this._generateWeblinks(params)); |
| }, |
| |
| /** |
| * @param {string} repo |
| * @param {string} commit |
| * @param {Object=} opt_options |
| * @return {{label: string, url: string}} |
| */ |
| getPatchSetWeblink(repo, commit, opt_options) { |
| const params = {type: GerritNav.WeblinkType.PATCHSET, repo, commit}; |
| if (opt_options) { |
| params.options = opt_options; |
| } |
| const result = this._generateWeblinks(params); |
| if (Array.isArray(result)) { |
| return result.pop(); |
| } else { |
| return result; |
| } |
| }, |
| |
| /** |
| * @param {string} repo |
| * @param {string} commit |
| * @param {Object=} opt_options |
| * @return { |
| * Array<{label: string, url: string}>| |
| * {label: string, url: string} |
| * } |
| */ |
| getChangeWeblinks(repo, commit, opt_options) { |
| const params = {type: GerritNav.WeblinkType.CHANGE, repo, commit}; |
| if (opt_options) { |
| params.options = opt_options; |
| } |
| return [].concat(this._generateWeblinks(params)); |
| }, |
| |
| getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, |
| title = '') { |
| sections = sections |
| .filter(section => (user === 'self' || !section.selfOnly)) |
| .map(section => Object.assign({}, section, { |
| name: section.name, |
| query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), |
| })); |
| return {title, sections}; |
| }, |
| }; |