diff options
Diffstat (limited to 'app')
14 files changed, 192 insertions, 67 deletions
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js index f38e4514393..2c462cdde91 100644 --- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js +++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js @@ -63,7 +63,7 @@ function maybeMerge(a, b) { function createSourceMapAttributes(hastNode, source) { const { position } = hastNode; - return position.end + return position && position.end ? { sourceMapKey: `${position.start.offset}:${position.end.offset}`, sourceMarkdown: source.substring(position.start.offset, position.end.offset), diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 055a32420b2..88f5192af77 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -12,22 +12,6 @@ const ignoreAttrs = { const tableMap = new WeakMap(); -// Source taken from -// prosemirror-markdown/src/to_markdown.js -export function isPlainURL(link, parent, index, side) { - if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false; - const content = parent.child(index + (side < 0 ? -1 : 0)); - if ( - !content.isText || - content.text !== link.attrs.href || - content.marks[content.marks.length - 1] !== link - ) - return false; - if (index === (side < 0 ? 1 : parent.childCount - 1)) return true; - const next = parent.child(index + (side < 0 ? -2 : 1)); - return !link.isInSet(next.marks); -} - function containsOnlyText(node) { if (node.childCount === 1) { const child = node.child(0); @@ -498,10 +482,79 @@ const linkType = (sourceMarkdown) => { return LINK_HTML; }; +const removeUrlProtocol = (url) => url.replace(/^\w+:\/?\/?/, ''); + +const normalizeUrl = (url) => decodeURIComponent(removeUrlProtocol(url)); + +/** + * Validates that the provided URL is well-formed + * + * @param {String} url + * @returns Returns true when the browser’s URL constructor + * can successfully parse the URL string + */ +const isValidUrl = (url) => { + try { + return new URL(url) && true; + } catch { + return false; + } +}; + +const findChildWithMark = (mark, parent) => { + let child; + let offset; + let index; + + parent.forEach((_child, _offset, _index) => { + if (mark.isInSet(_child.marks)) { + child = _child; + offset = _offset; + index = _index; + } + }); + + return child ? { child, offset, index } : null; +}; + +/** + * This function detects whether a link should be serialized + * as an autolink. + * + * See https://github.github.com/gfm/#autolinks-extension- + * to understand the parsing rules of autolinks. + * */ +const isAutoLink = (linkMark, parent) => { + const { title, href } = linkMark.attrs; + + if (title || !/^\w+:/.test(href)) { + return false; + } + + const { child } = findChildWithMark(linkMark, parent); + + if ( + !child || + !child.isText || + !isValidUrl(href) || + normalizeUrl(child.text) !== normalizeUrl(href) + ) { + return false; + } + + return true; +}; + +/** + * Returns true if the user used brackets to the define + * the autolink in the original markdown source + */ +const isBracketAutoLink = (sourceMarkdown) => /^<.+?>$/.test(sourceMarkdown); + export const link = { - open(state, mark, parent, index) { - if (isPlainURL(mark, parent, index, 1)) { - return '<'; + open(state, mark, parent) { + if (isAutoLink(mark, parent)) { + return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '<' : ''; } const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs; @@ -518,9 +571,9 @@ export const link = { return openTag('a', attrs); }, - close(state, mark, parent, index) { - if (isPlainURL(mark, parent, index, -1)) { - return '>'; + close(state, mark, parent) { + if (isAutoLink(mark, parent)) { + return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '>' : ''; } const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs; diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index a75262ee303..07316f9433a 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -19,8 +19,6 @@ import { scrollToElement } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import FileIcon from '~/vue_shared/components/file_icon.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants'; import { DIFF_FILE_HEADER } from '../i18n'; @@ -33,7 +31,6 @@ export default { components: { ClipboardButton, GlIcon, - FileIcon, DiffStats, GlBadge, GlButton, @@ -48,7 +45,7 @@ export default { GlTooltip: GlTooltipDirective, SafeHtml: GlSafeHtmlDirective, }, - mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })], + mixins: [IdState({ idProp: (vm) => vm.diffFile.file_hash })], i18n: { ...DIFF_FILE_HEADER, compareButtonLabel: __('Compare submodule commit revisions'), @@ -301,14 +298,6 @@ export default { :href="titleLink" @click="handleFileNameClick" > - <file-icon - v-if="!glFeatures.removeDiffHeaderIcons" - :file-name="filePath" - :size="16" - aria-hidden="true" - css-classes="gl-mr-2" - :submodule="diffFile.submodule" - /> <span v-if="isFileRenamed"> <strong v-gl-tooltip diff --git a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql index 824997f8e33..fb771d7ec8a 100644 --- a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql @@ -11,4 +11,7 @@ fragment TimelogFragment on Timelog { body } summary + userPermissions { + adminTimelog + } } diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js b/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js new file mode 100644 index 00000000000..70177d84b1b --- /dev/null +++ b/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js @@ -0,0 +1,20 @@ +import produce from 'immer'; + +export function removeTimelogFromStore(store, deletedTimelogId, query, variables) { + const sourceData = store.readQuery({ + query, + variables, + }); + + const data = produce(sourceData, (draftData) => { + draftData.issuable.timelogs.nodes = draftData.issuable.timelogs.nodes.filter( + ({ id }) => id !== deletedTimelogId, + ); + }); + + store.writeQuery({ + query, + variables, + data, + }); +} diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql b/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql new file mode 100644 index 00000000000..17bbad1acb1 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql @@ -0,0 +1,5 @@ +mutation deleteTimelog($input: TimelogDeleteInput!) { + timelogDelete(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index b4c4c31bd7a..79ef5a32474 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -1,11 +1,13 @@ <script> -import { GlLoadingIcon, GlTableLite } from '@gitlab/ui'; +import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui'; import createFlash from '~/flash'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import { timelogQueries } from '~/sidebar/constants'; +import deleteTimelogMutation from './graphql/mutations/delete_timelog.mutation.graphql'; +import { removeTimelogFromStore } from './graphql/cache_update'; const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)'; @@ -13,6 +15,10 @@ export default { components: { GlLoadingIcon, GlTableLite, + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, }, inject: ['issuableType'], props: { @@ -27,7 +33,7 @@ export default { }, }, data() { - return { report: [], isLoading: true }; + return { report: [], isLoading: true, removingIds: [] }; }, apollo: { report: { @@ -35,9 +41,7 @@ export default { return timelogQueries[this.issuableType].query; }, variables() { - return { - id: convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId), - }; + return this.getQueryVariables(); }, update(data) { this.isLoading = false; @@ -48,10 +52,23 @@ export default { }, }, }, + computed: { + deleteButtonTooltip() { + return s__('TimeTracking|Delete time spent'); + }, + }, methods: { + isDeletingTimelog(timelogId) { + return this.removingIds.includes(timelogId); + }, isIssue() { return this.issuableType === 'issue'; }, + getQueryVariables() { + return { + id: convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId), + }; + }, getGraphQLEntityType() { return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST; }, @@ -76,12 +93,44 @@ export default { stringifyTime(parseSeconds(seconds, { limitToHours: this.limitToHours })) ); }, + deleteTimelog(timelogId) { + this.removingIds.push(timelogId); + this.$apollo + .mutate({ + mutation: deleteTimelogMutation, + variables: { input: { id: timelogId } }, + update: (store) => { + removeTimelogFromStore( + store, + timelogId, + timelogQueries[this.issuableType].query, + this.getQueryVariables(), + ); + }, + }) + .then(({ data }) => { + if (data.timelogDelete?.errors?.length) { + throw new Error(data.timelogDelete.errors[0]); + } + }) + .catch((error) => { + createFlash({ + message: s__('TimeTracking|An error occurred while removing the timelog.'), + captureError: true, + error, + }); + }) + .finally(() => { + this.removingIds.splice(this.removingIds.indexOf(timelogId), 1); + }); + }, }, fields: [ - { key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' }, + { key: 'spentAt', label: __('Spent at'), sortable: true, tdClass: 'gl-w-quarter' }, { key: 'user', label: __('User'), sortable: true }, - { key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' }, - { key: 'summary', label: __('Summary / Note'), sortable: true }, + { key: 'timeSpent', label: __('Time spent'), sortable: true, tdClass: 'gl-w-15' }, + { key: 'summary', label: __('Summary / note'), sortable: true }, + { key: 'actions', label: '', tdClass: 'gl-w-10' }, ], }; </script> @@ -110,7 +159,28 @@ export default { <template #cell(summary)="{ item: { summary, note } }"> <div>{{ getSummary(summary, note) }}</div> </template> - <template #foot(note)> </template> + <template #foot(summary)> </template> + + <template + #cell(actions)="{ + item: { + id, + userPermissions: { adminTimelog }, + }, + }" + > + <div v-if="adminTimelog"> + <gl-button + v-gl-tooltip="{ title: deleteButtonTooltip }" + category="secondary" + icon="remove" + data-testid="deleteButton" + :loading="isDeletingTimelog(id)" + @click="deleteTimelog(id)" + /> + </div> + </template> + <template #foot(actions)> </template> </gl-table-lite> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 057bb9f0100..e39d9f9fb49 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -252,6 +252,7 @@ export default { size="lg" :title="__('Time tracking report')" :hide-footer="true" + @hide="refresh" > <time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" /> </gl-modal> diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 15f84e48179..cac0d5a45c9 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -307,7 +307,7 @@ export default { <actions-button :actions="actions" :selected-key="selection" - :variant="isBlob ? 'info' : 'default'" + :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" @select="select" /> diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c1416e20aea..b63fd941a9b 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -200,11 +200,6 @@ $tabs-holder-z-index: 250; } } -.assign-to-me-link { - padding-left: 12px; - white-space: nowrap; -} - .table-holder { .ci-table { th { @@ -252,13 +247,6 @@ $tabs-holder-z-index: 250; } } -.merge-request-tabs { - display: flex; - flex-wrap: nowrap; - margin-bottom: 0; - padding: 0; -} - .limit-container-width { .merge-request-tabs-container { max-width: $limited-layout-width; @@ -274,9 +262,6 @@ $tabs-holder-z-index: 250; } .merge-request-tabs-container { - display: flex; - justify-content: space-between; - @include media-breakpoint-down(xs) { .discussion-filter-container { margin-bottom: $gl-padding-4; diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5b4d79fab88..6dea73298a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -45,7 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:realtime_labels, project) push_frontend_feature_flag(:refactor_security_extension, @project) push_frontend_feature_flag(:mr_attention_requests, current_user) - push_frontend_feature_flag(:remove_diff_header_icons, project) push_frontend_feature_flag(:moved_mr_sidebar, project) push_frontend_feature_flag(:paginated_mr_discussions, project) end diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index 3c39c04d3a3..ef3174efcc7 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -18,11 +18,11 @@ = custom_icon ('illustration_no_commits') - else .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container + .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= sprite_icon('chevron-lg-left', size: 12) .fade-right= sprite_icon('chevron-lg-right', size: 12) - %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix + %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix %li.commits-tab.new-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do Commits diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 0ac5410eec9..e0adf34c6a8 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -21,8 +21,8 @@ .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } = render "projects/merge_requests/mr_box" .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container{ class: "#{'is-merge-request' if Feature.enabled?(:moved_mr_sidebar, @project) && !fluid_layout}" } - %ul.merge-request-tabs.nav-tabs.nav.nav-links{ class: "#{'gl-w-full gl-lg-w-auto!' if Feature.enabled?(:moved_mr_sidebar, @project)}" } + .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between{ class: "#{'is-merge-request' if Feature.enabled?(:moved_mr_sidebar, @project) && !fluid_layout}" } + %ul.merge-request-tabs.nav-tabs.nav.nav-links.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0{ class: "#{'gl-w-full gl-lg-w-auto!' if Feature.enabled?(:moved_mr_sidebar, @project)}" } = render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do = tab_link_for @merge_request, :show, force_link: @commit.present? do = _("Overview") diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml index 507c5a89649..f9c3c11eed8 100644 --- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml @@ -8,4 +8,4 @@ = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } = dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name)) - = link_to _('Assign to me'), '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" + = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" |