diff options
26 files changed, 311 insertions, 165 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 486da5bb906..882ae337c60 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -130,27 +130,6 @@ variables: REGISTRY_HOST: "registry.gitlab.com" REGISTRY_GROUP: "gitlab-org" - # Preparing custom clone path to reduce space used by all random forks - # on GitLab.com's Shared Runners. Our main forks - especially the security - # ones - will have this variable overwritten in the project settings, so that - # a security-related code or code using our protected variables will be never - # stored on the same path as the community forks. - # Part of the solution for the `no space left on device` problem described at - # https://gitlab.com/gitlab-org/gitlab/issues/197876. - # - # For this purpose the https://gitlab.com/gitlab-org-forks group was created - # to host a placeholder for the `/builds/gitlab-org-forks` path and ensure - # that no legitimate project will ever use it and - by mistake - execute its - # job on a shared working directory. It also requires proper configuration of - # the Runner that executes the job (which was prepared for our shared runners - # by https://ops.gitlab.net/gitlab-cookbooks/chef-repo/-/merge_requests/3977). - # - # Because of all of that PLEASE DO NOT CHANGE THE PATH. - # - # For more details and reasoning that brought this change please check - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24887 - GIT_CLONE_PATH: "/builds/gitlab-org-forks/${CI_PROJECT_NAME}" - include: - local: .gitlab/ci/*.gitlab-ci.yml - remote: 'https://gitlab.com/gitlab-org/frontend/untamper-my-lockfile/-/raw/main/templates/merge_request_pipelines.yml' diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue index 111b670596b..3cd1a2525e9 100644 --- a/app/assets/javascripts/batch_comments/components/review_bar.vue +++ b/app/assets/javascripts/batch_comments/components/review_bar.vue @@ -2,20 +2,10 @@ import { mapActions, mapGetters } from 'vuex'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants'; -import { PREVENT_LEAVING_PENDING_REVIEW } from '../i18n'; import PreviewDropdown from './preview_dropdown.vue'; import PublishButton from './publish_button.vue'; import SubmitDropdown from './submit_dropdown.vue'; -function closeInterrupt(event) { - event.preventDefault(); - - // This is the correct way to write backwards-compatible beforeunload listeners - // https://developer.chrome.com/blog/page-lifecycle-api/#the-beforeunload-event - /* eslint-disable-next-line no-return-assign, no-param-reassign */ - return (event.returnValue = PREVENT_LEAVING_PENDING_REVIEW); -} - export default { components: { PreviewDropdown, @@ -35,26 +25,8 @@ export default { }, mounted() { document.body.classList.add(REVIEW_BAR_VISIBLE_CLASS_NAME); - /* - * This stuff is a lot trickier than it looks. - * - * Mandatory reading: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - * Some notable sentences: - * - "[...] browsers may not display prompts created in beforeunload event handlers unless the - * page has been interacted with, or may even not display them at all." - * - "Especially on mobile, the beforeunload event is not reliably fired." - * - "The beforeunload event is not compatible with the back/forward cache (bfcache) [...] - * It is recommended that developers listen for beforeunload only in this scenario, and only - * when they actually have unsaved changes, so as to minimize the effect on performance." - * - * Please ensure that this is really not working before you modify it, because there are a LOT - * of scenarios where browser behavior will make it _seem_ like it's not working, but it actually - * is under the right combination of contexts. - */ - window.addEventListener('beforeunload', closeInterrupt, { capture: true }); }, beforeDestroy() { - window.removeEventListener('beforeunload', closeInterrupt, { capture: true }); document.body.classList.remove(REVIEW_BAR_VISIBLE_CLASS_NAME); }, methods: { diff --git a/app/assets/javascripts/batch_comments/i18n.js b/app/assets/javascripts/batch_comments/i18n.js deleted file mode 100644 index 6cdbf00f9ca..00000000000 --- a/app/assets/javascripts/batch_comments/i18n.js +++ /dev/null @@ -1,3 +0,0 @@ -import { __ } from '~/locale'; - -export const PREVENT_LEAVING_PENDING_REVIEW = __('There are unsubmitted review comments.'); diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js index 863d2a99972..a44b9827fe9 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js @@ -1,12 +1,9 @@ import { isEmpty } from 'lodash'; - import createFlash from '~/flash'; import { scrollToElement } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; - import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants'; import service from '../../../services/drafts_service'; - import * as types from './mutation_types'; export const saveDraft = ({ dispatch }, draft) => @@ -18,7 +15,6 @@ export const addDraftToDiscussion = ({ commit }, { endpoint, data }) => .then((res) => res.data) .then((res) => { commit(types.ADD_NEW_DRAFT, res); - return res; }) .catch(() => { @@ -33,7 +29,6 @@ export const createNewDraft = ({ commit }, { endpoint, data }) => .then((res) => res.data) .then((res) => { commit(types.ADD_NEW_DRAFT, res); - return res; }) .catch(() => { diff --git a/app/assets/javascripts/blob/blob_blame_link.js b/app/assets/javascripts/blob/blob_blame_link.js new file mode 100644 index 00000000000..41dfd7b82b8 --- /dev/null +++ b/app/assets/javascripts/blob/blob_blame_link.js @@ -0,0 +1,31 @@ +function addBlameLink(containerSelector, linkClass) { + const containerEl = document.querySelector(containerSelector); + + if (!containerEl) { + return; + } + + containerEl.addEventListener('mouseover', (e) => { + const isLineLink = e.target.classList.contains(linkClass); + if (isLineLink) { + const lineLink = e.target; + const lineLinkCopy = lineLink.cloneNode(true); + lineLinkCopy.classList.remove(linkClass, 'diff-line-num'); + + const { lineNumber } = lineLink.dataset; + const { blamePath } = document.querySelector('.line-numbers').dataset; + const blameLink = document.createElement('a'); + blameLink.classList.add('file-line-blame'); + blameLink.href = `${blamePath}#L${lineNumber}`; + + const wrapper = document.createElement('div'); + wrapper.classList.add('line-links', 'diff-line-num'); + + wrapper.appendChild(blameLink); + wrapper.appendChild(lineLinkCopy); + lineLink.replaceWith(wrapper); + } + }); +} + +export default addBlameLink; diff --git a/app/assets/javascripts/blob/blob_links_tracking.js b/app/assets/javascripts/blob/blob_links_tracking.js index 9a49aa8b0fc..713cc3fad05 100644 --- a/app/assets/javascripts/blob/blob_links_tracking.js +++ b/app/assets/javascripts/blob/blob_links_tracking.js @@ -1,7 +1,12 @@ import Tracking from '~/tracking'; -function addBlobLinksTracking(containerSelector, eventsToTrack) { - const containerEl = document.querySelector(containerSelector); +const eventsToTrack = [ + { selector: '.file-line-blame', property: 'blame' }, + { selector: '.file-line-num', property: 'link' }, +]; + +function addBlobLinksTracking() { + const containerEl = document.querySelector('.file-holder'); if (!containerEl) { return; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index f7849e8d588..f37a2987685 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -4,7 +4,6 @@ import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import LineHighlighter from '~/blob/line_highlighter'; import initBlobBundle from '~/blob_edit/blob_bundle'; -import addBlobLinksTracking from '~/blob/blob_links_tracking'; export default () => { new LineHighlighter(); // eslint-disable-line no-new @@ -16,12 +15,6 @@ export default () => { document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), ); - const eventsToTrack = [ - { selector: '.file-line-blame', property: 'blame' }, - { selector: '.file-line-num', property: 'link' }, - ]; - addBlobLinksTracking('#blob-content-holder', eventsToTrack); - const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 7999b916e0f..78572f11f6f 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -13,6 +13,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; import CodeIntelligence from '~/code_navigation/components/app.vue'; import LineHighlighter from '~/blob/line_highlighter'; +import addBlameLink from '~/blob/blob_blame_link'; import getRefMixin from '../mixins/get_ref'; import blobInfoQuery from '../queries/blob_info.query.graphql'; import userInfoQuery from '../queries/user_info.query.graphql'; @@ -242,6 +243,7 @@ export default { if (type === SIMPLE_BLOB_VIEWER) { new LineHighlighter(); // eslint-disable-line no-new + addBlameLink('.file-holder', 'js-line-links'); } }); diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index ccc8b44942a..f471db24889 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -3,6 +3,7 @@ import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui'; import LineHighlighter from '~/blob/line_highlighter'; import eventHub from '~/notes/event_hub'; import languageLoader from '~/content_editor/services/highlight_js_language_loader'; +import addBlobLinksTracking from '~/blob/blob_links_tracking'; import Tracking from '~/tracking'; import { EVENT_ACTION, @@ -66,6 +67,7 @@ export default { }, }, async created() { + addBlobLinksTracking(); this.trackEvent(EVENT_LABEL_VIEWER); if (this.unsupportedLanguage) { diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 2b76e70fa17..ab426f388c6 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -95,23 +95,14 @@ td.line-numbers { .blob-viewer { .line-numbers { + min-width: 6rem; // for server-side-rendering .line-links { @include gl-display-flex; - - - &:first-child { - margin-top: 10px; - } - - &:last-child { - margin-bottom: 10px; - } } // for client &.line-links { - min-width: 6rem; border-bottom-left-radius: 0; + pre { @@ -120,15 +111,15 @@ td.line-numbers { } } - .line-links { - &:hover a::before, - &:focus-within a::before { - @include gl-visibility-visible; - } + .line-numbers:not(.line-links) a:hover::before, + .line-numbers:not(.line-links) a:focus-within::before, + .line-links:hover a::before, + .line-links:focus-within a::before { + @include gl-visibility-visible; } + .file-line-num { - min-width: 4.5rem; @include gl-justify-content-end; @include gl-flex-grow-1; @include gl-pr-3; diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index b35dc3b00cb..bcddb889cf4 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -88,6 +88,12 @@ module EventsHelper end end + def event_target_path(event) + return Gitlab::UrlBuilder.build(event.target, only_path: true) if event.work_item? + + event.target_link_options + end + def event_feed_title(event) words = [] words << event.author_name diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 93c6efc9083..1f6ac29bffc 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -8,7 +8,7 @@ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name } = event.action_name %span.event-target-type.gl-mr-2= event.target_type_name - = link_to event.target_link_options, class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do + = link_to event_target_path(event), class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do = event.target.reference_link_text - unless event.milestone? %span.event-target-title.gl-text-overflow-ellipsis.gl-overflow-hidden.gl-mr-2{ dir: "auto" } diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 23a17c07ea8..130e73a069f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,17 +1,14 @@ #blob-content.file-content.code.js-syntax-highlight - offset = defined?(first_line_number) ? first_line_number : 1 - .line-numbers{ class: "gl-p-0\!" } + - blame_path = project_blame_path(@project, tree_join(@ref, blob.path)) + .line-numbers{ class: "gl-px-0!", data: { blame_path: blame_path } } - if blob.data.present? - link = blob_link if defined?(blob_link) - - blame_link = project_blame_path(@project, tree_join(@ref, blob.path)) - blob.data.each_line.each_with_index do |_, index| - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. - .line-links.diff-line-num - - if Feature.enabled?(:file_line_blame) - %a.file-line-blame{ href: "#{blame_link}#L#{i}" } - %a.file-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } - = i + %a.file-line-num.diff-line-num{ class: ("js-line-links" if Feature.enabled?(:file_line_blame)), href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } + = i - highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil .blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } } %pre.code.highlight diff --git a/data/whats_new/2022082200001_15_03.yml b/data/whats_new/2022082200001_15_03.yml new file mode 100644 index 00000000000..b0a2bf3b6e2 --- /dev/null +++ b/data/whats_new/2022082200001_15_03.yml @@ -0,0 +1,70 @@ +- name: "Create tasks in issues" + description: | + Tasks provide a robust way to refine an issue into smaller, discrete work units. Previously in GitLab, you could break down an issue into smaller parts using markdown checklists within the description. However, these checklist items could not be easily assigned, labeled, or managed anywhere outside of the description field. + + You can now create tasks within issues from the Child Items widget. Then, you can open the task directly within the issue to quickly update the title, set the weight, or add a description. Tasks break down work within projects for GitLab Free and increase the planning hierarchy for our GitLab Premium customers to three levels (epic, issue, and task). In our next iteration, you will be able to add labels, milestones, and iterations to each task. + + Tasks represent our first step toward evolving issues, epics, incidents, requirements, and test cases to [work items](https://docs.gitlab.com/ee/development/work_items.html). If you have feedback or suggestions about tasks, please comment on [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/363613). + stage: plan + self-managed: true + gitlab-com: true + available_in: [Free, Premium, Ultimate] + documentation_link: https://docs.gitlab.com/ee/user/tasks.html + image_url: https://about.gitlab.com/images/15_3/create-tasks.gif + published_at: 2022-08-22 + release: 15.3 +- name: "GitOps features are now free" + description: | + When you use GitOps to update a Kubernetes cluster, also called a pull-based deployment, you get an improved security model, better scalability and stability. + + The GitLab agent for Kubernetes has supported [GitOps workflows](https://docs.gitlab.com/ee/user/clusters/agent/gitops.html) from its initial release, but until now, the functionality was available only if you had a GitLab Premium or Ultimate subscription. Now if you have a Free subscription, you also get pull-based deployment support. The features available in GitLab Free should serve small, high-trust teams or be suitable to test the agent before upgrading to a higher tier. + + In the future, we plan to add [built-in multi-tenant support](https://gitlab.com/gitlab-org/gitlab/-/issues/337904) for Premium subscriptions. This feature would be similar to the impersonation feature already available for the [CI/CD workflow](https://docs.gitlab.com/ee/user/clusters/agent/ci_cd_workflow.html#restrict-project-and-group-access-by-using-impersonation). + stage: configure + self-managed: true + gitlab-com: true + available_in: [Free, Premium, Ultimate] + documentation_link: https://docs.gitlab.com/ee/user/clusters/agent/gitops.html + image_url: https://img.youtube.com/vi/jgVxOnMfOZA/hqdefault.jpg + published_at: 2022-08-22 + release: 15.3 +- name: "Submit merge request review with summary comment" + description: | + When you finish reviewing a merge request, there are probably some common things that you do, like summarizing your review for others or approving the changes if they look good to you. Those common tasks are now quicker and easier: when you submit your review, you can add a summary comment along with any [quick actions](https://docs.gitlab.com/ee/user/project/quick_actions.html) like `/approve`. + stage: create + self-managed: true + gitlab-com: true + available_in: [Free, Premium, Ultimate] + documentation_link: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#submit-a-review + image_url: https://about.gitlab.com/images/15_3/create-mr-review-summary.png + published_at: 2022-08-22 + release: 15.3 +- name: "Define password complexity requirements" + description: | + GitLab administrators can now define password complexity requirements in addition to minimum password length. For new passwords, you can now require: + + - Numbers. + - Uppercase letters. + - Lowercase letters. + - Symbols. + + Complex passwords are less likely to be compromised, and the ability to configure password complexity requirements helps administrators enforce their password policies. + stage: manage + self-managed: true + gitlab-com: false + available_in: [Premium, Ultimate] + documentation_link: https://docs.gitlab.com/ee/user/admin_area/settings/sign_up_restrictions.html#password-complexity-requirements + image_url: https://about.gitlab.com/images/15_3/manage-password-complexity-policy.png + published_at: 2022-08-22 + release: 15.3 +- name: "Maintain SAML Group Links with API" + description: | + Until now, SAML group links had to be configured in the UI. Now, you can manage SAML group links programmatically using the API so you can automate SAML groups management. + stage: manage + self-managed: true + gitlab-com: true + available_in: [Premium, Ultimate] + documentation_link: https://docs.gitlab.com/ee/api/groups.html#saml-group-links + image_url: https://img.youtube.com/vi/Pft61UFM5LM/hqdefault.jpg + published_at: 2022-08-22 + release: 15.3 diff --git a/doc/api/groups.md b/doc/api/groups.md index 588ab2a5821..9cbd12e664c 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1444,7 +1444,7 @@ response attributes: | Attribute | Type | Description | |:-------------------|:-------|:-------------------------------------------------------------------------------------| | `[].name` | string | Name of the SAML group | -| `[].access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | +| `[].access_level` | integer | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | Example request: @@ -1458,11 +1458,11 @@ Example response: [ { "name": "saml-group-1", - "access_level": "Guest" + "access_level": 10 }, { "name": "saml-group-2", - "access_level": "Maintainer" + "access_level": 40 } ] ``` @@ -1488,7 +1488,7 @@ response attributes: | Attribute | Type | Description | |:---------------|:-------|:-------------------------------------------------------------------------------------| | `name` | string | Name of the SAML group | -| `access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | +| `access_level` | integer | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | Example request: @@ -1501,7 +1501,7 @@ Example response: ```json { "name": "saml-group-1", -"access_level": "Guest" +"access_level": 10 } ``` @@ -1519,7 +1519,7 @@ Supported attributes: |:-------------------|:---------------|:---------|:-------------------------------------------------------------------------------------| | `id` | integer/string | yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) | | `saml_group_name` | string | yes | Name of a SAML group | -| `access_level` | string | yes | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | +| `access_level` | integer | yes | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | If successful, returns [`201`](index.md#status-codes) and the following response attributes: @@ -1527,7 +1527,7 @@ response attributes: | Attribute | Type | Description | |:---------------|:-------|:-------------------------------------------------------------------------------------| | `name` | string | Name of the SAML group | -| `access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | +| `access_level` | integer | Minimum [access level](members.md#valid-access-levels) for members of the SAML group | Example request: @@ -1540,7 +1540,7 @@ Example response: ```json { "name": "saml-group-1", -"access_level": "Guest" +"access_level": 10 } ``` diff --git a/doc/api/settings.md b/doc/api/settings.md index c736c0df1da..d11269113a1 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -274,6 +274,7 @@ listed in the descriptions of the relevant settings. | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. | | `package_registry_cleanup_policies_worker_capacity` | integer | no | Number of workers assigned to the packages cleanup policies. | | `deactivate_dormant_users` | boolean | no | Enable [automatic deactivation of dormant users](../user/admin_area/moderate_users.md#automatically-deactivate-dormant-users). | +| `deactivate_dormant_users_period` | integer | no | Length of time (in days) after which a user is considered dormant. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336747) in GitLab 15.3. | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. | | `default_branch_name` | string | no | [Instance-level custom initial branch name](../user/project/repository/branches/default.md#instance-level-custom-initial-branch-name) ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225258) in GitLab 13.2). | | `default_branch_protection` | integer | no | Determine if developers can push to the default branch. Can take: `0` _(not protected, both users with the Developer role or Maintainer role can push new commits and force push)_, `1` _(partially protected, users with the Developer role or Maintainer role can push new commits, but cannot force push)_ or `2` _(fully protected, users with the Developer or Maintainer role cannot push new commits, but users with the Developer or Maintainer role can; no one can force push)_ as a parameter. Default is `2`. | diff --git a/doc/user/permissions.md b/doc/user/permissions.md index c89dd3f65f7..eca94fce2e9 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -200,6 +200,10 @@ The following table lists project permissions available for each role: | [Security dashboard](application_security/security_dashboard/index.md):<br>Use security dashboard | | | ✓ | ✓ | ✓ | | [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability | | | ✓ | ✓ | ✓ | | [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability findings in [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ | +| [Tasks](tasks.md):<br>Create (*18*) | ✓ | ✓ | ✓ | ✓ | ✓ | +| [Tasks](tasks.md):<br>Edit | | ✓ | ✓ | ✓ | ✓ | +| [Tasks](tasks.md):<br>Remove from issue | | ✓ | ✓ | ✓ | ✓ | +| [Tasks](tasks.md):<br>Delete (*22*) | | | | | ✓ | | [Terraform](infrastructure/index.md):<br>Read Terraform state | | | ✓ | ✓ | ✓ | | [Terraform](infrastructure/index.md):<br>Manage Terraform state | | | | ✓ | ✓ | | [Test cases](../ci/test_cases/index.md):<br>Archive | | ✓ | ✓ | ✓ | ✓ | @@ -235,10 +239,11 @@ The following table lists project permissions available for each role: 16. In GitLab 14.5 or later, Guests are not allowed to [create incidents](../operations/incident_management/incidents.md#incident-creation). In GitLab 15.1 and later, a Guest who created an issue that was promoted to an incident cannot edit, close, or reopen their incident. 17. In projects that accept contributions from external members, users can create, edit, and close their own merge requests. -18. Authors and assignees of issues can modify the title and description even if they don't have the Reporter role. +18. Authors and assignees can modify the title and description even if they don't have the Reporter role. 19. Authors and assignees can close and reopen issues even if they don't have the Reporter role. 20. The ability to view the Container Registry and pull images is controlled by the [Container Registry's visibility permissions](packages/container_registry/index.md#container-registry-visibility-permissions). 21. Maintainers cannot create, demote, or remove Owners, and they cannot promote users to the Owner role. They also cannot approve Owner role access requests. +22. Authors of tasks can delete them even if they don't have the Owner role, but they have to have at least the Guest role for the project. <!-- markdownlint-enable MD029 --> diff --git a/doc/user/tasks.md b/doc/user/tasks.md index b5c2c4eb3a5..16e5c85a354 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -38,25 +38,64 @@ to work items and adding custom work item types, visit [epic 6033](https://gitlab.com/groups/gitlab-org/-/epics/6033) or [Plan direction page](https://about.gitlab.com/direction/plan/). +## View tasks + +View tasks in issues, in the **Child items** section. + +You can also [filter the list of issues](project/issues/managing_issues.md#filter-the-list-of-issues) +for `Type = task`. + ## Create a task +Prerequisites: + +- You must have at least the Guest role for the project, or the project must be public. + To create a task: -1. In an issue description, create a [task list](markdown.md#task-lists). -1. Hover over a task item and select **Create task** (**{doc-new}**). +1. In an issue description, in the **Child items** section, select **Add a task**. +1. Enter the task title. +1. Select **Create task**. ## Edit a task +Prerequisites: + +- You must have at least the Reporter role for the project. + To edit a task: -1. In the issue description, view the task links. -1. Select a link. The task is displayed. - - To edit the description, select **Edit**, then select **Save**. - - To edit the title or state, make your changes, then select any area outside the field. The changes are saved automatically. +1. In the issue description, in the **Child items** section, select the task you want to edit. + The task window opens. +1. Optional. To edit the title, select it and make your changes. +1. Optional. To edit the description, select the edit icon (**{pencil}**), make your changes, and + select **Save**. +1. Select the close icon (**{close}**). + +## Remove a task from an issue + +Prerequisites: + +- You must have at least the Reporter role for the project. + +You can remove a task from an issue. The task is not deleted, but the two are no longer connected. +It's not possible to connect them again. + +To remove a task from an issue: + +1. In the issue description, in the **Child items** section, next to the task you want to remove, select the options menu (**{ellipsis_v}**). +1. Select **Remove task**. ## Delete a task +Prerequisites: + +- You must either: + - Be the author of the task and have at least the Guest role for the project. + - Have the Owner role for the project. + To delete a task: -1. In the issue description, select the task. -1. From the options menu (**{ellipsis_v}**), select **Delete task**. +1. In the issue description, in the **Child items** section, select the task you want to edit. +1. In the task window, in the options menu (**{ellipsis_v}**), select **Delete task**. +1. Select **OK**. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e7b5e6ec73c..66a7012190a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39570,9 +39570,6 @@ msgstr "" msgid "There are several size limits in place." msgstr "" -msgid "There are unsubmitted review comments." -msgstr "" - msgid "There is already a repository with that name on disk" msgstr "" diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index f892b01e624..fafaea8ac68 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -101,7 +101,7 @@ RSpec.describe 'Merge request > Batch comments', :js do write_diff_comment - visit_overview_with_pending_comment + visit_overview end it 'can add comment to review' do @@ -232,14 +232,6 @@ RSpec.describe 'Merge request > Batch comments', :js do wait_for_requests end - def visit_overview_with_pending_comment - accept_alert do - visit project_merge_request_path(merge_request.project, merge_request) - end - - wait_for_requests - end - def write_diff_comment(**params) click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']")) diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index ddfed73e2ca..63e0ee4a251 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -209,7 +209,9 @@ RSpec.describe 'Projects > Settings > Repository settings' do end it 'generates an SSH public key on submission', :js do - fill_in 'url', with: 'ssh://user@localhost/project.git' + fill_in 'url', with: ssh_url + expect(page).to have_css(".js-mirror-url-hidden[value=\"#{ssh_url}\"]", visible: false) + select 'SSH public key', from: 'Authentication method' select_direction diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js index f98e0a4c64a..f50db6ab210 100644 --- a/spec/frontend/batch_comments/components/review_bar_spec.js +++ b/spec/frontend/batch_comments/components/review_bar_spec.js @@ -6,8 +6,6 @@ import createStore from '../create_batch_comments_store'; describe('Batch comments review bar component', () => { let store; let wrapper; - let addEventListenerSpy; - let removeEventListenerSpy; const createComponent = (propsData = {}) => { store = createStore(); @@ -20,58 +18,25 @@ describe('Batch comments review bar component', () => { beforeEach(() => { document.body.className = ''; - - addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); }); afterEach(() => { - addEventListenerSpy.mockRestore(); - removeEventListenerSpy.mockRestore(); wrapper.destroy(); }); - describe('when mounted', () => { - it('it adds review-bar-visible class to body', async () => { - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); - - createComponent(); - - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); - }); + it('it adds review-bar-visible class to body when review bar is mounted', async () => { + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); - it('it adds a blocking handler to the `beforeunload` window event', () => { - expect(addEventListenerSpy).not.toBeCalled(); + createComponent(); - createComponent(); - - expect(addEventListenerSpy).toHaveBeenCalledTimes(1); - expect(addEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), { - capture: true, - }); - }); + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); }); - describe('before destroyed', () => { - it('it removes review-bar-visible class to body', async () => { - createComponent(); - - wrapper.destroy(); + it('it removes review-bar-visible class to body when review bar is destroyed', async () => { + createComponent(); - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); - }); - - it('it removes the blocking handler from the `beforeunload` window event', () => { - createComponent(); - - expect(removeEventListenerSpy).not.toBeCalled(); - - wrapper.destroy(); + wrapper.destroy(); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - expect(removeEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), { - capture: true, - }); - }); + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); }); }); diff --git a/spec/frontend/blob/blob_blame_link_spec.js b/spec/frontend/blob/blob_blame_link_spec.js new file mode 100644 index 00000000000..0d19177a11f --- /dev/null +++ b/spec/frontend/blob/blob_blame_link_spec.js @@ -0,0 +1,47 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import addBlameLink from '~/blob/blob_blame_link'; + +describe('Blob links', () => { + const mouseoverEvent = new MouseEvent('mouseover', { + view: window, + bubbles: true, + cancelable: true, + }); + + beforeEach(() => { + setHTMLFixture(` + <div id="blob-content-holder"> + <div class="line-numbers" data-blame-path="/blamePath"> + <a id="L5" href="#L5" data-line-number="5" class="file-line-num js-line-links">5</a> + </div> + <pre id="LC5">Line 5 content</pre> + </div> + `); + + addBlameLink('#blob-content-holder', 'js-line-links'); + document.querySelector('.file-line-num').dispatchEvent(mouseoverEvent); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('adds wrapper elements with correct classes', () => { + const wrapper = document.querySelector('.line-links'); + + expect(wrapper).toBeTruthy(); + expect(wrapper.classList).toContain('diff-line-num'); + }); + + it('adds blame link with correct classes and path', () => { + const blameLink = document.querySelector('.file-line-blame'); + expect(blameLink).toBeTruthy(); + expect(blameLink.getAttribute('href')).toBe('/blamePath#L5'); + }); + + it('adds line link within wraper with correct classes and path', () => { + const lineLink = document.querySelector('.file-line-num'); + expect(lineLink).toBeTruthy(); + expect(lineLink.getAttribute('href')).toBe('#L5'); + }); +}); diff --git a/spec/frontend/blob/blob_links_tracking_spec.js b/spec/frontend/blob/blob_links_tracking_spec.js index 22e087bc180..8ef1e9f0ac9 100644 --- a/spec/frontend/blob/blob_links_tracking_spec.js +++ b/spec/frontend/blob/blob_links_tracking_spec.js @@ -15,7 +15,7 @@ describe('Blob links Tracking', () => { beforeEach(() => { setHTMLFixture(` - <div id="blob-content-holder"> + <div class="file-holder"> <div class="line-links diff-line-num"> <a href="#L5" class="file-line-blame"></a> <a id="L5" href="#L5" data-line-number="5" class="file-line-num">5</a> @@ -23,7 +23,7 @@ describe('Blob links Tracking', () => { <pre id="LC5">Line 5 content</pre> </div> `); - addBlobLinksTracking('#blob-content-holder', eventsToTrack); + addBlobLinksTracking(); jest.spyOn(Tracking, 'event'); }); diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 073cebeecd5..cc6804f0355 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -5,6 +5,25 @@ require 'spec_helper' RSpec.describe EventsHelper do include Gitlab::Routing + describe '#event_target_path' do + subject { helper.event_target_path(event.present) } + + context 'when target is a work item' do + let(:work_item) { create(:work_item) } + let(:event) { create(:event, target: work_item, target_type: 'WorkItem') } + + it { is_expected.to eq(Gitlab::UrlBuilder.build(work_item, only_path: true)) } + end + + context 'when target is not a work item' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:event) { create(:event, target: issue, project: project) } + + it { is_expected.to eq([project, issue]) } + end + end + describe '#event_commit_title' do let(:message) { 'foo & bar ' + 'A' * 70 + '\n' + 'B' * 80 } diff --git a/spec/views/events/event/_common.html.haml_spec.rb b/spec/views/events/event/_common.html.haml_spec.rb new file mode 100644 index 00000000000..0de84e2fdb8 --- /dev/null +++ b/spec/views/events/event/_common.html.haml_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'events/event/_common.html.haml' do + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + + context 'when it is a work item event' do + let(:work_item) { create(:work_item, project: project) } + + let(:event) do + create(:event, :created, project: project, target: work_item, target_type: 'WorkItem', author: user) + end + + it 'renders the correct url' do + render partial: 'events/event/common', locals: { event: event.present } + + expect(rendered).to have_link( + work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}" + ) + end + end + + context 'when it is an isssue event' do + let(:issue) { create(:issue, project: project) } + + let(:event) do + create(:event, :created, project: project, target: issue, author: user) + end + + it 'renders the correct url' do + render partial: 'events/event/common', locals: { event: event.present } + + expect(rendered).to have_link(issue.reference_link_text, href: "/#{project.full_path}/-/issues/#{issue.iid}") + end + end +end |