From 6659634b2bdc3ba362574541985c6852ad1574a4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 5 Dec 2023 15:10:08 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../shared/components/projects_dropdown_filter.vue | 2 +- .../password/components/password_input.vue | 6 - .../javascripts/authentication/password/index.js | 3 +- .../components/manage_two_factor_form.vue | 1 - app/assets/javascripts/behaviors/index.js | 3 +- app/assets/javascripts/behaviors/shortcuts.js | 36 --- .../javascripts/behaviors/shortcuts/index.js | 16 ++ .../javascripts/behaviors/shortcuts/shortcuts.js | 78 +++++- .../behaviors/shortcuts/shortcuts_blob.js | 8 +- .../behaviors/shortcuts/shortcuts_find_file.js | 10 +- .../behaviors/shortcuts/shortcuts_issuable.js | 9 +- .../behaviors/shortcuts/shortcuts_navigation.js | 9 +- .../behaviors/shortcuts/shortcuts_network.js | 10 +- .../behaviors/shortcuts/shortcuts_wiki.js | 10 +- .../filepath_form/components/template_selector.vue | 1 - app/assets/javascripts/issues/index.js | 5 +- .../components/model_version_detail.vue | 21 +- .../javascripts/ml/model_registry/translations.js | 2 + .../javascripts/pages/groups/boards/index.js | 3 +- .../pages/groups/shared/group_details.js | 3 +- .../javascripts/pages/projects/activity/index.js | 3 +- .../pages/projects/artifacts/browse/index.js | 3 +- .../pages/projects/artifacts/file/index.js | 3 +- .../javascripts/pages/projects/boards/index.js | 3 +- .../pages/projects/commit/show/index.js | 3 +- .../pages/projects/commits/show/index.js | 3 +- .../pages/projects/find_file/show/index.js | 3 +- app/assets/javascripts/pages/projects/index.js | 3 +- app/assets/javascripts/pages/projects/init_blob.js | 7 +- .../pages/projects/issues/index/index.js | 3 +- .../pages/projects/merge_requests/index/index.js | 3 +- .../projects/merge_requests/init_merge_request.js | 3 +- .../merge_requests/init_merge_request_show.js | 3 +- .../pages/projects/network/show/index.js | 4 +- .../javascripts/pages/projects/show/index.js | 3 +- .../javascripts/pages/projects/tree/show/index.js | 3 +- app/assets/javascripts/pages/shared/wikis/wikis.js | 3 +- .../branch_rules/components/branch_rule.vue | 4 +- .../repository/components/blob_controls.vue | 4 +- .../vue_shared/components/awards_list.vue | 4 +- .../projects/ml/show_ml_model_component.rb | 8 +- .../projects/ml/show_ml_model_version_component.rb | 12 +- app/finders/groups/custom_emoji_finder.rb | 26 ++ .../analytics/cycle_analytics/stages_resolver.rb | 28 +++ .../cycle_analytics/value_streams_resolver.rb | 20 ++ app/graphql/resolvers/custom_emoji_resolver.rb | 23 ++ .../resolvers/work_items/ancestors_resolver.rb | 2 + .../analytics/cycle_analytics/value_stream_type.rb | 5 + .../cycle_analytics/value_streams/stage_type.rb | 54 +++++ app/graphql/types/group_type.rb | 7 +- app/graphql/types/project_type.rb | 5 + .../degradation_type.rb | 2 +- .../analytics/cycle_analytics/value_stream.rb | 7 +- app/models/application_setting_implementation.rb | 1 + app/models/award_emoji.rb | 3 +- app/models/custom_emoji.rb | 31 ++- app/serializers/award_emoji_entity.rb | 1 - .../ml/find_or_create_model_version_service.rb | 10 +- app/validators/gitlab/emoji_name_validator.rb | 6 +- .../admin/application_settings/general.html.haml | 5 +- .../projects/ml/model_versions/show.html.haml | 2 +- app/views/projects/ml/models/show.html.haml | 2 +- .../users/deactivate_dormant_users_worker.rb | 6 +- .../updated_ai_powered_features_menu_for_sm.yml | 8 + .../development/use_sync_service_token_worker.yml | 4 +- config/gitlab_loose_foreign_keys.yml | 6 - config/initializers/custom_roles.rb | 5 + ..._remove_ignored_application_settings_columns.rb | 28 +++ db/schema_migrations/20231127174335 | 1 + db/structure.sql | 6 - .../package_information/postgresql_versions.md | 3 +- doc/api/graphql/reference/index.md | 63 ++++- doc/development/documentation/styleguide/index.md | 14 ++ doc/update/versions/gitlab_16_changes.md | 12 +- lib/banzai/filter/custom_emoji_filter.rb | 12 +- locale/gitlab.pot | 21 ++ package.json | 4 +- qa/qa/page/file/form.rb | 4 +- .../projects/ml/show_ml_model_component_spec.rb | 26 +- .../ml/show_ml_model_version_component_spec.rb | 31 ++- spec/db/schema_spec.rb | 1 + spec/factories/ml/model_versions.rb | 4 + spec/finders/groups/custom_emoji_finder_spec.rb | 65 +++++ .../password/components/password_input_spec.js | 1 - .../behaviors/shortcuts/shortcuts_issuable_spec.js | 12 +- .../frontend/behaviors/shortcuts/shortcuts_spec.js | 267 +++++++++++++++++++++ .../components/model_version_detail_spec.js | 52 ++-- spec/frontend/ml/model_registry/mock_data.js | 10 +- .../repository/components/blob_controls_spec.js | 3 + spec/frontend/shortcuts_spec.js | 154 ------------ .../cycle_analytics/value_stream_type_spec.rb | 2 +- .../value_streams/stage_type_spec.rb | 15 ++ spec/graphql/types/group_type_spec.rb | 33 +++ spec/graphql/types/project_type_spec.rb | 4 +- spec/lib/banzai/filter/custom_emoji_filter_spec.rb | 8 + spec/models/award_emoji_spec.rb | 11 + spec/models/custom_emoji_spec.rb | 41 ++++ spec/models/ml/candidate_spec.rb | 4 +- .../api/graphql/custom_emoji_query_spec.rb | 4 +- .../api/graphql/project/value_streams_spec.rb | 105 ++++++++ spec/serializers/discussion_entity_spec.rb | 7 - spec/services/ml/create_candidate_service_spec.rb | 2 +- .../application_settings/general.html.haml_spec.rb | 3 +- .../users/deactivate_dormant_users_worker_spec.rb | 91 ++++--- yarn.lock | 47 ++-- 105 files changed, 1312 insertions(+), 452 deletions(-) delete mode 100644 app/assets/javascripts/behaviors/shortcuts.js create mode 100644 app/assets/javascripts/behaviors/shortcuts/index.js create mode 100644 app/finders/groups/custom_emoji_finder.rb create mode 100644 app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb create mode 100644 app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb create mode 100644 app/graphql/resolvers/custom_emoji_resolver.rb create mode 100644 app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb create mode 100644 config/feature_flags/development/updated_ai_powered_features_menu_for_sm.yml create mode 100644 config/initializers/custom_roles.rb create mode 100644 db/post_migrate/20231127174335_remove_ignored_application_settings_columns.rb create mode 100644 db/schema_migrations/20231127174335 create mode 100644 spec/finders/groups/custom_emoji_finder_spec.rb create mode 100644 spec/frontend/behaviors/shortcuts/shortcuts_spec.js delete mode 100644 spec/frontend/shortcuts_spec.js create mode 100644 spec/graphql/types/analytics/cycle_analytics/value_streams/stage_type_spec.rb create mode 100644 spec/requests/api/graphql/project/value_streams_spec.rb diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue index 662451c5eb4..62924dcd0a8 100644 --- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue +++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue @@ -281,7 +281,7 @@ export default { :shape="$options.AVATAR_SHAPE_OPTION_RECT" />
-
{{ item.name }}
+
{{ item.name }}
{{ item.fullPath }}
diff --git a/app/assets/javascripts/authentication/password/components/password_input.vue b/app/assets/javascripts/authentication/password/components/password_input.vue index 6e3af96cf33..7f2a2beaa47 100644 --- a/app/assets/javascripts/authentication/password/components/password_input.vue +++ b/app/assets/javascripts/authentication/password/components/password_input.vue @@ -27,11 +27,6 @@ export default { required: false, default: null, }, - qaSelector: { - type: String, - required: false, - default: null, - }, testid: { type: String, required: false, @@ -80,7 +75,6 @@ export default { :autocomplete="autocomplete" :name="name" :minlength="minimumPasswordLength" - :data-qa-selector="qaSelector" :data-testid="testid" :title="title" :type="type" diff --git a/app/assets/javascripts/authentication/password/index.js b/app/assets/javascripts/authentication/password/index.js index a4f2d038cf7..903512a7b53 100644 --- a/app/assets/javascripts/authentication/password/index.js +++ b/app/assets/javascripts/authentication/password/index.js @@ -9,7 +9,7 @@ export const initPasswordInput = () => { } const { form } = el; - const { title, id, minimumPasswordLength, qaSelector, testid, autocomplete, name } = el.dataset; + const { title, id, minimumPasswordLength, testid, autocomplete, name } = el.dataset; // eslint-disable-next-line no-new new Vue({ @@ -21,7 +21,6 @@ export const initPasswordInput = () => { title, id, minimumPasswordLength, - qaSelector, testid, autocomplete, name, diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue index 907b68e6ffc..e97846bae29 100644 --- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue +++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue @@ -119,7 +119,6 @@ export default { type="password" name="current_password" :state="currentPasswordState" - data-qa-selector="current_password_field" /> diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 84ff8fa7f33..fe3868fdd04 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -6,9 +6,9 @@ import installGlEmojiElement from './gl_emoji'; import initCopyAsGFM from './markdown/copy_as_gfm'; import './quick_submit'; import './requires_input'; -import initPageShortcuts from './shortcuts'; import { initToastMessages } from './toasts'; import { initGlobalAlerts } from './global_alerts'; +import './shortcuts'; import './toggler_behavior'; import './preview_markdown'; @@ -17,7 +17,6 @@ installGlEmojiElement(); initCopyAsGFM(); initCopyToClipboard(); -initPageShortcuts(); initCollapseSidebarOnWindowResize(); initToastMessages(); diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js deleted file mode 100644 index 22a8be92e52..00000000000 --- a/app/assets/javascripts/behaviors/shortcuts.js +++ /dev/null @@ -1,36 +0,0 @@ -export default function initPageShortcuts() { - const { page } = document.body.dataset; - const pagesWithCustomShortcuts = [ - 'projects:activity', - 'projects:artifacts:browse', - 'projects:artifacts:file', - 'projects:blame:show', - 'projects:blob:show', - 'projects:commit:show', - 'projects:commits:show', - 'projects:find_file:show', - 'projects:issues:edit', - 'projects:issues:index', - 'projects:issues:new', - 'projects:issues:show', - 'projects:merge_requests:creations:diffs', - 'projects:merge_requests:creations:new', - 'projects:merge_requests:edit', - 'projects:merge_requests:index', - 'projects:merge_requests:show', - 'projects:network:show', - 'projects:show', - 'projects:tree:show', - 'groups:show', - ]; - - // the pages above have their own shortcuts sub-classes instantiated elsewhere - // TODO: replace this whitelist with something more automated/maintainable - // https://gitlab.com/gitlab-org/gitlab/-/issues/392845 - if (page && !pagesWithCustomShortcuts.includes(page)) { - import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts') - .then(({ default: Shortcuts }) => new Shortcuts()) - .catch(() => {}); - } - return false; -} diff --git a/app/assets/javascripts/behaviors/shortcuts/index.js b/app/assets/javascripts/behaviors/shortcuts/index.js new file mode 100644 index 00000000000..cc6d8a23f68 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/index.js @@ -0,0 +1,16 @@ +const shortcutsPromise = import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts') + .then(({ default: Shortcuts }) => new Shortcuts()) + .catch(() => {}); + +export const addShortcutsExtension = (ShortcutExtension, ...args) => + shortcutsPromise.then((shortcuts) => shortcuts.addExtension(ShortcutExtension, args)); + +export const resetShortcutsForTests = async () => { + if (process.env.NODE_ENV === 'test') { + const { Mousetrap, clearStopCallbacksForTests } = await import('~/lib/mousetrap'); + clearStopCallbacksForTests(); + Mousetrap.reset(); + const shortcuts = await shortcutsPromise; + shortcuts.extensions.clear(); + } +}; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 1d6819d4b04..e05694c0907 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -63,11 +63,17 @@ function getToolbarBtnToShortcutsMap($textarea) { export default class Shortcuts { constructor() { + if (process.env.NODE_ENV !== 'production' && this.constructor !== Shortcuts) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Shortcuts cannot be subclassed.'); + } + + this.extensions = new Map(); this.onToggleHelp = this.onToggleHelp.bind(this); this.helpModalElement = null; this.helpModalVueInstance = null; - this.bindCommands([ + this.addAll([ [TOGGLE_KEYBOARD_SHORTCUTS_DIALOG, this.onToggleHelp], [START_SEARCH, Shortcuts.focusSearch], [FOCUS_FILTER_BAR, this.focusFilter.bind(this)], @@ -94,22 +100,74 @@ export default class Shortcuts { const findFileURL = document.body.dataset.findFile; if (typeof findFileURL !== 'undefined' && findFileURL !== null) { - this.bindCommand(GO_TO_PROJECT_FIND_FILE, () => { + this.add(GO_TO_PROJECT_FIND_FILE, () => { visitUrl(findFileURL); }); } - const shortcutsModalTriggerEvent = 'click.shortcutsModalTrigger'; - // eslint-disable-next-line @gitlab/no-global-event-off - $(document) - .off(shortcutsModalTriggerEvent) - .on(shortcutsModalTriggerEvent, '.js-shortcuts-modal-trigger', this.onToggleHelp); + $(document).on('click', '.js-shortcuts-modal-trigger', this.onToggleHelp); if (shouldDisableShortcuts()) { disableShortcuts(); } } + /** + * Instantiate a legacy shortcut extension class. + * + * NOTE: The preferred approach for adding shortcuts is described in + * https://docs.gitlab.com/ee/development/fe_guide/keyboard_shortcuts.html. + * This method is only for existing legacy shortcut classes. + * + * A shortcut extension class packages up several shortcuts and behaviors for + * a page or set of pages. They are considered legacy because they usually do + * not follow modern best practices. For instance, they may hook into the UI + * in brittle ways, e.g.. querySelectors. + * + * Extension classes can declare dependencies on other shortcut extension + * classes by listing them in a static `dependencies` property. This is + * essentially a reimplementation of the previous subclassing approach, but + * with idempotency: a shortcut extension class can now only be added at most + * one time. + * + * Extension classes are instantiated and given the Shortcuts singleton + * instance as their first argument. If the class constructor needs + * additional arguments, pass them via the second argument as an array. + * + * See https://gitlab.com/gitlab-org/gitlab/-/issues/392845 for more context. + * + * @param {Function} Extension The extension class to add/instantiate. + * @param {Array} [args] A list of additional args to pass to the extension + * class constructor. + * @param {Set} [extensionsCurrentlyLoading] For internal use only. Do not + * use. + * @returns The instantiated shortcut extension class. + */ + addExtension(Extension, args = [], extensionsCurrentlyLoading = new Set()) { + extensionsCurrentlyLoading.add(Extension); + + let instance = this.extensions.get(Extension); + if (!instance) { + for (const Dep of Extension.dependencies ?? []) { + if (extensionsCurrentlyLoading.has(Dep) || Dep === Shortcuts) { + // We've encountered a circular dependency, so stop recursing. + // eslint-disable-next-line no-continue + continue; + } + + extensionsCurrentlyLoading.add(Dep); + + this.addExtension(Dep, [], extensionsCurrentlyLoading); + } + + instance = new Extension(this, ...args); + this.extensions.set(Extension, instance); + } + + extensionsCurrentlyLoading.delete(Extension); + return instance; + } + /** * Bind the keyboard shortcut(s) defined by the given command to the given * callback. @@ -120,7 +178,7 @@ export default class Shortcuts { * @returns {void} */ // eslint-disable-next-line class-methods-use-this - bindCommand(command, callback) { + add(command, callback) { Mousetrap.bind(keysFor(command), callback); } @@ -132,8 +190,8 @@ export default class Shortcuts { * command/callback pairs. * @returns {void} */ - bindCommands(commandsAndCallbacks) { - commandsAndCallbacks.forEach((commandAndCallback) => this.bindCommand(...commandAndCallback)); + addAll(commandsAndCallbacks) { + commandsAndCallbacks.forEach((commandAndCallback) => this.add(...commandAndCallback)); } onToggleHelp(e) { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index 65ae67d156f..a0bfd337d10 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -7,7 +7,6 @@ import { getShaFromUrl, } from '~/lib/utils/url_utility'; import { updateRefPortionOfTitle } from '~/repository/utils/title'; -import Shortcuts from './shortcuts'; const defaults = { fileBlobPermalinkUrl: null, @@ -19,15 +18,14 @@ function eventHasModifierKeys(event) { return event.ctrlKey || event.metaKey || event.shiftKey; } -export default class ShortcutsBlob extends Shortcuts { - constructor(opts) { +export default class ShortcutsBlob { + constructor(shortcuts, opts) { const options = { ...defaults, ...opts }; - super(); this.options = options; this.shortcircuitPermalinkButton(); - this.bindCommand(PROJECT_FILES_GO_TO_PERMALINK, this.moveToFilePermalink.bind(this)); + shortcuts.add(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 f26878cf161..393d0165a07 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js @@ -8,10 +8,8 @@ import { import { addStopCallback } from '~/lib/mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsFindFile extends ShortcutsNavigation { - constructor(projectFindFile) { - super(); - +export default class ShortcutsFindFile { + constructor(shortcuts, projectFindFile) { addStopCallback((e, element, combo) => { if ( element === projectFindFile.inputElement[0] && @@ -28,11 +26,13 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { return undefined; }); - this.bindCommands([ + shortcuts.addAll([ [PROJECT_FILES_MOVE_SELECTION_UP, projectFindFile.selectRowUp], [PROJECT_FILES_MOVE_SELECTION_DOWN, projectFindFile.selectRowDown], [PROJECT_FILES_GO_BACK, projectFindFile.goToTree], [PROJECT_FILES_OPEN_SELECTION, projectFindFile.goToBlob], ]); } + + static dependencies = [ShortcutsNavigation]; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index b0e515ac19d..cde6d59b210 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -16,12 +16,9 @@ import { MR_COPY_SOURCE_BRANCH_NAME, ISSUABLE_COPY_REF, } from './keybindings'; -import Shortcuts from './shortcuts'; - -export default class ShortcutsIssuable extends Shortcuts { - constructor() { - super(); +export default class ShortcutsIssuable { + constructor(shortcuts) { this.branchInMemoryButton = document.createElement('button'); this.branchClipboardInstance = new ClipboardJS(this.branchInMemoryButton); this.branchClipboardInstance.on('success', () => { @@ -40,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts { toast(s__('GlobalShortcuts|Unable to copy the reference at this time.')); }); - this.bindCommands([ + shortcuts.addAll([ [ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsIssuable.openSidebarDropdown('assignee')], [ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsIssuable.openSidebarDropdown('milestone')], [ISSUABLE_CHANGE_LABEL, () => ShortcutsIssuable.openSidebarDropdown('labels')], diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index 4691a4228e6..bae50c02599 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -21,13 +21,10 @@ import { PROJECT_FILES_GO_TO_COMPARE, NEW_ISSUE, } from './keybindings'; -import Shortcuts from './shortcuts'; -export default class ShortcutsNavigation extends Shortcuts { - constructor() { - super(); - - this.bindCommands([ +export default class ShortcutsNavigation { + constructor(shortcuts) { + shortcuts.addAll([ [GO_TO_PROJECT_OVERVIEW, () => findAndFollowLink('.shortcuts-project')], [GO_TO_PROJECT_ACTIVITY_FEED, () => findAndFollowLink('.shortcuts-project-activity')], [GO_TO_PROJECT_RELEASES, () => findAndFollowLink('.shortcuts-deployments-releases')], diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js index 02c6af53fc2..eee8c1acf1a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js @@ -8,11 +8,9 @@ import { } from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsNetwork extends ShortcutsNavigation { - constructor(graph) { - super(); - - this.bindCommands([ +export default class ShortcutsNetwork { + constructor(shortcuts, graph) { + shortcuts.addAll([ [REPO_GRAPH_SCROLL_LEFT, graph.scrollLeft], [REPO_GRAPH_SCROLL_RIGHT, graph.scrollRight], [REPO_GRAPH_SCROLL_UP, graph.scrollUp], @@ -21,4 +19,6 @@ export default class ShortcutsNetwork extends ShortcutsNavigation { [REPO_GRAPH_SCROLL_BOTTOM, graph.scrollBottom], ]); } + + static dependencies = [ShortcutsNavigation]; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js index 62d612cfa6d..5f45331bf76 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js @@ -2,13 +2,13 @@ import findAndFollowLink from '~/lib/utils/navigation_utility'; import { EDIT_WIKI_PAGE } from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsWiki extends ShortcutsNavigation { - constructor() { - super(); - - this.bindCommand(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki); +export default class ShortcutsWiki { + constructor(shortcuts) { + shortcuts.add(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki); } + static dependencies = [ShortcutsNavigation]; + static editWiki() { findAndFollowLink('.js-wiki-edit'); } diff --git a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue index 379d5e38197..e9f54639fdd 100644 --- a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue +++ b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue @@ -149,7 +149,6 @@ export default { block class="gl-font-regular" data-testid="template-selector" - data-qa-selector="template_selector" :toggle-text="dropdownToggleText" :search-placeholder="$options.i18n.searchPlaceholder" :items="dropdownItems" diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js index eea5207801c..b7b39d0ce08 100644 --- a/app/assets/javascripts/issues/index.js +++ b/app/assets/javascripts/issues/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import IssuableForm from 'ee_else_ce/issuable/issuable_form'; import IssuableLabelSelector from '~/issuable/issuable_label_selector'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import { initIssuableSidebar } from '~/issuable'; @@ -22,7 +23,7 @@ export function initForm() { new IssuableForm($('.issue-form')); // eslint-disable-line no-new IssuableLabelSelector(); new LabelsSelect(); // eslint-disable-line no-new - new ShortcutsNavigation(); // eslint-disable-line no-new + addShortcutsExtension(ShortcutsNavigation); initTitleSuggestions(); initTypePopover(); @@ -32,7 +33,7 @@ export function initForm() { export function initShow() { new Issue(); // eslint-disable-line no-new - new ShortcutsIssuable(); // eslint-disable-line no-new + addShortcutsExtension(ShortcutsIssuable); new ZenMode(); // eslint-disable-line no-new initAwardsApp(document.getElementById('js-vue-awards-block')); diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue index 19d91df43b2..8d3e8cf2023 100644 --- a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue +++ b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue @@ -1,12 +1,15 @@ diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js index bbafde0b943..f4ff0f4aa97 100644 --- a/app/assets/javascripts/ml/model_registry/translations.js +++ b/app/assets/javascripts/ml/model_registry/translations.js @@ -15,6 +15,8 @@ export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this export const modelsCountLabel = (modelCount) => n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount); +export const DESCRIPTION_LABEL = __('Description'); +export const NO_DESCRIPTION_PROVIDED_LABEL = s__('MlModelRegistry|No description provided'); export const INFO_LABEL = s__('MlModelRegistry|Info'); export const ID_LABEL = s__('MlModelRegistry|ID'); export const MLFLOW_ID_LABEL = s__('MlModelRegistry|MLflow run ID'); diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js index 23f5b083589..a591fed3d9b 100644 --- a/app/assets/javascripts/pages/groups/boards/index.js +++ b/app/assets/javascripts/pages/groups/boards/index.js @@ -1,5 +1,6 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initBoards from '~/boards'; -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); initBoards(); diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index 5d9eafe5672..46040cd6706 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -1,10 +1,11 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initInviteMembersBanner from '~/groups/init_invite_members_banner'; import initNotificationsDropdown from '~/notifications'; import ProjectsList from '~/projects_list'; export default function initGroupDetails() { - new ShortcutsNavigation(); // eslint-disable-line no-new + addShortcutsExtension(ShortcutsNavigation); initNotificationsDropdown(); diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js index 03fbad0f1ec..3138026e1db 100644 --- a/app/assets/javascripts/pages/projects/activity/index.js +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -1,5 +1,6 @@ import Activities from '~/activities'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; new Activities(); // eslint-disable-line no-new -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js index 60680ec7d1d..47cf348eb4d 100644 --- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -1,5 +1,6 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import BuildArtifacts from '~/build_artifacts'; -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); new BuildArtifacts(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js index 07ee4d686cc..3bc3b9dabbc 100644 --- a/app/assets/javascripts/pages/projects/artifacts/file/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -1,5 +1,6 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import { BlobViewer } from '~/blob/viewer/index'; -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); new BlobViewer(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 23f5b083589..a591fed3d9b 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,5 +1,6 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initBoards from '~/boards'; -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); initBoards(); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index c9f5895c7a3..d875f28433e 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Vue from 'vue'; import loadAwardsHandler from '~/awards_handler'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import Diff from '~/diff'; import { createAlert } from '~/alert'; @@ -20,7 +21,7 @@ import { initReportAbuse } from '~/projects/report_abuse'; initDiffStatsDropdown(); new ZenMode(); -new ShortcutsNavigation(); +addShortcutsExtension(ShortcutsNavigation); initCommitBoxInfo(); diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js index f5ecf9be591..e3b22bbfee0 100644 --- a/app/assets/javascripts/pages/projects/commits/show/index.js +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -1,10 +1,11 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import CommitsList from '~/commits'; import GpgBadges from '~/gpg_badges'; import { mountCommits, initCommitsRefSwitcher } from '~/projects/commits'; new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); GpgBadges.fetch(); mountCommits(document.getElementById('js-author-dropdown')); initCommitsRefSwitcher(); diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js index 22c21430e8b..4df84ac167c 100644 --- a/app/assets/javascripts/pages/projects/find_file/show/index.js +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file'; import ProjectFindFile from '~/projects/project_find_file'; import InitBlobRefSwitcher from '../ref_switcher'; @@ -11,4 +12,4 @@ const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { refType: findElement.dataset.refType, }); projectFindFile.load(findElement.dataset.fileFindUrl); -new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsFindFile, projectFindFile); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 1075241e172..dc00036864f 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,5 +1,6 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import Project from './project'; new Project(); // eslint-disable-line no-new -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index 244d1d5590e..6e3e1a35bd2 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -1,3 +1,4 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; @@ -18,10 +19,8 @@ export default () => { const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsNavigation(); // eslint-disable-line no-new - - // eslint-disable-next-line no-new - new ShortcutsBlob({ + addShortcutsExtension(ShortcutsNavigation); + addShortcutsExtension(ShortcutsBlob, { fileBlobPermalinkUrl, fileBlobPermalinkUrlElement, }); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index b320d8a61c2..322eaa845ec 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -1,6 +1,7 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list'; mountIssuesListApp(); mountJiraIssuesListApp(); -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index 3ae8018714a..a37c18e41ab 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,4 +1,5 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/filtered_search/constants'; @@ -16,7 +17,7 @@ initFilteredSearch({ useDefaultState: true, }); -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); initIssuableByEmail(); initCsvImportExportButtons(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 599fd225de9..0e66c3521dd 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -3,12 +3,13 @@ import $ from 'jquery'; import IssuableForm from 'ee_else_ce/issuable/issuable_form'; import IssuableLabelSelector from '~/issuable/issuable_label_selector'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import LabelsSelect from '~/labels/labels_select'; import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar'; export default () => { - new ShortcutsNavigation(); + addShortcutsExtension(ShortcutsNavigation); new IssuableForm($('.merge-request-form')); IssuableLabelSelector(); new LabelsSelect(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index af1635221ab..1cac330520f 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { s__ } from '~/locale'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import { initPipelineCountListener } from '~/commit/pipelines/utils'; import { initIssuableSidebar } from '~/issuable'; @@ -18,7 +19,7 @@ import getStateQuery from './queries/get_state.query.graphql'; export default function initMergeRequestShow(store) { new ZenMode(); // eslint-disable-line no-new initPipelineCountListener(document.querySelector('#commit-pipeline-table-view')); - new ShortcutsIssuable(true); // eslint-disable-line no-new + addShortcutsExtension(ShortcutsIssuable); initSourcegraph(); initIssuableSidebar(); initAwardsApp(document.getElementById('js-vue-awards-block')); diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js index a669ea5baaf..58b703bdfda 100644 --- a/app/assets/javascripts/pages/projects/network/show/index.js +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import Vue from 'vue'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network'; import RefSelector from '~/ref/components/ref_selector.vue'; import Network from '../network'; @@ -44,6 +45,5 @@ initRefSwitcher(); commit_id: $('.network-graph').attr('data-commit-id'), }); - // eslint-disable-next-line no-new - new ShortcutsNetwork(networkGraph.branch_graph); + addShortcutsExtension(ShortcutsNetwork, networkGraph.branch_graph); })(); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 98c58515d24..d0dd27f9fbf 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,3 +1,4 @@ +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert'; import leaveByUrl from '~/namespaces/leave_by_url'; @@ -38,7 +39,7 @@ leaveByUrl('project'); initVueNotificationsDropdown(); -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); initUploadFileTrigger(); initClustersDeprecationAlert(); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index d87f8898c63..edecb798686 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import initTree from 'ee_else_ce/repository'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import NewCommitForm from '~/new_commit_form'; import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal'; @@ -7,4 +8,4 @@ import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal'; new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new initTree(); initAmbiguousRefModal(); -new ShortcutsNavigation(); // eslint-disable-line no-new +addShortcutsExtension(ShortcutsNavigation); diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js index b32cc700e16..c98cda92a94 100644 --- a/app/assets/javascripts/pages/shared/wikis/wikis.js +++ b/app/assets/javascripts/pages/shared/wikis/wikis.js @@ -1,5 +1,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import Tracking from '~/tracking'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki'; const TRACKING_EVENT_NAME = 'view_wiki_page'; @@ -72,6 +73,6 @@ export default class Wikis { } static initShortcuts() { - new ShortcutsWiki(); // eslint-disable-line no-new + addShortcutsExtension(ShortcutsWiki); } } diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue index 0a5fa288828..9aca74c9863 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue @@ -12,8 +12,8 @@ export const i18n = { statusChecks: s__('BranchRules|%{total} status %{subject}'), approvalRules: s__('BranchRules|%{total} approval %{subject}'), matchingBranches: s__('BranchRules|%{total} matching %{subject}'), - pushAccessLevels: s__('BranchRules|Allowed to merge'), - mergeAccessLevels: s__('BranchRules|Allowed to push and merge'), + pushAccessLevels: s__('BranchRules|Allowed to push and merge'), + mergeAccessLevels: s__('BranchRules|Allowed to merge'), }; export default { diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue index 7a64792d476..c64100f4f36 100644 --- a/app/assets/javascripts/repository/components/blob_controls.vue +++ b/app/assets/javascripts/repository/components/blob_controls.vue @@ -4,6 +4,7 @@ import { __ } from '~/locale'; import { createAlert } from '~/alert'; import getRefMixin from '~/repository/mixins/get_ref'; import initSourcegraph from '~/sourcegraph'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import { updateElementsVisibility } from '../utils/dom'; @@ -105,8 +106,7 @@ export default { ); const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - // eslint-disable-next-line no-new - new ShortcutsBlob({ + addShortcutsExtension(ShortcutsBlob, { fileBlobPermalinkUrl, fileBlobPermalinkUrlElement, }); diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 59f03b41144..3c19df9c196 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -94,14 +94,12 @@ export default { return awardList.some((award) => award.user.id === this.currentUserId); }, createAwardList(name, list) { - const url = list.length ? list[0].url : null; - return { name, list, title: this.getAwardListTitle(list, name), classes: this.getAwardClassBindings(list), - html: glEmojiTag(name, { url }), + html: glEmojiTag(name), }; }, getAwardListTitle(awardsList, name) { diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb index 38a81a5837d..03300f01f64 100644 --- a/app/components/projects/ml/show_ml_model_component.rb +++ b/app/components/projects/ml/show_ml_model_component.rb @@ -3,10 +3,11 @@ module Projects module Ml class ShowMlModelComponent < ViewComponent::Base - attr_reader :model + attr_reader :model, :current_user - def initialize(model:) + def initialize(model:, current_user:) @model = model.present + @current_user = current_user end private @@ -35,7 +36,8 @@ module Projects version: model_version.version, description: model_version.description, project_path: project_path(model_version.project), - package_id: model_version.package_id + package_id: model_version.package_id, + **::Ml::CandidateDetailsPresenter.new(model_version.candidate, current_user).present } end end diff --git a/app/components/projects/ml/show_ml_model_version_component.rb b/app/components/projects/ml/show_ml_model_version_component.rb index 0e39a1cbcc6..a4c641f6d66 100644 --- a/app/components/projects/ml/show_ml_model_version_component.rb +++ b/app/components/projects/ml/show_ml_model_version_component.rb @@ -3,11 +3,12 @@ module Projects module Ml class ShowMlModelVersionComponent < ViewComponent::Base - attr_reader :model_version, :model + attr_reader :model_version, :model, :current_user - def initialize(model_version:) + def initialize(model_version:, current_user:) @model_version = model_version.present @model = model_version.model.present + @current_user = current_user end private @@ -24,12 +25,17 @@ module Projects model: { name: model.name, path: model.path - } + }, + **candidate_data } } Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) }) end + + def candidate_data + ::Ml::CandidateDetailsPresenter.new(model_version.candidate, current_user).present + end end end end diff --git a/app/finders/groups/custom_emoji_finder.rb b/app/finders/groups/custom_emoji_finder.rb new file mode 100644 index 00000000000..80a4e948f8b --- /dev/null +++ b/app/finders/groups/custom_emoji_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Groups + class CustomEmojiFinder < Base + include FinderWithGroupHierarchy + include Gitlab::Utils::StrongMemoize + + def initialize(group, params = {}) + @group = group + @params = params + @skip_authorization = true + end + + def execute + return CustomEmoji.none if Feature.disabled?(:custom_emoji, group) + + return CustomEmoji.for_resource(group) unless params[:include_ancestor_groups] + + CustomEmoji.for_namespaces(group_ids_for(group)) + end + + private + + attr_reader :group, :params, :skip_authorization + end +end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb new file mode 100644 index 00000000000..d14aae7002e --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class StagesResolver < BaseResolver + type [Types::Analytics::CycleAnalytics::ValueStreams::StageType], null: true + + def resolve + response = + ::Analytics::CycleAnalytics::Stages::ListService.new( + parent: namespace, + current_user: current_user, + params: { value_stream: object } + ).execute + + response[:stages] + end + + def namespace + object.project.project_namespace + end + end + end + end +end + +Resolvers::Analytics::CycleAnalytics::StagesResolver.prepend_mod diff --git a/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb new file mode 100644 index 00000000000..f3e3da86169 --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class ValueStreamsResolver < BaseResolver + type Types::Analytics::CycleAnalytics::ValueStreamType.connection_type, null: true + + def resolve + # FOSS only have default value stream available + [ + ::Analytics::CycleAnalytics::ValueStream.build_default_value_stream(object.project_namespace) + ] + end + end + end + end +end + +Resolvers::Analytics::CycleAnalytics::ValueStreamsResolver.prepend_mod diff --git a/app/graphql/resolvers/custom_emoji_resolver.rb b/app/graphql/resolvers/custom_emoji_resolver.rb new file mode 100644 index 00000000000..1e39fafe486 --- /dev/null +++ b/app/graphql/resolvers/custom_emoji_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + class CustomEmojiResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorizes_object! + + authorize :read_custom_emoji + + argument :include_ancestor_groups, + GraphQL::Types::Boolean, + required: false, + default_value: false, + description: 'Includes custom emoji from parent groups.' + + type Types::CustomEmojiType, null: true + + def resolve(**args) + Groups::CustomEmojiFinder.new(object, args).execute + end + end +end diff --git a/app/graphql/resolvers/work_items/ancestors_resolver.rb b/app/graphql/resolvers/work_items/ancestors_resolver.rb index 21e7b097738..efd01700a31 100644 --- a/app/graphql/resolvers/work_items/ancestors_resolver.rb +++ b/app/graphql/resolvers/work_items/ancestors_resolver.rb @@ -38,6 +38,8 @@ module Resolvers end def preload_resource_parents(work_items) + return unless current_user + projects = work_items.filter_map(&:project) namespaces = work_items.filter_map(&:namespace) group_namespaces = namespaces.select { |n| n.type == ::Group.sti_name } diff --git a/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb index 16ce9b82718..900d2873789 100644 --- a/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb +++ b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb @@ -26,6 +26,11 @@ module Types null: true, description: 'Project the value stream belongs to, returns empty if it belongs to a group.', alpha: { milestone: '15.6' } + + field :stages, + null: true, + resolver: Resolvers::Analytics::CycleAnalytics::StagesResolver, + description: 'Value Stream stages.' end end end diff --git a/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb b/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb new file mode 100644 index 00000000000..c8fdf8513be --- /dev/null +++ b/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Types + module Analytics + module CycleAnalytics + module ValueStreams + # rubocop: disable Graphql/AuthorizeTypes -- # Already authorized in parent value stream type. + class StageType < BaseObject + graphql_name 'ValueStreamStage' + + field :name, + GraphQL::Types::String, + null: false, + description: 'Name of the stage.' + + field :hidden, + GraphQL::Types::Boolean, + null: false, + description: 'Whether the stage is hidden.' + + field :custom, + GraphQL::Types::Boolean, + null: false, + description: 'Whether the stage is customized.' + + field :start_event_identifier, + StageEventEnum, + null: false, + description: 'Start event identifier.' + + field :end_event_identifier, + StageEventEnum, + null: false, + description: 'End event identifier.' + + def start_event_identifier + events_enum[object.start_event_identifier] + end + + def end_event_identifier + events_enum[object.end_event_identifier] + end + + def events_enum + Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum.with_indifferent_access + end + end + # rubocop: enable Graphql/AuthorizeTypes + end + end + end +end + +Types::Analytics::CycleAnalytics::ValueStreams::StageType.prepend_mod diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 74e7f256b44..a4eba3c63ae 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -21,7 +21,8 @@ module Types field :custom_emoji, type: Types::CustomEmojiType.connection_type, null: true, - description: 'Custom emoji within this namespace.', + resolver: Resolvers::CustomEmojiResolver, + description: 'Custom emoji in this namespace.', alpha: { milestone: '13.6' } field :share_with_group_lock, @@ -330,10 +331,6 @@ module Types group.dependency_proxy_setting || group.create_dependency_proxy_setting end - def custom_emoji - object.custom_emoji if Feature.enabled?(:custom_emoji) - end - private def group diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 97db338ad1c..befac90ef42 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -651,6 +651,11 @@ module Types description: 'Detailed import status of the project.', method: :import_state + field :value_streams, + description: 'Value streams available to the project.', + null: true, + resolver: Resolvers::Analytics::CycleAnalytics::ValueStreamsResolver + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end diff --git a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb index 7dd47611a2e..d4aca0a3792 100644 --- a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb +++ b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb @@ -35,7 +35,7 @@ module Types description: 'URL to the file along with line number.' field :engine_name, GraphQL::Types::String, - null: false, + null: true, description: 'Code quality plugin that reported the degradation.' end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb index 681d5828906..4d1d764755e 100644 --- a/app/models/analytics/cycle_analytics/value_stream.rb +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -42,8 +42,11 @@ module Analytics namespace.project end - def at_group_level? - project.nil? + def to_global_id + return super if persisted? + + # Returns default name as id for built in value stream at FOSS level + name end private diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 16991937e2f..bb1368f6dc1 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -110,6 +110,7 @@ module ApplicationSettingImplementation housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], + instance_level_ai_beta_features_enabled: false, instance_level_code_suggestions_enabled: false, invisible_captcha_enabled: false, issues_create_limit: 300, diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 4dc26a2d197..9c1005e19c7 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -66,7 +66,8 @@ class AwardEmoji < ApplicationRecord def url return if TanukiEmoji.find_by_alpha_code(name) - CustomEmoji.for_resource(resource_parent).by_name(name).select(:url).first&.url + Groups::CustomEmojiFinder.new(resource_parent, { include_ancestor_groups: true }).execute + .by_name(name)&.select(:url)&.first&.url end def expire_cache diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index c704795130b..a3318cd0bd8 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -25,20 +25,33 @@ class CustomEmoji < ApplicationRecord format: { with: /\A#{NAME_REGEXP}\z/o } scope :by_name, -> (names) { where(name: names) } + scope :for_namespaces, -> (namespace_ids) do + order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'name', + order_expression: CustomEmoji.arel_table[:name].asc, + nullable: :not_nullable, + distinct: true + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'current_namespace', + order_expression: Arel.sql("CASE WHEN namespace_id = #{namespace_ids.first} THEN 0 ELSE 1 END").asc, + nullable: :not_nullable, + add_to_projections: true + ) + ]) + where(namespace_id: namespace_ids) + .select("DISTINCT ON (name) *") + .order(order) + end alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467 - # Find custom emoji for the given resource. - # A resource can be either a Project or a Group, or anything responding to #root_ancestor. - # Usually it's the return value of #resource_parent on any model. scope :for_resource, -> (resource) do - return none if resource.nil? - - namespace = resource.root_ancestor - - return none if namespace.nil? || Feature.disabled?(:custom_emoji, namespace) + return none if resource.nil? || Feature.disabled?(:custom_emoji, resource) + return none unless resource.is_a?(Group) - namespace.custom_emoji + resource.custom_emoji end private diff --git a/app/serializers/award_emoji_entity.rb b/app/serializers/award_emoji_entity.rb index 6ca782d8203..212931a2fa9 100644 --- a/app/serializers/award_emoji_entity.rb +++ b/app/serializers/award_emoji_entity.rb @@ -3,5 +3,4 @@ class AwardEmojiEntity < Grape::Entity expose :name expose :user, using: API::Entities::UserSafe - expose :url end diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb index a5e9bf997cc..1c6f5bb96dd 100644 --- a/app/services/ml/find_or_create_model_version_service.rb +++ b/app/services/ml/find_or_create_model_version_service.rb @@ -15,10 +15,12 @@ module Ml model_version = Ml::ModelVersion.find_or_create!(model, @version, @package, @description) - model_version.candidate = ::Ml::CreateCandidateService.new( - model.default_experiment, - { model_version: model_version } - ).execute + unless model_version.candidate + model_version.candidate = ::Ml::CreateCandidateService.new( + model.default_experiment, + { model_version: model_version } + ).execute + end model_version end diff --git a/app/validators/gitlab/emoji_name_validator.rb b/app/validators/gitlab/emoji_name_validator.rb index c034a79214b..68743530d83 100644 --- a/app/validators/gitlab/emoji_name_validator.rb +++ b/app/validators/gitlab/emoji_name_validator.rb @@ -25,8 +25,12 @@ module Gitlab def valid_custom_emoji?(record, value) resource = record.try(:resource_parent) + namespace = resource.try(:namespace) - CustomEmoji.for_resource(resource).by_name(value.to_s).any? + return unless resource.is_a?(Group) || namespace.is_a?(Group) + + Groups::CustomEmojiFinder.new(resource, { include_ancestor_groups: true }).execute + .by_name(value.to_s).any? end end end diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index d84fbe94f65..0ba883872a1 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -120,4 +120,7 @@ = render_if_exists 'admin/application_settings/add_license' = render 'admin/application_settings/jira_connect' = render 'admin/application_settings/slack' -= render_if_exists 'admin/application_settings/ai_access' +- if Feature.enabled?(:updated_ai_powered_features_menu_for_sm) + = render_if_exists 'admin/application_settings/ai_powered' +- else + = render_if_exists 'admin/application_settings/ai_access' diff --git a/app/views/projects/ml/model_versions/show.html.haml b/app/views/projects/ml/model_versions/show.html.haml index 0b3d5462a89..1b4bdd29842 100644 --- a/app/views/projects/ml/model_versions/show.html.haml +++ b/app/views/projects/ml/model_versions/show.html.haml @@ -3,4 +3,4 @@ - breadcrumb_title @model_version.version - page_title "#{@model_version.name} / #{@model_version.version}" -= render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version)) += render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version, current_user: current_user)) diff --git a/app/views/projects/ml/models/show.html.haml b/app/views/projects/ml/models/show.html.haml index be611e55304..e0067143450 100644 --- a/app/views/projects/ml/models/show.html.haml +++ b/app/views/projects/ml/models/show.html.haml @@ -2,4 +2,4 @@ - breadcrumb_title @model.name - page_title @model.name -= render(Projects::Ml::ShowMlModelComponent.new(model: @model)) += render(Projects::Ml::ShowMlModelComponent.new(model: @model, current_user: current_user)) diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb index 33c54f07521..5cd1d2938ee 100644 --- a/app/workers/users/deactivate_dormant_users_worker.rb +++ b/app/workers/users/deactivate_dormant_users_worker.rb @@ -18,8 +18,10 @@ module Users admin_bot = Users::Internal.admin_bot return unless admin_bot - deactivate_users(User.dormant, admin_bot) - deactivate_users(User.with_no_activity, admin_bot) + Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do + deactivate_users(User.dormant, admin_bot) + deactivate_users(User.with_no_activity, admin_bot) + end end private diff --git a/config/feature_flags/development/updated_ai_powered_features_menu_for_sm.yml b/config/feature_flags/development/updated_ai_powered_features_menu_for_sm.yml new file mode 100644 index 00000000000..64377eacd5d --- /dev/null +++ b/config/feature_flags/development/updated_ai_powered_features_menu_for_sm.yml @@ -0,0 +1,8 @@ +--- +name: updated_ai_powered_features_menu_for_sm +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138337 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433255 +milestone: '16.7' +type: development +group: group::cloud connector +default_enabled: false diff --git a/config/feature_flags/development/use_sync_service_token_worker.yml b/config/feature_flags/development/use_sync_service_token_worker.yml index 9adda88ea4e..162387468ae 100644 --- a/config/feature_flags/development/use_sync_service_token_worker.yml +++ b/config/feature_flags/development/use_sync_service_token_worker.yml @@ -1,8 +1,8 @@ --- name: use_sync_service_token_worker -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135486 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136078 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431608 milestone: '16.7' type: development group: group::cloud connector -default_enabled: true +default_enabled: false diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml index d2477740fba..ea501630e77 100644 --- a/config/gitlab_loose_foreign_keys.yml +++ b/config/gitlab_loose_foreign_keys.yml @@ -10,15 +10,9 @@ application_settings: - table: users column: usage_stats_set_by_user_id on_delete: async_nullify - - table: namespaces - column: instance_administrators_group_id - on_delete: async_nullify - table: projects column: file_template_project_id on_delete: async_nullify - - table: projects - column: instance_administration_project_id - on_delete: async_nullify - table: namespaces column: custom_project_templates_group_id on_delete: async_nullify diff --git a/config/initializers/custom_roles.rb b/config/initializers/custom_roles.rb new file mode 100644 index 00000000000..e2a61655db6 --- /dev/null +++ b/config/initializers/custom_roles.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +return unless Gitlab.ee? + +Gitlab::CustomRoles::Definition.load_abilities! diff --git a/db/post_migrate/20231127174335_remove_ignored_application_settings_columns.rb b/db/post_migrate/20231127174335_remove_ignored_application_settings_columns.rb new file mode 100644 index 00000000000..07cabb93d96 --- /dev/null +++ b/db/post_migrate/20231127174335_remove_ignored_application_settings_columns.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class RemoveIgnoredApplicationSettingsColumns < Gitlab::Database::Migration[2.2] + milestone '16.7' + + disable_ddl_transaction! + + PROJECT_INDEX_NAME = 'index_applicationsettings_on_instance_administration_project_id' + GROUP_INDEX_NAME = 'index_application_settings_on_instance_administrators_group_id' + + def up + remove_column(:application_settings, :instance_administration_project_id) + remove_column(:application_settings, :instance_administrators_group_id) + end + + def down + unless column_exists?(:users, :instance_administration_project_id) + add_column(:application_settings, :instance_administration_project_id, :bigint) + end + + unless column_exists?(:users, :instance_administrators_group_id) + add_column(:application_settings, :instance_administrators_group_id, :integer) + end + + add_concurrent_index(:application_settings, :instance_administration_project_id, name: PROJECT_INDEX_NAME) + add_concurrent_index(:application_settings, :instance_administrators_group_id, name: GROUP_INDEX_NAME) + end +end diff --git a/db/schema_migrations/20231127174335 b/db/schema_migrations/20231127174335 new file mode 100644 index 00000000000..5a47c758a30 --- /dev/null +++ b/db/schema_migrations/20231127174335 @@ -0,0 +1 @@ +a12b08baa00906fad3acd0f3c0490d1fc6880eb627f7c2cc025edf481c8f9e0b \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 60906ed74e7..b5de92bf170 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11894,7 +11894,6 @@ CREATE TABLE application_settings ( raw_blob_request_limit integer DEFAULT 300 NOT NULL, allow_local_requests_from_web_hooks_and_services boolean DEFAULT false NOT NULL, allow_local_requests_from_system_hooks boolean DEFAULT true NOT NULL, - instance_administration_project_id bigint, asset_proxy_enabled boolean DEFAULT false NOT NULL, asset_proxy_url character varying, encrypted_asset_proxy_secret_key text, @@ -11940,7 +11939,6 @@ CREATE TABLE application_settings ( encrypted_slack_app_verification_token_iv character varying(255), force_pages_access_control boolean DEFAULT false NOT NULL, updating_name_disabled_for_users boolean DEFAULT false NOT NULL, - instance_administrators_group_id integer, elasticsearch_indexed_field_length_limit integer DEFAULT 0 NOT NULL, elasticsearch_max_bulk_size_mb smallint DEFAULT 10 NOT NULL, elasticsearch_max_bulk_concurrency smallint DEFAULT 10 NOT NULL, @@ -31716,16 +31714,12 @@ CREATE INDEX index_application_settings_on_custom_project_templates_group_id ON CREATE INDEX index_application_settings_on_file_template_project_id ON application_settings USING btree (file_template_project_id); -CREATE INDEX index_application_settings_on_instance_administrators_group_id ON application_settings USING btree (instance_administrators_group_id); - CREATE UNIQUE INDEX index_application_settings_on_push_rule_id ON application_settings USING btree (push_rule_id); CREATE INDEX index_application_settings_on_usage_stats_set_by_user_id ON application_settings USING btree (usage_stats_set_by_user_id); CREATE INDEX index_application_settings_web_ide_oauth_application_id ON application_settings USING btree (web_ide_oauth_application_id); -CREATE INDEX index_applicationsettings_on_instance_administration_project_id ON application_settings USING btree (instance_administration_project_id); - CREATE INDEX index_approval_group_rules_groups_on_group_id ON approval_group_rules_groups USING btree (group_id); CREATE INDEX index_approval_group_rules_on_scan_result_policy_id ON approval_group_rules USING btree (scan_result_policy_id); diff --git a/doc/administration/package_information/postgresql_versions.md b/doc/administration/package_information/postgresql_versions.md index 3a499be43b3..76d7244ac3e 100644 --- a/doc/administration/package_information/postgresql_versions.md +++ b/doc/administration/package_information/postgresql_versions.md @@ -29,8 +29,9 @@ The lowest supported PostgreSQL versions are listed in the Read more about update policies and warnings in the PostgreSQL [upgrade docs](https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server). -| GitLab version | PostgreSQL versions | Default version for fresh installs | Default version for upgrades | Notes | +| First GitLab version | PostgreSQL versions | Default version for fresh installs | Default version for upgrades | Notes | | -------------- | ------------------- | ---------------------------------- | ---------------------------- | ----- | +| 16.4.3, 16.5.3, 16.6.1 | 13.12, 14.9 | 13.12 | 13.12 | | | 16.2.0 | 13.11, 14.8 | 13.11 | 13.11 | For upgrades, users can manually upgrade to 14.8 following the [upgrade documentation](https://docs.gitlab.com/omnibus/settings/database.html#gitlab-162-and-later). | | 16.0.2 | 13.11 | 13.11 | 13.11 | | | 16.0.0 | 13.8 | 13.8 | 13.8 | | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d5e80e0549f..693349cf049 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13442,6 +13442,29 @@ The edge type for [`UserCore`](#usercore). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`UserCore`](#usercore) | The item at the end of the edge. | +#### `ValueStreamConnection` + +The connection type for [`ValueStream`](#valuestream). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[ValueStreamEdge]`](#valuestreamedge) | A list of edges. | +| `nodes` | [`[ValueStream]`](#valuestream) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `ValueStreamEdge` + +The edge type for [`ValueStream`](#valuestream). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`ValueStream`](#valuestream) | The item at the end of the edge. | + #### `VulnerabilitiesCountByDayConnection` The connection type for [`VulnerabilitiesCountByDay`](#vulnerabilitiescountbyday). @@ -15980,7 +16003,7 @@ Represents a degradation on the compared codequality report. | Name | Type | Description | | ---- | ---- | ----------- | | `description` | [`String!`](#string) | Description of the code quality degradation. | -| `engineName` | [`String!`](#string) | Code quality plugin that reported the degradation. | +| `engineName` | [`String`](#string) | Code quality plugin that reported the degradation. | | `filePath` | [`String!`](#string) | Relative path to the file containing the code quality degradation. | | `fingerprint` | [`String!`](#string) | Unique fingerprint to identify the code quality degradation. For example, an MD5 hash. | | `line` | [`Int!`](#int) | Line on which the code quality degradation occurred. | @@ -18978,7 +19001,6 @@ GPG signature for a signed commit. | `containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. | | `containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. | | `crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. | -| `customEmoji` **{warning-solid}** | [`CustomEmojiConnection`](#customemojiconnection) | **Introduced** in 13.6. This feature is an Experiment. It can be changed or removed at any time. Custom emoji within this namespace. | | `dependencyProxyBlobCount` | [`Int!`](#int) | Number of dependency proxy blobs cached in the group. | | `dependencyProxyBlobs` | [`DependencyProxyBlobConnection`](#dependencyproxyblobconnection) | Dependency Proxy blobs. (see [Connections](#connections)) | | `dependencyProxyImageCount` | [`Int!`](#int) | Number of dependency proxy images cached in the group. | @@ -19027,6 +19049,7 @@ GPG signature for a signed commit. | `totalRepositorySizeExcess` | [`Float`](#float) | Total excess repository size of all projects in the root namespace in bytes. This only applies to namespaces under Project limit enforcement. | | `twoFactorGracePeriod` | [`Int`](#int) | Time before two-factor authentication is enforced. | | `userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. | +| `valueStreams` | [`ValueStreamConnection`](#valuestreamconnection) | Value streams available to the group. (see [Connections](#connections)) | | `visibility` | [`String`](#string) | Visibility of the namespace. | | `vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities of the group and its subgroups. (see [Connections](#connections)) | | `webUrl` | [`String!`](#string) | Web URL of the group. | @@ -19270,6 +19293,26 @@ four standard [pagination arguments](#connection-pagination-arguments): | `from` | [`ISO8601Date!`](#iso8601date) | Start date of the reporting time range. | | `to` | [`ISO8601Date!`](#iso8601date) | End date of the reporting time range. The end date must be within 93 days after the start date. | +##### `Group.customEmoji` + +Custom emoji in this namespace. + +WARNING: +**Introduced** in 13.6. +This feature is an Experiment. It can be changed or removed at any time. + +Returns [`CustomEmojiConnection`](#customemojiconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `includeAncestorGroups` | [`Boolean`](#boolean) | Includes custom emoji from parent groups. | + ##### `Group.customizableDashboardVisualizations` Visualizations of the group or associated configuration project. @@ -23891,6 +23934,7 @@ Represents vulnerability finding of a security report on the pipeline. | `trackingKey` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.0. This feature is an Experiment. It can be changed or removed at any time. Tracking key assigned to the project. | | `userAccessAuthorizedAgents` | [`ClusterAgentAuthorizationUserAccessConnection`](#clusteragentauthorizationuseraccessconnection) | Authorized cluster agents for the project through user_access keyword. (see [Connections](#connections)) | | `userPermissions` | [`ProjectPermissions!`](#projectpermissions) | Permissions for the current user on the resource. | +| `valueStreams` | [`ValueStreamConnection`](#valuestreamconnection) | Value streams available to the project. (see [Connections](#connections)) | | `visibility` | [`String`](#string) | Visibility of the project. | | `vulnerabilityImages` | [`VulnerabilityContainerImageConnection`](#vulnerabilitycontainerimageconnection) | Container images reported on the project vulnerabilities. (see [Connections](#connections)) | | `vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities. (see [Connections](#connections)) | @@ -27608,6 +27652,7 @@ fields relate to interactions between the two entities. | `name` | [`String!`](#string) | Name of the value stream. | | `namespace` | [`Namespace!`](#namespace) | Namespace the value stream belongs to. | | `project` **{warning-solid}** | [`Project`](#project) | **Introduced** in 15.6. This feature is an Experiment. It can be changed or removed at any time. Project the value stream belongs to, returns empty if it belongs to a group. | +| `stages` | [`[ValueStreamStage!]`](#valuestreamstage) | Value Stream stages. | ### `ValueStreamAnalyticsMetric` @@ -27644,6 +27689,20 @@ Represents a recorded measurement (object count) for the requested group. | `name` | [`String!`](#string) | Name of the link group. | | `url` | [`String!`](#string) | Drill-down URL. | +### `ValueStreamStage` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `custom` | [`Boolean!`](#boolean) | Whether the stage is customized. | +| `endEventIdentifier` | [`ValueStreamStageEvent!`](#valuestreamstageevent) | End event identifier. | +| `endEventLabel` | [`Label`](#label) | Label associated with end event. | +| `hidden` | [`Boolean!`](#boolean) | Whether the stage is hidden. | +| `name` | [`String!`](#string) | Name of the stage. | +| `startEventIdentifier` | [`ValueStreamStageEvent!`](#valuestreamstageevent) | Start event identifier. | +| `startEventLabel` | [`Label`](#label) | Label associated with start event. | + ### `VulnerabilitiesCountByDay` Represents the count of vulnerabilities by severity on a particular day. This data is retained for 365 days. diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 4ca2a72fddb..6109d4d603d 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -189,6 +189,20 @@ Words that indicate you are not writing from a customer perspective are [allow and enable](word_list.md#allow-enable). Try instead to use [you](word_list.md#you-your-yours) and to speak directly to the user. +### Building trust + +Product documentation should be focused on providing clear, concise information, +without the addition of sales or marketing text. + +- Do not use words like [easily](word_list.md#easily) or [simply](word_list.md#simply-simple). +- Do not use marketing phrases like "This feature will save you time and money." + +Instead, focus on facts and achievable goals. Be specific. For example: + +- The build time can decrease when you use this feature. +- You can use this feature to save time when you create a project. The API creates the file and you + do not need to manually intervene. + ### Capitalization As a company, we tend toward lowercase. diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md index 0227eb8b8a9..354a1596a8c 100644 --- a/doc/update/versions/gitlab_16_changes.md +++ b/doc/update/versions/gitlab_16_changes.md @@ -102,9 +102,9 @@ Specific information applies to installations using Geo: **Affected releases**: | Affected minor releases | Affected patch releases | Fixed in | - | ------ | ------ | ------ | - | 16.4 | All | None | - | 16.5 | All | None | + | ----------------------- | ----------------------- | -------- | + | 16.4 | 16.4.0 - 16.4.2 | 16.4.3 | + | 16.5 | 16.5.0 - 16.5.1 | 16.5.2 | - After [Group Wiki](../../user/project/wiki/group.md) verification was added in GitLab 16.3, missing Group Wiki repositories are being incorrectly flagged as failing verification. This issue is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these Group Wiki repositories. @@ -232,9 +232,9 @@ Specific information applies to installations using Geo: **Affected releases**: | Affected minor releases | Affected patch releases | Fixed in | - | ------ | ------ | ------ | - | 16.4 | All | None | - | 16.5 | All | None | + | ----------------------- | ----------------------- | -------- | + | 16.4 | 16.4.0 - 16.4.2 | 16.4.3 | + | 16.5 | 16.5.0 - 16.5.1 | 16.5.2 | - An [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/419370) with sync states getting stuck in pending state results in replication being stuck indefinitely for impacted items leading to risk of data loss in the event of a failover. This mostly impact repository syncs but can also can also affect container registry syncs. You are advised to upgrade to a fixed version to avoid risk of data loss. diff --git a/lib/banzai/filter/custom_emoji_filter.rb b/lib/banzai/filter/custom_emoji_filter.rb index dddaaebc9de..4dd6bada306 100644 --- a/lib/banzai/filter/custom_emoji_filter.rb +++ b/lib/banzai/filter/custom_emoji_filter.rb @@ -48,10 +48,9 @@ module Banzai private def has_custom_emoji? - strong_memoize(:has_custom_emoji) do - CustomEmoji.for_resource(resource_parent).any? - end + all_custom_emoji&.any? end + strong_memoize_attr :has_custom_emoji? def resource_parent context[:project] || context[:group] @@ -62,9 +61,12 @@ module Banzai end def all_custom_emoji - @all_custom_emoji ||= - CustomEmoji.for_resource(resource_parent).by_name(custom_emoji_candidates).index_by(&:name) + Groups::CustomEmojiFinder.new(resource_parent, { include_ancestor_groups: true }) + .execute + .by_name(custom_emoji_candidates) + .index_by(&:name) end + strong_memoize_attr :all_custom_emoji end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 66468232633..e9121dbb371 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1917,6 +1917,18 @@ msgstr "" msgid "AI-generated summary" msgstr "" +msgid "AIPoweredSM|AI-powered features" +msgstr "" + +msgid "AIPoweredSM|By enabling this feature, you agree to the %{terms_link_start}GitLab Testing Agreement%{link_end}." +msgstr "" + +msgid "AIPoweredSM|Enable %{link_start}AI-powered features%{link_end} for this instance." +msgstr "" + +msgid "AIPoweredSM|Enable Experiment and Beta AI-powered features" +msgstr "" + msgid "AISummary|Generates a summary of all comments" msgstr "" @@ -11936,6 +11948,12 @@ msgstr "" msgid "CodeSuggestionsSM|Code Suggestions" msgstr "" +msgid "CodeSuggestionsSM|Code Suggestions %{beta}" +msgstr "" + +msgid "CodeSuggestionsSM|Enable Code Suggestions for this instance" +msgstr "" + msgid "CodeSuggestionsSM|Enable Code Suggestions for this instance %{beta}" msgstr "" @@ -30861,6 +30879,9 @@ msgstr "" msgid "MlModelRegistry|Model registry" msgstr "" +msgid "MlModelRegistry|No description provided" +msgstr "" + msgid "MlModelRegistry|No logged metadata" msgstr "" diff --git a/package.json b/package.json index 62ae74702ee..7f96ffcbaa2 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "visibilityjs": "^1.2.4", "vue": "2.7.15", "vue-apollo": "^3.0.7", - "vue-loader": "15.10.2", + "vue-loader": "15.11.1", "vue-observe-visibility": "^1.0.0", "vue-resize": "^1.0.1", "vue-router": "3.6.5", @@ -223,7 +223,7 @@ "web-streams-polyfill": "^3.2.1", "web-vitals": "^0.2.4", "webpack": "^4.47.0", - "webpack-bundle-analyzer": "^4.9.1", + "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^4.10.0", "webpack-stats-plugin": "^0.3.1", "worker-loader": "^3.0.8", diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb index 30cd4f11bb4..61216f7b28d 100644 --- a/qa/qa/page/file/form.rb +++ b/qa/qa/page/file/form.rb @@ -15,7 +15,7 @@ module QA end view 'app/assets/javascripts/blob/filepath_form/components/template_selector.vue' do - element :template_selector + element 'template-selector' end def add_name(name) @@ -35,7 +35,7 @@ module QA def select_template(template_type, template) case template_type when '.gitignore', '.gitlab-ci.yml', 'Dockerfile', 'LICENSE' - click_element :template_selector + click_element 'template-selector' else raise %(Unsupported template_type "#{template_type}". Please confirm that it is a valid option.) end diff --git a/spec/components/projects/ml/show_ml_model_component_spec.rb b/spec/components/projects/ml/show_ml_model_component_spec.rb index 02fad55e0be..ed989421679 100644 --- a/spec/components/projects/ml/show_ml_model_component_spec.rb +++ b/spec/components/projects/ml/show_ml_model_component_spec.rb @@ -3,11 +3,14 @@ require "spec_helper" RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_category: :mlops do - let_it_be(:project) { build_stubbed(:project) } - let_it_be(:model1) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) } + let_it_be(:project) { create(:project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:model1) { create(:ml_models, :with_latest_version_and_package, project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + + let_it_be(:experiment) { model1.default_experiment } + let_it_be(:candidate) { model1.latest_version.candidate } subject(:component) do - described_class.new(model: model1) + described_class.new(model: model1, current_user: model1.user) end describe 'rendered' do @@ -28,7 +31,22 @@ RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_cat 'version' => model1.latest_version.version, 'description' => model1.latest_version.description, 'projectPath' => "/#{project.full_path}", - 'packageId' => model1.latest_version.package_id + 'packageId' => model1.latest_version.package_id, + 'candidate' => { + 'info' => { + 'iid' => candidate.iid, + 'eid' => candidate.eid, + 'pathToArtifact' => nil, + 'experimentName' => candidate.experiment.name, + 'pathToExperiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}", + 'ciJob' => nil + }, + 'metrics' => [], + 'params' => [], + 'metadata' => [] + } }, 'versionCount' => 1 } diff --git a/spec/components/projects/ml/show_ml_model_version_component_spec.rb b/spec/components/projects/ml/show_ml_model_version_component_spec.rb index a7dad5e4b2b..89f0c8633c2 100644 --- a/spec/components/projects/ml/show_ml_model_version_component_spec.rb +++ b/spec/components/projects/ml/show_ml_model_version_component_spec.rb @@ -3,12 +3,20 @@ require "spec_helper" RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do - let_it_be(:project) { build_stubbed(:project) } - let_it_be(:model) { build_stubbed(:ml_models, project: project) } - let_it_be(:version) { build_stubbed(:ml_model_versions, :with_package, model: model, description: 'abc') } + let_it_be(:project) { create(:project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:user) { project.owner } + let_it_be(:model) { create(:ml_models, project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + let_it_be(:experiment) { model.default_experiment } + let_it_be(:candidate) do + create(:ml_candidates, :with_artifact, experiment: experiment, user: user, project: project) # rubocop:disable RSpec/FactoryBot/AvoidCreate -- build_stubbed breaks because it doesn't create iids properly. + end + + let_it_be(:version) do + build_stubbed(:ml_model_versions, :with_package, model: model, candidate: candidate, description: 'abc') + end subject(:component) do - described_class.new(model_version: version) + described_class.new(model_version: version, current_user: user) end describe 'rendered' do @@ -30,6 +38,21 @@ RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feat 'model' => { 'name' => model.name, 'path' => "/#{project.full_path}/-/ml/models/#{model.id}" + }, + 'candidate' => { + 'info' => { + 'iid' => candidate.iid, + 'eid' => candidate.eid, + 'pathToArtifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}", + 'experimentName' => candidate.experiment.name, + 'pathToExperiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}", + 'ciJob' => nil + }, + 'metrics' => [], + 'params' => [], + 'metadata' => [] } } }) diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 09f39506448..92835d21bb6 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -10,6 +10,7 @@ RSpec.describe 'Database schema', feature_category: :database do let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb } IGNORED_INDEXES_ON_FKS = { + application_settings: %w[instance_administration_project_id instance_administrators_group_id], # `search_index_id index_type` is the composite foreign key configured for `search_namespace_index_assignments`, # but in Search::NamespaceIndexAssignment model, only `search_index_id` is used as foreign key and indexed search_namespace_index_assignments: [%w[search_index_id index_type]], diff --git a/spec/factories/ml/model_versions.rb b/spec/factories/ml/model_versions.rb index a097640b134..fd7ed857ee2 100644 --- a/spec/factories/ml/model_versions.rb +++ b/spec/factories/ml/model_versions.rb @@ -8,6 +8,10 @@ FactoryBot.define do project { model.project } description { 'Some description' } + candidate do + association :ml_candidates, experiment: model.default_experiment, project: project, model_version: instance + end + trait :with_package do package do association :ml_model_package, name: model.name, version: version, project: project diff --git a/spec/finders/groups/custom_emoji_finder_spec.rb b/spec/finders/groups/custom_emoji_finder_spec.rb new file mode 100644 index 00000000000..f1044997d4f --- /dev/null +++ b/spec/finders/groups/custom_emoji_finder_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::CustomEmojiFinder, feature_category: :code_review_workflow do + describe '#execute' do + let(:params) { {} } + + subject(:execute) { described_class.new(group, params).execute } + + context 'when inside a group' do + let_it_be(:group) { create(:group) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: group) } + + it 'returns custom emoji from group' do + expect(execute).to contain_exactly(custom_emoji) + end + end + + context 'when group is nil' do + let_it_be(:group) { nil } + + it 'returns nil' do + expect(execute).to be_empty + end + end + + context 'when group is a subgroup' do + let_it_be(:parent) { create(:group) } + let_it_be(:group) { create(:group, parent: parent) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: group) } + + it 'returns custom emoji' do + expect(described_class.new(group, params).execute).to contain_exactly(custom_emoji) + end + end + + describe 'when custom emoji is in parent group' do + let_it_be(:parent) { create(:group) } + let_it_be(:group) { create(:group, parent: parent) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: parent) } + let(:params) { { include_ancestor_groups: true } } + + it 'returns custom emoji' do + expect(execute).to contain_exactly(custom_emoji) + end + + context 'when params is empty' do + let(:params) { {} } + + it 'returns empty record' do + expect(execute).to eq([]) + end + end + + context 'when include_ancestor_groups is false' do + let(:params) { { include_ancestor_groups: false } } + + it 'returns empty record' do + expect(execute).to eq([]) + end + end + end + end +end diff --git a/spec/frontend/authentication/password/components/password_input_spec.js b/spec/frontend/authentication/password/components/password_input_spec.js index 5b2a9da993b..62438e824cf 100644 --- a/spec/frontend/authentication/password/components/password_input_spec.js +++ b/spec/frontend/authentication/password/components/password_input_spec.js @@ -9,7 +9,6 @@ describe('PasswordInput', () => { title: 'This field is required', id: 'new_user_password', minimumPasswordLength: '8', - qaSelector: 'new_user_password_field', testid: 'new_user_password', autocomplete: 'new-password', name: 'new_user', diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js index ae7f5416c0c..6db99e796d6 100644 --- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -30,14 +30,11 @@ describe('ShortcutsIssuable', () => {
`, ); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - - window.shortcut = new ShortcutsIssuable(true); }); afterEach(() => { $(FORM_SELECTOR).remove(); - delete window.shortcut; resetHTMLFixture(); }); @@ -55,6 +52,15 @@ describe('ShortcutsIssuable', () => { }); }; + it('sets up commands on instantiation', () => { + const mockShortcutsInstance = { addAll: jest.fn() }; + + // eslint-disable-next-line no-new + new ShortcutsIssuable(mockShortcutsInstance); + + expect(mockShortcutsInstance.addAll).toHaveBeenCalled(); + }); + describe('with empty selection', () => { it('does not return an error', () => { ShortcutsIssuable.replyWithSelectedText(true); diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_spec.js new file mode 100644 index 00000000000..5f71eb24758 --- /dev/null +++ b/spec/frontend/behaviors/shortcuts/shortcuts_spec.js @@ -0,0 +1,267 @@ +import $ from 'jquery'; +import { flatten } from 'lodash'; +import htmlSnippetsShow from 'test_fixtures/snippets/show.html'; +import { Mousetrap } from '~/lib/mousetrap'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/shortcuts'; +import MarkdownPreview from '~/behaviors/preview_markdown'; + +describe('Shortcuts', () => { + let shortcuts; + + beforeAll(() => { + shortcuts = new Shortcuts(); + }); + + const mockSuperSidebarSearchButton = () => { + const button = document.createElement('button'); + button.id = 'super-sidebar-search'; + return button; + }; + + beforeEach(() => { + setHTMLFixture(htmlSnippetsShow); + document.body.appendChild(mockSuperSidebarSearchButton()); + + new Shortcuts(); // eslint-disable-line no-new + new MarkdownPreview(); // eslint-disable-line no-new + + jest.spyOn(HTMLElement.prototype, 'click'); + + jest.spyOn(Mousetrap.prototype, 'stopCallback'); + jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation(); + jest.spyOn(Mousetrap.prototype, 'unbind').mockImplementation(); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('does not allow subclassing', () => { + const createSubclass = () => { + class Subclass extends Shortcuts {} + + return new Subclass(); + }; + + expect(createSubclass).toThrow(/cannot be subclassed/); + }); + + describe('markdown shortcuts', () => { + let shortcutElements; + + beforeEach(() => { + // Get all shortcuts specified with md-shortcuts attributes in the fixture. + // `shortcuts` will look something like this: + // [ + // [ 'mod+b' ], + // [ 'mod+i' ], + // [ 'mod+k' ] + // ] + shortcutElements = $('.edit-note .js-md') + .map(function getShortcutsFromToolbarBtn() { + const mdShortcuts = $(this).data('md-shortcuts'); + + // jQuery.map() automatically unwraps arrays, so we + // have to double wrap the array to counteract this + return mdShortcuts ? [mdShortcuts] : undefined; + }) + .get(); + }); + + describe('initMarkdownEditorShortcuts', () => { + let $textarea; + let localMousetrapInstance; + + beforeEach(() => { + $textarea = $('.edit-note textarea'); + Shortcuts.initMarkdownEditorShortcuts($textarea); + localMousetrapInstance = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); + }); + + it('attaches a Mousetrap handler for every markdown shortcut specified with md-shortcuts', () => { + const expectedCalls = shortcutElements.map((s) => [s, expect.any(Function)]); + + expect(Mousetrap.prototype.bind.mock.calls).toEqual(expectedCalls); + }); + + it('attaches a stopCallback that allows each markdown shortcut specified with md-shortcuts', () => { + flatten(shortcutElements).forEach((s) => { + expect( + localMousetrapInstance.stopCallback.call(localMousetrapInstance, null, null, s), + ).toBe(false); + }); + }); + }); + + describe('removeMarkdownEditorShortcuts', () => { + it('does nothing if initMarkdownEditorShortcuts was not previous called', () => { + Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); + + expect(Mousetrap.prototype.unbind.mock.calls).toEqual([]); + }); + + it('removes Mousetrap handlers for every markdown shortcut specified with md-shortcuts', () => { + Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea')); + Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); + + const expectedCalls = shortcutElements.map((s) => [s]); + + expect(Mousetrap.prototype.unbind.mock.calls).toEqual(expectedCalls); + }); + }); + }); + + describe('focusSearch', () => { + let event; + + beforeEach(() => { + event = new KeyboardEvent('keydown', { cancelable: true }); + Shortcuts.focusSearch(event); + }); + + it('clicks the super sidebar search button', () => { + expect(HTMLElement.prototype.click).toHaveBeenCalled(); + const thisArg = HTMLElement.prototype.click.mock.contexts[0]; + expect(thisArg.id).toBe('super-sidebar-search'); + }); + + it('cancels the default behaviour of the event', () => { + expect(event.defaultPrevented).toBe(true); + }); + }); + + describe('adding shortcuts', () => { + it('add calls Mousetrap.bind correctly', () => { + const mockCommand = { defaultKeys: ['m'] }; + const mockCallback = () => {}; + + shortcuts.add(mockCommand, mockCallback); + + expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(1); + const [callArguments] = Mousetrap.prototype.bind.mock.calls; + expect(callArguments[0]).toEqual(mockCommand.defaultKeys); + expect(callArguments[1]).toBe(mockCallback); + }); + + it('addAll calls Mousetrap.bind correctly', () => { + const mockCommandsAndCallbacks = [ + [{ defaultKeys: ['1'] }, () => {}], + [{ defaultKeys: ['2'] }, () => {}], + ]; + + shortcuts.addAll(mockCommandsAndCallbacks); + + expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(mockCommandsAndCallbacks.length); + const { calls } = Mousetrap.prototype.bind.mock; + + mockCommandsAndCallbacks.forEach(([mockCommand, mockCallback], i) => { + expect(calls[i][0]).toEqual(mockCommand.defaultKeys); + expect(calls[i][1]).toBe(mockCallback); + }); + }); + }); + + describe('addExtension', () => { + it('instantiates the given extension', () => { + const MockExtension = jest.fn(); + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts, 'foo'); + expect(returnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('instantiates declared dependencies', () => { + const MockDependency = jest.fn(); + const MockExtension = jest.fn(); + + MockExtension.dependencies = [MockDependency]; + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + + expect(MockDependency).toHaveBeenCalledTimes(1); + expect(MockDependency.mock.instances).toHaveLength(1); + expect(MockDependency).toHaveBeenCalledWith(shortcuts); + + expect(returnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('does not instantiate an extension more than once', () => { + const MockExtension = jest.fn(); + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + const secondReturnValue = shortcuts.addExtension(MockExtension, ['bar']); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts, 'foo'); + expect(returnValue).toBe(MockExtension.mock.instances[0]); + expect(secondReturnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('allows extensions to redundantly depend on Shortcuts', () => { + const MockExtension = jest.fn(); + MockExtension.dependencies = [Shortcuts]; + + shortcuts.addExtension(MockExtension); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts); + + // Ensure it wasn't instantiated + expect(shortcuts.extensions.has(Shortcuts)).toBe(false); + }); + + it('allows extensions to incorrectly depend on themselves', () => { + const A = jest.fn(); + A.dependencies = [A]; + shortcuts.addExtension(A); + expect(A).toHaveBeenCalledTimes(1); + expect(A).toHaveBeenCalledWith(shortcuts); + }); + + it('handles extensions with circular dependencies', () => { + const A = jest.fn(); + const B = jest.fn(); + const C = jest.fn(); + + A.dependencies = [B]; + B.dependencies = [C]; + C.dependencies = [A]; + + shortcuts.addExtension(A); + + expect(A).toHaveBeenCalledTimes(1); + expect(B).toHaveBeenCalledTimes(1); + expect(C).toHaveBeenCalledTimes(1); + }); + + it('handles complex (diamond) dependency graphs', () => { + const X = jest.fn(); + const A = jest.fn(); + const C = jest.fn(); + const D = jest.fn(); + const E = jest.fn(); + + // Form this dependency graph: + // + // X ───► A ───► C + // │ ▲ + // └────► D ─────┘ + // │ + // └────► E + X.dependencies = [A, D]; + A.dependencies = [C]; + D.dependencies = [C, E]; + + shortcuts.addExtension(X); + + expect(X).toHaveBeenCalledTimes(1); + expect(A).toHaveBeenCalledTimes(1); + expect(C).toHaveBeenCalledTimes(1); + expect(D).toHaveBeenCalledTimes(1); + expect(E).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js index aeb9d13ad97..d1874346ad7 100644 --- a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js +++ b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import CandidateDetail from '~/ml/model_registry/components/candidate_detail.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { makeModelVersion, MODEL_VERSION } from '../mock_data'; @@ -15,36 +16,51 @@ const createWrapper = (modelVersion = MODEL_VERSION) => { }; const findPackageFiles = () => wrapper.findComponent(PackageFiles); +const findCandidateDetail = () => wrapper.findComponent(CandidateDetail); describe('ml/model_registry/components/model_version_detail.vue', () => { - describe('description', () => { + describe('base behaviour', () => { beforeEach(() => createWrapper()); it('shows the description', () => { expect(wrapper.text()).toContain(MODEL_VERSION.description); }); - }); - describe('package files', () => { - describe('if package exists', () => { - beforeEach(() => createWrapper()); - - it('renders files', () => { - expect(findPackageFiles().props()).toEqual({ - packageId: 'gid://gitlab/Packages::Package/12', - projectPath: MODEL_VERSION.projectPath, - packageType: 'ml_model', - canDelete: false, - }); - }); + it('shows the candidate', () => { + expect(findCandidateDetail().props('candidate')).toBe(MODEL_VERSION.candidate); }); - describe('if package does not exist', () => { - beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 }))); + it('shows the mlflow label string', () => { + expect(wrapper.text()).toContain('MLflow run ID'); + }); - it('does not render files', () => { - expect(findPackageFiles().exists()).toBe(false); + it('shows the mlflow id', () => { + expect(wrapper.text()).toContain(MODEL_VERSION.candidate.info.eid); + }); + + it('renders files', () => { + expect(findPackageFiles().props()).toEqual({ + packageId: 'gid://gitlab/Packages::Package/12', + projectPath: MODEL_VERSION.projectPath, + packageType: 'ml_model', + canDelete: false, }); }); }); + + describe('if package does not exist', () => { + beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 }))); + + it('does not render files', () => { + expect(findPackageFiles().exists()).toBe(false); + }); + }); + + describe('if model version does not have description', () => { + beforeEach(() => createWrapper(makeModelVersion({ description: null }))); + + it('renders no description provided label', () => { + expect(wrapper.text()).toContain('No description provided'); + }); + }); }); diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js index 07cf59388ea..78e22eda7b9 100644 --- a/spec/frontend/ml/model_registry/mock_data.js +++ b/spec/frontend/ml/model_registry/mock_data.js @@ -56,12 +56,18 @@ export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION }) export const MODEL = makeModel(); -export const makeModelVersion = ({ version = '1.2.3', model = MODEL, packageId = 12 } = {}) => ({ +export const makeModelVersion = ({ + version = '1.2.3', + model = MODEL, + packageId = 12, + description = 'Model version description', +} = {}) => ({ version, model, packageId, - description: 'Model version description', + description, projectPath: 'path/to/project', + candidate: newCandidate(), }); export const MODEL_VERSION = makeModelVersion(); diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js index 3ced5f6c4d2..53ebabebf1d 100644 --- a/spec/frontend/repository/components/blob_controls_spec.js +++ b/spec/frontend/repository/components/blob_controls_spec.js @@ -8,6 +8,7 @@ import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql' import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createRouter from '~/repository/router'; import { updateElementsVisibility } from '~/repository/utils/dom'; +import { resetShortcutsForTests } from '~/behaviors/shortcuts'; import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import { blobControlsDataMock, refMock } from '../mock_data'; @@ -32,6 +33,8 @@ const createComponent = async () => { mockResolver = jest.fn().mockResolvedValue({ data: { project } }); + await resetShortcutsForTests(); + wrapper = shallowMountExtended(BlobControls, { router, apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]), diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js deleted file mode 100644 index a348ee77be1..00000000000 --- a/spec/frontend/shortcuts_spec.js +++ /dev/null @@ -1,154 +0,0 @@ -import $ from 'jquery'; -import { flatten } from 'lodash'; -import htmlSnippetsShow from 'test_fixtures/snippets/show.html'; -import { Mousetrap } from '~/lib/mousetrap'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/shortcuts'; -import MarkdownPreview from '~/behaviors/preview_markdown'; - -describe('Shortcuts', () => { - let shortcuts; - - beforeAll(() => { - shortcuts = new Shortcuts(); - }); - - const mockSuperSidebarSearchButton = () => { - const button = document.createElement('button'); - button.id = 'super-sidebar-search'; - return button; - }; - - beforeEach(() => { - setHTMLFixture(htmlSnippetsShow); - document.body.appendChild(mockSuperSidebarSearchButton()); - - new Shortcuts(); // eslint-disable-line no-new - new MarkdownPreview(); // eslint-disable-line no-new - - jest.spyOn(HTMLElement.prototype, 'click'); - - jest.spyOn(Mousetrap.prototype, 'stopCallback'); - jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation(); - jest.spyOn(Mousetrap.prototype, 'unbind').mockImplementation(); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - describe('markdown shortcuts', () => { - let shortcutElements; - - beforeEach(() => { - // Get all shortcuts specified with md-shortcuts attributes in the fixture. - // `shortcuts` will look something like this: - // [ - // [ 'mod+b' ], - // [ 'mod+i' ], - // [ 'mod+k' ] - // ] - shortcutElements = $('.edit-note .js-md') - .map(function getShortcutsFromToolbarBtn() { - const mdShortcuts = $(this).data('md-shortcuts'); - - // jQuery.map() automatically unwraps arrays, so we - // have to double wrap the array to counteract this - return mdShortcuts ? [mdShortcuts] : undefined; - }) - .get(); - }); - - describe('initMarkdownEditorShortcuts', () => { - let $textarea; - let localMousetrapInstance; - - beforeEach(() => { - $textarea = $('.edit-note textarea'); - Shortcuts.initMarkdownEditorShortcuts($textarea); - localMousetrapInstance = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); - }); - - it('attaches a Mousetrap handler for every markdown shortcut specified with md-shortcuts', () => { - const expectedCalls = shortcutElements.map((s) => [s, expect.any(Function)]); - - expect(Mousetrap.prototype.bind.mock.calls).toEqual(expectedCalls); - }); - - it('attaches a stopCallback that allows each markdown shortcut specified with md-shortcuts', () => { - flatten(shortcutElements).forEach((s) => { - expect( - localMousetrapInstance.stopCallback.call(localMousetrapInstance, null, null, s), - ).toBe(false); - }); - }); - }); - - describe('removeMarkdownEditorShortcuts', () => { - it('does nothing if initMarkdownEditorShortcuts was not previous called', () => { - Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); - - expect(Mousetrap.prototype.unbind.mock.calls).toEqual([]); - }); - - it('removes Mousetrap handlers for every markdown shortcut specified with md-shortcuts', () => { - Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea')); - Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); - - const expectedCalls = shortcutElements.map((s) => [s]); - - expect(Mousetrap.prototype.unbind.mock.calls).toEqual(expectedCalls); - }); - }); - }); - - describe('focusSearch', () => { - let event; - - beforeEach(() => { - event = new KeyboardEvent('keydown', { cancelable: true }); - Shortcuts.focusSearch(event); - }); - - it('clicks the super sidebar search button', () => { - expect(HTMLElement.prototype.click).toHaveBeenCalled(); - const thisArg = HTMLElement.prototype.click.mock.contexts[0]; - expect(thisArg.id).toBe('super-sidebar-search'); - }); - - it('cancels the default behaviour of the event', () => { - expect(event.defaultPrevented).toBe(true); - }); - }); - - describe('bindCommand(s)', () => { - it('bindCommand calls Mousetrap.bind correctly', () => { - const mockCommand = { defaultKeys: ['m'] }; - const mockCallback = () => {}; - - shortcuts.bindCommand(mockCommand, mockCallback); - - expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(1); - const [callArguments] = Mousetrap.prototype.bind.mock.calls; - expect(callArguments[0]).toEqual(mockCommand.defaultKeys); - expect(callArguments[1]).toBe(mockCallback); - }); - - it('bindCommands calls Mousetrap.bind correctly', () => { - const mockCommandsAndCallbacks = [ - [{ defaultKeys: ['1'] }, () => {}], - [{ defaultKeys: ['2'] }, () => {}], - ]; - - shortcuts.bindCommands(mockCommandsAndCallbacks); - - expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(mockCommandsAndCallbacks.length); - const { calls } = Mousetrap.prototype.bind.mock; - - mockCommandsAndCallbacks.forEach(([mockCommand, mockCallback], i) => { - expect(calls[i][0]).toEqual(mockCommand.defaultKeys); - expect(calls[i][1]).toBe(mockCallback); - }); - }); - }); -}); diff --git a/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb index 5e2638210d3..1d5a8dbebd6 100644 --- a/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb +++ b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb @@ -7,5 +7,5 @@ RSpec.describe Types::Analytics::CycleAnalytics::ValueStreamType, feature_catego specify { expect(described_class).to require_graphql_authorizations(:read_cycle_analytics) } - specify { expect(described_class).to have_graphql_fields(:id, :name, :namespace, :project) } + specify { expect(described_class).to have_graphql_fields(:id, :name, :namespace, :project, :stages) } end diff --git a/spec/graphql/types/analytics/cycle_analytics/value_streams/stage_type_spec.rb b/spec/graphql/types/analytics/cycle_analytics/value_streams/stage_type_spec.rb new file mode 100644 index 00000000000..92276647e1b --- /dev/null +++ b/spec/graphql/types/analytics/cycle_analytics/value_streams/stage_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Analytics::CycleAnalytics::ValueStreams::StageType, feature_category: :value_stream_management do + let(:fields) do + %i[ + name start_event_identifier + end_event_identifier hidden custom + ] + end + + specify { expect(described_class.graphql_name).to eq('ValueStreamStage') } + specify { expect(described_class).to have_graphql_fields(fields).at_least } +end diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb index 6622551f063..d3f9053faf3 100644 --- a/spec/graphql/types/group_type_spec.rb +++ b/spec/graphql/types/group_type_spec.rb @@ -125,4 +125,37 @@ RSpec.describe GitlabSchema.types['Group'] do expect { clean_state_query }.not_to exceed_all_query_limit(control) end end + + describe 'custom emoji' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: group) } + let_it_be(:custom_emoji_subgroup) { create(:custom_emoji, group: subgroup) } + let(:query) do + %( + query { + group(fullPath: "#{subgroup.full_path}") { + customEmoji(includeAncestorGroups: true) { + nodes { + id + } + } + } + } + ) + end + + before_all do + group.add_reporter(user) + end + + describe 'when includeAncestorGroups is true' do + it 'returns emoji from ancestor groups' do + result = GitlabSchema.execute(query, context: { current_user: user }).as_json + + expect(result.dig('data', 'group', 'customEmoji', 'nodes').count).to eq(2) + end + end + end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index fc3dd28c297..3965312316b 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe GitlabSchema.types['Project'] do +RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_projects do include GraphqlHelpers include ProjectForksHelper using RSpec::Parameterized::TableSyntax @@ -41,7 +41,7 @@ RSpec.describe GitlabSchema.types['Project'] do recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users - ci_cd_settings detailed_import_status + ci_cd_settings detailed_import_status value_streams ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb index 7fd25eac81b..4fc9d9dd4f6 100644 --- a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb @@ -55,4 +55,12 @@ RSpec.describe Banzai::Filter::CustomEmojiFilter, feature_category: :team_planni filter('

:tanuki:

:party-parrot:

') end.not_to exceed_all_query_limit(control_count.count) end + + it 'uses custom emoji from ancestor group' do + subgroup = create(:group, parent: group) + + doc = filter('

:tanuki:

', group: subgroup) + + expect(doc.css('gl-emoji').size).to eq 1 + end end diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index 87abd8a676d..a901453ba9f 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -319,6 +319,17 @@ RSpec.describe AwardEmoji, feature_category: :team_planning do expect(new_award.url).to eq(custom_emoji.url) end + describe 'when inside subgroup' do + let_it_be(:subgroup) { create(:group, parent: custom_emoji.group) } + let_it_be(:project) { create(:project, namespace: subgroup) } + + it 'is set for custom emoji' do + new_award = build_award(custom_emoji.name) + + expect(new_award.url).to eq(custom_emoji.url) + end + end + context 'feature flag disabled' do before do stub_feature_flags(custom_emoji: false) diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 15655d08556..cbdf05cf28f 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -48,4 +48,45 @@ RSpec.describe CustomEmoji do expect(emoji.errors.messages).to eq(file: ["is blocked: Only allowed schemes are http, https"]) end end + + describe '#for_resource' do + let_it_be(:group) { create(:group) } + let_it_be(:custom_emoji) { create(:custom_emoji, namespace: group) } + + context 'when custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it { expect(described_class.for_resource(group)).to eq([]) } + end + + context 'when group is nil' do + let_it_be(:group) { nil } + + it { expect(described_class.for_resource(group)).to eq([]) } + end + + context 'when resource is a project' do + let_it_be(:project) { create(:project) } + + it { expect(described_class.for_resource(project)).to eq([]) } + end + + it { expect(described_class.for_resource(group)).to eq([custom_emoji]) } + end + + describe '#for_namespaces' do + let_it_be(:group) { create(:group) } + let_it_be(:custom_emoji) { create(:custom_emoji, namespace: group, name: 'parrot') } + + it { expect(described_class.for_namespaces([group.id])).to eq([custom_emoji]) } + + context 'with subgroup' do + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_emoji) { create(:custom_emoji, namespace: subgroup, name: 'parrot') } + + it { expect(described_class.for_namespaces([subgroup.id, group.id])).to eq([subgroup_emoji]) } + end + end end diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb index 503d3514a72..678224a3c8e 100644 --- a/spec/models/ml/candidate_spec.rb +++ b/spec/models/ml/candidate_spec.rb @@ -38,8 +38,8 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d describe 'validation' do let_it_be(:model) { create(:ml_models, project: candidate.project) } - let_it_be(:model_version1) { create(:ml_model_versions, model: model) } - let_it_be(:model_version2) { create(:ml_model_versions, model: model) } + let_it_be(:model_version1) { create(:ml_model_versions, model: model, candidate: nil) } + let_it_be(:model_version2) { create(:ml_model_versions, model: model, candidate: nil) } let_it_be(:validation_candidate) do create(:ml_candidates, model_version: model_version1, project: candidate.project) end diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 1858ea831dd..c89ad0002b4 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,14 +35,14 @@ RSpec.describe 'getting custom emoji within namespace', feature_category: :share expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + it 'returns empty array when the custom_emoji feature flag is disabled' do stub_feature_flags(custom_emoji: false) post_graphql(custom_emoji_query(group), current_user: current_user) expect(response).to have_gitlab_http_status(:ok) expect(graphql_data['group']).to be_present - expect(graphql_data['group']['customEmoji']).to be_nil + expect(graphql_data['group']['customEmoji']['nodes']).to eq([]) end it 'returns nil group when unauthorised' do diff --git a/spec/requests/api/graphql/project/value_streams_spec.rb b/spec/requests/api/graphql/project/value_streams_spec.rb new file mode 100644 index 00000000000..01e937c1e47 --- /dev/null +++ b/spec/requests/api/graphql/project/value_streams_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.value_streams', feature_category: :value_stream_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + <<~QUERY + query($fullPath: ID!) { + project(fullPath: $fullPath) { + valueStreams { + nodes { + name + stages { + name + startEventIdentifier + endEventIdentifier + } + } + } + } + } + QUERY + end + + context 'when user has permissions to read value streams' do + let(:expected_value_stream) do + { + 'project' => { + 'valueStreams' => { + 'nodes' => [ + { + 'name' => 'default', + 'stages' => expected_stages + } + ] + } + } + } + end + + let(:expected_stages) do + [ + { + 'name' => 'issue', + 'startEventIdentifier' => 'ISSUE_CREATED', + 'endEventIdentifier' => 'ISSUE_STAGE_END' + }, + { + 'name' => 'plan', + 'startEventIdentifier' => 'PLAN_STAGE_START', + 'endEventIdentifier' => 'ISSUE_FIRST_MENTIONED_IN_COMMIT' + }, + { + 'name' => 'code', + 'startEventIdentifier' => 'CODE_STAGE_START', + 'endEventIdentifier' => 'MERGE_REQUEST_CREATED' + }, + { + 'name' => 'test', + 'startEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_STARTED', + 'endEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_FINISHED' + }, + { + 'name' => 'review', + 'startEventIdentifier' => 'MERGE_REQUEST_CREATED', + 'endEventIdentifier' => 'MERGE_REQUEST_MERGED' + }, + { + 'name' => 'staging', + 'startEventIdentifier' => 'MERGE_REQUEST_MERGED', + 'endEventIdentifier' => 'MERGE_REQUEST_FIRST_DEPLOYED_TO_PRODUCTION' + } + ] + end + + before_all do + project.add_guest(user) + end + + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it_behaves_like 'a working graphql query' + + it 'returns only `default` value stream' do + expect(graphql_data).to eq(expected_value_stream) + end + end + + context 'when user does not have permission to read value streams' do + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it 'returns nil' do + expect(graphql_data_at(:project, :valueStreams)).to be_nil + end + end +end diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb index 0fe10ed2c6d..4b818ce35e6 100644 --- a/spec/serializers/discussion_entity_spec.rb +++ b/spec/serializers/discussion_entity_spec.rb @@ -53,13 +53,6 @@ RSpec.describe DiscussionEntity do .to match_schema('entities/note_user_entity') end - it 'exposes the url for custom award emoji' do - custom_emoji = create(:custom_emoji, group: group) - create(:award_emoji, awardable: note, name: custom_emoji.name) - - expect(subject[:notes].last[:award_emoji].first.keys).to include(:url) - end - context 'when is LegacyDiffDiscussion' do let(:discussion) { create(:legacy_diff_note_on_merge_request, noteable: note.noteable, project: project).to_discussion } diff --git a/spec/services/ml/create_candidate_service_spec.rb b/spec/services/ml/create_candidate_service_spec.rb index fb3456b0bcc..b1a053711d7 100644 --- a/spec/services/ml/create_candidate_service_spec.rb +++ b/spec/services/ml/create_candidate_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe ::Ml::CreateCandidateService, feature_category: :mlops do describe '#execute' do - let_it_be(:model_version) { create(:ml_model_versions) } + let_it_be(:model_version) { create(:ml_model_versions, candidate: nil) } let_it_be(:experiment) { create(:ml_experiments, project: model_version.project) } let(:params) { {} } diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb index 99564003d59..ad581ee6093 100644 --- a/spec/views/admin/application_settings/general.html.haml_spec.rb +++ b/spec/views/admin/application_settings/general.html.haml_spec.rb @@ -115,7 +115,7 @@ RSpec.describe 'admin/application_settings/general.html.haml' do end # for the licensed tests, refer to ee/spec/views/admin/application_settings/general.html.haml_spec.rb - describe 'instance-level code suggestions settings', :without_license, feature_category: :code_suggestions do + describe 'instance-level ai-powered settings', :without_license, feature_category: :code_suggestions do before do allow(::Gitlab).to receive(:org_or_com?).and_return(gitlab_org_or_com?) @@ -125,6 +125,7 @@ RSpec.describe 'admin/application_settings/general.html.haml' do shared_examples 'does not render the form' do it 'does not render the form' do expect(rendered).not_to have_field('application_setting_instance_level_code_suggestions_enabled') + expect(rendered).not_to have_field('application_setting_instance_level_ai_beta_features_enabled') end end diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb index c28be165fd7..574dc922a36 100644 --- a/spec/workers/users/deactivate_dormant_users_worker_spec.rb +++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb @@ -10,34 +10,27 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost let_it_be(:inactive) { create(:user, last_activity_on: nil, created_at: User::MINIMUM_DAYS_CREATED.days.ago.to_date) } let_it_be(:inactive_recently_created) { create(:user, last_activity_on: nil, created_at: (User::MINIMUM_DAYS_CREATED - 1).days.ago.to_date) } - let(:admin_bot) { create(:user, :admin_bot) } - let(:deactivation_service) { instance_spy(Users::DeactivateService) } - - before do - allow(Users::DeactivateService).to receive(:new).and_return(deactivation_service) - end - subject(:worker) { described_class.new } it 'does not run for SaaS', :saas do - # Now makes a call to current settings to determine period of dormancy - worker.perform - expect(deactivation_service).not_to have_received(:execute) - end - - context 'when automatic deactivation of dormant users is enabled' do - before do - stub_application_setting(deactivate_dormant_users: true) + expect_any_instance_of(::Users::DeactivateService) do |deactivation_service| + expect(deactivation_service).not_to have_received(:execute) end + end - it 'deactivates dormant users' do - worker.perform - - expect(deactivation_service).to have_received(:execute).twice + shared_examples 'deactivates dormant users' do + specify do + expect { worker.perform } + .to change { dormant.reload.state } + .to('deactivated') + .and change { inactive.reload.state } + .to('deactivated') end + end + shared_examples 'deactivates certain user types' do where(:user_type, :expected_state) do :human | 'deactivated' :support_bot | 'active' @@ -52,33 +45,69 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost end with_them do - it 'deactivates certain user types' do + specify do user = create(:user, user_type: user_type, state: :active, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date) worker.perform - if expected_state == 'deactivated' - expect(deactivation_service).to have_received(:execute).with(user) - else - expect(deactivation_service).not_to have_received(:execute).with(user) + expect_any_instance_of(::Users::DeactivateService) do |deactivation_service| + if expected_state == 'deactivated' + expect(deactivation_service).to receive(:execute).with(user).and_call_original + else + expect(deactivation_service).not_to have_received(:execute).with(user) + end end + + expect(user.reload.state).to eq expected_state end end + end - it 'does not deactivate non-active users' do + shared_examples 'does not deactivate non-active users' do + specify do human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date) service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date) worker.perform - expect(deactivation_service).not_to have_received(:execute).with(human_user) - expect(deactivation_service).not_to have_received(:execute).with(service_user) + expect_any_instance_of(::Users::DeactivateService) do |deactivation_service| + expect(deactivation_service).not_to have_received(:execute).with(human_user) + expect(deactivation_service).not_to have_received(:execute).with(service_user) + end end + end - it 'does not deactivate recently created users' do + shared_examples 'does not deactivate recently created users' do + specify do worker.perform - expect(deactivation_service).not_to have_received(:execute).with(inactive_recently_created) + expect_any_instance_of(::Users::DeactivateService) do |deactivation_service| + expect(deactivation_service).not_to have_received(:execute).with(inactive_recently_created) + end + end + end + + context 'when automatic deactivation of dormant users is enabled' do + before do + stub_application_setting(deactivate_dormant_users: true) + end + + context 'when admin mode is not enabled', :do_not_mock_admin_mode_setting do + include_examples 'deactivates dormant users' + include_examples 'deactivates certain user types' + include_examples 'does not deactivate non-active users' + include_examples 'does not deactivate recently created users' + end + + context 'when admin mode is enabled', :request_store do + before do + stub_application_setting(admin_mode: true) + end + + include_examples 'deactivates dormant users' + include_examples 'deactivates certain user types' + include_examples 'does not deactivate non-active users' + include_examples 'does not deactivate recently created users' end end @@ -90,7 +119,9 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost it 'does nothing' do worker.perform - expect(deactivation_service).not_to have_received(:execute) + expect_any_instance_of(::Users::DeactivateService) do |deactivation_service| + expect(deactivation_service).not_to have_received(:execute) + end end end end diff --git a/yarn.lock b/yarn.lock index 3ed1e4f7f55..b72a2afd272 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5393,6 +5393,11 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -7449,10 +7454,10 @@ html-entities@^2.3.2: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== -html-escaper@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" - integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== +html-escaper@^2.0.0, html-escaper@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-tags@^3.3.1: version "3.3.1" @@ -8890,11 +8895,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw== - lodash.find@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" @@ -8980,11 +8980,6 @@ lodash.pick@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= -lodash.pullall@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.pullall/-/lodash.pullall-4.2.0.tgz#9d98b8518b7c965b0fae4099bd9fb7df8bbf38ba" - integrity sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg== - lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -13488,10 +13483,10 @@ vue-hot-reload-api@^2.3.0: hash-sum "^2.0.0" loader-utils "^2.0.0" -vue-loader@15.10.2: - version "15.10.2" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.10.2.tgz#6dccfda8661caa7f5415806a5e386fd3258d8112" - integrity sha512-ndeSe/8KQc/nlA7TJ+OBhv2qalmj1s+uBs7yHDRFaAXscFTApBzY9F1jES3bautmgWjDlDct0fw8rPuySDLwxw== +vue-loader@15.11.1: + version "15.11.1" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.11.1.tgz#dee91169211276ed43c5715caef88a56b1f497b0" + integrity sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q== dependencies: "@vue/component-compiler-utils" "^3.1.0" hash-sum "^1.0.2" @@ -13692,24 +13687,20 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== -webpack-bundle-analyzer@^4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz#d00bbf3f17500c10985084f22f1a2bf45cb2f09d" - integrity sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w== +webpack-bundle-analyzer@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== dependencies: "@discoveryjs/json-ext" "0.5.7" acorn "^8.0.4" acorn-walk "^8.0.0" commander "^7.2.0" + debounce "^1.2.1" escape-string-regexp "^4.0.0" gzip-size "^6.0.0" + html-escaper "^2.0.2" is-plain-object "^5.0.0" - lodash.debounce "^4.0.8" - lodash.escape "^4.0.1" - lodash.flatten "^4.4.0" - lodash.invokemap "^4.6.0" - lodash.pullall "^4.2.0" - lodash.uniqby "^4.7.0" opener "^1.5.2" picocolors "^1.0.0" sirv "^2.0.3" -- cgit v1.2.3