diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-13 12:08:41 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-13 12:08:41 +0000 |
commit | 6e91fbf77476011a7fd86ca3467aad6d7b110ff3 (patch) | |
tree | cace6db4e7ebef8b15a6a7fc8fbe8ff0d89bea90 /app/assets | |
parent | 15ae4a8da83661f2b714d804721001a53b354d28 (diff) | |
download | gitlab-ce-6e91fbf77476011a7fd86ca3467aad6d7b110ff3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
26 files changed, 581 insertions, 205 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js new file mode 100644 index 00000000000..30c6205b7ff --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/utils.js @@ -0,0 +1,40 @@ +import { masks } from 'dateformat'; +import { formatDate } from '~/lib/utils/datetime_utility'; + +const { isoDate } = masks; + +/** + * Takes an array of items and returns one item per month with the average of the `count`s from that month + * @param {Array} items + * @param {Number} items[index].count value to be averaged + * @param {String} items[index].recordedAt item dateTime time stamp to be collected into a month + * @param {Object} options + * @param {Object} options.shouldRound an option to specify whether the retuned averages should be rounded + * @return {Array} items collected into [month, average], + * where month is a dateTime string representing the first of the given month + * and average is the average of the count + */ +export function getAverageByMonth(items = [], options = {}) { + const { shouldRound = false } = options; + const itemsMap = items.reduce((memo, item) => { + const { count, recordedAt } = item; + const date = new Date(recordedAt); + const month = formatDate(new Date(date.getFullYear(), date.getMonth(), 1), isoDate); + if (memo[month]) { + const { sum, recordCount } = memo[month]; + return { ...memo, [month]: { sum: sum + count, recordCount: recordCount + 1 } }; + } + + return { ...memo, [month]: { sum: count, recordCount: 1 } }; + }, {}); + + return Object.keys(itemsMap).map(month => { + const { sum, recordCount } = itemsMap[month]; + const avg = sum / recordCount; + if (shouldRound) { + return [month, Math.round(avg)]; + } + + return [month, avg]; + }); +} diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 09abdbe25d7..1b747fb7f20 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -122,67 +122,20 @@ export default { </script> <template> - <li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row"> - <div class="d-flex align-items-center align-self-start"> - <input - v-if="isSelectable" - class="mr-2" - type="checkbox" - :checked="checked" - @change="$emit('handleCheckboxChange', $event.target.checked)" - /> - <user-avatar-link - :link-href="authorUrl" - :img-src="authorAvatar" - :img-alt="authorName" - :img-size="40" - class="avatar-cell d-none d-sm-block" - /> - </div> - <div class="commit-detail flex-list"> - <div class="commit-content qa-commit-content"> - <a - :href="commit.commit_url" - class="commit-row-message item-title" - v-html="commit.title_html" - ></a> - - <span class="commit-row-message d-block d-sm-none">· {{ commit.short_id }}</span> - - <gl-button - v-if="commit.description_html && collapsible" - class="js-toggle-button" - size="small" - icon="ellipsis_h" - :aria-label="__('Toggle commit description')" - /> - - <div class="committer"> - <a - :href="authorUrl" - :class="authorClass" - :data-user-id="authorId" - v-text="authorName" - ></a> - {{ s__('CommitWidget|authored') }} - <time-ago-tooltip :time="commit.authored_date" /> - </div> - - <pre - v-if="commit.description_html" - :class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }" - class="commit-row-description gl-mb-3 text-dark" - v-html="commitDescription" - ></pre> - </div> - <div class="commit-actions flex-row d-none d-sm-flex"> + <li :class="{ 'js-toggle-container': collapsible }" class="commit"> + <div + class="d-block d-sm-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse" + > + <div + class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end" + > <div v-if="commit.signature_html" v-html="commit.signature_html"></div> <commit-pipeline-status v-if="commit.pipeline_status_path" :endpoint="commit.pipeline_status_path" - class="d-inline-flex" + class="d-inline-flex mb-2" /> - <gl-button-group class="gl-ml-4" data-testid="commit-sha-group"> + <gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group"> <gl-button label class="gl-font-monospace" v-text="commit.short_id" /> <clipboard-button :text="commit.id" @@ -226,6 +179,62 @@ export default { </gl-button-group> </div> </div> + <div> + <div class="d-flex float-left align-items-center align-self-start"> + <input + v-if="isSelectable" + class="mr-2" + type="checkbox" + :checked="checked" + @change="$emit('handleCheckboxChange', $event.target.checked)" + /> + <user-avatar-link + :link-href="authorUrl" + :img-src="authorAvatar" + :img-alt="authorName" + :img-size="40" + class="avatar-cell d-none d-sm-block" + /> + </div> + <div class="commit-detail flex-list"> + <div class="commit-content qa-commit-content"> + <a + :href="commit.commit_url" + class="commit-row-message item-title" + v-html="commit.title_html" + ></a> + + <span class="commit-row-message d-block d-sm-none">· {{ commit.short_id }}</span> + + <gl-button + v-if="commit.description_html && collapsible" + class="js-toggle-button" + size="small" + icon="ellipsis_h" + :aria-label="__('Toggle commit description')" + /> + + <div class="committer"> + <a + :href="authorUrl" + :class="authorClass" + :data-user-id="authorId" + v-text="authorName" + ></a> + {{ s__('CommitWidget|authored') }} + <time-ago-tooltip :time="commit.authored_date" /> + </div> + </div> + </div> + </div> + </div> + <div> + <pre + v-if="commit.description_html" + :class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }" + class="commit-row-description gl-mb-3 text-dark" + v-html="commitDescription" + ></pre> </div> </li> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 73c56514fce..f36fe87ccfa 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; -import consts from '../../stores/modules/commit/constants'; import { createUnexpectedCommitError } from '../../lib/errors'; export default { @@ -45,12 +44,11 @@ export default { return this.currentActivityView === leftSidebarViews.commit.name; }, commitErrorPrimaryAction() { - if (!this.lastCommitError?.canCreateBranch) { - return undefined; - } + const { primaryAction } = this.lastCommitError || {}; return { - text: __('Create new branch'), + button: primaryAction ? { text: primaryAction.text } : undefined, + callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}), }; }, }, @@ -78,9 +76,6 @@ export default { commit() { return this.commitChanges(); }, - forceCreateNewBranch() { - return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); - }, handleCompactState() { if (this.lastCommitMsg) { this.isCompact = false; @@ -188,9 +183,9 @@ export default { ref="commitErrorModal" modal-id="ide-commit-error-modal" :title="lastCommitError.title" - :action-primary="commitErrorPrimaryAction" + :action-primary="commitErrorPrimaryAction.button" :action-cancel="{ text: __('Cancel') }" - @ok="forceCreateNewBranch" + @ok="commitErrorPrimaryAction.callback" > <div v-safe-html="lastCommitError.messageHTML"></div> </gl-modal> diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js index 6ae18bc8180..e62d9d1e77f 100644 --- a/app/assets/javascripts/ide/lib/errors.js +++ b/app/assets/javascripts/ide/lib/errors.js @@ -1,25 +1,49 @@ import { escape } from 'lodash'; import { __ } from '~/locale'; +import consts from '../stores/modules/commit/constants'; const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; +const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/; -export const createUnexpectedCommitError = () => ({ +const createNewBranchAndCommit = store => + store + .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH) + .then(() => store.dispatch('commit/commitChanges')); + +export const createUnexpectedCommitError = message => ({ title: __('Unexpected error'), - messageHTML: __('Could not commit. An unexpected error occurred.'), - canCreateBranch: false, + messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'), }); export const createCodeownersCommitError = message => ({ title: __('CODEOWNERS rule violation'), messageHTML: escape(message), - canCreateBranch: true, + primaryAction: { + text: __('Create new branch'), + callback: createNewBranchAndCommit, + }, }); export const createBranchChangedCommitError = message => ({ title: __('Branch changed'), messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, - canCreateBranch: true, + primaryAction: { + text: __('Create new branch'), + callback: createNewBranchAndCommit, + }, +}); + +export const branchAlreadyExistsCommitError = message => ({ + title: __('Branch already exists'), + messageHTML: `${escape(message)}<br/><br/>${__( + 'Would you like to try auto-generating a branch name?', + )}`, + primaryAction: { + text: __('Create new branch'), + callback: store => + store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)), + }, }); export const parseCommitError = e => { @@ -33,7 +57,9 @@ export const parseCommitError = e => { return createCodeownersCommitError(message); } else if (BRANCH_CHANGED_REGEX.test(message)) { return createBranchChangedCommitError(message); + } else if (BRANCH_ALREADY_EXISTS.test(message)) { + return branchAlreadyExistsCommitError(message); } - return createUnexpectedCommitError(); + return createUnexpectedCommitError(message); }; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index b8304a9b68d..500ce9f32d5 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -6,6 +6,7 @@ import { PERMISSION_CREATE_MR, PERMISSION_PUSH_CODE, } from '../constants'; +import { addNumericSuffix } from '~/ide/utils'; import Api from '~/api'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => { let newPath = path; while (getters.entryExists(newPath)) { - newPath = newPath.replace( - /([ _-]?)(\d*)(\..+?$|$)/, - (_, before, number, after) => `${before || '_'}${Number(number) + 1}${after}`, - ); + newPath = addNumericSuffix(newPath); } return newPath; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 90a6c644d17..e0d2028d2e1 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -8,6 +8,7 @@ import consts from './constants'; import { leftSidebarViews } from '../../../constants'; import eventHub from '../../../eventhub'; import { parseCommitError } from '../../../lib/errors'; +import { addNumericSuffix } from '~/ide/utils'; export const updateCommitMessage = ({ commit }, message) => { commit(types.UPDATE_COMMIT_MESSAGE, message); @@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => { commit(types.UPDATE_COMMIT_MESSAGE, ''); }; -export const updateCommitAction = ({ commit, getters }, commitAction) => { - commit(types.UPDATE_COMMIT_ACTION, { - commitAction, - }); - commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption); +export const updateCommitAction = ({ commit }, commitAction) => { + commit(types.UPDATE_COMMIT_ACTION, { commitAction }); }; export const toggleShouldCreateMR = ({ commit }) => { @@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => { commit(types.UPDATE_NEW_BRANCH_NAME, branchName); }; +export const addSuffixToBranchName = ({ commit, state }) => { + const newBranchName = addNumericSuffix(state.newBranchName, true); + + commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName); +}; + export const setLastCommitMessage = ({ commit, rootGetters }, data) => { const { currentProject } = rootGetters; const commitStats = data.stats @@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { // Pull commit options out because they could change // During some of the pre and post commit processing - const { shouldCreateMR, isCreatingNewBranch, branchName } = getters; + const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const stageFilesPromise = rootState.stagedFiles.length ? Promise.resolve() @@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); - if (shouldCreateMR) { + if (shouldCreateMR && !shouldHideNewMrOption) { const { currentProject } = rootGetters; const targetBranch = isCreatingNewBranch ? rootState.currentBranchId diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js index 2cf6e8e6f36..c4bfad6405e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -10,9 +10,7 @@ export default { Object.assign(state, { commitAction }); }, [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { - Object.assign(state, { - newBranchName, - }); + Object.assign(state, { newBranchName }); }, [types.UPDATE_LOADING](state, submitCommitLoading) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index f7ecf6340d9..404c5c571ba 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -139,6 +139,34 @@ export function getFileEOL(content = '') { return content.includes('\r\n') ? 'CRLF' : 'LF'; } +/** + * Adds or increments the numeric suffix to a filename/branch name. + * Retains underscore or dash before the numeric suffix if it already exists. + * + * Examples: + * hello -> hello-1 + * hello-2425 -> hello-2425 + * hello.md -> hello-1.md + * hello_2.md -> hello_3.md + * hello_ -> hello_1 + * master-patch-22432 -> master-patch-22433 + * patch_332 -> patch_333 + * + * @param {string} filename File name or branch name + * @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing? + */ +export function addNumericSuffix(filename, randomize = false) { + return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => { + const n = randomize + ? Math.random() + .toString() + .substring(2, 7) + .slice(-5) + : Number(number) + 1; + return `${before || '-'}${n}${after}`; + }); +} + export const measurePerformance = ( mark, measureName, diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 061a5becbed..50f2136325d 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -16,6 +16,7 @@ import { GlEmptyState, } from '@gitlab/ui'; import Api from '~/api'; +import Tracking from '~/tracking'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; @@ -41,6 +42,7 @@ import { TH_SEVERITY_TEST_ID, TH_PUBLISHED_TEST_ID, INCIDENT_DETAILS_PATH, + trackIncidentCreateNewOptions, } from '../constants'; const tdClass = @@ -58,6 +60,7 @@ const initialPaginationState = { }; export default { + trackIncidentCreateNewOptions, i18n: I18N, statusTabs: INCIDENT_STATUS_TABS, fields: [ @@ -335,6 +338,11 @@ export default { navigateToIncidentDetails({ iid }) { return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid)); }, + navigateToCreateNewIncident() { + const { category, action } = this.$options.trackIncidentCreateNewOptions; + Tracking.event(category, action); + this.redirecting = true; + }, handlePageChange(page) { const { startCursor, endCursor } = this.incidents.pageInfo; @@ -458,7 +466,7 @@ export default { category="primary" variant="success" :href="newIncidentPath" - @click="redirecting = true" + @click="navigateToCreateNewIncident" > {{ $options.i18n.createIncidentBtnLabel }} </gl-button> diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index 797439495e3..bdabf1c3e42 100644 --- a/app/assets/javascripts/incidents/constants.js +++ b/app/assets/javascripts/incidents/constants.js @@ -1,3 +1,4 @@ +/* eslint-disable @gitlab/require-i18n-strings */ import { s__, __ } from '~/locale'; export const I18N = { @@ -34,6 +35,14 @@ export const INCIDENT_STATUS_TABS = [ }, ]; +/** + * Tracks snowplow event when user clicks create new incident + */ +export const trackIncidentCreateNewOptions = { + category: 'Incident Management', + action: 'create_incident_button_clicks', +}; + export const DEFAULT_PAGE_SIZE = 20; export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' }; diff --git a/app/assets/javascripts/issuable_show/components/issuable_description.vue b/app/assets/javascripts/issuable_show/components/issuable_description.vue new file mode 100644 index 00000000000..091a4be5bd8 --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_description.vue @@ -0,0 +1,31 @@ +<script> +import $ from 'jquery'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import '~/behaviors/markdown/render_gfm'; + +export default { + directives: { + SafeHtml, + }, + props: { + issuable: { + type: Object, + required: true, + }, + }, + mounted() { + this.renderGFM(); + }, + methods: { + renderGFM() { + $(this.$refs.gfmContainer).renderGFM(); + }, + }, +}; +</script> + +<template> + <div class="description"> + <div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue new file mode 100644 index 00000000000..7b9a83a740f --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue @@ -0,0 +1,135 @@ +<script> +import $ from 'jquery'; +import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; + +import Autosave from '~/autosave'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; + +import eventHub from '../event_hub'; + +export default { + components: { + GlForm, + GlFormGroup, + GlFormInput, + MarkdownField, + }, + props: { + issuable: { + type: Object, + required: true, + }, + enableAutocomplete: { + type: Boolean, + required: true, + }, + descriptionPreviewPath: { + type: String, + required: true, + }, + descriptionHelpPath: { + type: String, + required: true, + }, + }, + data() { + const { title, description } = this.issuable; + + return { + title, + description, + }; + }, + created() { + eventHub.$on('update.issuable', this.resetAutosave); + eventHub.$on('close.form', this.resetAutosave); + }, + mounted() { + this.initAutosave(); + }, + beforeDestroy() { + eventHub.$off('update.issuable', this.resetAutosave); + eventHub.$off('close.form', this.resetAutosave); + }, + methods: { + initAutosave() { + const { titleInput, descriptionInput } = this.$refs; + + if (!titleInput || !descriptionInput) return; + + this.autosaveTitle = new Autosave($(titleInput.$el), [ + document.location.pathname, + document.location.search, + 'title', + ]); + + this.autosaveDescription = new Autosave($(descriptionInput.$el), [ + document.location.pathname, + document.location.search, + 'description', + ]); + }, + resetAutosave() { + this.autosaveTitle.reset(); + this.autosaveDescription.reset(); + }, + }, +}; +</script> + +<template> + <gl-form> + <gl-form-group + data-testid="title" + :label="__('Title')" + :label-sr-only="true" + label-for="issuable-title" + class="col-12" + > + <gl-form-input + id="issuable-title" + ref="titleInput" + v-model.trim="title" + :placeholder="__('Title')" + :aria-label="__('Title')" + :autofocus="true" + class="qa-title-input" + /> + </gl-form-group> + <gl-form-group + data-testid="description" + :label="__('Description')" + :label-sr-only="true" + label-for="issuable-description" + class="col-12 common-note-form" + > + <markdown-field + :markdown-preview-path="descriptionPreviewPath" + :markdown-docs-path="descriptionHelpPath" + :enable-autocomplete="enableAutocomplete" + :textarea-value="description" + > + <template #textarea> + <textarea + id="issuable-description" + ref="descriptionInput" + v-model="description" + :data-supports-quick-actions="enableAutocomplete" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files hereā¦')" + class="note-textarea js-gfm-input js-autosize markdown-area + qa-description-textarea" + dir="auto" + ></textarea> + </template> + </markdown-field> + </gl-form-group> + <div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 clearfix"> + <slot + name="edit-form-actions" + :issuable-title="title" + :issuable-description="description" + ></slot> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/issuable_show/components/issuable_title.vue b/app/assets/javascripts/issuable_show/components/issuable_title.vue new file mode 100644 index 00000000000..d3b42fd2ffb --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_title.vue @@ -0,0 +1,96 @@ +<script> +import { + GlIcon, + GlButton, + GlIntersectionObserver, + GlTooltipDirective, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlButton, + GlIntersectionObserver, + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml, + }, + props: { + issuable: { + type: Object, + required: true, + }, + statusBadgeClass: { + type: String, + required: true, + }, + statusIcon: { + type: String, + required: true, + }, + enableEdit: { + type: Boolean, + required: true, + }, + }, + data() { + return { + stickyTitleVisible: false, + }; + }, + methods: { + handleTitleAppear() { + this.stickyTitleVisible = false; + }, + handleTitleDisappear() { + this.stickyTitleVisible = true; + }, + }, +}; +</script> + +<template> + <div> + <div class="title-container"> + <h2 v-safe-html="issuable.titleHtml" class="title qa-title" dir="auto"></h2> + <gl-button + v-if="enableEdit" + v-gl-tooltip.bottom + :title="__('Edit title and description')" + icon="pencil" + class="btn-edit js-issuable-edit qa-edit-button" + @click="$emit('edit-issuable', $event)" + /> + </div> + <gl-intersection-observer @appear="handleTitleAppear" @disappear="handleTitleDisappear"> + <transition name="issuable-header-slide"> + <div + v-if="stickyTitleVisible" + class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3" + data-testid="header" + > + <div + class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5" + > + <p + data-testid="status" + class="issuable-status-box status-box gl-my-0" + :class="statusBadgeClass" + > + <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" /> + <span class="gl-display-none d-sm-block"><slot name="status-badge"></slot></span> + </p> + <p + class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0" + :title="issuable.title" + > + {{ issuable.title }} + </p> + </div> + </div> + </transition> + </gl-intersection-observer> + </div> +</template> diff --git a/app/assets/javascripts/issuable_show/event_hub.js b/app/assets/javascripts/issuable_show/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/issuable_show/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue index 60ad468c293..0518fac98fc 100644 --- a/app/assets/javascripts/packages/details/components/composer_installation.vue +++ b/app/assets/javascripts/packages/details/components/composer_installation.vue @@ -14,12 +14,12 @@ export default { }, computed: { ...mapState(['composerHelpPath']), - ...mapGetters(['composerRegistryInclude', 'composerPackageInclude']), + ...mapGetters(['composerRegistryInclude', 'composerPackageInclude', 'groupExists']), }, i18n: { - registryInclude: s__('PackageRegistry|composer.json registry include'), + registryInclude: s__('PackageRegistry|Add composer registry'), copyRegistryInclude: s__('PackageRegistry|Copy registry include'), - packageInclude: s__('PackageRegistry|composer.json require package include'), + packageInclude: s__('PackageRegistry|Install package version'), copyPackageInclude: s__('PackageRegistry|Copy require package include'), infoLine: s__( 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}', @@ -32,31 +32,33 @@ export default { <template> <div> - <h3 class="gl-font-lg">{{ __('Installation') }}</h3> + <div v-if="groupExists"> + <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <code-instruction - :label="$options.i18n.registryInclude" - :instruction="composerRegistryInclude" - :copy-text="$options.i18n.copyRegistryInclude" - :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" - :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" - data-testid="registry-include" - /> + <code-instruction + :label="$options.i18n.registryInclude" + :instruction="composerRegistryInclude" + :copy-text="$options.i18n.copyRegistryInclude" + :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + data-testid="registry-include" + /> - <code-instruction - :label="$options.i18n.packageInclude" - :instruction="composerPackageInclude" - :copy-text="$options.i18n.copyPackageInclude" - :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" - :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" - data-testid="package-include" - /> - <span data-testid="help-text"> - <gl-sprintf :message="$options.i18n.infoLine"> - <template #link="{ content }"> - <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </span> + <code-instruction + :label="$options.i18n.packageInclude" + :instruction="composerPackageInclude" + :copy-text="$options.i18n.copyPackageInclude" + :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + data-testid="package-include" + /> + <span data-testid="help-text"> + <gl-sprintf :message="$options.i18n.infoLine"> + <template #link="{ content }"> + <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </div> </div> </template> diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js index bb0ae3e9ab7..14e76ac84bd 100644 --- a/app/assets/javascripts/packages/details/store/getters.js +++ b/app/assets/javascripts/packages/details/store/getters.js @@ -102,11 +102,12 @@ repository = ${pypiSetupPath} username = __token__ password = <your personal access token>`; -export const composerRegistryInclude = ({ composerPath }) => { - const base = { type: 'composer', url: composerPath }; - return JSON.stringify(base); -}; -export const composerPackageInclude = ({ packageEntity }) => { - const base = { [packageEntity.name]: packageEntity.version }; - return JSON.stringify(base); -}; +export const composerRegistryInclude = ({ composerPath, composerConfigRepositoryName }) => + // eslint-disable-next-line @gitlab/require-i18n-strings + `composer config repositories.${composerConfigRepositoryName} '{"type": "composer", "url": "${composerPath}"}'`; + +export const composerPackageInclude = ({ packageEntity }) => + // eslint-disable-next-line @gitlab/require-i18n-strings + `composer req ${[packageEntity.name]}:${packageEntity.version}`; + +export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0; diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index 089cac9ee4c..e18cfefc3ca 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -1,16 +1,12 @@ <script> import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { __ } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import eventHub from '../event_hub'; export default { name: 'ServiceDeskSetting', - directives: { - tooltip, - }, components: { ClipboardButton, GlButton, diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 3e87833f7f5..0e2bccfabdd 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -2,6 +2,7 @@ /* eslint-disable vue/no-v-html */ import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; +import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql'; import { sprintf, s__ } from '~/locale'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; @@ -9,7 +10,6 @@ import CiIcon from '../../vue_shared/components/ci_icon.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; -import pathLastCommitQuery from '../queries/path_last_commit.query.graphql'; export default { components: { diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 65da8f70b40..0e4d724e949 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql'; import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility'; import createRouter from './router'; import App from './components/app.vue'; @@ -18,6 +19,10 @@ export default function setupVueRepositoryList() { const { dataset } = el; const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset; const router = createRouter(projectPath, escapedRef); + const pathRegex = /-\/tree\/[^/]+\/(.+$)/; + const matches = window.location.href.match(pathRegex); + + const currentRoutePath = matches ? matches[1] : ''; apolloProvider.clients.defaultClient.cache.writeData({ data: { @@ -29,6 +34,43 @@ export default function setupVueRepositoryList() { }, }); + const initLastCommitApp = () => + new Vue({ + el: document.getElementById('js-last-commit'), + router, + apolloProvider, + render(h) { + return h(LastCommit, { + props: { + currentPath: this.$route.params.path, + }, + }); + }, + }); + + if (window.gl.startup_graphql_calls) { + const query = window.gl.startup_graphql_calls.find( + call => call.operationName === 'pathLastCommit', + ); + query.fetchCall + .then(res => res.json()) + .then(res => { + apolloProvider.clients.defaultClient.writeQuery({ + query: PathLastCommitQuery, + data: res.data, + variables: { + projectPath, + ref, + path: currentRoutePath, + }, + }); + }) + .catch(() => {}) + .finally(() => initLastCommitApp()); + } else { + initLastCommitApp(); + } + router.afterEach(({ params: { path } }) => { setTitle(path, ref, fullName); }); @@ -77,20 +119,6 @@ export default function setupVueRepositoryList() { }); } - // eslint-disable-next-line no-new - new Vue({ - el: document.getElementById('js-last-commit'), - router, - apolloProvider, - render(h) { - return h(LastCommit, { - props: { - currentPath: this.$route.params.path, - }, - }); - }, - }); - const treeHistoryLinkEl = document.getElementById('js-tree-history-link'); const { historyLink } = treeHistoryLinkEl.dataset; diff --git a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql b/app/assets/javascripts/repository/queries/path_last_commit.query.graphql deleted file mode 100644 index 51f3f790a5d..00000000000 --- a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql +++ /dev/null @@ -1,38 +0,0 @@ -query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { - project(fullPath: $projectPath) { - repository { - tree(path: $path, ref: $ref) { - lastCommit { - sha - title - titleHtml - descriptionHtml - message - webPath - authoredDate - authorName - authorGravatar - author { - name - avatarUrl - webPath - } - signatureHtml - pipelines(ref: $ref, first: 1) { - edges { - node { - detailedStatus { - detailsPath - icon - tooltip - text - group - } - } - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index acf6f65b1a0..46749fc5e87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -4,6 +4,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_ import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue'; import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; +import { GlSafeHtmlDirective } from '@gitlab/ui'; import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; @@ -52,6 +53,9 @@ export default { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 // eslint-disable-next-line @gitlab/require-i18n-strings name: 'MRWidget', + directives: { + SafeHtml: GlSafeHtmlDirective, + }, components: { Loading, 'mr-widget-header': WidgetHeader, @@ -510,7 +514,7 @@ export default { </mr-widget-alert-message> <mr-widget-alert-message v-if="mr.mergeError" type="danger"> - {{ mergeError }} + <span v-safe-html="mergeError"></span> </mr-widget-alert-message> <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue index e9b99c6ea78..11049028ff6 100644 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ b/app/assets/javascripts/vue_shared/components/split_button.vue @@ -1,19 +1,15 @@ <script> import { isString } from 'lodash'; -import { - GlDeprecatedDropdown, - GlDeprecatedDropdownDivider, - GlDeprecatedDropdownItem, -} from '@gitlab/ui'; +import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui'; const isValidItem = item => isString(item.eventName) && isString(item.title) && isString(item.description); export default { components: { - GlDeprecatedDropdown, - GlDeprecatedDropdownDivider, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, }, props: { @@ -32,7 +28,7 @@ export default { variant: { type: String, required: false, - default: 'secondary', + default: 'default', }, }, @@ -61,8 +57,8 @@ export default { </script> <template> - <gl-deprecated-dropdown - :menu-class="`dropdown-menu-selectable ${menuClass}`" + <gl-dropdown + :menu-class="menuClass" split :text="dropdownToggleText" :variant="variant" @@ -70,20 +66,20 @@ export default { @click="triggerEvent" > <template v-for="(item, itemIndex) in actionItems"> - <gl-deprecated-dropdown-item + <gl-dropdown-item :key="item.eventName" - :active="selectedItem === item" - active-class="is-active" + :is-check-item="true" + :is-checked="selectedItem === item" @click="changeSelectedItem(item)" > <strong>{{ item.title }}</strong> <div>{{ item.description }}</div> - </gl-deprecated-dropdown-item> + </gl-dropdown-item> - <gl-deprecated-dropdown-divider + <gl-dropdown-divider v-if="itemIndex < actionItems.length - 1" :key="`${item.eventName}-divider`" /> </template> - </gl-deprecated-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index d3ab4be925b..d49134eb648 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -10,8 +10,6 @@ @import './pages/detail_page'; @import './pages/editor'; @import './pages/environment_logs'; -@import './pages/error_list'; -@import './pages/error_tracking_list'; @import './pages/events'; @import './pages/experience_level'; @import './pages/experimental_separate_sign_up'; diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/page_bundles/error_tracking_index.scss index 3ec3e4f6b43..65bddfb7890 100644 --- a/app/assets/stylesheets/pages/error_list.scss +++ b/app/assets/stylesheets/page_bundles/error_tracking_index.scss @@ -1,4 +1,10 @@ +@import 'page_bundles/mixins_and_variables_and_functions'; + .error-list { + .dropdown { + min-width: auto; + } + .sort-control { .btn { padding-right: 2rem; @@ -17,7 +23,7 @@ min-height: 68px; &:last-child { - background-color: $gray-10; + background-color: var(--gray-10, $gray-10); &::before { content: none !important; diff --git a/app/assets/stylesheets/page_bundles/merge_conflicts.scss b/app/assets/stylesheets/page_bundles/merge_conflicts.scss index 25d913c79de..b0655408edf 100644 --- a/app/assets/stylesheets/page_bundles/merge_conflicts.scss +++ b/app/assets/stylesheets/page_bundles/merge_conflicts.scss @@ -226,6 +226,14 @@ $colors: ( .solarized-dark { @include color-scheme('solarized-dark'); } + .none { + .line_content.header { + button { + color: $gray-900; + } + } + } + .diff-wrap-lines .line_content { white-space: normal; min-height: 19px; diff --git a/app/assets/stylesheets/pages/error_tracking_list.scss b/app/assets/stylesheets/pages/error_tracking_list.scss deleted file mode 100644 index cc391ca6c97..00000000000 --- a/app/assets/stylesheets/pages/error_tracking_list.scss +++ /dev/null @@ -1,5 +0,0 @@ -.error-list { - .dropdown { - min-width: auto; - } -} |