diff options
Diffstat (limited to 'app/assets/javascripts')
13 files changed, 270 insertions, 159 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 735cbb8e356..aee9990bc0b 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -113,10 +113,9 @@ const Api = { .get(url, { params: Object.assign(defaults, options), }) - .then(({ data }) => { + .then(({ data, headers }) => { callback(data); - - return data; + return { data, headers }; }); }, diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js index 41b660a243f..92ac3a2c94d 100644 --- a/app/assets/javascripts/frequent_items/store/mutations.js +++ b/app/assets/javascripts/frequent_items/store/mutations.js @@ -47,7 +47,8 @@ export default { hasSearchQuery: true, }); }, - [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) { + [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) { + const rawItems = results.data; Object.assign(state, { items: rawItems.map(rawItem => ({ id: rawItem.id, diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue index ef126166e8b..03a697d11ed 100644 --- a/app/assets/javascripts/jobs/components/log/log.vue +++ b/app/assets/javascripts/jobs/components/log/log.vue @@ -11,11 +11,35 @@ export default { computed: { ...mapState(['traceEndpoint', 'trace', 'isTraceComplete']), }, + updated() { + this.$nextTick(() => { + this.handleScrollDown(); + }); + }, + mounted() { + this.$nextTick(() => { + this.handleScrollDown(); + }); + }, methods: { - ...mapActions(['toggleCollapsibleLine']), + ...mapActions(['toggleCollapsibleLine', 'scrollBottom']), handleOnClickCollapsibleLine(section) { this.toggleCollapsibleLine(section); }, + /** + * The job log is sent in HTML, which means we need to use `v-html` to render it + * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated + * in this case because it runs before `v-html` has finished running, since there's no + * Vue binding. + * In order to scroll the page down after `v-html` has finished, we need to use setTimeout + */ + handleScrollDown() { + if (this.isScrolledToBottomBeforeReceivingTrace) { + setTimeout(() => { + this.scrollBottom(); + }, 0); + } + }, }, }; </script> diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue new file mode 100644 index 00000000000..4c9075912ee --- /dev/null +++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue @@ -0,0 +1,133 @@ +<script> +import { mapActions } from 'vuex'; +import _ from 'underscore'; + +import { s__, __, sprintf } from '~/locale'; +import { truncateSha } from '~/lib/utils/text_utility'; + +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import noteEditedText from './note_edited_text.vue'; +import noteHeader from './note_header.vue'; + +export default { + name: 'DiffDiscussionHeader', + components: { + userAvatarLink, + noteEditedText, + noteHeader, + }, + props: { + discussion: { + type: Object, + required: true, + }, + }, + computed: { + notes() { + return this.discussion.notes; + }, + firstNote() { + return this.notes[0]; + }, + lastNote() { + return this.notes[this.notes.length - 1]; + }, + author() { + return this.firstNote.author; + }, + resolvedText() { + return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved'); + }, + lastUpdatedBy() { + return this.notes.length > 1 ? this.lastNote.author : null; + }, + lastUpdatedAt() { + return this.notes.length > 1 ? this.lastNote.created_at : null; + }, + headerText() { + const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; + const linkEnd = '</a>'; + + const { commit_id: commitId } = this.discussion; + let commitDisplay = commitId; + + if (commitId) { + commitDisplay = `<span class="commit-sha">${truncateSha(commitId)}</span>`; + } + + const { + for_commit: isForCommit, + diff_discussion: isDiffDiscussion, + active: isActive, + } = this.discussion; + + let text = s__('MergeRequests|started a thread'); + if (isForCommit) { + text = s__( + 'MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}', + ); + } else if (isDiffDiscussion && commitId) { + text = isActive + ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}') + : s__( + 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitDisplay}%{linkEnd}', + ); + } else if (isDiffDiscussion) { + text = isActive + ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}') + : s__( + 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}', + ); + } + + return sprintf(text, { commitDisplay, linkStart, linkEnd }, false); + }, + }, + methods: { + ...mapActions(['toggleDiscussion']), + toggleDiscussionHandler() { + this.toggleDiscussion({ discussionId: this.discussion.id }); + }, + }, +}; +</script> + +<template> + <div class="discussion-header note-wrapper"> + <div v-once class="timeline-icon align-self-start flex-shrink-0"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + /> + </div> + <div class="timeline-content w-100"> + <note-header + :author="author" + :created-at="firstNote.created_at" + :note-id="firstNote.id" + :include-toggle="true" + :expanded="discussion.expanded" + @toggleHandler="toggleDiscussionHandler" + > + <span v-html="headerText"></span> + </note-header> + <note-edited-text + v-if="discussion.resolved" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" + /> + <note-edited-text + v-else-if="lastUpdatedAt" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" + :action-text="__('Last updated')" + class-name="discussion-headline-light js-discussion-headline" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index cb1975a8962..47ec740b63a 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,18 +1,15 @@ <script> -import _ from 'underscore'; import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; -import { truncateSha } from '~/lib/utils/text_utility'; -import { s__, __, sprintf } from '~/locale'; +import { s__, __ } from '~/locale'; import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import icon from '~/vue_shared/components/icon.vue'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import noteHeader from './note_header.vue'; +import diffDiscussionHeader from './diff_discussion_header.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; -import noteEditedText from './note_edited_text.vue'; import noteForm from './note_form.vue'; import diffWithNote from './diff_with_note.vue'; import noteable from '../mixins/noteable'; @@ -27,9 +24,8 @@ export default { components: { icon, userAvatarLink, - noteHeader, + diffDiscussionHeader, noteSignedOutWidget, - noteEditedText, noteForm, DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'), TimelineEntryItem, @@ -92,9 +88,6 @@ export default { currentUser() { return this.getUserData; }, - author() { - return this.firstNote.author; - }, autosaveKey() { return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id); }, @@ -104,27 +97,6 @@ export default { firstNote() { return this.discussion.notes.slice(0, 1)[0]; }, - lastUpdatedBy() { - const { notes } = this.discussion; - - if (notes.length > 1) { - return notes[notes.length - 1].author; - } - - return null; - }, - lastUpdatedAt() { - const { notes } = this.discussion; - - if (notes.length > 1) { - return notes[notes.length - 1].created_at; - } - - return null; - }, - resolvedText() { - return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved'); - }, shouldShowJumpToNextDiscussion() { return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion'); }, @@ -150,40 +122,6 @@ export default { shouldHideDiscussionBody() { return this.shouldRenderDiffs && !this.isExpanded; }, - actionText() { - const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; - const linkEnd = '</a>'; - - let { commit_id: commitId } = this.discussion; - if (commitId) { - commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`; - } - - const { - for_commit: isForCommit, - diff_discussion: isDiffDiscussion, - active: isActive, - } = this.discussion; - - let text = s__('MergeRequests|started a thread'); - if (isForCommit) { - text = s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}'); - } else if (isDiffDiscussion && commitId) { - text = isActive - ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}') - : s__( - 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}', - ); - } else if (isDiffDiscussion) { - text = isActive - ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}') - : s__( - 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}', - ); - } - - return sprintf(text, { commitId, linkStart, linkEnd }, false); - }, diffLine() { if (this.line) { return this.line; @@ -208,16 +146,11 @@ export default { methods: { ...mapActions([ 'saveNote', - 'toggleDiscussion', 'removePlaceholderNotes', 'toggleResolveNote', 'expandDiscussion', 'removeConvertedDiscussion', ]), - truncateSha, - toggleDiscussionHandler() { - this.toggleDiscussion({ discussionId: this.discussion.id }); - }, showReplyForm() { this.isReplying = true; }, @@ -311,43 +244,7 @@ export default { class="discussion js-discussion-container" data-qa-selector="discussion_content" > - <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> - <div v-once class="timeline-icon align-self-start flex-shrink-0"> - <user-avatar-link - v-if="author" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - /> - </div> - <div class="timeline-content w-100"> - <note-header - :author="author" - :created-at="firstNote.created_at" - :note-id="firstNote.id" - :include-toggle="true" - :expanded="discussion.expanded" - @toggleHandler="toggleDiscussionHandler" - > - <span v-html="actionText"></span> - </note-header> - <note-edited-text - v-if="discussion.resolved" - :edited-at="discussion.resolved_at" - :edited-by="discussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline" - /> - <note-edited-text - v-else-if="lastUpdatedAt" - :edited-at="lastUpdatedAt" - :edited-by="lastUpdatedBy" - action-text="Last updated" - class-name="discussion-headline-light js-discussion-headline" - /> - </div> - </div> + <diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" /> <div v-if="!shouldHideDiscussionBody" class="discussion-body"> <component :is="wrapperComponent" diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue index 54a441de886..073cfcd7694 100644 --- a/app/assets/javascripts/releases/detail/components/app.vue +++ b/app/assets/javascripts/releases/detail/components/app.vue @@ -1,6 +1,7 @@ <script> import { mapState, mapActions } from 'vuex'; import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; +import _ from 'underscore'; import { __, sprintf } from '~/locale'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; @@ -23,6 +24,7 @@ export default { 'markdownDocsPath', 'markdownPreviewPath', 'releasesPagePath', + 'updateReleaseApiDocsPath', ]), showForm() { return !this.isFetchingRelease && !this.fetchError; @@ -42,6 +44,20 @@ export default { tagName() { return this.$store.state.release.tagName; }, + tagNameHintText() { + return sprintf( + __( + 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}', + ), + { + linkStart: `<a href="${_.escape( + this.updateReleaseApiDocsPath, + )}" target="_blank" rel="noopener noreferrer">`, + linkEnd: '</a>', + }, + false, + ); + }, releaseTitle: { get() { return this.$store.state.release.name; @@ -77,22 +93,22 @@ export default { <div class="d-flex flex-column"> <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p> <form v-if="showForm" @submit.prevent="updateRelease()"> - <div class="row"> - <gl-form-group class="col-md-6 col-lg-5 col-xl-4"> - <label for="git-ref">{{ __('Tag name') }}</label> - <gl-form-input - id="git-ref" - v-model="tagName" - type="text" - class="form-control" - aria-describedby="tag-name-help" - disabled - /> - <div id="tag-name-help" class="form-text text-muted"> - {{ __('Choose an existing tag, or create a new one') }} + <gl-form-group> + <div class="row"> + <div class="col-md-6 col-lg-5 col-xl-4"> + <label for="git-ref">{{ __('Tag name') }}</label> + <gl-form-input + id="git-ref" + v-model="tagName" + type="text" + class="form-control" + aria-describedby="tag-name-help" + disabled + /> </div> - </gl-form-group> - </div> + </div> + <div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div> + </gl-form-group> <gl-form-group> <label for="release-title">{{ __('Release title') }}</label> <gl-form-input diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js index ff98e2bed78..7e3d975f1ae 100644 --- a/app/assets/javascripts/releases/detail/store/state.js +++ b/app/assets/javascripts/releases/detail/store/state.js @@ -4,6 +4,7 @@ export default () => ({ releasesPagePath: null, markdownDocsPath: null, markdownPreviewPath: null, + updateReleaseApiDocsPath: null, release: null, diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index de7350f0d2f..d826f209815 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -16,7 +16,6 @@ export default function setupVueRepositoryList() { const { dataset } = el; const { projectPath, projectShortPath, ref, fullName } = dataset; const router = createRouter(projectPath, ref); - const hideOnRootEls = document.querySelectorAll('.js-hide-on-root'); apolloProvider.clients.defaultClient.cache.writeData({ data: { @@ -28,20 +27,7 @@ export default function setupVueRepositoryList() { }); router.afterEach(({ params: { pathMatch } }) => { - const isRoot = pathMatch === undefined || pathMatch === '/'; - setTitle(pathMatch, ref, fullName); - - if (!isRoot) { - document - .querySelectorAll('.js-keep-hidden-on-navigation') - .forEach(elem => elem.classList.add('hidden')); - } - - document - .querySelectorAll('.js-hide-on-navigation') - .forEach(elem => elem.classList.toggle('hidden', !isRoot)); - hideOnRootEls.forEach(elem => elem.classList.toggle('hidden', isRoot)); }); const breadcrumbEl = document.getElementById('js-repo-breadcrumb'); diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue index 967f4a99281..29786bf4ec8 100644 --- a/app/assets/javascripts/repository/pages/index.vue +++ b/app/assets/javascripts/repository/pages/index.vue @@ -1,13 +1,25 @@ <script> -import TreeContent from '../components/tree_content.vue'; +import TreePage from './tree.vue'; +import { updateElementsVisibility } from '../utils/dom'; export default { components: { - TreeContent, + TreePage, + }, + mounted() { + this.updateProjectElements(true); + }, + beforeDestroy() { + this.updateProjectElements(false); + }, + methods: { + updateProjectElements(isShow) { + updateElementsVisibility('.js-show-on-project-root', isShow); + }, }, }; </script> <template> - <tree-content /> + <tree-page path="/" /> </template> diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue index 19300099449..dd4d437f4dd 100644 --- a/app/assets/javascripts/repository/pages/tree.vue +++ b/app/assets/javascripts/repository/pages/tree.vue @@ -1,5 +1,6 @@ <script> import TreeContent from '../components/tree_content.vue'; +import { updateElementsVisibility } from '../utils/dom'; export default { components: { @@ -12,6 +13,23 @@ export default { default: '/', }, }, + computed: { + isRoot() { + return this.path === '/'; + }, + }, + watch: { + isRoot: { + immediate: true, + handler: 'updateElements', + }, + }, + methods: { + updateElements(isRoot) { + updateElementsVisibility('.js-show-on-root', isRoot); + updateElementsVisibility('.js-hide-on-root', !isRoot); + }, + }, }; </script> diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js new file mode 100644 index 00000000000..963e6fc0bc4 --- /dev/null +++ b/app/assets/javascripts/repository/utils/dom.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/prefer-default-export +export const updateElementsVisibility = (selector, isVisible) => { + document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible)); +}; diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js index 87d54c01200..ff16fbdd420 100644 --- a/app/assets/javascripts/repository/utils/title.js +++ b/app/assets/javascripts/repository/utils/title.js @@ -1,10 +1,14 @@ +const DEFAULT_TITLE = '· GitLab'; // eslint-disable-next-line import/prefer-default-export export const setTitle = (pathMatch, ref, project) => { - if (!pathMatch) return; + if (!pathMatch) { + document.title = `${project} ${DEFAULT_TITLE}`; + return; + } const path = pathMatch.replace(/^\//, ''); const isEmpty = path === ''; /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`; + document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project} ${DEFAULT_TITLE}`; }; diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index 478e44d104c..f984a0a6203 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import ProjectListItem from './project_list_item.vue'; const SEARCH_INPUT_TIMEOUT_MS = 500; @@ -10,6 +10,7 @@ export default { components: { GlLoadingIcon, GlSearchBoxByType, + GlInfiniteScroll, ProjectListItem, }, props: { @@ -41,6 +42,11 @@ export default { required: false, default: false, }, + totalResults: { + type: Number, + required: false, + default: 0, + }, }, data() { return { @@ -51,6 +57,9 @@ export default { projectClicked(project) { this.$emit('projectClicked', project); }, + bottomReached() { + this.$emit('bottomReached'); + }, isSelected(project) { return Boolean(_.find(this.selectedProjects, { id: project.id })); }, @@ -71,18 +80,25 @@ export default { @input="onInput" /> <div class="d-flex flex-column"> - <gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" /> - <div v-if="!showLoadingIndicator" class="d-flex flex-column"> - <project-list-item - v-for="project in projectSearchResults" - :key="project.id" - :selected="isSelected(project)" - :project="project" - :matcher="searchQuery" - class="js-project-list-item" - @click="projectClicked(project)" - /> - </div> + <gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" /> + <gl-infinite-scroll + :max-list-height="402" + :fetched-items="projectSearchResults.length" + :total-items="totalResults" + @bottomReached="bottomReached" + > + <div v-if="!showLoadingIndicator" slot="items" class="d-flex flex-column"> + <project-list-item + v-for="project in projectSearchResults" + :key="project.id" + :selected="isSelected(project)" + :project="project" + :matcher="searchQuery" + class="js-project-list-item" + @click="projectClicked(project)" + /> + </div> + </gl-infinite-scroll> <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message"> {{ __('Sorry, no projects matched your search') }} </div> |