summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue22
-rw-r--r--app/assets/javascripts/issues/show/utils.js50
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue5
-rw-r--r--app/assets/stylesheets/framework/source_editor.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss41
-rw-r--r--app/assets/stylesheets/pages/issuable.scss8
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/user.rb13
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