diff options
Diffstat (limited to 'app')
37 files changed, 445 insertions, 220 deletions
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue index a17378edafd..b0f19e5b585 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue @@ -214,7 +214,7 @@ export default { <gl-form-checkbox v-model="issueTransitionEnabled" :disabled="isInheriting" - data-qa-selector="service_jira_issue_transition_enabled" + data-qa-selector="service_jira_issue_transition_enabled_checkbox" > {{ s__('JiraService|Enable Jira transitions') }} </gl-form-checkbox> @@ -232,7 +232,7 @@ export default { name="service[jira_issue_transition_automatic]" :value="issueTransitionOption.value" :disabled="isInheriting" - :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`" + :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`" > {{ issueTransitionOption.label }} diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 955f25a6f26..a509828815a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -254,6 +254,37 @@ export const timeIntervalInWords = (intervalInSeconds) => { : secondsText; }; +/** + * Similar to `timeIntervalInWords`, but rounds the return value + * to 1/10th of the largest time unit. For example: + * + * 30 => 30 seconds + * 90 => 1.5 minutes + * 7200 => 2 hours + * 86400 => 1 day + * ... etc. + * + * The largest supported unit is "days". + * + * @param {Number} intervalInSeconds The time interval in seconds + * @returns {String} A humanized description of the time interval + */ +export const humanizeTimeInterval = (intervalInSeconds) => { + if (intervalInSeconds < 60 /* = 1 minute */) { + const seconds = Math.round(intervalInSeconds * 10) / 10; + return n__('%d second', '%d seconds', seconds); + } else if (intervalInSeconds < 3600 /* = 1 hour */) { + const minutes = Math.round(intervalInSeconds / 6) / 10; + return n__('%d minute', '%d minutes', minutes); + } else if (intervalInSeconds < 86400 /* = 1 day */) { + const hours = Math.round(intervalInSeconds / 360) / 10; + return n__('%d hour', '%d hours', hours); + } + + const days = Math.round(intervalInSeconds / 8640) / 10; + return n__('%d day', '%d days', days); +}; + export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue new file mode 100644 index 00000000000..8de6e910bb6 --- /dev/null +++ b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue @@ -0,0 +1,77 @@ +<script> +import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +export default { + name: 'LockPopovers', + components: { + GlPopover, + GlSprintf, + GlLink, + }, + data() { + return { + targets: [], + }; + }, + mounted() { + this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-popover-target')].map( + (el) => { + const { + dataset: { popoverData }, + } = el; + + const { + lockedByAncestor, + lockedByApplicationSetting, + ancestorNamespace, + } = convertObjectPropsToCamelCase(JSON.parse(popoverData || '{}'), { deep: true }); + + return { + el, + lockedByAncestor, + lockedByApplicationSetting, + ancestorNamespace, + }; + }, + ); + }, +}; +</script> + +<template> + <div> + <template + v-for="( + { el, lockedByApplicationSetting, lockedByAncestor, ancestorNamespace }, index + ) in targets" + > + <gl-popover + v-if="lockedByApplicationSetting || lockedByAncestor" + :key="index" + :target="el" + placement="top" + > + <template #title>{{ s__('CascadingSettings|Setting enforced') }}</template> + <p data-testid="cascading-settings-lock-popover"> + <template v-if="lockedByApplicationSetting">{{ + s__('CascadingSettings|This setting has been enforced by an instance admin.') + }}</template> + + <gl-sprintf + v-else-if="lockedByAncestor && ancestorNamespace" + :message=" + s__('CascadingSettings|This setting has been enforced by an owner of %{link}.') + " + > + <template #link> + <gl-link :href="ancestorNamespace.path" class="gl-font-sm">{{ + ancestorNamespace.fullName + }}</gl-link> + </template> + </gl-sprintf> + </p> + </gl-popover> + </template> + </div> +</template> diff --git a/app/assets/javascripts/namespaces/cascading_settings/index.js b/app/assets/javascripts/namespaces/cascading_settings/index.js new file mode 100644 index 00000000000..3e44d1e9e2d --- /dev/null +++ b/app/assets/javascripts/namespaces/cascading_settings/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import LockPopovers from './components/lock_popovers.vue'; + +export const initCascadingSettingsLockPopovers = () => { + const el = document.querySelector('.js-cascading-settings-lock-popovers'); + + if (!el) return false; + + return new Vue({ + el, + render(createElement) { + return createElement(LockPopovers); + }, + }); +}; diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 176d2406751..49b9822795c 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import TransferDropdown from '~/groups/transfer_dropdown'; import groupsSelect from '~/groups_select'; +import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; @@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => { projectSelect(); initSearchSettings(); + initCascadingSettingsLockPopovers(); return new TransferDropdown(); }); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 6ff7b356438..63048777724 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -54,6 +54,7 @@ export default { data() { return { hoveredJobName: '', + hoveredSourceJobName: '', highlightedJobs: [], measurements: { width: 0, @@ -93,6 +94,9 @@ export default { shouldHideLinks() { return this.isStageView; }, + shouldShowStageName() { + return !this.isStageView; + }, // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( @@ -148,6 +152,9 @@ export default { setJob(jobName) { this.hoveredJobName = jobName; }, + setSourceJob(jobName) { + this.hoveredSourceJobName = jobName; + }, slidePipelineContainer() { this.$refs.mainPipelineContainer.scrollBy({ left: ONE_COL_WIDTH, @@ -204,11 +211,13 @@ export default { <stage-column-component v-for="column in layout" :key="column.id || column.name" - :title="column.name" + :name="column.name" :groups="column.groups" :action="column.status.action" :highlighted-jobs="highlightedJobs" + :show-stage-name="shouldShowStageName" :job-hovered="hoveredJobName" + :source-job-hovered="hoveredSourceJobName" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipeline.id" @refreshPipelineGraph="$emit('refreshPipelineGraph')" @@ -227,7 +236,7 @@ export default { :column-title="__('Downstream')" :type="$options.pipelineTypeConstants.DOWNSTREAM" :view-type="viewType" - @downstreamHovered="setJob" + @downstreamHovered="setSourceJob" @pipelineExpandToggle="togglePipelineExpanded" @scrollContainer="slidePipelineContainer" @error="onError" diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 6e1d818fba0..78fee6a75a8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -1,6 +1,4 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import JobItem from './job_item.vue'; @@ -11,12 +9,8 @@ import JobItem from './job_item.vue'; * */ export default { - directives: { - GlTooltip: GlTooltipDirective, - }, components: { JobItem, - CiIcon, }, props: { group: { @@ -28,6 +22,11 @@ export default { required: false, default: -1, }, + stageName: { + type: String, + required: false, + default: '', + }, }, computed: { computedJobId() { @@ -51,22 +50,21 @@ export default { <template> <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> <button - v-gl-tooltip.hover="{ boundary: 'viewport' }" - :title="tooltipText" type="button" data-toggle="dropdown" data-display="static" class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!" > <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> - <span class="gl-display-flex gl-align-items-center gl-min-w-0"> - <ci-icon :status="group.status" :size="24" class="gl-line-height-0" /> - <span class="gl-text-truncate mw-70p gl-pl-3"> - {{ group.name }} - </span> - </span> + <job-item + :dropdown-length="group.size" + :group-tooltip="tooltipText" + :job="group" + :stage-name="stageName" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> - <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span> + <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div> </div> </button> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 7ff9ad8da2d..0f95037597f 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { sprintf } from '~/locale'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import ActionComponent from '../jobs_shared/action_component.vue'; import JobNameComponent from '../jobs_shared/job_name_component.vue'; @@ -38,6 +39,7 @@ export default { hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, + CiIcon, JobNameComponent, GlLink, }, @@ -65,6 +67,11 @@ export default { required: false, default: Infinity, }, + groupTooltip: { + type: String, + required: false, + default: '', + }, jobHovered: { type: String, required: false, @@ -80,24 +87,47 @@ export default { required: false, default: -1, }, + sourceJobHovered: { + type: String, + required: false, + default: '', + }, + stageName: { + type: String, + required: false, + default: '', + }, }, computed: { boundary() { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; }, + computedJobId() { + return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + }, detailsPath() { return accessValue(this.dataMethod, 'detailsPath', this.status); }, hasDetails() { return accessValue(this.dataMethod, 'hasDetails', this.status); }, - computedJobId() { - return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + nameComponent() { + return this.hasDetails ? 'gl-link' : 'div'; + }, + showStageName() { + return Boolean(this.stageName); }, status() { return this.job && this.job.status ? this.job.status : {}; }, + testId() { + return this.hasDetails ? 'job-with-link' : 'job-without-link'; + }, tooltipText() { + if (this.groupTooltip) { + return this.groupTooltip; + } + const textBuilder = []; const { name: jobName } = this.job; @@ -129,7 +159,7 @@ export default { return this.job.status && this.job.status.action && this.job.status.action.path; }, relatedDownstreamHovered() { - return this.job.name === this.jobHovered; + return this.job.name === this.sourceJobHovered; }, relatedDownstreamExpanded() { return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded; @@ -156,44 +186,45 @@ export default { <template> <div :id="computedJobId" - class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" + class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between gl-w-full" data-qa-selector="job_item_container" > - <gl-link - v-if="hasDetails" + <component + :is="nameComponent" v-gl-tooltip="{ boundary: 'viewport', placement: 'bottom', customClass: 'gl-pointer-events-none', }" - :href="detailsPath" :title="tooltipText" :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" - data-testid="job-with-link" + :href="detailsPath" + class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full" + :data-testid="testId" @click.stop="hideTooltips" @mouseout="hideTooltips" > - <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> - </gl-link> - - <div - v-else - v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" - :title="tooltipText" - :class="jobClasses" - class="js-job-component-tooltip non-details-job-component menu-item" - data-testid="job-without-link" - @mouseout="hideTooltips" - > - <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> - </div> + <div class="ci-job-name-component gl-display-flex gl-align-items-center"> + <ci-icon :size="24" :status="job.status" class="gl-line-height-0" /> + <div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full"> + <div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div> + <div + v-if="showStageName" + data-testid="stage-name-in-job" + class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal" + > + {{ stageName }} + </div> + </div> + </div> + </component> <action-component v-if="hasAction" :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" + class="gl-mr-1" data-qa-selector="action_button" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 5cc9eec9440..fa2f381c8a4 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -22,12 +22,12 @@ export default { type: Array, required: true, }, - pipelineId: { - type: Number, + name: { + type: String, required: true, }, - title: { - type: String, + pipelineId: { + type: Number, required: true, }, action: { @@ -50,6 +50,16 @@ export default { required: false, default: () => ({}), }, + showStageName: { + type: Boolean, + required: false, + default: false, + }, + sourceJobHovered: { + type: String, + required: false, + default: '', + }, }, titleClasses: [ 'gl-font-weight-bold', @@ -75,7 +85,7 @@ export default { }); }, formattedTitle() { - return capitalize(escape(this.title)); + return capitalize(escape(this.name)); }, hasAction() { return !isEmpty(this.action); @@ -145,14 +155,20 @@ export default { v-if="singleJobExists(group)" :job="group.jobs[0]" :job-hovered="jobHovered" + :source-job-hovered="sourceJobHovered" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" + :stage-name="showStageName ? group.stageName : ''" css-class-job-name="gl-build-content" :class="{ 'gl-opacity-3': isFadedOut(group.name) }" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> <div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> - <job-group-dropdown :group="group" :pipeline-id="pipelineId" /> + <job-group-dropdown + :group="group" + :stage-name="showStageName ? group.stageName : ''" + :pipeline-id="pipelineId" + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js index 15316fe3279..2d24beb8323 100644 --- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js +++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js @@ -5,7 +5,19 @@ const unwrapGroups = (stages) => { const { groups: { nodes: groups }, } = stage; - return { node: { ...stage, groups }, lookup: { stageIdx: idx } }; + + /* + Being peformance conscious here means we don't want to spread and copy the + group value just to add one parameter. + */ + /* eslint-disable no-param-reassign */ + const groupsWithStageName = groups.map((group) => { + group.stageName = stage.name; + return group; + }); + /* eslint-enable no-param-reassign */ + + return { node: { ...stage, groups: groupsWithStageName }, lookup: { stageIdx: idx } }; }); }; diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue index d92ccbd5f67..d96d1035ed0 100644 --- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue +++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue @@ -72,6 +72,7 @@ export default { <gl-dropdown-item v-if="canCherryPick" data-testid="cherry-pick-link" + data-qa-selector="cherry_pick_button" @click="showModal($options.openCherryPickModal)" > {{ s__('ChangeTypeAction|Cherry-pick') }} diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 4a8e1424fa8..5f11bd62fdc 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -3,8 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import PipelineCharts from './pipeline_charts.vue'; -const charts = ['pipelines', 'deployments']; - export default { components: { GlTabs, @@ -12,6 +10,8 @@ export default { PipelineCharts, DeploymentFrequencyCharts: () => import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'), + LeadTimeCharts: () => + import('ee_component/projects/pipelines/charts/components/lead_time_charts.vue'), }, inject: { shouldRenderDeploymentFrequencyCharts: { @@ -24,20 +24,29 @@ export default { selectedTab: 0, }; }, + computed: { + charts() { + if (this.shouldRenderDeploymentFrequencyCharts) { + return ['pipelines', 'deployments', 'lead-time']; + } + + return ['pipelines', 'lead-time']; + }, + }, created() { this.selectTab(); window.addEventListener('popstate', this.selectTab); }, methods: { selectTab() { - const [chart] = getParameterValues('chart') || charts; - const tab = charts.indexOf(chart); + const [chart] = getParameterValues('chart') || this.charts; + const tab = this.charts.indexOf(chart); this.selectedTab = tab >= 0 ? tab : 0; }, onTabChange(index) { if (index !== this.selectedTab) { this.selectedTab = index; - const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname); + const path = mergeUrlParams({ chart: this.charts[index] }, window.location.pathname); updateHistory({ url: path, title: window.title }); } }, @@ -46,14 +55,16 @@ export default { </script> <template> <div> - <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange"> + <gl-tabs :value="selectedTab" @input="onTabChange"> <gl-tab :title="__('Pipelines')"> <pipeline-charts /> </gl-tab> - <gl-tab :title="__('Deployments')"> + <gl-tab v-if="shouldRenderDeploymentFrequencyCharts" :title="__('Deployments')"> <deployment-frequency-charts /> </gl-tab> + <gl-tab :title="__('Lead Time')"> + <lead-time-charts /> + </gl-tab> </gl-tabs> - <pipeline-charts v-else /> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index 6388b817e46..41b5983ae0c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -44,7 +44,8 @@ export default { :checked="value" :disabled="isDisabled" name="squash" - class="qa-squash-checkbox js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center" + class="js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center" + data-qa-selector="squash_checkbox" :title="tooltipTitle" @change="(checked) => $emit('input', checked)" > diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index f4ac4f81eac..4a387edbe3f 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -1,13 +1,5 @@ <script> -import { - GlDrawer, - GlInfiniteScroll, - GlResizeObserverDirective, - GlTabs, - GlTab, - GlBadge, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlDrawer, GlInfiniteScroll, GlResizeObserverDirective } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; import Tracking from '~/tracking'; import { getDrawerBodyHeight } from '../utils/get_drawer_body_height'; @@ -20,37 +12,24 @@ export default { components: { GlDrawer, GlInfiniteScroll, - GlTabs, - GlTab, SkeletonLoader, Feature, - GlBadge, - GlLoadingIcon, }, directives: { GlResizeObserver: GlResizeObserverDirective, }, mixins: [trackingMixin], props: { - storageKey: { + versionDigest: { type: String, required: true, }, - versions: { - type: Array, - required: true, - }, - gitlabDotCom: { - type: Boolean, - required: false, - default: false, - }, }, computed: { ...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']), }, mounted() { - this.openDrawer(this.storageKey); + this.openDrawer(this.versionDigest); this.fetchItems(); const body = document.querySelector('body'); @@ -70,16 +49,6 @@ export default { const height = getDrawerBodyHeight(this.$refs.drawer.$el); this.setDrawerBodyHeight(height); }, - featuresForVersion(version) { - return this.features.filter((feature) => { - return feature.release === parseFloat(version); - }); - }, - fetchVersion(version) { - if (this.featuresForVersion(version).length === 0) { - this.fetchItems({ version }); - } - }, }, }; </script> @@ -99,7 +68,6 @@ export default { </template> <template v-if="features.length"> <gl-infinite-scroll - v-if="gitlabDotCom" :fetched-items="features.length" :max-list-height="drawerBodyHeight" class="gl-p-0" @@ -109,26 +77,6 @@ export default { <feature v-for="feature in features" :key="feature.title" :feature="feature" /> </template> </gl-infinite-scroll> - <gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0"> - <gl-tab - v-for="(version, index) in versions" - :key="version" - @click="fetchVersion(version)" - > - <template #title> - <span>{{ version }}</span> - <gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge> - </template> - <gl-loading-icon v-if="fetching" size="lg" class="text-center" /> - <template v-else> - <feature - v-for="feature in featuresForVersion(version)" - :key="feature.title" - :feature="feature" - /> - </template> - </gl-tab> - </gl-tabs> </template> <div v-else class="gl-mt-5"> <skeleton-loader /> diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index 6da141cb19a..3ac3a3a3611 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { mapState } from 'vuex'; import App from './components/app.vue'; import store from './store'; -import { getStorageKey, setNotification } from './utils/notification'; +import { getVersionDigest, setNotification } from './utils/notification'; let whatsNewApp; @@ -27,9 +27,7 @@ export default (el) => { render(createElement) { return createElement('app', { props: { - storageKey: getStorageKey(el), - versions: JSON.parse(el.getAttribute('data-versions')), - gitlabDotCom: el.getAttribute('data-gitlab-dot-com'), + versionDigest: getVersionDigest(el), }, }); }, diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js index 4b3cfa55977..1dc92ea2606 100644 --- a/app/assets/javascripts/whats_new/store/actions.js +++ b/app/assets/javascripts/whats_new/store/actions.js @@ -1,19 +1,20 @@ import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { STORAGE_KEY } from '../utils/notification'; import * as types from './mutation_types'; export default { closeDrawer({ commit }) { commit(types.CLOSE_DRAWER); }, - openDrawer({ commit }, storageKey) { + openDrawer({ commit }, versionDigest) { commit(types.OPEN_DRAWER); - if (storageKey) { - localStorage.setItem(storageKey, JSON.stringify(false)); + if (versionDigest) { + localStorage.setItem(STORAGE_KEY, versionDigest); } }, - fetchItems({ commit, state }, { page, version } = { page: null, version: null }) { + fetchItems({ commit, state }, { page } = { page: null }) { if (state.fetching) { return false; } @@ -24,7 +25,6 @@ export default { .get('/-/whats_new', { params: { page, - version, }, }) .then(({ data, headers }) => { diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js index 52ca8058d1c..3d4326c4b3a 100644 --- a/app/assets/javascripts/whats_new/utils/notification.js +++ b/app/assets/javascripts/whats_new/utils/notification.js @@ -1,11 +1,18 @@ -export const getStorageKey = (appEl) => appEl.getAttribute('data-storage-key'); +export const STORAGE_KEY = 'display-whats-new-notification'; + +export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest'); export const setNotification = (appEl) => { - const storageKey = getStorageKey(appEl); + const versionDigest = getVersionDigest(appEl); const notificationEl = document.querySelector('.header-help'); let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count'); - if (JSON.parse(localStorage.getItem(storageKey)) === false) { + const legacyStorageKey = 'display-whats-new-notification-13.10'; + const localStoragePairs = [ + [legacyStorageKey, false], + [STORAGE_KEY, versionDigest], + ]; + if (localStoragePairs.some((pair) => localStorage.getItem(pair[0]) === pair[1].toString())) { notificationEl.classList.remove('with-notifications'); if (notificationCountEl) { notificationCountEl.parentElement.removeChild(notificationCountEl); diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index a005daef2e4..2f3cf889549 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -148,7 +148,19 @@ } .gl-build-content { - @include build-content(); + display: inline-block; + padding: 8px 10px 9px; + width: 100%; + border: 1px solid var(--border-color, $border-color); + border-radius: 30px; + background-color: var(--white, $white); + + &:hover, + &:focus { + background-color: var(--gray-50, $gray-50); + border: 1px solid $dropdown-toggle-active-border-color; + color: var(--gl-text-color, $gl-text-color); + } } .gl-ci-action-icon-container { diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index e8e681ce649..7bfcda67aa2 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -5,19 +5,23 @@ module CreatesCommit include Gitlab::Utils::StrongMemoize # rubocop:disable Gitlab/ModuleWithInstanceVariables - def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) - if user_access(@project).can_push_to_branch?(branch_name_or_ref) - @project_to_commit_into = @project + def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil) + target_project ||= @project + + if user_access(target_project).can_push_to_branch?(branch_name_or_ref) + @project_to_commit_into = target_project @branch_name ||= @ref else - @project_to_commit_into = current_user.fork_of(@project) + @project_to_commit_into = current_user.fork_of(target_project) @branch_name ||= @project_to_commit_into.repository.next_branch('patch') end @start_branch ||= @ref || @branch_name + start_project = Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) ? @project_to_commit_into : @project + commit_params = @commit_params.merge( - start_project: @project, + start_project: start_project, start_branch: @start_branch, branch_name: @branch_name ) @@ -27,7 +31,7 @@ module CreatesCommit if result[:status] == :success update_flash_notice(success_notice) - success_path = final_success_path(success_path) + success_path = final_success_path(success_path, target_project) respond_to do |format| format.html { redirect_to success_path } @@ -79,9 +83,9 @@ module CreatesCommit end end - def final_success_path(success_path) + def final_success_path(success_path, target_project) if create_merge_request? - merge_request_exists? ? existing_merge_request_path : new_merge_request_path + merge_request_exists? ? existing_merge_request_path : new_merge_request_path(target_project) else success_path = success_path.call if success_path.respond_to?(:call) @@ -90,12 +94,12 @@ module CreatesCommit end # rubocop:disable Gitlab/ModuleWithInstanceVariables - def new_merge_request_path + def new_merge_request_path(target_project) project_new_merge_request_path( @project_to_commit_into, merge_request: { source_project_id: @project_to_commit_into.id, - target_project_id: @project.id, + target_project_id: target_project.id, source_branch: @branch_name, target_branch: @start_branch } diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb index 826fae834fa..4ea07c814ef 100644 --- a/app/controllers/concerns/renders_commits.rb +++ b/app/controllers/concerns/renders_commits.rb @@ -17,12 +17,13 @@ module RendersCommits def set_commits_for_rendering(commits, commits_count: nil) @total_commit_count = commits_count || commits.size limited, @hidden_commit_count = limited_commits(commits, @total_commit_count) - commits.each(&:lazy_author) # preload authors prepare_commits_for_rendering(limited) end # rubocop: enable Gitlab/ModuleWithInstanceVariables def prepare_commits_for_rendering(commits) + commits.each(&:lazy_author) # preload commits' authors + Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables commits diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index fdf66340cbb..1e65974a3cd 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -114,7 +114,7 @@ class Projects::CommitController < Projects::ApplicationController @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", - success_path: -> { successful_change_path }, failure_path: failed_change_path) + success_path: -> { successful_change_path(@project) }, failure_path: failed_change_path) end def cherry_pick @@ -122,10 +122,15 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @start_branch.blank? + target_project = find_cherry_pick_target_project + return render_404 unless target_project + @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked into #{@branch_name}.", - success_path: -> { successful_change_path }, failure_path: failed_change_path) + success_path: -> { successful_change_path(target_project) }, + failure_path: failed_change_path, + target_project: target_project) end private @@ -138,8 +143,8 @@ class Projects::CommitController < Projects::ApplicationController params[:create_merge_request].present? || !can?(current_user, :push_code, @project) end - def successful_change_path - referenced_merge_request_url || project_commits_url(@project, @branch_name) + def successful_change_path(target_project) + referenced_merge_request_url || project_commits_url(target_project, @branch_name) end def failed_change_path @@ -218,4 +223,14 @@ class Projects::CommitController < Projects::ApplicationController @start_branch = params[:start_branch] @commit_params = { commit: @commit } end + + def find_cherry_pick_target_project + return @project if params[:target_project_id].blank? + return @project unless Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) + + MergeRequestTargetProjectFinder + .new(current_user: current_user, source_project: @project, project_feature: :repository) + .execute + .find_by_id(params[:target_project_id]) + end end diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb index 12a52f30bd0..e24b0bbc7bb 100644 --- a/app/controllers/whats_new_controller.rb +++ b/app/controllers/whats_new_controller.rb @@ -5,7 +5,7 @@ class WhatsNewController < ApplicationController skip_before_action :authenticate_user! - before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? } + before_action :check_valid_page_param, :set_pagination_headers feature_category :navigation @@ -29,19 +29,11 @@ class WhatsNewController < ApplicationController def highlights strong_memoize(:highlights) do - if has_version_param? - ReleaseHighlight.for_version(version: params[:version]) - else - ReleaseHighlight.paginated(page: current_page) - end + ReleaseHighlight.paginated(page: current_page) end end def set_pagination_headers response.set_header('X-Next-Page', highlights.next_page) end - - def has_version_param? - params[:version].present? - end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 652ba9950bc..e7a81eb5629 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -139,7 +139,7 @@ module CommitsHelper def cherry_pick_projects_data(project) return [] unless Feature.enabled?(:pick_into_project, project, default_enabled: :yaml) - target_projects(project).map do |project| + [project, project.forked_from_project].compact.map do |project| { id: project.id.to_s, name: project.full_path, diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 8cf5cd49322..a4521541bf9 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -56,6 +56,33 @@ module NamespacesHelper namespaces_options(selected, **options) end + def cascading_namespace_settings_enabled? + NamespaceSetting.cascading_settings_feature_enabled? + end + + def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) + locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend + + popover_data = { + locked_by_application_setting: group.namespace_settings.public_send("#{attribute}_locked_by_application_setting?"), # rubocop:disable GitlabSecurity/PublicSend + locked_by_ancestor: locked_by_ancestor + } + + if locked_by_ancestor + ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend + + popover_data[:ancestor_namespace] = { + full_name: ancestor_namespace.full_name, + path: settings_path_helper.call(ancestor_namespace) + } + end + + { + popover_data: popover_data.to_json, + testid: 'cascading-settings-lock-icon' + } + end + private # Many importers create a temporary Group, so use the real diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index bbf5bde5904..23ed2fc987c 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -5,15 +5,7 @@ module WhatsNewHelper ReleaseHighlight.most_recent_item_count end - def whats_new_storage_key - most_recent_version = ReleaseHighlight.versions&.first - - return unless most_recent_version - - ['display-whats-new-notification', most_recent_version].join('-') - end - - def whats_new_versions - ReleaseHighlight.versions + def whats_new_version_digest + ReleaseHighlight.most_recent_version_digest end end diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb index 1efba6380e9..98d9899a349 100644 --- a/app/models/release_highlight.rb +++ b/app/models/release_highlight.rb @@ -3,17 +3,6 @@ class ReleaseHighlight CACHE_DURATION = 1.hour FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') - RELEASE_VERSIONS_IN_A_YEAR = 12 - - def self.for_version(version:) - index = self.versions.index(version) - - return if index.nil? - - page = index + 1 - - self.paginated(page: page) - end def self.paginated(page: 1) key = self.cache_key("items:page-#{page}") @@ -82,15 +71,15 @@ class ReleaseHighlight end end - def self.versions - key = self.cache_key('versions') + def self.most_recent_version_digest + key = self.cache_key('most_recent_version_digest') Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do - versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path| - /\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".") - end + version = self.paginated&.items&.first&.[]('release')&.to_s + + next if version.nil? - versions.uniq + Digest::SHA256.hexdigest(version) end end diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index a14d342bc14..9dce33bf037 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -2,10 +2,9 @@ - page_title _("Broadcast Messages") %h3.page-title - Broadcast Messages + = _('Broadcast Messages') %p.light - Broadcast messages are displayed for every user and can be used to notify - users about scheduled maintenance, recent upgrades and more. + = _('Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.') = render 'form' @@ -15,12 +14,12 @@ %table.table.table-responsive %thead %tr - %th Status - %th Preview - %th Starts - %th Ends - %th Target Path - %th Type + %th= _('Status') + %th= _('Preview') + %th= _('Starts') + %th= _('Ends') + %th= _(' Target Path') + %th= _(' Type') %th %tbody - @broadcast_messages.each do |message| @@ -38,7 +37,7 @@ %td = message.broadcast_type.capitalize %td.gl-white-space-nowrap.gl-display-flex - = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-icon gl-button' - = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2' + = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button' + = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2' = paginate @broadcast_messages, theme: 'gitlab' diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 57c0801074b..90a49e4bbe3 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -18,11 +18,11 @@ = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do = _("Your projects") - %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count) + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count) = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do = _("Starred projects") - %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count) + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count) = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, data: {placement: 'right'} do = _("Explore projects") diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml index 27ef586d90f..9d469ff6e7b 100644 --- a/app/views/devise/mailer/_confirmation_instructions_account.html.haml +++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml @@ -2,15 +2,15 @@ - if @resource.unconfirmed_email.present? || !@resource.created_recently? #content = email_default_heading(@resource.unconfirmed_email || @resource.email) - %p Click the link below to confirm your email address. + %p= _('Click the link below to confirm your email address.') #cta - = link_to 'Confirm your email address', confirmation_link + = link_to _('Confirm your email address'), confirmation_link - else #content - if Gitlab.com? - = email_default_heading('Thanks for signing up to GitLab!') + = email_default_heading(_('Thanks for signing up to GitLab!')) - else - = email_default_heading("Welcome, #{@resource.name}!") - %p To get started, click the link below to confirm your account. + = email_default_heading(_("Welcome, %{name}!") % { name: @resource.name }) + %p= _("To get started, click the link below to confirm your account.") #cta - = link_to 'Confirm your account', confirmation_link + = link_to _('Confirm your account'), confirmation_link diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 71e61436410..ee9eed7e6f6 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,6 +3,7 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = expanded_by_default? += render 'shared/namespaces/cascading_settings/lock_popovers' %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } .settings-header diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 3e29e42126e..fcfe70bd694 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -8,27 +8,27 @@ = render 'shared/allow_request_access', form: f .form-group.gl-mb-3 - .form-check - = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' - = f.label :share_with_group_lock, class: 'form-check-label' do - %span.d-block + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input' + = f.label :share_with_group_lock, class: 'custom-control-label' do + %span - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %span.js-descr.text-muted= share_with_group_lock_help_text(@group) + %p.js-descr.help-text= share_with_group_lock_help_text(@group) .form-group.gl-mb-3 - .form-check - = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' - = f.label :emails_disabled, class: 'form-check-label' do - %span.d-block= s_('GroupSettings|Disable email notifications') - %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input' + = f.label :emails_disabled, class: 'custom-control-label' do + %span= s_('GroupSettings|Disable email notifications') + %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') .form-group.gl-mb-3 - .form-check - = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input' - = f.label :mentions_disabled, class: 'form-check-label' do - %span.d-block= s_('GroupSettings|Disable group mentions') - %span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input' + = f.label :mentions_disabled, class: 'custom-control-label' do + %span= s_('GroupSettings|Disable group mentions') + %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') = render 'groups/settings/project_access_token_creation', f: f, group: @group = render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group diff --git a/app/views/groups/settings/_project_access_token_creation.html.haml b/app/views/groups/settings/_project_access_token_creation.html.haml index ac0ebfbd7f5..8be17c6cc30 100644 --- a/app/views/groups/settings/_project_access_token_creation.html.haml +++ b/app/views/groups/settings/_project_access_token_creation.html.haml @@ -1,10 +1,10 @@ - return unless render_setting_to_allow_project_access_token_creation?(group) .form-group.gl-mb-3 - .form-check - = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'form-check-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } - = f.label :resource_access_token_creation_allowed, class: 'form-check-label' do - %span.gl-display-block= s_('GroupSettings|Allow project access token creation') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } + = f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do + %span= s_('GroupSettings|Allow project access token creation') - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens') - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link } - %span.text-muted= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + %p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7203919bc85..3225dad5d57 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -120,7 +120,7 @@ = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') -#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } } +#whats-new-app{ data: { version_digest: whats_new_version_digest } } - if can?(current_user, :update_user_status, current_user) .js-set-status-modal-wrapper{ data: user_status_data } diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml index f79b741ced0..61fe2f1e711 100644 --- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml +++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml @@ -1,5 +1,5 @@ %li - %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key }, class: 'gl-display-flex!' } + %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' } = _("What's new") %span.js-whats-new-notification-count.whats-new-notification-count = whats_new_most_recent_release_items_count diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml new file mode 100644 index 00000000000..1e9aa4ec5ff --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml @@ -0,0 +1,14 @@ +- attribute = local_assigns.fetch(:attribute, nil) +- group = local_assigns.fetch(:group, nil) +- form = local_assigns.fetch(:form, nil) + +- return unless attribute && group && form && cascading_namespace_settings_enabled? +- return if group.namespace_settings.public_send("#{attribute}_locked?") + +- lock_attribute = "lock_#{attribute}" + +.gl-form-checkbox.custom-control.custom-checkbox + = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' } + = form.label lock_attribute, class: 'custom-control-label' do + %span= s_('CascadingSettings|Enforce for all subgroups') + %p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.') diff --git a/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml new file mode 100644 index 00000000000..91458bf180b --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml @@ -0,0 +1 @@ +.js-cascading-settings-lock-popovers diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml new file mode 100644 index 00000000000..6596ce2bc73 --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml @@ -0,0 +1,21 @@ +- attribute = local_assigns.fetch(:attribute, nil) +- group = local_assigns.fetch(:group, nil) +- form = local_assigns.fetch(:form, nil) +- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil) +- help_text = local_assigns.fetch(:help_text, nil) + +- return unless attribute && group && form && settings_path_helper + +- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?") + += form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do + %span.position-relative.gl-pr-6.gl-display-inline-flex + = yield + - if setting_locked + %button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!', + type: 'button', + data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) } + = sprite_icon('lock', size: 16) + - if help_text + %p.help-text + = help_text |