diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/issues/show/components/description.vue | 22 | ||||
-rw-r--r-- | app/assets/javascripts/issues/show/utils.js | 50 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/comment_form.vue | 5 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/source_editor.scss | 5 | ||||
-rw-r--r-- | app/assets/stylesheets/page_bundles/issues_show.scss | 41 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/issuable.scss | 8 | ||||
-rw-r--r-- | app/helpers/issues_helper.rb | 2 | ||||
-rw-r--r-- | app/models/member.rb | 1 | ||||
-rw-r--r-- | app/models/user.rb | 13 |
9 files changed, 106 insertions, 41 deletions
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 4f97458dcd1..daa1632c4aa 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -12,6 +12,7 @@ import Vue from 'vue'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import createFlash from '~/flash'; +import { IssuableType } from '~/issues/constants'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { __, s__, sprintf } from '~/locale'; @@ -66,7 +67,7 @@ export default { issuableType: { type: String, required: false, - default: 'issue', + default: IssuableType.Issue, }, updateUrl: { type: String, @@ -177,7 +178,9 @@ export default { onError: this.taskListUpdateError.bind(this), }); - this.renderSortableLists(); + if (this.issuableType === IssuableType.Issue) { + this.renderSortableLists(); + } } }, renderSortableLists() { @@ -185,6 +188,10 @@ export default { const lists = document.querySelectorAll('.description ul, .description ol'); lists.forEach((list) => { + if (list.children.length <= 1) { + return; + } + Array.from(list.children).forEach((listItem) => { listItem.prepend(this.createDragIconElement()); this.addPointerEventListeners(listItem); @@ -211,13 +218,18 @@ export default { }, addPointerEventListeners(listItem) { const pointeroverListener = (event) => { - if (isDragging() || this.isUpdating) { + const dragIcon = event.target.closest('li').querySelector('.drag-icon'); + if (!dragIcon || isDragging() || this.isUpdating) { return; } - event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign + dragIcon.style.visibility = 'visible'; }; const pointeroutListener = (event) => { - event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign + const dragIcon = event.target.closest('li').querySelector('.drag-icon'); + if (!dragIcon) { + return; + } + dragIcon.style.visibility = 'hidden'; }; // We use pointerover/pointerout instead of CSS so that when we hover over a diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js index 60e66f59f92..05b06586362 100644 --- a/app/assets/javascripts/issues/show/utils.js +++ b/app/assets/javascripts/issues/show/utils.js @@ -1,39 +1,35 @@ import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility'; /** - * Get the index from sourcepos that represents the line of - * the description when the description is split by newline. + * Returns the start and end `sourcepos` rows, converted to zero-based numbering. * * @param {String} sourcepos Source position in format `23:3-23:14` - * @returns {Number} Index of description split by newline + * @returns {Array<Number>} Start and end `sourcepos` rows, zero-based numbered */ -const getDescriptionIndex = (sourcepos) => { - const [startRange] = sourcepos.split(HYPHEN); +const getSourceposRows = (sourcepos) => { + const [startRange, endRange] = sourcepos.split(HYPHEN); const [startRow] = startRange.split(COLON); - return startRow - 1; + const [endRow] = endRange.split(COLON); + return [startRow - 1, endRow - 1]; }; /** - * Given a `ul` or `ol` element containing a new sort order, this function performs - * a depth-first search to get the new sort order in the form of sourcepos indices. + * Given a `ul` or `ol` element containing a new sort order, this function returns + * an array of this new order which is derived from its list items' sourcepos values. * * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order - * @returns {Array<Number>} An array representing the new order of the list + * @returns {Array<Number>} A numerical array representing the new order of the list. + * The numbers represent the rows of the original markdown source. */ const getNewSourcePositions = (list) => { const newSourcePositions = []; - function pushPositionOfChildListItems(el) { - if (!el) { - return; + Array.from(list.children).forEach((listItem) => { + const [start, end] = getSourceposRows(listItem.dataset.sourcepos); + for (let i = start; i <= end; i += 1) { + newSourcePositions.push(i); } - if (el.tagName === 'LI') { - newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos)); - } - Array.from(el.children).forEach(pushPositionOfChildListItems); - } - - pushPositionOfChildListItems(list); + }); return newSourcePositions; }; @@ -56,17 +52,17 @@ const getNewSourcePositions = (list) => { * And a reordered list (due to dragging Item 2 into Item 1's position) like: * * <pre> - * <ul data-sourcepos="3:1-8:0"> - * <li data-sourcepos="4:1-4:8"> + * <ul data-sourcepos="3:1-7:8"> + * <li data-sourcepos="4:1-6:10"> * Item 2 - * <ul data-sourcepos="5:1-6:10"> - * <li data-sourcepos="5:1-5:10">Item 3</li> - * <li data-sourcepos="6:1-6:10">Item 4</li> + * <ul data-sourcepos="5:3-6:10"> + * <li data-sourcepos="5:3-5:10">Item 3</li> + * <li data-sourcepos="6:3-6:10">Item 4</li> * </ul> * </li> * <li data-sourcepos="3:1-3:8">Item 1</li> - * <li data-sourcepos="7:1-8:0">Item 5</li> - * <ul> + * <li data-sourcepos="7:1-7:8">Item 5</li> + * </ul> * </pre> * * This function returns: @@ -87,7 +83,7 @@ const getNewSourcePositions = (list) => { */ export const convertDescriptionWithNewSort = (description, list) => { const descriptionLines = description.split(NEWLINE); - const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos); + const [startIndexOfList] = getSourceposRows(list.dataset.sourcepos); getNewSourcePositions(list) .map((lineIndex) => descriptionLines[lineIndex]) diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 4e03bed8737..8ef071034e5 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -111,7 +111,7 @@ export default { return this.getNoteableData.current_user.can_create_note; }, canSetConfidential() { - return this.getNoteableData.current_user.can_update; + return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic); }, issueActionButtonTitle() { const openOrClose = this.isOpen ? 'close' : 'reopen'; @@ -166,6 +166,9 @@ export default { isIssue() { return constants.NOTEABLE_TYPE_MAPPING[this.noteableType] === constants.ISSUE_NOTEABLE_TYPE; }, + isEpic() { + return constants.NOTEABLE_TYPE_MAPPING[this.noteableType] === constants.EPIC_NOTEABLE_TYPE; + }, trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); }, diff --git a/app/assets/stylesheets/framework/source_editor.scss b/app/assets/stylesheets/framework/source_editor.scss index 8b694b9be05..046b8636f65 100644 --- a/app/assets/stylesheets/framework/source_editor.scss +++ b/app/assets/stylesheets/framework/source_editor.scss @@ -83,6 +83,11 @@ } } } + + // Remove custom focus from element + .inputarea { + @include gl-shadow-none; + } } .active-line-text { diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss index 9873a0121c0..ade649faaae 100644 --- a/app/assets/stylesheets/page_bundles/issues_show.scss +++ b/app/assets/stylesheets/page_bundles/issues_show.scss @@ -3,8 +3,8 @@ .description { ul, ol { - /* We're changing list-style-position to inside because the default of outside - * doesn't move the negative margin to the left of the bullet. */ + /* We're changing list-style-position to inside because the default of + * outside doesn't move negative margin to the left of the bullet. */ list-style-position: inside; } @@ -21,6 +21,43 @@ inset-block-start: 0.3rem; inset-inline-start: 1rem; } + + /* The inside bullet aligns itself to the bottom, which we see when text to the right of + * a multi-line list item wraps. We fix this by aligning it to the top, and excluding + * other elements adversely affected by this. Targeting ::marker doesn't seem to work. */ + > *:not(code):not(input):not(.gl-label) { + vertical-align: top; + } + + /* The inside bullet is treated like an element inside the li element, so when we have a + * multi-paragraph list item, the text doesn't start on the right of the bullet because + * it is a block level p element. We make it inline to fix this. */ + > p:first-of-type { + display: inline-block; + max-width: calc(100% - 1.5rem); + } + + /* We fix the other paragraphs not indenting to the + * right of the bullet due to the inside bullet. */ + p ~ a, + p ~ blockquote, + p ~ code, + p ~ details, + p ~ dl, + p ~ h1, + p ~ h2, + p ~ h3, + p ~ h4, + p ~ h5, + p ~ h6, + p ~ hr, + p ~ ol, + p ~ p, + p ~ table:not(.code), /* We need :not(.code) to override typography.scss */ + p ~ ul, + p ~ .markdown-code-block { + margin-inline-start: 1rem; + } } ul.task-list { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4093ef087dc..086abcf3f86 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -769,8 +769,12 @@ .add-issuable-form-input-wrapper { &.focus { - border-color: $blue-300; - box-shadow: 0 0 4px $dropdown-input-focus-shadow; + border-color: $gray-700; + @include gl-focus; + + input { + @include gl-shadow-none; + } } .gl-show-field-errors &.form-control:not(textarea) { diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 04de77dd484..60dba73447c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -205,7 +205,7 @@ module IssuesHelper is_anonymous_search_disabled: Feature.enabled?(:disable_anonymous_search, type: :ops).to_s, is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s, is_public_visibility_restricted: - Gitlab::CurrentSettings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC).to_s, + Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s, is_signed_in: current_user.present?.to_s, jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'), rss_path: url_for(safe_params.merge(rss_url_options)), diff --git a/app/models/member.rb b/app/models/member.rb index a5084c8a60c..45ad47f56a4 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -170,6 +170,7 @@ class Member < ApplicationRecord scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :with_user, -> (user) { where(user: user) } scope :by_access_level, -> (access_level) { active.where(access_level: access_level) } + scope :all_by_access_level, -> (access_level) { where(access_level: access_level) } scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) } diff --git a/app/models/user.rb b/app/models/user.rb index 8aae4441852..b9a8e5855bf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1366,9 +1366,16 @@ class User < ApplicationRecord end def solo_owned_groups - @solo_owned_groups ||= owned_groups.includes(:owners).select do |group| - group.owners == [self] - end + # For each owned group, count the owners found in self and ancestors. + counts = GroupMember + .from('unnest(namespaces.traversal_ids) AS ancestors(ancestor_id), members') + .where('members.source_id = ancestors.ancestor_id') + .all_by_access_level(GroupMember::OWNER) + .having('count(members.user_id) = 1') + + Group + .from(owned_groups, :namespaces) + .where_exists(counts) end def with_defaults |