diff options
28 files changed, 568 insertions, 69 deletions
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue index 13388f02f1f..cfd6b21fa66 100644 --- a/app/assets/javascripts/boards/components/board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -27,7 +27,7 @@ export default { }, computed: { urlParams() { - const { authorUsername, labelName, search } = this.filterParams; + const { authorUsername, labelName, assigneeUsername, search } = this.filterParams; let notParams = {}; if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { @@ -35,6 +35,7 @@ export default { { 'not[label_name][]': this.filterParams.not.labelName, 'not[author_username]': this.filterParams.not.authorUsername, + 'not[assignee_username]': this.filterParams.not.assigneeUsername, }, undefined, ); @@ -44,6 +45,7 @@ export default { ...notParams, author_username: authorUsername, 'label_name[]': labelName, + assignee_username: assigneeUsername, search, }; }, @@ -62,7 +64,7 @@ export default { this.performSearch(); }, getFilteredSearchValue() { - const { authorUsername, labelName, search } = this.filterParams; + const { authorUsername, labelName, assigneeUsername, search } = this.filterParams; const filteredSearchValue = []; if (authorUsername) { @@ -72,6 +74,13 @@ export default { }); } + if (assigneeUsername) { + filteredSearchValue.push({ + type: 'assignee_username', + value: { data: assigneeUsername, operator: '=' }, + }); + } + if (labelName?.length) { filteredSearchValue.push( ...labelName.map((label) => ({ @@ -88,6 +97,13 @@ export default { }); } + if (this.filterParams['not[assigneeUsername]']) { + filteredSearchValue.push({ + type: 'assignee_username', + value: { data: this.filterParams['not[assigneeUsername]'], operator: '!=' }, + }); + } + if (this.filterParams['not[labelName]']) { filteredSearchValue.push( ...this.filterParams['not[labelName]'].map((label) => ({ @@ -121,6 +137,9 @@ export default { case 'author_username': filterParams.authorUsername = filter.value.data; break; + case 'assignee_username': + filterParams.assigneeUsername = filter.value.data; + break; case 'label_name': labels.push(filter.value.data); break; diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue new file mode 100644 index 00000000000..d8dac17d326 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue @@ -0,0 +1,102 @@ +<script> +import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; +import issueBoardFilters from '~/boards/issue_board_filters'; +import { TYPE_USER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { __ } from '~/locale'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; + +export default { + i18n: { + search: __('Search'), + label: __('Label'), + author: __('Author'), + assignee: __('Assignee'), + is: __('is'), + isNot: __('is not'), + }, + components: { BoardFilteredSearch }, + props: { + fullPath: { + type: String, + required: true, + }, + boardType: { + type: String, + required: true, + }, + }, + computed: { + tokens() { + const { label, is, isNot, author, assignee } = this.$options.i18n; + const { fetchAuthors, fetchLabels } = issueBoardFilters( + this.$apollo, + this.fullPath, + this.boardType, + ); + + return [ + { + icon: 'labels', + title: label, + type: 'label_name', + operators: [ + { value: '=', description: is }, + { value: '!=', description: isNot }, + ], + token: LabelToken, + unique: false, + symbol: '~', + fetchLabels, + }, + { + icon: 'pencil', + title: author, + type: 'author_username', + operators: [ + { value: '=', description: is }, + { value: '!=', description: isNot }, + ], + symbol: '@', + token: AuthorToken, + unique: true, + fetchAuthors, + preloadedAuthors: this.preloadedAuthors(), + }, + { + icon: 'user', + title: assignee, + type: 'assignee_username', + operators: [ + { value: '=', description: is }, + { value: '!=', description: isNot }, + ], + token: AuthorToken, + unique: true, + fetchAuthors, + preloadedAuthors: this.preloadedAuthors(), + }, + ]; + }, + }, + methods: { + preloadedAuthors() { + return gon?.current_user_id + ? [ + { + id: convertToGraphQLId(TYPE_USER, gon.current_user_id), + name: gon.current_user_fullname, + username: gon.current_username, + avatarUrl: gon.current_user_avatar_url, + }, + ] + : []; + }, + }, +}; +</script> + +<template> + <board-filtered-search data-testid="issue-board-filtered-search" :tokens="tokens" /> +</template> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 9416fbb7825..de7c8a3bd6b 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -25,6 +25,7 @@ import '~/boards/filters/due_date_filters'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import FilteredSearchBoards from '~/boards/filtered_search_boards'; +import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards'; import store from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; import toggleFocusMode from '~/boards/toggle_focus'; @@ -78,6 +79,10 @@ export default () => { issueBoardsApp.$destroy(true); } + if (gon?.features?.issueBoardsFilteredSearch) { + initBoardsFilteredSearch(apolloProvider); + } + if (!gon?.features?.graphqlBoardLists) { boardsStore.create(); boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); @@ -184,9 +189,14 @@ export default () => { eventHub.$off('initialBoardLoad', this.initialBoardLoad); }, mounted() { - this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit); - - this.filterManager.setup(); + if (!gon?.features?.issueBoardsFilteredSearch) { + this.filterManager = new FilteredSearchBoards( + boardsStore.filter, + true, + boardsStore.cantEdit, + ); + this.filterManager.setup(); + } this.performSearch(); diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js new file mode 100644 index 00000000000..699d7e12de4 --- /dev/null +++ b/app/assets/javascripts/boards/issue_board_filters.js @@ -0,0 +1,47 @@ +import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql'; +import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql'; +import { BoardType } from './constants'; +import boardLabels from './graphql/board_labels.query.graphql'; + +export default function issueBoardFilters(apollo, fullPath, boardType) { + const isGroupBoard = boardType === BoardType.group; + const isProjectBoard = boardType === BoardType.project; + const transformLabels = ({ data }) => { + return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || []; + }; + + const boardAssigneesQuery = () => { + return isGroupBoard ? groupBoardMembers : projectBoardMembers; + }; + + const fetchAuthors = (authorsSearchTerm) => { + return apollo + .query({ + query: boardAssigneesQuery(), + variables: { + fullPath, + search: authorsSearchTerm, + }, + }) + .then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user)); + }; + + const fetchLabels = (labelSearchTerm) => { + return apollo + .query({ + query: boardLabels, + variables: { + fullPath, + searchTerm: labelSearchTerm, + isGroup: isGroupBoard, + isProject: isProjectBoard, + }, + }) + .then(transformLabels); + }; + + return { + fetchLabels, + fetchAuthors, + }; +} diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js new file mode 100644 index 00000000000..7732091ef34 --- /dev/null +++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue'; +import store from '~/boards/stores'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { queryToObject } from '~/lib/utils/url_utility'; + +export default (apolloProvider) => { + const el = document.getElementById('js-issue-board-filtered-search'); + const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true }); + + const initialFilterParams = { + ...convertObjectPropsToCamelCase(rawFilterParams, {}), + }; + + if (!el) { + return null; + } + + return new Vue({ + el, + provide: { + initialFilterParams, + }, + store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094 + apolloProvider, + render: (createElement) => + createElement(IssueBoardFilteredSearch, { + props: { fullPath: store.state?.fullPath || '', boardType: store.state?.boardType || '' }, + }), + }); +}; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 3cc62cf124c..ad0b27c9693 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -178,7 +178,7 @@ export default { </div> </div> <div v-if="isGroupPendingRemoval"> - <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge> + <gl-badge variant="warning">{{ __('pending deletion') }}</gl-badge> </div> <div class="metadata d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"> <item-actions diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index e09df8a5d26..7a37d1eb93d 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -73,7 +73,7 @@ export default { icon-name="star" /> <div v-if="isProjectPendingRemoval"> - <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge> + <gl-badge variant="warning">{{ __('pending deletion') }}</gl-badge> </div> <div v-if="isProject" class="last-updated"> <time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" /> diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue index 88f4bba5e2a..d41488acf46 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue @@ -101,7 +101,7 @@ export default { v-if="isGroupPendingRemoval" variant="warning" class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1" - >{{ __('pending removal') }}</gl-badge + >{{ __('pending deletion') }}</gl-badge > <user-access-role-badge v-if="group.permission" class="gl-mt-3"> {{ group.permission }} diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue index f6e88738002..f1fe8cf10fd 100644 --- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue +++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue @@ -103,6 +103,7 @@ export default { v-model="targetBranch" class="gl-font-monospace!" required + data-qa-selector="target_branch_field" /> <gl-form-checkbox v-if="!isCurrentBranchTarget" diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue index e01f9cc79c0..ee6d4ff7c4d 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue @@ -211,6 +211,7 @@ export default { :header-text="$options.i18n.dropdownHeader" :text="currentBranch" icon="branch" + data-qa-selector="branch_selector_button" > <gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" /> <gl-dropdown-section-header> @@ -228,6 +229,7 @@ export default { :key="branch" :is-checked="currentBranch === branch" :is-check-item="true" + data-qa-selector="menu_branch_button" @click="selectBranch(branch)" > {{ branch }} diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 3d8cdd766bf..04b4d8ea9a7 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action do push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) + push_frontend_feature_flag(:issue_boards_filtered_search, group, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml) diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 43c9046f850..035b76abfd6 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action do push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml) + push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) end diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index c03697a4076..737a0ff8c5b 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,6 +25,8 @@ = check_box_tag checkbox_id, nil, false, class: "check-all-issues left" - if is_epic_board #js-board-filtered-search{ data: { full_path: @group&.full_path } } + - elsif Feature.enabled?(:issue_boards_filtered_search, board&.resource_parent) && board + #js-issue-board-filtered-search - else .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row .filtered-search-box diff --git a/config/feature_flags/development/issue_boards_filtered_search.yml b/config/feature_flags/development/issue_boards_filtered_search.yml new file mode 100644 index 00000000000..38cf723d69e --- /dev/null +++ b/config/feature_flags/development/issue_boards_filtered_search.yml @@ -0,0 +1,8 @@ +--- +name: issue_boards_filtered_search +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61752 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331649 +milestone: '14.1' +type: development +group: group::product planning +default_enabled: false diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index ed9411cdcb7..ae921d4822a 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -193,7 +193,7 @@ on adding these events into GitLab: Don't see the event you want in any of the epics linked above? You can use the **Audit Event Proposal** issue template to [create an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Audit%20Event%20Proposal) -to request it. +to request it, or you can [add it yourself](../development/audit_event_guide/). ### Disabled events diff --git a/doc/ci/unit_test_reports.md b/doc/ci/unit_test_reports.md index 4a8286b3081..f845c79ef45 100644 --- a/doc/ci/unit_test_reports.md +++ b/doc/ci/unit_test_reports.md @@ -350,6 +350,11 @@ If parsing JUnit report XML results in an error, an indicator is shown next to t ![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png) +NOTE: +GitLab.com has a 500,000 [test case parsing limit](../user/gitlab_com/#gitlab-cicd). Self-managed administrators can manage this setting on their instance. + +GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional. + ## Viewing JUnit screenshots on GitLab > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0 behind the `:junit_pipeline_screenshots_view` feature flag, disabled by default. diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 363f01377a2..3fba9078bab 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -9,6 +9,11 @@ description: 'Writing styles, markup, formatting, and other standards for GitLab To help ensure consistency in the documentation, follow this guidance. +For guidance not on this page, we defer to these style guides: + +- [Microsoft Style Guide](https://docs.microsoft.com/en-us/style-guide/welcome/) +- [Google Developer Documentation Style Guide](https://developers.google.com/style) + <!-- vale off --> <!-- markdownlint-disable --> @@ -22,7 +27,13 @@ Use **administration**, **administrator**, **administer**, or **Admin Area** ins ## allow, enable -Try to avoid, unless you are talking about security-related features. For example, instead of "This feature allows you to create a pipeline," use "Use this feature to create a pipeline." This phrasing is more active and is from the user perspective, rather than the person who implemented the feature. [View details](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/allow-allows). +Try to avoid, unless you are talking about security-related features. For example: + +- Avoid: This feature allows you to create a pipeline. +- Use instead: Use this feature to create a pipeline. + +This phrasing is more active and is from the user perspective, rather than the person who implemented the feature. +[View details in the Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/allow-allows). ## and/or @@ -44,13 +55,14 @@ Do not use when talking about the product or its features. The documentation des When writing about the Developer role: -- Use a capital "D." -- Do not use the phrase, "if you are a developer" to mean someone who is assigned the Developer - role. Instead, write it out. "If you are assigned the Developer role..." -- To describe a situation where the Developer role is the minimum required, use the phrase "at least - the Developer role..." +- Use a capital **D**. +- Do not use the phrase, **if you are a developer** to mean someone who is assigned the Developer + role. Instead, write it out. For example, **if you are assigned the Developer role**. +- To describe a situation where the Developer role is the minimum required: + - Avoid: **the Developer role or higher** + - Use instead: **at least the Developer role** -Do not use "Developer permissions." A user who is assigned the Developer role has a set of associated permissions. +Do not use **Developer permissions**. A user who is assigned the Developer role has a set of associated permissions. ## disable @@ -59,7 +71,7 @@ Use **inactive** or **off** instead. ([Vale](../testing.md#vale) rule: [`Inclusi ## easily -Do not use. If the user doesn't find the process to be these things, we lose their trust. +Do not use. If the user doesn't find the process to be easy, we lose their trust. ## e.g. @@ -99,17 +111,18 @@ Refers to the product license for GitLab instances managed by customers themselv When writing about the Guest role: -- Use a capital "G." -- Do not use the phrase, "if you are a guest" to mean someone who is assigned the Guest role. - Instead, write it out. "If you are assigned the Guest role..." -- To describe a situation where the Guest role is the minimum required, use the phrase "at - least the Guest role..." +- Use a capital **G**. +- Do not use the phrase, **if you are a guest** to mean someone who is assigned the Guest + role. Instead, write it out. For example, **if you are assigned the Guest role**. +- To describe a situation where the Guest role is the minimum required: + - Avoid: **the Guest role or higher** + - Use instead: **at least the Guest role** -Do not use "Guest permissions." A user who is assigned the Guest role has a set of associated permissions. +Do not use **Guest permissions**. A user who is assigned the Guest role has a set of associated permissions. ## handy -Do not use. If the user doesn't find the process to be these things, we lose their trust. +Do not use. If the user doesn't find the feature or process to be handy, we lose their trust. ## high availability, HA @@ -127,13 +140,14 @@ Do not use Latin abbreviations. Use **that is** instead. ([Vale](../testing.md#v When writing about the Maintainer role: -- Use a capital "M." -- Do not use the phrase, "if you are a maintainer" to mean someone who is assigned the Maintainer - role. Instead, write it out. "If you are assigned the Maintainer role..." -- To describe a situation where the Maintainer role is the minimum required, use the phrase "at - least the Maintainer role..." +- Use a capital **M**. +- Do not use the phrase, **if you are a maintainer** to mean someone who is assigned the Maintainer + role. Instead, write it out. For example, **if you are assigned the Maintainer role**. +- To describe a situation where the Maintainer role is the minimum required: + - Avoid: **the Maintainer role or higher** + - Use instead: **at least the Maintainer role** -Do not use "Maintainer permissions." A user who is assigned the Maintainer role has a set of associated permissions. +Do not use **Maintainer permissions**. A user who is assigned the Maintainer role has a set of associated permissions. ## mankind @@ -163,11 +177,11 @@ Lowercase. If you use **MR** as the acronym, spell it out on first use. When writing about the Owner role: -- Use a capital "O." -- Do not use the phrase, "if you are an owner" to mean someone who is assigned the Owner role. - Instead, write it out. "If you are assigned the Owner role..." +- Use a capital **O**. +- Do not use the phrase, **if you are an owner** to mean someone who is assigned the Owner + role. Instead, write it out. For example, **if you are assigned the Owner role**. -Do not use "Owner permissions." A user who is assigned the Owner role has a set of associated permissions. +Do not use **Owner permissions**. A user who is assigned the Owner role has a set of associated permissions. ## permissions @@ -183,10 +197,16 @@ Do not use. Doing so may negatively affect other users and contributors, which i ## Reporter -When writing about the Reporter role, use a capital "R." Do not use the phrase, "if you are a reporter" -to mean someone who is assigned the Reporter role. Instead, write it out. "If you are assigned the Reporter role..." +When writing about the Reporter role: + +- Use a capital **R**. +- Do not use the phrase, **if you are a reporter** to mean someone who is assigned the Reporter + role. Instead, write it out. For example, **if you are assigned the Reporter role**. +- To describe a situation where the Reporter role is the minimum required: + - Avoid: **the Reporter role or higher** + - Use instead: **at least the Reporter role** -Do not use "Reporter permissions." A user who is assigned the Reporter role has a set of associated permissions. +Do not use **Reporter permissions**. A user who is assigned the Reporter role has a set of associated permissions. ## roles @@ -202,10 +222,10 @@ Do not use when talking about increasing GitLab performance for additional users ## setup, set up -Use **setup** as a noun, and **set up** as a verb. Examples: +Use **setup** as a noun, and **set up** as a verb. For example: -- `Your remote office setup is amazing.` -- `To set up your remote office correctly, first consider the ergonomics of your work area.` +- Your remote office setup is amazing. +- To set up your remote office correctly, consider the ergonomics of your work area. ## simply, simple @@ -213,7 +233,7 @@ Do not use. If the user doesn't find the process to be simple, we lose their tru ## slashes -Instead of **and/or**, use **or** or another sensible construction. This rule also applies to other slashes, like **follow/unfollow**. Some exceptions (like **CI/CD**) are allowed. +Instead of **and/or**, use **or** or re-write the sentence. This rule also applies to other slashes, like **follow/unfollow**. Some exceptions (like **CI/CD**) are allowed. ## slave @@ -221,11 +241,14 @@ Do not use. Another option is **secondary**. ([Vale](../testing.md#vale) rule: [ ## subgroup -Use instead of `sub-group`. +Use instead of **sub-group**. ## that -Do not use. Example: `the file that you save` can be `the file you save`. +Do not use. For example: + +- Avoid: The file that you save... +- Use instead: The file you save... ## they @@ -235,7 +258,7 @@ a gender-neutral pronoun. ## useful -Do not use. If the user doesn't find the process to be these things, we lose their trust. +Do not use. If the user doesn't find the process to be useful, we lose their trust. ## utilize @@ -247,12 +270,12 @@ Do not use Latin abbreviations. Use **with**, **through**, or **by using** inste ## we -Try to avoid "we" and focus instead on how the user can accomplish something in GitLab. +Try to avoid **we** and focus instead on how the user can accomplish something in GitLab. -Instead of: We created a feature for you to add widgets. -Use: Use widgets when you have work you want to organize. +- Avoid: We created a feature for you to add widgets. +- Instead, use: Use widgets when you have work you want to organize. -One exception: You can use "we recommend" instead of "it is recommended" or "GitLab recommends." +One exception: You can use **we recommend** instead of **it is recommended** or **GitLab recommends**. ## whitelist diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index e5949935fcc..9f1df5c2b89 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -267,7 +267,8 @@ Learn how to [export a project](import_export.md#importing-the-project) in GitLa ### Advanced settings -Here you can run housekeeping, archive, rename, transfer, [remove a fork relationship](#removing-a-fork-relationship), or remove a project. +Here you can run housekeeping, archive, rename, transfer, +[remove a fork relationship](#removing-a-fork-relationship), or delete a project. #### Archiving a project @@ -372,17 +373,16 @@ To delete a project: 1. In the "Delete project" section, click the **Delete project** button. 1. Confirm the action when asked to. -This action: +This action deletes a project including all associated resources (issues, merge requests, and so on). -- Deletes a project including all associated resources (issues, merge requests etc). -- From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium](https://about.gitlab.com/pricing/) or higher tiers, - group Owners can [configure](../../group/index.md#enable-delayed-project-removal) projects within a group - to be deleted after a delayed period. - When enabled, actual deletion happens after number of days - specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). +In [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) and later, on Premium or higher tiers, +group Owners can [configure](../../group/index.md#enable-delayed-project-removal) projects in a group +to be deleted after a delayed period. +When enabled, actual deletion happens after number of days +specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). WARNING: -The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to +The default behavior of [delayed project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to [Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2. #### Restore a project **(PREMIUM)** diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dd5bee39a50..2e533c6ecdd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10671,7 +10671,7 @@ msgstr "" msgid "Deleting the project will delete its repository and all related resources including issues, merge requests, etc." msgstr "" -msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only." +msgid "Deletion pending. This project will be deleted on %{date}. Repository and other project resources are read-only." msgstr "" msgid "Denied" @@ -32661,7 +32661,7 @@ msgstr "" msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." msgstr "" -msgid "The group will be placed in 'pending removal' state" +msgid "The group will be placed in 'pending deletion' state" msgstr "" msgid "The group_project_ids parameter is only allowed for a group" @@ -33741,10 +33741,10 @@ msgstr "" msgid "This project path either does not exist or you do not have access." msgstr "" -msgid "This project will be removed on %{date}" +msgid "This project will be deleted on %{date}" msgstr "" -msgid "This project will be removed on %{date} since its parent group '%{parent_group_name}' has been scheduled for removal." +msgid "This project will be deleted on %{date} since its parent group '%{parent_group_name}' has been scheduled for deletion." msgstr "" msgid "This project will live in your group %{strong_open}%{namespace}%{strong_close}. A project is where you house your files (repository), plan your work (issues), publish your documentation (wiki), and so much more." @@ -35340,7 +35340,7 @@ msgstr "" msgid "Uploads" msgstr "" -msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. Until that time:" +msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently deleted after %{deletion_adjourned_period} days on %{date}. Until that time:" msgstr "" msgid "Upstream" @@ -39523,7 +39523,7 @@ msgstr "" msgid "pending comment" msgstr "" -msgid "pending removal" +msgid "pending deletion" msgstr "" msgid "per day" @@ -297,6 +297,10 @@ module QA autoload :New, 'qa/page/project/pipeline/new' end + module PipelineEditor + autoload :Show, 'qa/page/project/pipeline_editor/show' + end + module Tag autoload :Index, 'qa/page/project/tag/index' autoload :New, 'qa/page/project/tag/new' diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb new file mode 100644 index 00000000000..38c87c8daa1 --- /dev/null +++ b/qa/qa/page/project/pipeline_editor/show.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module PipelineEditor + class Show < QA::Page::Base + view 'app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue' do + element :branch_selector_button + element :menu_branch_button + end + + view 'app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue' do + element :target_branch_field + end + + def has_branch_selector_button? + has_element? :branch_selector_button + end + + def click_branch_selector_button + wait_until(reload: false) do + has_element?(:branch_selector_button) + end + click_element(:branch_selector_button, skip_finished_loading_check: true) + end + + def select_branch_from_dropdown(branch_to_switch_to) + wait_until(reload: false) do + has_element?(:menu_branch_button) + end + click_element(:menu_branch_button, text: branch_to_switch_to, skip_finished_loading_check: true) + end + + def target_branch_name + wait_until(reload: false) do + has_element?(:target_branch_field) + end + find_element(:target_branch_field, skip_finished_loading_check: true).value + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index 7cb2fd6c655..c8c90df2c1f 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -20,6 +20,24 @@ module QA click_element(:sidebar_menu_link, menu_item: 'CI/CD') end end + + def go_to_pipeline_editor + hover_ci_cd_pipelines do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Editor') + end + end + end + + private + + def hover_ci_cd_pipelines + within_sidebar do + find_element(:sidebar_menu_link, menu_item: 'CI/CD').hover + + yield + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_branch_switcher_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_branch_switcher_spec.rb new file mode 100644 index 00000000000..bb9b5feed2e --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_branch_switcher_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify' do + describe 'Pipeline editor', :requires_admin do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-editor-project' + end + end + + let!(:commit) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: default_file_content + } + ] + ) + end + end + + let!(:production_push) do + Resource::Repository::Push.fabricate! do |push| + push.repository_http_uri = project.repository_http_location.uri + push.branch_name = 'production' + push.file_name = '.gitlab-ci.yml' + push.file_content = production_file_content + end + end + + let(:default_file_content) do + <<~YAML + stages: + - test + + initialize: + stage: test + script: + - echo "initialized in #{project.default_branch}" + YAML + end + + let(:production_file_content) do + <<~YAML + stages: + - test + + initialize: + stage: test + script: + - echo "initialized in production" + YAML + end + + before do + Runtime::Feature.enable(:pipeline_editor_branch_switcher) + Flow::Login.sign_in + project.visit! + Page::Project::Menu.perform(&:go_to_pipeline_editor) + end + + after do + Runtime::Feature.disable(:pipeline_editor_branch_switcher) + project.remove_via_api! + Page::Main::Menu.perform(&:sign_out) + end + + it 'can switch branches and target branch field updates accordingly', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1856' do + Page::Project::PipelineEditor::Show.perform do |show| + expect(show).to have_branch_selector_button + + show.click_branch_selector_button + show.select_branch_from_dropdown(production_push.branch_name) + + expect(show.target_branch_name).to eq(production_push.branch_name) + + show.click_branch_selector_button + show.select_branch_from_dropdown(project.default_branch) + + expect(show.target_branch_name).to eq(project.default_branch) + end + end + end + end +end diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js new file mode 100644 index 00000000000..0e3cf59901e --- /dev/null +++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js @@ -0,0 +1,44 @@ +import { shallowMount } from '@vue/test-utils'; +import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; +import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue'; +import { BoardType } from '~/boards/constants'; +import issueBoardFilters from '~/boards/issue_board_filters'; +import { mockTokens } from '../mock_data'; + +describe('IssueBoardFilter', () => { + let wrapper; + + const createComponent = ({ initialFilterParams = {} } = {}) => { + wrapper = shallowMount(IssueBoardFilteredSpec, { + provide: { initialFilterParams }, + props: { fullPath: '', boardType: '' }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('finds BoardFilteredSearch', () => { + expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true); + }); + + it.each([[BoardType.group], [BoardType.project]])( + 'when boardType is %s we pass the correct tokens to BoardFilteredSearch', + (boardType) => { + const { fetchAuthors, fetchLabels } = issueBoardFilters({}, '', boardType); + + const tokens = mockTokens(fetchLabels, fetchAuthors); + + expect(wrapper.find(BoardFilteredSearch).props('tokens').toString()).toBe( + tokens.toString(), + ); + }, + ); + }); +}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index bcaca9522e4..8bc80fc2007 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -5,6 +5,9 @@ import Vue from 'vue'; import '~/boards/models/list'; import { ListType } from '~/boards/constants'; import boardsStore from '~/boards/stores/boards_store'; +import { __ } from '~/locale'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; export const boardObj = { id: 1, @@ -526,3 +529,44 @@ export const mockMoveData = { originalIssue: { foo: 'bar' }, ...mockMoveIssueParams, }; + +export const mockTokens = (fetchLabels, fetchAuthors) => [ + { + icon: 'labels', + title: __('Label'), + type: 'label_name', + operators: [ + { value: '=', description: 'is' }, + { value: '!=', description: 'is not' }, + ], + token: LabelToken, + unique: false, + symbol: '~', + fetchLabels, + }, + { + icon: 'pencil', + title: __('Author'), + type: 'author_username', + operators: [ + { value: '=', description: 'is' }, + { value: '!=', description: 'is not' }, + ], + symbol: '@', + token: AuthorToken, + unique: true, + fetchAuthors, + }, + { + icon: 'user', + title: __('Assignee'), + type: 'assignee_username', + operators: [ + { value: '=', description: 'is' }, + { value: '!=', description: 'is not' }, + ], + token: AuthorToken, + unique: true, + fetchAuthors, + }, +]; diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index b1211e075bf..2369685f506 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -162,11 +162,11 @@ describe('GroupItemComponent', () => { wrapper = createComponent({ group }); }); - it('renders the group pending removal badge', () => { + it('renders the group pending deletion badge', () => { const badgeEl = wrapper.vm.$el.querySelector('.badge-warning'); expect(badgeEl).toBeDefined(); - expect(badgeEl.innerHTML).toContain('pending removal'); + expect(badgeEl.innerHTML).toContain('pending deletion'); }); }); @@ -176,10 +176,10 @@ describe('GroupItemComponent', () => { wrapper = createComponent({ group }); }); - it('does not render the group pending removal badge', () => { + it('does not render the group pending deletion badge', () => { const groupTextContainer = wrapper.vm.$el.querySelector('.group-text-container'); - expect(groupTextContainer).not.toContain('pending removal'); + expect(groupTextContainer).not.toContain('pending deletion'); }); it('renders `item-actions` component and passes correct props to it', () => { diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js index b5425fa6f2e..490dafed4ae 100644 --- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js @@ -34,10 +34,10 @@ describe('Fork groups list item component', () => { }); }; - it('renders pending removal badge if applicable', () => { + it('renders pending deletion badge if applicable', () => { createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } }); - expect(wrapper.find(GlBadge).text()).toBe('pending removal'); + expect(wrapper.find(GlBadge).text()).toBe('pending deletion'); }); it('renders go to fork button if has forked project', () => { diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7383fc91bc1..d339ac67810 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -293,6 +293,8 @@ RSpec.configure do |config| # As we're ready to change `master` usages to `main`, let's enable it stub_feature_flags(main_branch_over_master: false) + stub_feature_flags(issue_boards_filtered_search: false) + # Disable issue respositioning to avoid heavy load on database when importing big projects. # This is only turned on when app is handling heavy project imports. # Can be removed when we find a better way to deal with the problem. |