diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-18 09:11:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-18 09:11:52 +0300 |
commit | 4d16568658ac6fb0003b407e07a76c11e607f44f (patch) | |
tree | 9084e7660f101d2cd70568f293257678ac5f2ef5 /app | |
parent | f5410eefec8642bed6e7e3051319c52d7cbfb101 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
41 files changed, 977 insertions, 178 deletions
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index 5efc7063efa..27901120c53 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -55,12 +55,13 @@ export function getUserProjects(userId, query, options, callback) { .catch(() => flash(__('Something went wrong while fetching projects'))); } -export function updateUserStatus({ emoji, message, availability }) { +export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) { const url = buildApiUrl(USER_POST_STATUS_PATH); return axios.put(url, { emoji, message, availability, + clear_status_after: clearStatusAfter, }); } diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index a8fe00d26e6..cb5fb5b4bed 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -1,6 +1,6 @@ import { memoize } from 'lodash'; import AccessorUtilities from '~/lib/utils/accessor'; -import { s__ } from '~/locale'; +import { __ } from '~/locale'; const isCustomizable = (command) => 'customizable' in command ? Boolean(command.customizable) : true; @@ -33,42 +33,608 @@ export const getCustomizations = memoize(() => { }); // All available commands +export const TOGGLE_KEYBOARD_SHORTCUTS_DIALOG = { + id: 'globalShortcuts.toggleKeyboardShortcutsDialog', + description: __('Toggle keyboard shortcuts help dialog'), + defaultKeys: ['?'], +}; + +export const GO_TO_YOUR_PROJECTS = { + id: 'globalShortcuts.goToYourProjects', + description: __('Go to your projects'), + defaultKeys: ['shift+p'], +}; + +export const GO_TO_YOUR_GROUPS = { + id: 'globalShortcuts.goToYourGroups', + description: __('Go to your groups'), + defaultKeys: ['shift+g'], +}; + +export const GO_TO_ACTIVITY_FEED = { + id: 'globalShortcuts.goToActivityFeed', + description: __('Go to the activity feed'), + defaultKeys: ['shift+a'], +}; + +export const GO_TO_MILESTONE_LIST = { + id: 'globalShortcuts.goToMilestoneList', + description: __('Go to the milestone list'), + defaultKeys: ['shift+l'], +}; + +export const GO_TO_YOUR_SNIPPETS = { + id: 'globalShortcuts.goToYourSnippets', + description: __('Go to your snippets'), + defaultKeys: ['shift+s'], +}; + +export const START_SEARCH = { + id: 'globalShortcuts.startSearch', + description: __('Start search'), + defaultKeys: ['s', '/'], +}; + +export const FOCUS_FILTER_BAR = { + id: 'globalShortcuts.focusFilterBar', + description: __('Focus filter bar'), + defaultKeys: ['f'], +}; + +export const GO_TO_YOUR_ISSUES = { + id: 'globalShortcuts.goToYourIssues', + description: __('Go to your issues'), + defaultKeys: ['shift+i'], +}; + +export const GO_TO_YOUR_MERGE_REQUESTS = { + id: 'globalShortcuts.goToYourMergeRequests', + description: __('Go to your merge requests'), + defaultKeys: ['shift+m'], +}; + +export const GO_TO_YOUR_TODO_LIST = { + id: 'globalShortcuts.goToYourTodoList', + description: __('Go to your To-Do list'), + defaultKeys: ['shift+t'], +}; + export const TOGGLE_PERFORMANCE_BAR = { id: 'globalShortcuts.togglePerformanceBar', - description: s__('KeyboardShortcuts|Toggle the Performance Bar'), - // eslint-disable-next-line @gitlab/require-i18n-strings - defaultKeys: ['p b'], + description: __('Toggle the Performance Bar'), + defaultKeys: ['p b'], // eslint-disable-line @gitlab/require-i18n-strings }; export const TOGGLE_CANARY = { id: 'globalShortcuts.toggleCanary', - description: s__('KeyboardShortcuts|Toggle GitLab Next'), - // eslint-disable-next-line @gitlab/require-i18n-strings - defaultKeys: ['g x'], + description: __('Toggle GitLab Next'), + defaultKeys: ['g x'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const BOLD_TEXT = { + id: 'editing.boldText', + description: __('Bold text'), + defaultKeys: ['mod+b'], + customizable: false, +}; + +export const ITALIC_TEXT = { + id: 'editing.italicText', + description: __('Italic text'), + defaultKeys: ['mod+i'], + customizable: false, +}; + +export const LINK_TEXT = { + id: 'editing.linkText', + description: __('Link text'), + defaultKeys: ['mod+k'], + customizable: false, +}; + +export const TOGGLE_MARKDOWN_PREVIEW = { + id: 'editing.toggleMarkdownPreview', + description: __('Toggle Markdown preview'), + // Note: Ideally, keyboard shortcuts should be made cross-platform by using the special `mod` key + // instead of binding both `ctrl` and `command` versions of the shortcut. + // See https://docs.gitlab.com/ee/development/fe_guide/keyboard_shortcuts.html#make-cross-platform-shortcuts. + // However, this particular shortcut has been in place since before the `mod` key was available. + // We've chosen to leave this implemented as-is for the time being to avoid breaking people's workflows. + // See discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45308#note_527490548. + defaultKeys: ['ctrl+shift+p', 'command+shift+p'], +}; + +export const EDIT_RECENT_COMMENT = { + id: 'editing.editRecentComment', + description: __('Edit your most recent comment in a thread (from an empty textarea)'), + defaultKeys: ['up'], +}; + +export const EDIT_WIKI_PAGE = { + id: 'wiki.editWikiPage', + description: __('Edit wiki page'), + defaultKeys: ['e'], +}; + +export const REPO_GRAPH_SCROLL_LEFT = { + id: 'repositoryGraph.scrollLeft', + description: __('Scroll left'), + defaultKeys: ['left', 'h'], +}; + +export const REPO_GRAPH_SCROLL_RIGHT = { + id: 'repositoryGraph.scrollRight', + description: __('Scroll right'), + defaultKeys: ['right', 'l'], +}; + +export const REPO_GRAPH_SCROLL_UP = { + id: 'repositoryGraph.scrollUp', + description: __('Scroll up'), + defaultKeys: ['up', 'k'], +}; + +export const REPO_GRAPH_SCROLL_DOWN = { + id: 'repositoryGraph.scrollDown', + description: __('Scroll down'), + defaultKeys: ['down', 'j'], +}; + +export const REPO_GRAPH_SCROLL_TOP = { + id: 'repositoryGraph.scrollToTop', + description: __('Scroll to top'), + defaultKeys: ['shift+up', 'shift+k'], +}; + +export const REPO_GRAPH_SCROLL_BOTTOM = { + id: 'repositoryGraph.scrollToBottom', + description: __('Scroll to bottom'), + defaultKeys: ['shift+down', 'shift+j'], +}; + +export const GO_TO_PROJECT_OVERVIEW = { + id: 'project.goToOverview', + description: __("Go to the project's overview page"), + defaultKeys: ['g p'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_ACTIVITY_FEED = { + id: 'project.goToActivityFeed', + description: __("Go to the project's activity feed"), + defaultKeys: ['g v'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_RELEASES = { + id: 'project.goToReleases', + description: __('Go to releases'), + defaultKeys: ['g r'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_FILES = { + id: 'project.goToFiles', + description: __('Go to files'), + defaultKeys: ['g f'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_FIND_FILE = { + id: 'project.goToFindFile', + description: __('Go to find file'), + defaultKeys: ['t'], +}; + +export const GO_TO_PROJECT_COMMITS = { + id: 'project.goToCommits', + description: __('Go to commits'), + defaultKeys: ['g c'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_REPO_GRAPH = { + id: 'project.goToRepoGraph', + description: __('Go to repository graph'), + defaultKeys: ['g n'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_REPO_CHARTS = { + id: 'project.goToRepoCharts', + description: __('Go to repository charts'), + defaultKeys: ['g d'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_ISSUES = { + id: 'project.goToIssues', + description: __('Go to issues'), + defaultKeys: ['g i'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const NEW_ISSUE = { + id: 'project.newIssue', + description: __('New issue'), + defaultKeys: ['i'], +}; + +export const GO_TO_PROJECT_ISSUE_BOARDS = { + id: 'project.goToIssueBoards', + description: __('Go to issue boards'), + defaultKeys: ['g b'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_MERGE_REQUESTS = { + id: 'project.goToMergeRequests', + description: __('Go to merge requests'), + defaultKeys: ['g m'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_JOBS = { + id: 'project.goToJobs', + description: __('Go to jobs'), + defaultKeys: ['g j'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_METRICS = { + id: 'project.goToMetrics', + description: __('Go to metrics'), + defaultKeys: ['g l'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_ENVIRONMENTS = { + id: 'project.goToEnvironments', + description: __('Go to environments'), + defaultKeys: ['g e'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_KUBERNETES = { + id: 'project.goToKubernetes', + description: __('Go to kubernetes'), + defaultKeys: ['g k'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_SNIPPETS = { + id: 'project.goToSnippets', + description: __('Go to snippets'), + defaultKeys: ['g s'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const GO_TO_PROJECT_WIKI = { + id: 'project.goToWiki', + description: __('Go to wiki'), + defaultKeys: ['g w'], // eslint-disable-line @gitlab/require-i18n-strings +}; + +export const PROJECT_FILES_MOVE_SELECTION_UP = { + id: 'projectFiles.moveSelectionUp', + description: __('Move selection up'), + defaultKeys: ['up'], +}; + +export const PROJECT_FILES_MOVE_SELECTION_DOWN = { + id: 'projectFiles.moveSelectionDown', + description: __('Move selection down'), + defaultKeys: ['down'], +}; + +export const PROJECT_FILES_OPEN_SELECTION = { + id: 'projectFiles.openSelection', + description: __('Open Selection'), + defaultKeys: ['enter'], +}; + +export const PROJECT_FILES_GO_BACK = { + id: 'projectFiles.goBack', + description: __('Go back (while searching for files)'), + defaultKeys: ['esc'], +}; + +export const PROJECT_FILES_GO_TO_PERMALINK = { + id: 'projectFiles.goToFilePermalink', + description: __('Go to file permalink (while viewing a file)'), + defaultKeys: ['y'], +}; + +export const ISSUABLE_COMMENT_OR_REPLY = { + id: 'issuables.commentReply', + description: __('Comment/Reply (quoting selected text)'), + defaultKeys: ['r'], +}; + +export const ISSUABLE_EDIT_DESCRIPTION = { + id: 'issuables.editDescription', + description: __('Edit description'), + defaultKeys: ['e'], +}; + +export const ISSUABLE_CHANGE_LABEL = { + id: 'issuables.changeLabel', + description: __('Change label'), + defaultKeys: ['l'], +}; + +export const ISSUE_MR_CHANGE_ASSIGNEE = { + id: 'issuesMRs.changeAssignee', + description: __('Change assignee'), + defaultKeys: ['a'], +}; + +export const ISSUE_MR_CHANGE_MILESTONE = { + id: 'issuesMRs.changeMilestone', + description: __('Change milestone'), + defaultKeys: ['m'], +}; + +export const MR_NEXT_FILE_IN_DIFF = { + id: 'mergeRequests.nextFileInDiff', + description: __('Next file in diff'), + defaultKeys: [']', 'j'], +}; + +export const MR_PREVIOUS_FILE_IN_DIFF = { + id: 'mergeRequests.previousFileInDiff', + description: __('Previous file in diff'), + defaultKeys: ['[', 'k'], +}; + +export const MR_GO_TO_FILE = { + id: 'mergeRequests.goToFile', + description: __('Go to file'), + defaultKeys: ['t', 'mod+p'], + customizable: false, +}; + +export const MR_NEXT_UNRESOLVED_DISCUSSION = { + id: 'mergeRequests.nextUnresolvedDiscussion', + description: __('Next unresolved discussion'), + defaultKeys: ['n'], +}; + +export const MR_PREVIOUS_UNRESOLVED_DISCUSSION = { + id: 'mergeRequests.previousUnresolvedDiscussion', + description: __('Previous unresolved discussion'), + defaultKeys: ['p'], +}; + +export const MR_COPY_SOURCE_BRANCH_NAME = { + id: 'mergeRequests.copySourceBranchName', + description: __('Copy source branch name'), + defaultKeys: ['b'], +}; + +export const MR_COMMITS_NEXT_COMMIT = { + id: 'mergeRequestCommits.nextCommit', + description: __('Next commit'), + defaultKeys: ['c'], +}; + +export const MR_COMMITS_PREVIOUS_COMMIT = { + id: 'mergeRequestCommits.previousCommit', + description: __('Previous commit'), + defaultKeys: ['x'], +}; + +export const ISSUE_NEXT_DESIGN = { + id: 'issues.nextDesign', + description: __('Next design'), + defaultKeys: ['right'], +}; + +export const ISSUE_PREVIOUS_DESIGN = { + id: 'issues.previousDesign', + description: __('Previous design'), + defaultKeys: ['left'], +}; + +export const ISSUE_CLOSE_DESIGN = { + id: 'issues.closeDesign', + description: __('Close design'), + defaultKeys: ['esc'], +}; + +export const WEB_IDE_GO_TO_FILE = { + id: 'webIDE.goToFile', + description: __('Go to file'), + defaultKeys: ['mod+p'], }; export const WEB_IDE_COMMIT = { id: 'webIDE.commit', - description: s__('KeyboardShortcuts|Commit (when editing commit message)'), + description: __('Commit (when editing commit message)'), defaultKeys: ['mod+enter'], customizable: false, }; +export const METRICS_EXPAND_PANEL = { + id: 'metrics.expandPanel', + description: __('Expand panel'), + defaultKeys: ['e'], + customizable: false, +}; + +export const METRICS_VIEW_LOGS = { + id: 'metrics.viewLogs', + description: __('View logs'), + defaultKeys: ['l'], + customizable: false, +}; + +export const METRICS_DOWNLOAD_CSV = { + id: 'metrics.downloadCSV', + description: __('Download CSV'), + defaultKeys: ['d'], + customizable: false, +}; + +export const METRICS_COPY_LINK_TO_CHART = { + id: 'metrics.copyLinkToChart', + description: __('Copy link to chart'), + defaultKeys: ['c'], + customizable: false, +}; + +export const METRICS_SHOW_ALERTS = { + id: 'metrics.showAlerts', + description: __('Alerts'), + defaultKeys: ['a'], + customizable: false, +}; + // All keybinding groups export const GLOBAL_SHORTCUTS_GROUP = { id: 'globalShortcuts', - name: s__('KeyboardShortcuts|Global Shortcuts'), - keybindings: [TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY], + name: __('Global Shortcuts'), + keybindings: [ + TOGGLE_KEYBOARD_SHORTCUTS_DIALOG, + GO_TO_YOUR_PROJECTS, + GO_TO_YOUR_GROUPS, + GO_TO_ACTIVITY_FEED, + GO_TO_MILESTONE_LIST, + GO_TO_YOUR_SNIPPETS, + START_SEARCH, + FOCUS_FILTER_BAR, + GO_TO_YOUR_ISSUES, + GO_TO_YOUR_MERGE_REQUESTS, + GO_TO_YOUR_TODO_LIST, + TOGGLE_PERFORMANCE_BAR, + ], +}; + +export const EDITING_SHORTCUTS_GROUP = { + id: 'editing', + name: __('Editing'), + keybindings: [BOLD_TEXT, ITALIC_TEXT, LINK_TEXT, TOGGLE_MARKDOWN_PREVIEW, EDIT_RECENT_COMMENT], +}; + +export const WIKI_SHORTCUTS_GROUP = { + id: 'wiki', + name: __('Wiki'), + keybindings: [EDIT_WIKI_PAGE], +}; + +export const REPOSITORY_GRAPH_SHORTCUTS_GROUP = { + id: 'repositoryGraph', + name: __('Repository Graph'), + keybindings: [ + REPO_GRAPH_SCROLL_LEFT, + REPO_GRAPH_SCROLL_RIGHT, + REPO_GRAPH_SCROLL_UP, + REPO_GRAPH_SCROLL_DOWN, + REPO_GRAPH_SCROLL_TOP, + REPO_GRAPH_SCROLL_BOTTOM, + ], +}; + +export const PROJECT_SHORTCUTS_GROUP = { + id: 'project', + name: __('Project'), + keybindings: [ + GO_TO_PROJECT_OVERVIEW, + GO_TO_PROJECT_ACTIVITY_FEED, + GO_TO_PROJECT_RELEASES, + GO_TO_PROJECT_FILES, + GO_TO_PROJECT_FIND_FILE, + GO_TO_PROJECT_COMMITS, + GO_TO_PROJECT_REPO_GRAPH, + GO_TO_PROJECT_REPO_CHARTS, + GO_TO_PROJECT_ISSUES, + NEW_ISSUE, + GO_TO_PROJECT_ISSUE_BOARDS, + GO_TO_PROJECT_MERGE_REQUESTS, + GO_TO_PROJECT_JOBS, + GO_TO_PROJECT_METRICS, + GO_TO_PROJECT_ENVIRONMENTS, + GO_TO_PROJECT_KUBERNETES, + GO_TO_PROJECT_SNIPPETS, + GO_TO_PROJECT_WIKI, + ], +}; + +export const PROJECT_FILES_SHORTCUTS_GROUP = { + id: 'projectFiles', + name: __('Project Files'), + keybindings: [ + PROJECT_FILES_MOVE_SELECTION_UP, + PROJECT_FILES_MOVE_SELECTION_DOWN, + PROJECT_FILES_OPEN_SELECTION, + PROJECT_FILES_GO_BACK, + PROJECT_FILES_GO_TO_PERMALINK, + ], +}; + +export const ISSUABLE_SHORTCUTS_GROUP = { + id: 'issuables', + name: __('Epics, Issues, and Merge Requests'), + keybindings: [ISSUABLE_COMMENT_OR_REPLY, ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_CHANGE_LABEL], +}; + +export const ISSUE_MR_SHORTCUTS_GROUP = { + id: 'issuesMRs', + name: __('Issues and Merge Requests'), + keybindings: [ISSUE_MR_CHANGE_ASSIGNEE, ISSUE_MR_CHANGE_MILESTONE], }; -export const WEB_IDE_GROUP = { +export const MR_SHORTCUTS_GROUP = { + id: 'mergeRequests', + name: __('Merge Requests'), + keybindings: [ + MR_NEXT_FILE_IN_DIFF, + MR_PREVIOUS_FILE_IN_DIFF, + MR_GO_TO_FILE, + MR_NEXT_UNRESOLVED_DISCUSSION, + MR_PREVIOUS_UNRESOLVED_DISCUSSION, + MR_COPY_SOURCE_BRANCH_NAME, + ], +}; + +export const MR_COMMITS_SHORTCUTS_GROUP = { + id: 'mergeRequestCommits', + name: __('Merge Request Commits'), + keybindings: [MR_COMMITS_NEXT_COMMIT, MR_COMMITS_PREVIOUS_COMMIT], +}; + +export const ISSUES_SHORTCUTS_GROUP = { + id: 'issues', + name: __('Issues'), + keybindings: [ISSUE_NEXT_DESIGN, ISSUE_PREVIOUS_DESIGN, ISSUE_CLOSE_DESIGN], +}; + +export const WEB_IDE_SHORTCUTS_GROUP = { id: 'webIDE', - name: s__('KeyboardShortcuts|Web IDE'), - keybindings: [WEB_IDE_COMMIT], + name: __('Web IDE'), + keybindings: [WEB_IDE_GO_TO_FILE, WEB_IDE_COMMIT], +}; + +export const METRICS_SHORTCUTS_GROUP = { + id: 'metrics', + name: __('Metrics'), + keybindings: [ + METRICS_EXPAND_PANEL, + METRICS_VIEW_LOGS, + METRICS_DOWNLOAD_CSV, + METRICS_COPY_LINK_TO_CHART, + METRICS_SHOW_ALERTS, + ], +}; + +export const MISC_SHORTCUTS_GROUP = { + id: 'misc', + name: __('Miscellaneous'), + keybindings: [TOGGLE_CANARY], }; /** All keybindings, grouped and ordered with descriptions */ -export const keybindingGroups = [GLOBAL_SHORTCUTS_GROUP, WEB_IDE_GROUP]; +export const keybindingGroups = [ + GLOBAL_SHORTCUTS_GROUP, + EDITING_SHORTCUTS_GROUP, + WIKI_SHORTCUTS_GROUP, + REPOSITORY_GRAPH_SHORTCUTS_GROUP, + PROJECT_SHORTCUTS_GROUP, + PROJECT_FILES_SHORTCUTS_GROUP, + ISSUABLE_SHORTCUTS_GROUP, + ISSUE_MR_SHORTCUTS_GROUP, + MR_SHORTCUTS_GROUP, + MR_COMMITS_SHORTCUTS_GROUP, + ISSUES_SHORTCUTS_GROUP, + WEB_IDE_SHORTCUTS_GROUP, + METRICS_SHORTCUTS_GROUP, + MISC_SHORTCUTS_GROUP, +]; /** * Gets keyboard shortcuts associated with a command diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index e4ec68601e0..03cba78cf31 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -6,13 +6,29 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import findAndFollowLink from '~/lib/utils/navigation_utility'; import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility'; - -import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings'; +import { + keysFor, + TOGGLE_KEYBOARD_SHORTCUTS_DIALOG, + START_SEARCH, + FOCUS_FILTER_BAR, + TOGGLE_PERFORMANCE_BAR, + TOGGLE_CANARY, + TOGGLE_MARKDOWN_PREVIEW, + GO_TO_YOUR_TODO_LIST, + GO_TO_ACTIVITY_FEED, + GO_TO_YOUR_ISSUES, + GO_TO_YOUR_MERGE_REQUESTS, + GO_TO_YOUR_PROJECTS, + GO_TO_YOUR_GROUPS, + GO_TO_MILESTONE_LIST, + GO_TO_YOUR_SNIPPETS, + GO_TO_PROJECT_FIND_FILE, +} from './keybindings'; import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; const defaultStopCallback = Mousetrap.prototype.stopCallback; Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { - if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { + if (keysFor(TOGGLE_MARKDOWN_PREVIEW).indexOf(combo) !== -1) { return false; } @@ -58,28 +74,41 @@ export default class Shortcuts { this.helpModalElement = null; this.helpModalVueInstance = null; - Mousetrap.bind('?', this.onToggleHelp); - Mousetrap.bind('s', Shortcuts.focusSearch); - Mousetrap.bind('/', Shortcuts.focusSearch); - Mousetrap.bind('f', this.focusFilter.bind(this)); + Mousetrap.bind(keysFor(TOGGLE_KEYBOARD_SHORTCUTS_DIALOG), this.onToggleHelp); + Mousetrap.bind(keysFor(START_SEARCH), Shortcuts.focusSearch); + Mousetrap.bind(keysFor(FOCUS_FILTER_BAR), this.focusFilter.bind(this)); Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar); Mousetrap.bind(keysFor(TOGGLE_CANARY), Shortcuts.onToggleCanary); const findFileURL = document.body.dataset.findFile; - Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); - Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); - Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); - Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); - Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); - Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); - Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); - Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); - - Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview); + Mousetrap.bind(keysFor(GO_TO_YOUR_TODO_LIST), () => findAndFollowLink('.shortcuts-todos')); + Mousetrap.bind(keysFor(GO_TO_ACTIVITY_FEED), () => + findAndFollowLink('.dashboard-shortcuts-activity'), + ); + Mousetrap.bind(keysFor(GO_TO_YOUR_ISSUES), () => + findAndFollowLink('.dashboard-shortcuts-issues'), + ); + Mousetrap.bind(keysFor(GO_TO_YOUR_MERGE_REQUESTS), () => + findAndFollowLink('.dashboard-shortcuts-merge_requests'), + ); + Mousetrap.bind(keysFor(GO_TO_YOUR_PROJECTS), () => + findAndFollowLink('.dashboard-shortcuts-projects'), + ); + Mousetrap.bind(keysFor(GO_TO_YOUR_GROUPS), () => + findAndFollowLink('.dashboard-shortcuts-groups'), + ); + Mousetrap.bind(keysFor(GO_TO_MILESTONE_LIST), () => + findAndFollowLink('.dashboard-shortcuts-milestones'), + ); + Mousetrap.bind(keysFor(GO_TO_YOUR_SNIPPETS), () => + findAndFollowLink('.dashboard-shortcuts-snippets'), + ); + + Mousetrap.bind(keysFor(TOGGLE_MARKDOWN_PREVIEW), Shortcuts.toggleMarkdownPreview); if (typeof findFileURL !== 'undefined' && findFileURL !== null) { - Mousetrap.bind('t', () => { + Mousetrap.bind(keysFor(GO_TO_PROJECT_FIND_FILE), () => { visitUrl(findFileURL); }); } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index 11b4fcd4e1c..ab7fcbb35f1 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -1,4 +1,5 @@ import Mousetrap from 'mousetrap'; +import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings'; import { getLocationHash, updateHistory, @@ -28,7 +29,7 @@ export default class ShortcutsBlob extends Shortcuts { this.shortcircuitPermalinkButton(); - Mousetrap.bind('y', this.moveToFilePermalink.bind(this)); + Mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.moveToFilePermalink.bind(this)); } moveToFilePermalink() { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js index f0d2ecfd210..992e571e596 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js @@ -1,4 +1,11 @@ import Mousetrap from 'mousetrap'; +import { + keysFor, + PROJECT_FILES_MOVE_SELECTION_UP, + PROJECT_FILES_MOVE_SELECTION_DOWN, + PROJECT_FILES_OPEN_SELECTION, + PROJECT_FILES_GO_BACK, +} from '~/behaviors/shortcuts/keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsFindFile extends ShortcutsNavigation { @@ -10,7 +17,10 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { if ( element === projectFindFile.inputElement[0] && - (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') + (keysFor(PROJECT_FILES_MOVE_SELECTION_UP).includes(combo) || + keysFor(PROJECT_FILES_MOVE_SELECTION_DOWN).includes(combo) || + keysFor(PROJECT_FILES_GO_BACK).includes(combo) || + keysFor(PROJECT_FILES_OPEN_SELECTION).includes(combo)) ) { // when press up/down key in textbox, cursor prevent to move to home/end e.preventDefault(); @@ -20,9 +30,9 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { return oldStopCallback.call(this, e, element, combo); }; - Mousetrap.bind('up', projectFindFile.selectRowUp); - Mousetrap.bind('down', projectFindFile.selectRowDown); - Mousetrap.bind('esc', projectFindFile.goToTree); - Mousetrap.bind('enter', projectFindFile.goToBlob); + Mousetrap.bind(keysFor(PROJECT_FILES_MOVE_SELECTION_UP), projectFindFile.selectRowUp); + Mousetrap.bind(keysFor(PROJECT_FILES_MOVE_SELECTION_DOWN), projectFindFile.selectRowDown); + Mousetrap.bind(keysFor(PROJECT_FILES_GO_BACK), projectFindFile.goToTree); + Mousetrap.bind(keysFor(PROJECT_FILES_OPEN_SELECTION), projectFindFile.goToBlob); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 476745beb19..a55bdf231c0 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -5,18 +5,33 @@ import { getSelectedFragment } from '~/lib/utils/common_utils'; import { isElementVisible } from '~/lib/utils/dom_utils'; import Sidebar from '../../right_sidebar'; import { CopyAsGFM } from '../markdown/copy_as_gfm'; +import { + keysFor, + ISSUE_MR_CHANGE_ASSIGNEE, + ISSUE_MR_CHANGE_MILESTONE, + ISSUABLE_CHANGE_LABEL, + ISSUABLE_COMMENT_OR_REPLY, + ISSUABLE_EDIT_DESCRIPTION, + MR_COPY_SOURCE_BRANCH_NAME, +} from './keybindings'; import Shortcuts from './shortcuts'; export default class ShortcutsIssuable extends Shortcuts { constructor() { super(); - Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); - Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); - Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); - Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText); - Mousetrap.bind('e', ShortcutsIssuable.editIssue); - Mousetrap.bind('b', ShortcutsIssuable.copyBranchName); + Mousetrap.bind(keysFor(ISSUE_MR_CHANGE_ASSIGNEE), () => + ShortcutsIssuable.openSidebarDropdown('assignee'), + ); + Mousetrap.bind(keysFor(ISSUE_MR_CHANGE_MILESTONE), () => + ShortcutsIssuable.openSidebarDropdown('milestone'), + ); + Mousetrap.bind(keysFor(ISSUABLE_CHANGE_LABEL), () => + ShortcutsIssuable.openSidebarDropdown('labels'), + ); + Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), ShortcutsIssuable.replyWithSelectedText); + Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue); + Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), ShortcutsIssuable.copyBranchName); } static replyWithSelectedText() { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index b46b4132ba8..b188d3b0ec3 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -1,27 +1,63 @@ import Mousetrap from 'mousetrap'; import findAndFollowLink from '../../lib/utils/navigation_utility'; +import { + keysFor, + GO_TO_PROJECT_OVERVIEW, + GO_TO_PROJECT_ACTIVITY_FEED, + GO_TO_PROJECT_RELEASES, + GO_TO_PROJECT_FILES, + GO_TO_PROJECT_COMMITS, + GO_TO_PROJECT_JOBS, + GO_TO_PROJECT_REPO_GRAPH, + GO_TO_PROJECT_REPO_CHARTS, + GO_TO_PROJECT_ISSUES, + GO_TO_PROJECT_ISSUE_BOARDS, + GO_TO_PROJECT_MERGE_REQUESTS, + GO_TO_PROJECT_WIKI, + GO_TO_PROJECT_SNIPPETS, + GO_TO_PROJECT_KUBERNETES, + GO_TO_PROJECT_ENVIRONMENTS, + GO_TO_PROJECT_METRICS, + NEW_ISSUE, +} from './keybindings'; import Shortcuts from './shortcuts'; export default class ShortcutsNavigation extends Shortcuts { constructor() { super(); - Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); - Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity')); - Mousetrap.bind('g r', () => findAndFollowLink('.shortcuts-project-releases')); - Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); - Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); - Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); - Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); - Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); - Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); - Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); - Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); - Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); - Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); - Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); - Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); - Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics')); - Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_OVERVIEW), () => findAndFollowLink('.shortcuts-project')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_ACTIVITY_FEED), () => + findAndFollowLink('.shortcuts-project-activity'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_RELEASES), () => + findAndFollowLink('.shortcuts-project-releases'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_FILES), () => findAndFollowLink('.shortcuts-tree')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_COMMITS), () => findAndFollowLink('.shortcuts-commits')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_JOBS), () => findAndFollowLink('.shortcuts-builds')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_REPO_GRAPH), () => + findAndFollowLink('.shortcuts-network'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_REPO_CHARTS), () => + findAndFollowLink('.shortcuts-repository-charts'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_ISSUES), () => findAndFollowLink('.shortcuts-issues')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_ISSUE_BOARDS), () => + findAndFollowLink('.shortcuts-issue-boards'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_MERGE_REQUESTS), () => + findAndFollowLink('.shortcuts-merge_requests'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_WIKI), () => findAndFollowLink('.shortcuts-wiki')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_SNIPPETS), () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_KUBERNETES), () => + findAndFollowLink('.shortcuts-kubernetes'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_ENVIRONMENTS), () => + findAndFollowLink('.shortcuts-environments'), + ); + Mousetrap.bind(keysFor(GO_TO_PROJECT_METRICS), () => findAndFollowLink('.shortcuts-metrics')); + Mousetrap.bind(keysFor(NEW_ISSUE), () => findAndFollowLink('.shortcuts-new-issue')); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js index 3e791e4673a..c33c092b009 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js @@ -1,15 +1,24 @@ import Mousetrap from 'mousetrap'; +import { + keysFor, + REPO_GRAPH_SCROLL_BOTTOM, + REPO_GRAPH_SCROLL_DOWN, + REPO_GRAPH_SCROLL_LEFT, + REPO_GRAPH_SCROLL_RIGHT, + REPO_GRAPH_SCROLL_TOP, + REPO_GRAPH_SCROLL_UP, +} from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsNetwork extends ShortcutsNavigation { constructor(graph) { super(); - Mousetrap.bind(['left', 'h'], graph.scrollLeft); - Mousetrap.bind(['right', 'l'], graph.scrollRight); - Mousetrap.bind(['up', 'k'], graph.scrollUp); - Mousetrap.bind(['down', 'j'], graph.scrollDown); - Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop); - Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_LEFT), graph.scrollLeft); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_RIGHT), graph.scrollRight); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_UP), graph.scrollUp); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_DOWN), graph.scrollDown); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_TOP), graph.scrollTop); + Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_BOTTOM), graph.scrollBottom); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js index c609936a02a..59c1d2654bc 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js @@ -1,11 +1,12 @@ import Mousetrap from 'mousetrap'; import findAndFollowLink from '../../lib/utils/navigation_utility'; +import { keysFor, EDIT_WIKI_PAGE } from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { super(); - Mousetrap.bind('e', ShortcutsWiki.editWiki); + Mousetrap.bind(keysFor(EDIT_WIKI_PAGE), ShortcutsWiki.editWiki); } static editWiki() { diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue index 6091a3183ac..8535f818b9c 100644 --- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue +++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue @@ -2,6 +2,11 @@ /* global Mousetrap */ import 'mousetrap'; import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui'; +import { + keysFor, + ISSUE_PREVIOUS_DESIGN, + ISSUE_NEXT_DESIGN, +} from '~/behaviors/shortcuts/keybindings'; import { s__, sprintf } from '~/locale'; import allDesignsMixin from '../../mixins/all_designs'; import { DESIGN_ROUTE_NAME } from '../../router/constants'; @@ -46,11 +51,14 @@ export default { }, }, mounted() { - Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign)); - Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign)); + Mousetrap.bind(keysFor(ISSUE_PREVIOUS_DESIGN), () => + this.navigateToDesign(this.previousDesign), + ); + Mousetrap.bind(keysFor(ISSUE_NEXT_DESIGN), () => this.navigateToDesign(this.nextDesign)); }, beforeDestroy() { - Mousetrap.unbind(['left', 'right'], this.navigateToDesign); + Mousetrap.unbind(keysFor(ISSUE_PREVIOUS_DESIGN)); + Mousetrap.unbind(keysFor(ISSUE_NEXT_DESIGN)); }, methods: { navigateToDesign(design) { diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 8a11c25a795..ad78433c7ce 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -2,6 +2,7 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; import Mousetrap from 'mousetrap'; import { ApolloMutation } from 'vue-apollo'; +import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings'; import createFlash from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -171,7 +172,7 @@ export default { }, }, mounted() { - Mousetrap.bind('esc', this.closeDesign); + Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign); this.trackPageViewEvent(); // Set active discussion immediately. @@ -180,7 +181,7 @@ export default { this.updateActiveDiscussionFromUrl(); }, beforeDestroy() { - Mousetrap.unbind('esc', this.closeDesign); + Mousetrap.unbind(keysFor(ISSUE_CLOSE_DESIGN)); }, methods: { addImageDiffNoteToStore(store, { data: { createImageDiffNote } }) { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 253e1e3b70e..98f1ee9242f 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -3,6 +3,13 @@ import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import Mousetrap from 'mousetrap'; import { mapState, mapGetters, mapActions } from 'vuex'; +import { + keysFor, + MR_PREVIOUS_FILE_IN_DIFF, + MR_NEXT_FILE_IN_DIFF, + MR_COMMITS_NEXT_COMMIT, + MR_COMMITS_PREVIOUS_COMMIT, +} from '~/behaviors/shortcuts/keybindings'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { isSingleViewStyle } from '~/helpers/diffs_helper'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; @@ -406,30 +413,23 @@ export default { } }, setEventListeners() { - Mousetrap.bind(['[', 'k', ']', 'j'], (e, combo) => { - switch (combo) { - case '[': - case 'k': - this.jumpToFile(-1); - break; - case ']': - case 'j': - this.jumpToFile(+1); - break; - default: - break; - } - }); + Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1)); + Mousetrap.bind(keysFor(MR_NEXT_FILE_IN_DIFF), () => this.jumpToFile(+1)); if (this.commit) { - Mousetrap.bind('c', () => this.moveToNeighboringCommit({ direction: 'next' })); - Mousetrap.bind('x', () => this.moveToNeighboringCommit({ direction: 'previous' })); + Mousetrap.bind(keysFor(MR_COMMITS_NEXT_COMMIT), () => + this.moveToNeighboringCommit({ direction: 'next' }), + ); + Mousetrap.bind(keysFor(MR_COMMITS_PREVIOUS_COMMIT), () => + this.moveToNeighboringCommit({ direction: 'previous' }), + ); } }, removeEventListeners() { - Mousetrap.unbind(['[', 'k', ']', 'j']); - Mousetrap.unbind('c'); - Mousetrap.unbind('x'); + Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF)); + Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF)); + Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT)); + Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT)); }, jumpToFile(step) { const targetIndex = this.currentDiffIndex + step; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 22c648a76a7..4fed7f555f6 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -46,6 +46,7 @@ function initStatusTriggers() { currentMessage, currentAvailability, canSetUserAvailability, + currentClearStatusAfter, } = setStatusModalWrapperEl.dataset; return { @@ -54,6 +55,7 @@ function initStatusTriggers() { currentMessage, currentAvailability, canSetUserAvailability, + currentClearStatusAfter, }; }, render(createElement) { @@ -63,6 +65,7 @@ function initStatusTriggers() { currentMessage, currentAvailability, canSetUserAvailability, + currentClearStatusAfter, } = this; return createElement(SetStatusModalWrapper, { @@ -72,6 +75,7 @@ function initStatusTriggers() { currentMessage, currentAvailability, canSetUserAvailability, + currentClearStatusAfter, }, }); }, diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue index fa3c900c337..7e8bb75902b 100644 --- a/app/assets/javascripts/notes/components/discussion_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_navigator.vue @@ -1,6 +1,11 @@ <script> /* global Mousetrap */ import 'mousetrap'; +import { + keysFor, + MR_NEXT_UNRESOLVED_DISCUSSION, + MR_PREVIOUS_UNRESOLVED_DISCUSSION, +} from '~/behaviors/shortcuts/keybindings'; import eventHub from '~/notes/event_hub'; import discussionNavigation from '~/notes/mixins/discussion_navigation'; @@ -10,12 +15,12 @@ export default { eventHub.$on('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion); }, mounted() { - Mousetrap.bind('n', this.jumpToNextDiscussion); - Mousetrap.bind('p', this.jumpToPreviousDiscussion); + Mousetrap.bind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION), this.jumpToNextDiscussion); + Mousetrap.bind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION), this.jumpToPreviousDiscussion); }, beforeDestroy() { - Mousetrap.unbind('n'); - Mousetrap.unbind('p'); + Mousetrap.unbind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION)); + Mousetrap.unbind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION)); eventHub.$off('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion); }, diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index bed264341a5..bff90254c04 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -1,14 +1,23 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui'; +import { + GlToast, + GlModal, + GlTooltipDirective, + GlIcon, + GlFormCheckbox, + GlDropdown, + GlDropdownItem, +} from '@gitlab/ui'; import $ from 'jquery'; import Vue from 'vue'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import * as Emoji from '~/emoji'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { __, s__ } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import { updateUserStatus } from '~/rest_api'; +import { timeRanges } from '~/vue_shared/constants'; import EmojiMenuInModal from './emoji_menu_in_modal'; import { isUserBusy } from './utils'; @@ -20,11 +29,21 @@ export const AVAILABILITY_STATUS = { Vue.use(GlToast); +const statusTimeRanges = [ + { + label: __('Never'), + name: 'never', + }, + ...timeRanges, +]; + export default { components: { GlIcon, GlModal, GlFormCheckbox, + GlDropdown, + GlDropdownItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -53,6 +72,11 @@ export default { required: false, default: false, }, + currentClearStatusAfter: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -65,6 +89,10 @@ export default { modalId: 'set-user-status-modal', noEmoji: true, availability: isUserBusy(this.currentAvailability), + clearStatusAfter: statusTimeRanges[0].label, + clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), { + date: this.currentClearStatusAfter, + }), }; }, computed: { @@ -161,12 +189,16 @@ export default { this.setStatus(); }, setStatus() { - const { emoji, message, availability } = this; + const { emoji, message, availability, clearStatusAfter } = this; updateUserStatus({ emoji, message, availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET, + clearStatusAfter: + clearStatusAfter === statusTimeRanges[0].label + ? null + : clearStatusAfter.replace(' ', '_'), }) .then(this.onUpdateSuccess) .catch(this.onUpdateFail); @@ -183,7 +215,11 @@ export default { this.closeModal(); }, + setClearStatusAfter(after) { + this.clearStatusAfter = after; + }, }, + statusTimeRanges, }; </script> @@ -268,10 +304,31 @@ export default { </div> <div class="gl-display-flex"> <span class="gl-text-gray-600 gl-ml-5"> - {{ s__('SetStatusModal|"Busy" will be shown next to your name') }} + {{ s__('SetStatusModal|A busy indicator is shown next to your name and avatar.') }} </span> </div> </div> + <div class="form-group"> + <div class="gl-display-flex gl-align-items-baseline"> + <span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span> + <gl-dropdown :text="clearStatusAfter" data-testid="clear-status-at-dropdown"> + <gl-dropdown-item + v-for="after in $options.statusTimeRanges" + :key="after.name" + :data-testid="after.name" + @click="setClearStatusAfter(after.label)" + >{{ after.label }}</gl-dropdown-item + > + </gl-dropdown> + </div> + <div + v-if="currentClearStatusAfter.length" + class="gl-mt-3 gl-text-gray-400 gl-font-sm" + data-testid="clear-status-at-message" + > + {{ clearStatusAfterMessage }} + </div> + </div> </div> </div> </gl-modal> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index 4ec54b33bce..fbadb202d51 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -3,6 +3,7 @@ import { GlIcon } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import Mousetrap from 'mousetrap'; import VirtualList from 'vue-virtual-scroll-list'; +import { keysFor, MR_GO_TO_FILE } from '~/behaviors/shortcuts/keybindings'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import Item from './item.vue'; @@ -128,7 +129,7 @@ export default { this.focusedIndex = 0; } - Mousetrap.bind(['t', 'mod+p'], (e) => { + Mousetrap.bind(keysFor(MR_GO_TO_FILE), (e) => { if (e.preventDefault) { e.preventDefault(); } diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 5bc1786d692..f52dc43aaff 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,6 +1,7 @@ <script> import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import $ from 'jquery'; +import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; @@ -116,6 +117,11 @@ export default { .catch(() => {}); }, }, + shortcuts: { + bold: keysFor(BOLD_TEXT), + italic: keysFor(ITALIC_TEXT), + link: keysFor(LINK_TEXT), + }, }; </script> @@ -143,7 +149,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) " - shortcuts="mod+b" + :shortcuts="$options.shortcuts.bold" icon="bold" /> <toolbar-button @@ -151,7 +157,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) " - shortcuts="mod+i" + :shortcuts="$options.shortcuts.italic" icon="italic" /> <toolbar-button @@ -208,7 +214,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) " - shortcuts="mod+k" + :shortcuts="$options.shortcuts.link" icon="link" /> </div> diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index f24750243fc..82005c548f2 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -35,7 +35,6 @@ class GraphqlController < ApplicationController def execute result = multiplex? ? execute_multiplex : execute_query - render json: result end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 7ab5dc36e4a..eb083950fff 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -12,7 +12,6 @@ class GitlabSchema < GraphQL::Schema use GraphQL::Pagination::Connections use BatchLoader::GraphQL - use Gitlab::Graphql::Authorize use Gitlab::Graphql::Pagination::Connections use Gitlab::Graphql::GenericTracing use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index ac5ddc5bd4c..ec0f8b54789 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -2,7 +2,7 @@ module Mutations class BaseMutation < GraphQL::Schema::RelayClassicMutation - prepend Gitlab::Graphql::Authorize::AuthorizeResource + include Gitlab::Graphql::Authorize::AuthorizeResource prepend Gitlab::Graphql::CopyFieldDescription prepend ::Gitlab::Graphql::GlobalIDCompatibility @@ -29,10 +29,30 @@ module Mutations def ready?(**args) if Gitlab::Database.read_only? - raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE + raise_resource_not_available_error! ERROR_MESSAGE else true end end + + def load_application_object(argument, lookup_as_type, id, context) + ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e| + Gitlab::ErrorTracking.track_exception(e) + # The default behaviour is to abort processing and return nil for the + # entire mutation field, but not set any top-level errors. We prefer to + # at least say that something went wrong. + raise_resource_not_available_error! + end + end + + def self.authorized?(object, context) + # we never provide an object to mutations, but we do need to have a user. + context[:current_user].present? && !context[:current_user].blocked? + end + + # See: AuthorizeResource#authorized_resource? + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end end end diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb index ce2126e26c0..f32205643da 100644 --- a/app/graphql/mutations/boards/issues/issue_move_list.rb +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -52,13 +52,10 @@ module Mutations super end - def resolve(board:, **args) + def resolve(board:, project_path:, iid:, **args) Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/247861') - raise_resource_not_available_error! unless board - authorize_board!(board) - - issue = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + issue = authorized_find!(project_path: project_path, iid: iid) move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args)) move_issue(board, issue, move_params) @@ -84,12 +81,6 @@ module Mutations def move_arguments(args) args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id) end - - def authorize_board!(board) - return if Ability.allowed?(current_user, :read_issue_board, board.resource_parent) - - raise_resource_not_available_error! - end end end end diff --git a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb index 94a72bca7c7..fb6682f8d7e 100644 --- a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb +++ b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb @@ -3,7 +3,7 @@ module Resolvers module AlertManagement class HttpIntegrationsResolver < BaseResolver - alias_method :project, :synchronized_object + alias_method :project, :object type Types::AlertManagement::HttpIntegrationType.connection_type, null: true diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb index 4d1fe367277..e027e0412bd 100644 --- a/app/graphql/resolvers/alert_management/integrations_resolver.rb +++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb @@ -3,7 +3,7 @@ module Resolvers module AlertManagement class IntegrationsResolver < BaseResolver - alias_method :project, :synchronized_object + alias_method :project, :object type Types::AlertManagement::IntegrationType.connection_type, null: true diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 67bba079512..7c408382cbe 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -138,16 +138,6 @@ module Resolvers end end - # TODO: remove! This should never be necessary - # Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/13984, - # since once we use that authorization approach, the object is guaranteed to - # be synchronized before any field. - def synchronized_object - strong_memoize(:synchronized_object) do - ::Gitlab::Graphql::Lazy.force(object) - end - end - def single? false end @@ -160,5 +150,13 @@ module Resolvers def select_result(results) results end + + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(try(:required_permissions)) + end + + def self.authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end end end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index e66f7b97b40..0b699006626 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -3,13 +3,12 @@ module Resolvers class BoardListsResolver < BaseResolver include BoardIssueFilterable - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead type Types::BoardListType, null: true - extras [:lookahead] - authorize :read_issue_board_list + authorizes_object! argument :id, Types::GlobalIDType[List], required: false, @@ -21,15 +20,11 @@ module Resolvers alias_method :board, :object - def resolve(lookahead: nil, id: nil, issue_filters: {}) - authorize!(board) - + def resolve_with_lookahead(id: nil, issue_filters: {}) lists = board_lists(id) context.scoped_set!(:issue_filters, issue_filters(issue_filters)) - if load_preferences?(lookahead) - List.preload_preferences_for_user(lists, current_user) - end + List.preload_preferences_for_user(lists, current_user) if load_preferences? offset_pagination(lists) end @@ -46,9 +41,8 @@ module Resolvers service.execute(board, create_default_lists: false) end - def load_preferences?(lookahead) - lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) || - lookahead&.selection(:nodes)&.selects?(:collapsed) + def load_preferences? + node_selection&.selects?(:collapsed) end def extract_list_id(gid) diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb index 637d690e4cd..85362ab1422 100644 --- a/app/graphql/resolvers/board_resolver.rb +++ b/app/graphql/resolvers/board_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class BoardResolver < BaseResolver.single - alias_method :parent, :synchronized_object + alias_method :parent, :object type Types::BoardType, null: true diff --git a/app/graphql/resolvers/concerns/manual_authorization.rb b/app/graphql/resolvers/concerns/manual_authorization.rb deleted file mode 100644 index 182110b9594..00000000000 --- a/app/graphql/resolvers/concerns/manual_authorization.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this entirely when framework authorization is released -# See: https://gitlab.com/gitlab-org/gitlab/-/issues/290216 -module ManualAuthorization - def resolve(**args) - super - rescue ::Gitlab::Graphql::Errors::ResourceNotAvailable - nil - end -end diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb index 2bad974daf7..34a4c67bc56 100644 --- a/app/graphql/resolvers/group_merge_requests_resolver.rb +++ b/app/graphql/resolvers/group_merge_requests_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class GroupMergeRequestsResolver < MergeRequestsResolver include GroupIssuableResolver - alias_method :group, :synchronized_object + alias_method :group, :object type Types::MergeRequestType.connection_type, null: true diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index 8fd33c6626e..1f7a4b48aae 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class MergeRequestResolver < BaseResolver.single include ResolvesMergeRequests - alias_method :project, :synchronized_object + alias_method :project, :object type ::Types::MergeRequestType, null: true diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index ecbdaaa3f55..5994dc449f6 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type ::Types::MergeRequestType.connection_type, null: true - alias_method :project, :synchronized_object + alias_method :project, :object def self.accept_assignee argument :assignee_username, GraphQL::STRING_TYPE, diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 944b61f0c3a..c94e3d9e1d8 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -56,7 +56,7 @@ module Resolvers end def parent - synchronized_object + object end def parent_id_parameters(args) diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index f618bf2df77..ec31a7dbe6d 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -3,11 +3,11 @@ module Resolvers module Projects class ServicesResolver < BaseResolver - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource type Types::Projects::ServiceType.connection_type, null: true authorize :admin_project + authorizes_object! argument :active, GraphQL::BOOLEAN_TYPE, @@ -20,15 +20,7 @@ module Resolvers alias_method :project, :object - def resolve(**args) - authorize!(project) - - services(args[:active], args[:type]) - end - - private - - def services(active, type) + def resolve(active: nil, type: nil) servs = project.services servs = servs.by_active_flag(active) unless active.nil? servs = servs.by_type(type) unless type.blank? diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb index 569b82149d3..4328d38d485 100644 --- a/app/graphql/resolvers/snippets/blobs_resolver.rb +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -3,12 +3,12 @@ module Resolvers module Snippets class BlobsResolver < BaseResolver - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource type Types::Snippets::BlobType.connection_type, null: true authorize :read_snippet calls_gitaly! + authorizes_object! alias_method :snippet, :object @@ -17,7 +17,6 @@ module Resolvers description: 'Paths of the blobs.' def resolve(paths: []) - authorize!(snippet) return [snippet.blob] if snippet.empty_repo? if paths.empty? diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb index 47967fe69f9..0b39d3945f6 100644 --- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb +++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb @@ -13,7 +13,7 @@ module Resolvers description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.' attr_reader :project - alias_method :user, :synchronized_object + alias_method :user, :object def ready?(project_id: nil, project_path: nil, **args) return early_return unless can_read_profile? diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index 4d470aceca4..527269846ff 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -36,6 +36,18 @@ module Types def enum @enum_values ||= {}.with_indifferent_access end + + def authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end + + def authorize(*abilities) + @abilities = abilities + end + + def authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end end end end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 78ab6890923..99f32894925 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -2,7 +2,6 @@ module Types class BaseField < GraphQL::Schema::Field - prepend Gitlab::Graphql::Authorize include GitlabStyleDeprecations argument_class ::Types::BaseArgument @@ -13,6 +12,7 @@ module Types @calls_gitaly = !!kwargs.delete(:calls_gitaly) @constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0 @requires_argument = !!kwargs.delete(:requires_argument) + @authorize = Array.wrap(kwargs.delete(:authorize)) kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity]) @feature_flag = kwargs[:feature_flag] kwargs = check_feature_flag(kwargs) @@ -22,8 +22,8 @@ module Types # We want to avoid the overhead of this in prod extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env? - extension ::Gitlab::Graphql::Present::FieldExtension + extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension end def may_call_gitaly? @@ -34,6 +34,19 @@ module Types @requires_argument || arguments.values.any? { |argument| argument.type.non_null? } end + # By default fields authorize against the current object, but that is not how our + # resolvers work - they use declarative permissions to authorize fields + # manually (so we make them opt in). + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/300922 + # (separate out authorize into permissions on the object, and on the + # resolved values) + # We do not support argument authorization in our schema. If/when we do, + # we should call `super` here, to apply argument authorization checks. + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/324647 + def authorized?(object, args, ctx) + field_authorized?(object, ctx) && resolver_authorized?(object, ctx) + end + def base_complexity complexity = DEFAULT_COMPLEXITY complexity += 1 if calls_gitaly? @@ -58,6 +71,26 @@ module Types attr_reader :feature_flag + def field_authorized?(object, ctx) + authorization.ok?(object, ctx[:current_user]) + end + + # Historically our resolvers have used declarative permission checks only + # for _what they resolved_, not the _object they resolved these things from_ + # We preserve these semantics here, and only apply resolver authorization + # if the resolver has opted in. + def resolver_authorized?(object, ctx) + if @resolver_class && @resolver_class.try(:authorizes_object?) + @resolver_class.authorized?(object, ctx) + else + true + end + end + + def authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(@authorize) + end + def feature_documentation_message(key, description) "#{description} Available only when feature flag `#{key}` is enabled." end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb index 4b1f3193136..c21c95876be 100644 --- a/app/graphql/types/base_interface.rb +++ b/app/graphql/types/base_interface.rb @@ -5,5 +5,11 @@ module Types include GraphQL::Schema::Interface field_class ::Types::BaseField + + definition_methods do + def authorized?(object, context) + resolve_type(object, context).authorized?(object, context) + end + end end end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 9c36c83d4a3..cd677e50d28 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -19,6 +19,14 @@ module Types GitlabSchema.id_from_object(object) end + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end + + def self.authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end + def current_user context[:current_user] end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb index 30a5668c0bb..aeafbf85020 100644 --- a/app/graphql/types/base_union.rb +++ b/app/graphql/types/base_union.rb @@ -2,5 +2,8 @@ module Types class BaseUnion < GraphQL::Schema::Union + def self.authorized?(object, context) + resolve_type(object, context).authorized?(object, context) + end end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 8920133734c..6997c8cffda 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -159,13 +159,20 @@ module PageLayoutHelper end def user_status_properties(user) - default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI } + default_properties = { + current_emoji: '', + current_message: '', + can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), + default_emoji: UserStatus::DEFAULT_EMOJI + } + return default_properties unless user&.status default_properties.merge({ current_emoji: user.status.emoji.to_s, current_message: user.status.message.to_s, - current_availability: user.status.availability.to_s + current_availability: user.status.availability.to_s, + current_clear_status_after: user.status.clear_status_at.to_s }) end diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb index 9960fb4bf12..6640b0c5e94 100644 --- a/app/presenters/packages/detail/package_presenter.rb +++ b/app/presenters/packages/detail/package_presenter.rb @@ -64,7 +64,6 @@ module Packages id: pipeline_info.id, sha: pipeline_info.sha, ref: pipeline_info.ref, - git_commit_message: pipeline_info.git_commit_message, user: build_user_info(pipeline_info.user), project: { name: pipeline_info.project.name, |