diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-15 21:09:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-15 21:09:37 +0300 |
commit | d811b6d8f61b45cd12f94251abff9102b8cefc19 (patch) | |
tree | 064f5bdcf19c1480959ff02b752ab8fafa46c0fc /app | |
parent | 1581767ea15fcfa63919d39c0d3579da0a4de96e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
20 files changed, 292 insertions, 97 deletions
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js index 58494c5a2b8..fd9b0160b0d 100644 --- a/app/assets/javascripts/api/analytics_api.js +++ b/app/assets/javascripts/api/analytics_api.js @@ -1,6 +1,8 @@ import axios from '~/lib/utils/axios_utils'; import { buildApiUrl } from './api_utils'; +const GROUP_VSA_PATH_BASE = + '/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id'; const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams'; const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`; @@ -13,6 +15,12 @@ const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => { return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath); }; +const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) => + buildApiUrl(GROUP_VSA_PATH_BASE) + .replace(':id', groupId) + .replace(':value_stream_id', valueStreamId) + .replace(':stage_id', stageId); + export const getProjectValueStreams = (projectPath) => { const url = buildProjectValueStreamPath(projectPath); return axios.get(url); @@ -30,3 +38,14 @@ export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) export const getProjectValueStreamMetrics = (requestPath, params) => axios.get(requestPath, { params }); + +/** + * Shared group VSA paths + * We share some endpoints across and group and project level VSA + * When used for project level VSA, requests should include the `project_id` in the params object + */ + +export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => { + const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId }); + return axios.get(`${stageBase}/median`, { params }); +}; diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js index 57cb220d9c9..615f96c3860 100644 --- a/app/assets/javascripts/cycle_analytics/index.js +++ b/app/assets/javascripts/cycle_analytics/index.js @@ -8,11 +8,24 @@ Vue.use(Translate); export default () => { const store = createStore(); const el = document.querySelector('#js-cycle-analytics'); - const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset; + const { + noAccessSvgPath, + noDataSvgPath, + requestPath, + fullPath, + projectId, + groupPath, + } = el.dataset; store.dispatch('initializeVsa', { + projectId: parseInt(projectId, 10), + groupPath, requestPath, fullPath, + features: { + cycleAnalyticsForGroups: + (groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false, + }, }); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js index ab852cbbb2d..955f0c7271e 100644 --- a/app/assets/javascripts/cycle_analytics/store/actions.js +++ b/app/assets/javascripts/cycle_analytics/store/actions.js @@ -3,6 +3,7 @@ import { getProjectValueStreams, getProjectValueStreamStageData, getProjectValueStreamMetrics, + getValueStreamStageMedian, } from '~/api/analytics_api'; import createFlash from '~/flash'; import { __ } from '~/locale'; @@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { }; export const fetchValueStreams = ({ commit, dispatch, state }) => { - const { fullPath } = state; + const { + fullPath, + features: { cycleAnalyticsForGroups }, + } = state; commit(types.REQUEST_VALUE_STREAMS); + const stageRequests = ['setSelectedStage']; + if (cycleAnalyticsForGroups) { + stageRequests.push('fetchStageMedians'); + } + return getProjectValueStreams(fullPath) .then(({ data }) => dispatch('receiveValueStreamsSuccess', data)) - .then(() => dispatch('setSelectedStage')) + .then(() => Promise.all(stageRequests.map((r) => dispatch(r)))) .catch(({ response: { status } }) => { commit(types.RECEIVE_VALUE_STREAMS_ERROR, status); }); }; -export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => { +export const fetchCycleAnalyticsData = ({ + state: { requestPath }, + getters: { legacyFilterParams }, + commit, +}) => { commit(types.REQUEST_CYCLE_ANALYTICS_DATA); - return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate }) + return getProjectValueStreamMetrics(requestPath, legacyFilterParams) .then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data)) .catch(() => { commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR); @@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com }); }; -export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => { +export const fetchStageData = ({ + state: { requestPath, selectedStage }, + getters: { legacyFilterParams }, + commit, +}) => { commit(types.REQUEST_STAGE_DATA); return getProjectValueStreamStageData({ requestPath, stageId: selectedStage.id, - params: { 'cycle_analytics[start_date]': startDate }, + params: legacyFilterParams, }) .then(({ data }) => { // when there's a query timeout, the request succeeds but the error is encoded in the response data @@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate .catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR)); }; +const getStageMedians = ({ stageId, vsaParams, filterParams = {} }) => { + return getValueStreamStageMedian({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({ + id: stageId, + value: data?.value || null, + })); +}; + +export const fetchStageMedians = ({ + state: { stages }, + getters: { requestParams: vsaParams, filterParams }, + commit, +}) => { + commit(types.REQUEST_STAGE_MEDIANS); + return Promise.all( + stages.map(({ id: stageId }) => + getStageMedians({ + vsaParams, + stageId, + filterParams, + }), + ), + ) + .then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data)) + .catch((error) => { + commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error); + createFlash({ + message: __('There was an error fetching median data for stages'), + }); + }); +}; + export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => { const stage = selectedStage || stages[0]; commit(types.SET_SELECTED_STAGE, stage); diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js index c60a70ef147..66971ea8a2e 100644 --- a/app/assets/javascripts/cycle_analytics/store/getters.js +++ b/app/assets/javascripts/cycle_analytics/store/getters.js @@ -1,3 +1,5 @@ +import dateFormat from 'dateformat'; +import { dateFormats } from '~/analytics/shared/constants'; import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils'; export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => { @@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage selectedStage, }); }; + +export const requestParams = (state) => { + const { + selectedStage: { id: stageId = null }, + groupPath: groupId, + selectedValueStream: { id: valueStreamId }, + } = state; + return { valueStreamId, groupId, stageId }; +}; + +const dateRangeParams = ({ createdAfter, createdBefore }) => ({ + created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null, + created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null, +}); + +export const legacyFilterParams = ({ startDate }) => { + return { + 'cycle_analytics[start_date]': startDate, + }; +}; + +export const filterParams = ({ id, ...rest }) => { + return { + project_ids: [id], + ...dateRangeParams(rest), + }; +}; diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js index 4f3d430ec9f..11ed62a4081 100644 --- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js +++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js @@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA'; export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS'; export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR'; + +export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS'; +export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS'; +export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR'; diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js index 0ae80116cd2..a8b7a607b66 100644 --- a/app/assets/javascripts/cycle_analytics/store/mutations.js +++ b/app/assets/javascripts/cycle_analytics/store/mutations.js @@ -1,11 +1,23 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { decorateData, decorateEvents, formatMedianValues } from '../utils'; +import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; +import { + decorateData, + decorateEvents, + formatMedianValues, + calculateFormattedDayInPast, +} from '../utils'; import * as types from './mutation_types'; export default { - [types.INITIALIZE_VSA](state, { requestPath, fullPath }) { + [types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) { state.requestPath = requestPath; state.fullPath = fullPath; + state.groupPath = groupPath; + state.id = projectId; + const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); + state.createdBefore = now; + state.createdAfter = past; + state.features = features; }, [types.SET_LOADING](state, loadingState) { state.isLoading = loadingState; @@ -18,6 +30,9 @@ export default { }, [types.SET_DATE_RANGE](state, { startDate }) { state.startDate = startDate; + const { now, past } = calculateFormattedDayInPast(startDate); + state.createdBefore = now; + state.createdAfter = past; }, [types.REQUEST_VALUE_STREAMS](state) { state.valueStreams = []; @@ -46,17 +61,25 @@ export default { [types.REQUEST_CYCLE_ANALYTICS_DATA](state) { state.isLoading = true; state.hasError = false; + if (!state.features.cycleAnalyticsForGroups) { + state.medians = {}; + } }, [types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) { const { summary, medians } = decorateData(data); + if (!state.features.cycleAnalyticsForGroups) { + state.medians = formatMedianValues(medians); + } state.permissions = data.permissions; state.summary = summary; - state.medians = formatMedianValues(medians); state.hasError = false; }, [types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) { state.isLoading = false; state.hasError = true; + if (!state.features.cycleAnalyticsForGroups) { + state.medians = {}; + } }, [types.REQUEST_STAGE_DATA](state) { state.isLoadingStage = true; @@ -78,4 +101,13 @@ export default { state.hasError = true; state.selectedStageError = error; }, + [types.REQUEST_STAGE_MEDIANS](state) { + state.medians = {}; + }, + [types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians) { + state.medians = formatMedianValues(medians); + }, + [types.RECEIVE_STAGE_MEDIANS_ERROR](state) { + state.medians = {}; + }, }; diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js index 02f953d9517..4d61077fb99 100644 --- a/app/assets/javascripts/cycle_analytics/store/state.js +++ b/app/assets/javascripts/cycle_analytics/store/state.js @@ -1,9 +1,13 @@ import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; export default () => ({ + features: {}, + id: null, requestPath: '', fullPath: '', startDate: DEFAULT_DAYS_TO_DISPLAY, + createdAfter: null, + createdBefore: null, stages: [], summary: [], analytics: [], @@ -19,4 +23,5 @@ export default () => ({ isLoadingStage: false, isEmptyStage: false, permissions: {}, + parentPath: null, }); diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js index 40ad7d8b2fc..a1690dd1513 100644 --- a/app/assets/javascripts/cycle_analytics/utils.js +++ b/app/assets/javascripts/cycle_analytics/utils.js @@ -1,6 +1,9 @@ +import dateFormat from 'dateformat'; import { unescape } from 'lodash'; +import { dateFormats } from '~/analytics/shared/constants'; import { sanitize } from '~/lib/dompurify'; import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility'; import { parseSeconds } from '~/lib/utils/datetime_utility'; import { s__, sprintf } from '../locale'; import DEFAULT_EVENT_OBJECTS from './default_event_objects'; @@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) => export const filterStagesByHiddenStatus = (stages = [], isHidden = true) => stages.filter(({ hidden = false }) => hidden === isHidden); + +const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate); + +/** + * Takes an integer specifying the number of days to subtract + * from the date specified will return the 2 dates, formatted as ISO dates + * + * @param {Number} daysInPast - Number of days in the past to subtract + * @param {Date} [today=new Date] - Date to subtract days from, defaults to today + * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates + */ +export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => { + return { + now: toIsoFormat(today), + past: toIsoFormat(getDateInPast(today, daysInPast)), + }; +}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index eaf396a7a59..5ee00464a8b 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => { export function insertFinalNewline(content, endOfLine = '\n') { return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content; } + +export const markdownConfig = { + // allowedTags from GitLab's inline HTML guidelines + // https://docs.gitlab.com/ee/user/markdown.html#inline-html + ALLOWED_TAGS: [ + 'a', + 'abbr', + 'b', + 'blockquote', + 'br', + 'code', + 'dd', + 'del', + 'div', + 'dl', + 'dt', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'ins', + 'kbd', + 'li', + 'ol', + 'p', + 'pre', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'span', + 'strike', + 'strong', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + 'tt', + 'ul', + 'var', + ], + ALLOWED_ATTR: ['class', 'style', 'href', 'src'], + ALLOW_DATA_ATTR: false, +}; diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index a7fcce02ab3..0f4cec67ce8 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -3,7 +3,7 @@ import katex from 'katex'; import marked from 'marked'; import { sanitize } from '~/lib/dompurify'; -import { hasContent } from '~/lib/utils/text_utility'; +import { hasContent, markdownConfig } from '~/lib/utils/text_utility'; import Prompt from './prompt.vue'; const renderer = new marked.Renderer(); @@ -140,63 +140,7 @@ export default { markdown() { renderer.attachments = this.cell.attachments; - return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { - // allowedTags from GitLab's inline HTML guidelines - // https://docs.gitlab.com/ee/user/markdown.html#inline-html - ALLOWED_TAGS: [ - 'a', - 'abbr', - 'b', - 'blockquote', - 'br', - 'code', - 'dd', - 'del', - 'div', - 'dl', - 'dt', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'img', - 'ins', - 'kbd', - 'li', - 'ol', - 'p', - 'pre', - 'q', - 'rp', - 'rt', - 'ruby', - 's', - 'samp', - 'span', - 'strike', - 'strong', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr', - 'tt', - 'ul', - 'var', - ], - ALLOWED_ATTR: ['class', 'style', 'href', 'src'], - ALLOW_DATA_ATTR: false, - }); + return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), markdownConfig); }, }, }; diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index c91ae1ed14e..5ea431224ce 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -13,6 +13,7 @@ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; +import { renderMarkdown } from '../utils'; import { getStartLineNumber, getEndLineNumber, @@ -300,7 +301,7 @@ export default { this.isRequesting = true; this.oldContent = this.note.note_html; // eslint-disable-next-line vue/no-mutating-props - this.note.note_html = escape(noteText); + this.note.note_html = renderMarkdown(noteText); this.updateNote(data) .then(() => { diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 7bd8d28282c..6a4a3263e4a 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -312,25 +312,23 @@ export const saveNote = ({ commit, dispatch }, noteData) => { $('.notes-form .flash-container').hide(); // hide previous flash notification commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders - if (replyId) { - if (hasQuickActions) { - placeholderText = utils.stripQuickActions(placeholderText); - } + if (hasQuickActions) { + placeholderText = utils.stripQuickActions(placeholderText); + } - if (placeholderText.length) { - commit(types.SHOW_PLACEHOLDER_NOTE, { - noteBody: placeholderText, - replyId, - }); - } + if (placeholderText.length) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + noteBody: placeholderText, + replyId, + }); + } - if (hasQuickActions) { - commit(types.SHOW_PLACEHOLDER_NOTE, { - isSystemNote: true, - noteBody: utils.getQuickActionText(note), - replyId, - }); - } + if (hasQuickActions) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + isSystemNote: true, + noteBody: utils.getQuickActionText(note), + replyId, + }); } const processQuickActions = (res) => { @@ -400,9 +398,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const removePlaceholder = (res) => { - if (replyId) { - commit(types.REMOVE_PLACEHOLDER_NOTES); - } + commit(types.REMOVE_PLACEHOLDER_NOTES); return res; }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index b04b1d28ffa..956221d69ae 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -279,7 +279,7 @@ export const getDiscussion = (state) => (discussionId) => export const commentsDisabled = (state) => state.commentsDisabled; export const suggestionsCount = (state, getters) => - Object.values(getters.notesById).filter((n) => n.suggestions.length).length; + Object.values(getters.notesById).filter((n) => n.suggestions?.length).length; export const hasDrafts = (state, getters, rootState, rootGetters) => Boolean(rootGetters['batchComments/hasDrafts']); diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js index 7966a884eab..ec18a570960 100644 --- a/app/assets/javascripts/notes/utils.js +++ b/app/assets/javascripts/notes/utils.js @@ -1,4 +1,7 @@ /* eslint-disable @gitlab/require-i18n-strings */ +import marked from 'marked'; +import { sanitize } from '~/lib/dompurify'; +import { markdownConfig } from '~/lib/utils/text_utility'; /** * Tracks snowplow event when User toggles timeline view @@ -10,3 +13,7 @@ export const trackToggleTimelineView = (enabled) => ({ label: 'Status', property: enabled, }); + +export const renderMarkdown = (rawMarkdown) => { + return sanitize(marked(rawMarkdown), markdownConfig); +}; diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 69afd711797..d6501a37a35 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -16,12 +16,15 @@ * :note="{body: 'This is a note'}" * /> */ +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { mapGetters } from 'vuex'; +import { renderMarkdown } from '~/notes/utils'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { name: 'PlaceholderNote', + directives: { SafeHtml }, components: { userAvatarLink, TimelineEntryItem, @@ -34,6 +37,9 @@ export default { }, computed: { ...mapGetters(['getUserData']), + renderedNote() { + return renderMarkdown(this.note.body); + }, }, }; </script> @@ -57,9 +63,7 @@ export default { </div> </div> <div class="note-body"> - <div class="note-text md"> - <p>{{ note.body }}</p> - </div> + <div v-safe-html="renderedNote" class="note-text md"></div> </div> </div> </timeline-entry-item> diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index d1d27286c68..3142ef0f404 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -13,6 +13,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController feature_category :planning_analytics + before_action do + push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups) + end + def show @cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params)) diff --git a/app/graphql/types/user_callout_type.rb b/app/graphql/types/user_callout_type.rb index 12f4fdea878..0ff32d68400 100644 --- a/app/graphql/types/user_callout_type.rb +++ b/app/graphql/types/user_callout_type.rb @@ -4,7 +4,7 @@ module Types class UserCalloutType < BaseObject # rubocop:disable Graphql/AuthorizeTypes graphql_name 'UserCallout' - field :feature_name, UserCalloutFeatureNameEnum, null: false, + field :feature_name, UserCalloutFeatureNameEnum, null: true, description: 'Name of the feature that the callout is for.' field :dismissed_at, Types::TimeType, null: true, description: 'Date when the callout was dismissed.' diff --git a/app/models/wiki.rb b/app/models/wiki.rb index a58994f8c86..e114e30d589 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -87,7 +87,8 @@ class Wiki end def create_wiki_repository - change_head_to_default_branch if repository.create_if_not_exists + repository.create_if_not_exists + change_head_to_default_branch raise CouldNotCreateWikiError unless repository_exists? rescue StandardError => err @@ -249,7 +250,7 @@ class Wiki override :default_branch def default_branch - super || wiki.class.default_ref(container) + super || Gitlab::Git::Wiki.default_ref(container) end def wiki_base_path @@ -323,6 +324,12 @@ class Wiki end def change_head_to_default_branch + # If the wiki has commits in the 'HEAD' branch means that the current + # HEAD is pointing to the right branch. If not, it could mean that either + # the repo has just been created or that 'HEAD' is pointing + # to the wrong branch and we need to rewrite it + return if repository.raw_repository.commit_count('HEAD') != 0 + repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}") end end diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb index 82179459345..88a593cce48 100644 --- a/app/services/wikis/create_attachment_service.rb +++ b/app/services/wikis/create_attachment_service.rb @@ -21,7 +21,11 @@ module Wikis end def create_commit! + wiki.create_wiki_repository + commit_result(create_transformed_commit(@file_content)) + rescue Wiki::CouldNotCreateWikiError + raise_error("Error creating the wiki repository") end private diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c1f6cfc40c3..3c9762e200a 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,6 +1,6 @@ - page_title _("Value Stream Analytics") - add_page_specific_style 'page_bundles/cycle_analytics' - svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") } -- initial_data = { request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs) +- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs) #js-cycle-analytics{ data: initial_data } |