/** * @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: 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 * ':'. * @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}; }, };