summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-17 15:16:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-17 15:16:12 +0000
commit8432be20de0a29f4dde4980efe37d013c9e90034 (patch)
treeaf163db5a7c8ac17ca4da59d505c75552452956c /app
parentdd33e917374b611cd5a596c7fa51b47af6e153f6 (diff)
downloadgitlab-ce-8432be20de0a29f4dde4980efe37d013c9e90034.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/pages/projects/planning_hierarchy/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue41
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/index.js31
-rw-r--r--app/assets/javascripts/work_items/constants.js49
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/app.vue96
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue119
-rw-r--r--app/assets/javascripts/work_items_hierarchy/constants.js12
-rw-r--r--app/assets/javascripts/work_items_hierarchy/hierarchy_util.js10
-rw-r--r--app/assets/javascripts/work_items_hierarchy/static_response.js142
-rw-r--r--app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js26
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/pages/hierarchy.scss15
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss3
-rw-r--r--app/controllers/concerns/work_items_hierarchy.rb15
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/graphql/types/project_type.rb51
-rw-r--r--app/graphql/types/projects/topic_type.rb3
-rw-r--r--app/graphql/types/release_type.rb11
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml61
-rw-r--r--app/views/projects/edit.html.haml7
-rw-r--r--app/views/shared/planning_hierarchy.html.haml5
-rw-r--r--app/views/shared/web_hooks/_hook.html.haml15
26 files changed, 621 insertions, 113 deletions
diff --git a/app/assets/javascripts/pages/projects/planning_hierarchy/index.js b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js
new file mode 100644
index 00000000000..d5dfe2d5f37
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js
@@ -0,0 +1,3 @@
+import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
+
+initWorkItemsHierarchy();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 384ee1f5034..9393ec68586 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -1,6 +1,6 @@
<script>
-import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
-
+import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { __, s__ } from '~/locale';
import {
@@ -41,16 +41,19 @@ export default {
pucWarningHelpText: s__(
'ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits.',
),
+ confirmButtonText: __('Save changes'),
},
components: {
projectFeatureSetting,
projectSettingRow,
+ GlButton,
GlIcon,
GlSprintf,
GlLink,
GlFormCheckbox,
GlToggle,
+ ConfirmDanger,
},
mixins: [settingsMixin],
@@ -163,6 +166,15 @@ export default {
required: false,
default: '',
},
+ confirmationPhrase: {
+ type: String,
+ required: true,
+ },
+ showVisibilityConfirmModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const defaults = {
@@ -274,6 +286,12 @@ export default {
cveIdRequestIsDisabled() {
return this.visibilityLevel !== visibilityOptions.PUBLIC;
},
+ isVisibilityReduced() {
+ return (
+ this.showVisibilityConfirmModal &&
+ this.visibilityLevel < this.currentSettings.visibilityLevel
+ );
+ },
},
watch: {
@@ -774,5 +792,24 @@ export default {
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
</gl-form-checkbox>
</project-setting-row>
+ <confirm-danger
+ v-if="isVisibilityReduced"
+ button-class="qa-visibility-features-permissions-save-button"
+ button-variant="confirm"
+ :disabled="false"
+ :phrase="confirmationPhrase"
+ :button-text="$options.i18n.confirmButtonText"
+ data-testid="project-features-save-button"
+ @confirm="$emit('confirm')"
+ />
+ <gl-button
+ v-else
+ type="submit"
+ variant="confirm"
+ data-testid="project-features-save-button"
+ button-class="qa-visibility-features-permissions-save-button"
+ >
+ {{ $options.i18n.confirmButtonText }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js
index d7bae44e96e..de8b1cc400e 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/index.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import settingsPanel from './components/settings_panel.vue';
export default function initProjectPermissionsSettings() {
@@ -6,8 +7,36 @@ export default function initProjectPermissionsSettings() {
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
+ const {
+ targetFormId,
+ additionalInformation,
+ confirmDangerMessage,
+ confirmButtonText,
+ showVisibilityConfirmModal,
+ htmlConfirmationMessage,
+ phrase: confirmationPhrase,
+ } = mountPoint.dataset;
+
return new Vue({
el: mountPoint,
- render: (createElement) => createElement(settingsPanel, { props: { ...componentProps } }),
+ provide: {
+ additionalInformation,
+ confirmDangerMessage,
+ confirmButtonText,
+ htmlConfirmationMessage: parseBoolean(htmlConfirmationMessage),
+ },
+ render: (createElement) =>
+ createElement(settingsPanel, {
+ props: {
+ ...componentProps,
+ confirmationPhrase,
+ showVisibilityConfirmModal: parseBoolean(showVisibilityConfirmModal),
+ },
+ on: {
+ confirm: () => {
+ if (targetFormId) document.getElementById(targetFormId)?.submit();
+ },
+ },
+ }),
});
}
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 995c02a2c5b..6b72c89bb29 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -1,5 +1,54 @@
+import { __ } from '~/locale';
+
export const widgetTypes = {
title: 'TITLE',
};
export const WI_TITLE_TRACK_LABEL = 'item_title';
+
+export const workItemTypes = {
+ EPIC: {
+ title: __('Epic'),
+ icon: 'epic',
+ color: '#694CC0',
+ backgroundColor: '#E1D8F9',
+ },
+ ISSUE: {
+ title: __('Issue'),
+ icon: 'issues',
+ color: '#1068BF',
+ backgroundColor: '#CBE2F9',
+ },
+ TASK: {
+ title: __('Task'),
+ icon: 'task-done',
+ color: '#217645',
+ backgroundColor: '#C3E6CD',
+ },
+ INCIDENT: {
+ title: __('Incident'),
+ icon: 'issue-type-incident',
+ backgroundColor: '#db2a0f',
+ color: '#FDD4CD',
+ iconSize: 16,
+ },
+ SUB_EPIC: {
+ title: __('Child epic'),
+ icon: 'epic',
+ color: '#AB6100',
+ backgroundColor: '#F5D9A8',
+ },
+ REQUIREMENT: {
+ title: __('Requirement'),
+ icon: 'requirements',
+ color: '#0068c5',
+ backgroundColor: '#c5e3fb',
+ },
+ TEST_CASE: {
+ title: __('Test case'),
+ icon: 'issue-type-test-case',
+ backgroundColor: '#007a3f',
+ color: '#bae8cb',
+ iconSize: 16,
+ },
+};
diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue
new file mode 100644
index 00000000000..ef8632df3be
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue
@@ -0,0 +1,96 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import Cookies from 'js-cookie';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { workItemTypes } from '~/work_items/constants';
+import RESPONSE from '../static_response';
+import { WORK_ITEMS_SURVEY_COOKIE_NAME } from '../constants';
+import Hierarchy from './hierarchy.vue';
+
+export default {
+ components: {
+ GlBanner,
+ Hierarchy,
+ },
+ inject: ['illustrationPath', 'licensePlan'],
+ data() {
+ return {
+ bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
+ workItemHierarchy: RESPONSE[this.licensePlan],
+ };
+ },
+ computed: {
+ hasUnavailableStructure() {
+ return this.workItemTypes.unavailable.length > 0;
+ },
+ workItemTypes() {
+ return this.workItemHierarchy.reduce(
+ (itemTypes, item) => {
+ const key = item.available ? 'available' : 'unavailable';
+ itemTypes[key].push({
+ ...item,
+ ...workItemTypes[item.type],
+ nestedTypes: item.nestedTypes
+ ? item.nestedTypes.map((type) => workItemTypes[type])
+ : null,
+ });
+ return itemTypes;
+ },
+ { available: [], unavailable: [] },
+ );
+ },
+ },
+ methods: {
+ handleClose() {
+ Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
+ this.bannerVisible = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-banner
+ v-if="bannerVisible"
+ class="gl-mt-4 gl-px-5!"
+ :title="s__('Hierarchy|Help us improve work items in GitLab!')"
+ :button-text="s__('Hierarchy|Take the work items survey')"
+ button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
+ :svg-path="illustrationPath"
+ @close="handleClose"
+ >
+ <p>
+ {{
+ s__(
+ 'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
+ )
+ }}
+ </p>
+ </gl-banner>
+ <h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
+ <p>
+ {{
+ s__(
+ 'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
+ )
+ }}
+ </p>
+
+ <div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
+ <p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
+ <hierarchy :work-item-types="workItemTypes.available" />
+
+ <div
+ v-if="hasUnavailableStructure"
+ data-testid="unavailable-structure"
+ class="gl-font-weight-bold gl-mt-5 gl-mb-2"
+ >
+ {{ s__('Hierarchy|Unavailable structure') }}
+ </div>
+ <p v-if="hasUnavailableStructure" class="gl-mb-3!">
+ {{ s__('Hierarchy|These items are unavailable in the current structure.') }}
+ </p>
+ <hierarchy :work-item-types="workItemTypes.unavailable" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue
new file mode 100644
index 00000000000..9b81218b6e4
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue
@@ -0,0 +1,119 @@
+<script>
+import { GlIcon, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlBadge,
+ },
+ props: {
+ workItemTypes: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ isLastItem(index, workItem) {
+ const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
+ const isLastItemInArray = index === workItem.nestedTypes.length - 1;
+
+ return isLastItemInArray && hasMoreThanOneItem;
+ },
+ nestedWorkItemTypeMargin(index, workItem) {
+ const isLastItemInArray = index === workItem.nestedTypes.length - 1;
+ const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
+
+ if (isLastItemInArray && hasMoreThanOneItem) {
+ return 'gl-ml-0';
+ }
+
+ return 'gl-ml-6';
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div
+ v-for="workItem in workItemTypes"
+ :key="workItem.id"
+ class="gl-mb-3"
+ :class="{ flex: !workItem.available }"
+ >
+ <span
+ class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
+ data-testid="work-item-wrapper"
+ >
+ <span
+ :style="{
+ backgroundColor: workItem.backgroundColor,
+ color: workItem.color,
+ }"
+ class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
+ >
+ <gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
+ </span>
+
+ {{ workItem.title }}
+ </span>
+
+ <gl-badge
+ v-if="!workItem.available"
+ variant="info"
+ icon="license"
+ size="sm"
+ class="gl-ml-3 gl-align-self-center"
+ >{{ workItem.license }}</gl-badge
+ >
+
+ <div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
+ <svg
+ v-if="workItem.nestedTypes.length > 1"
+ class="hierarchy-rounded-arrow-tail gl-text-gray-400"
+ data-testid="hierarchy-rounded-arrow-tail"
+ width="2"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <line
+ x1="0.75"
+ y1="1"
+ x2="0.75"
+ y2="100%"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ />
+ </svg>
+ <template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
+ <div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
+ <gl-icon name="arrow-down" class="gl-text-gray-400" />
+ </div>
+ <gl-icon
+ v-if="isLastItem(index, workItem)"
+ :key="nestedWorkItem.id"
+ name="level-up"
+ class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
+ />
+ <span
+ :key="nestedWorkItem.id"
+ class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
+ :class="nestedWorkItemTypeMargin(index, workItem)"
+ >
+ <span
+ :style="{
+ backgroundColor: nestedWorkItem.backgroundColor,
+ color: nestedWorkItem.color,
+ }"
+ class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
+ >
+ <gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
+ </span>
+
+ {{ nestedWorkItem.title }}
+ </span>
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items_hierarchy/constants.js b/app/assets/javascripts/work_items_hierarchy/constants.js
new file mode 100644
index 00000000000..f001f556c0e
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/constants.js
@@ -0,0 +1,12 @@
+export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
+
+/**
+ * Hard-coded strings since we're rendering hierarchy
+ * items from mock responses. Remove this when we
+ * have a real hierarchy endpoint.
+ */
+export const LICENSE_PLAN = {
+ FREE: 'free',
+ PREMIUM: 'premium',
+ ULTIMATE: 'ultimate',
+};
diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
new file mode 100644
index 00000000000..61d93acdb91
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
@@ -0,0 +1,10 @@
+import { LICENSE_PLAN } from './constants';
+
+export function inferLicensePlan({ hasSubEpics, hasEpics }) {
+ if (hasSubEpics) {
+ return LICENSE_PLAN.ULTIMATE;
+ } else if (hasEpics) {
+ return LICENSE_PLAN.PREMIUM;
+ }
+ return LICENSE_PLAN.FREE;
+}
diff --git a/app/assets/javascripts/work_items_hierarchy/static_response.js b/app/assets/javascripts/work_items_hierarchy/static_response.js
new file mode 100644
index 00000000000..d1e2e486082
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/static_response.js
@@ -0,0 +1,142 @@
+const FREE_TIER = 'free';
+const ULTIMATE_TIER = 'ultimate';
+const PREMIUM_TIER = 'premium';
+
+const RESPONSE = {
+ [FREE_TIER]: [
+ {
+ id: '1',
+ type: 'ISSUE',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '4',
+ type: 'EPIC',
+ available: false,
+ license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '5',
+ type: 'SUB_EPIC',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ ],
+
+ [PREMIUM_TIER]: [
+ {
+ id: '1',
+ type: 'EPIC',
+ available: true,
+ license: null,
+ nestedTypes: ['ISSUE'],
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '5',
+ type: 'SUB_EPIC',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ ],
+
+ [ULTIMATE_TIER]: [
+ {
+ id: '1',
+ type: 'EPIC',
+ available: true,
+ license: null,
+ nestedTypes: ['SUB_EPIC', 'ISSUE'],
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ ],
+};
+
+export default RESPONSE;
diff --git a/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
new file mode 100644
index 00000000000..2258c725301
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import App from './components/app.vue';
+import { inferLicensePlan } from './hierarchy_util';
+
+export const initWorkItemsHierarchy = () => {
+ const el = document.querySelector('#js-work-items-hierarchy');
+
+ const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
+
+ const licensePlan = inferLicensePlan({
+ hasEpics: parseBoolean(hasEpics),
+ hasSubEpics: parseBoolean(hasSubEpics),
+ });
+
+ return new Vue({
+ el,
+ provide: {
+ illustrationPath,
+ licensePlan,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 8f3b5b3b7cc..072b78305a9 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -32,3 +32,4 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
+@import './pages/hierarchy';
diff --git a/app/assets/stylesheets/pages/hierarchy.scss b/app/assets/stylesheets/pages/hierarchy.scss
new file mode 100644
index 00000000000..0812e4cc41e
--- /dev/null
+++ b/app/assets/stylesheets/pages/hierarchy.scss
@@ -0,0 +1,15 @@
+.hierarchy-rounded-arrow-tail {
+ position: absolute;
+ top: 4px;
+ left: 5px;
+ height: calc(100% - 20px);
+}
+
+.hierarchy-icon-wrapper {
+ height: $default-icon-size;
+ width: $default-icon-size;
+}
+
+.hierarchy-rounded-arrow {
+ transform: scale(1, -1) rotate(90deg);
+}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index e616996bd14..3ed257caf60 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -772,6 +772,9 @@ svg {
.gl-mt-2 {
margin-top: 0.25rem;
}
+.gl-mt-5 {
+ margin-top: 1rem;
+}
.gl-mb-3 {
margin-bottom: 0.5rem;
}
diff --git a/app/controllers/concerns/work_items_hierarchy.rb b/app/controllers/concerns/work_items_hierarchy.rb
new file mode 100644
index 00000000000..6008256408c
--- /dev/null
+++ b/app/controllers/concerns/work_items_hierarchy.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module WorkItemsHierarchy
+ extend ActiveSupport::Concern
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def planning_hierarchy
+ return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
+
+ render 'shared/planning_hierarchy'
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
+
+WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 04dde5ef7b2..27483d455b1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
include RecordUserLastActivity
include ImportUrlParams
include FiltersEvents
+ include WorkItemsHierarchy
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@@ -52,6 +53,7 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names]
+ feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:refs]
urgency :high, [:unfoldered_environment_names]
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index c988a232a87..f4067552f55 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -27,7 +27,6 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Short description of the project.'
- markdown_field :description_html, null: true
field :tag_list, GraphQL::Types::String, null: true,
deprecated: { reason: 'Use `topics`', milestone: '13.12' },
@@ -75,21 +74,6 @@ module Types
field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'URL to avatar image file of the project.'
- {
- issues: "Issues are",
- merge_requests: "Merge Requests are",
- wiki: 'Wikis are',
- snippets: 'Snippets are',
- container_registry: 'Container Registry is'
- }.each do |feature, name_string|
- field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
- description: "Indicates if #{name_string} enabled for the current user"
-
- define_method "#{feature}_enabled" do
- object.feature_available?(feature, context[:current_user])
- end
- end
-
field :jobs_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.'
@@ -391,15 +375,6 @@ module Types
null: true,
description: 'Template used to create squash commit message in merge requests.'
- def label(title:)
- BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
- LabelsFinder
- .new(current_user, project: args[:key], title: titles)
- .execute
- .each { |label| loader.call(label.title, label) }
- end
- end
-
field :labels,
Types::LabelType.connection_type,
null: true,
@@ -411,6 +386,32 @@ module Types
description: 'Work item types available to the project.',
feature_flag: :work_items
+ def label(title:)
+ BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
+ LabelsFinder
+ .new(current_user, project: args[:key], title: titles)
+ .execute
+ .each { |label| loader.call(label.title, label) }
+ end
+ end
+
+ {
+ issues: "Issues are",
+ merge_requests: "Merge Requests are",
+ wiki: 'Wikis are',
+ snippets: 'Snippets are',
+ container_registry: 'Container Registry is'
+ }.each do |feature, name_string|
+ field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
+ description: "Indicates if #{name_string} enabled for the current user"
+
+ define_method "#{feature}_enabled" do
+ object.feature_available?(feature, context[:current_user])
+ end
+ end
+
+ markdown_field :description_html, null: true
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/projects/topic_type.rb b/app/graphql/types/projects/topic_type.rb
index 79ab69e794b..c579f2f2b9d 100644
--- a/app/graphql/types/projects/topic_type.rb
+++ b/app/graphql/types/projects/topic_type.rb
@@ -14,11 +14,12 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Description of the topic.'
- markdown_field :description_html, null: true
field :avatar_url, GraphQL::Types::String, null: true,
description: 'URL to avatar image file of the topic.'
+ markdown_field :description_html, null: true
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index fcc9ec49252..fbc3779ea9b 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -20,7 +20,6 @@ module Types
authorize: :download_code
field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
- markdown_field :description_html, null: true
field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :created_at, Types::TimeType, null: true,
@@ -42,14 +41,16 @@ module Types
field :author, Types::UserType, null: true,
description: 'User that created the release.'
- def author
- Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
- end
-
field :commit, Types::CommitType, null: true,
complexity: 10, calls_gitaly: true,
description: 'Commit associated with the release.'
+ markdown_field :description_html, null: true
+
+ def author
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
+ end
+
def commit
return if release.sha.nil?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index de696c2ec87..084c962d34c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -672,18 +672,16 @@ module ProjectsHelper
html_escape(message) % { strong_start: strong_start, strong_end: strong_end, project_name: project.name, group_name: project.group ? project.group.name : nil }
end
- def visibility_confirm_modal_data(project, remove_form_id = nil)
+ def visibility_confirm_modal_data(project, target_form_id = nil)
{
- remove_form_id: remove_form_id,
- qa_selector: 'visibility_features_permissions_save_button',
- button_text: _('Save changes'),
+ target_form_id: target_form_id,
button_testid: 'reduce-project-visibility-button',
- button_variant: 'confirm',
confirm_button_text: _('Reduce project visibility'),
confirm_danger_message: confirm_reduce_visibility_message(project),
phrase: project.full_path,
additional_information: _('Note: current forks will keep their visibility level.'),
- html_confirmation_message: true
+ html_confirmation_message: true.to_s,
+ show_visibility_confirm_modal: show_visibility_confirm_modal?(project).to_s
}
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index c0c2ea42d46..c8a0b247b59 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -3,7 +3,7 @@
class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
- DEFAULT_GIT_DEPTH = 50
+ DEFAULT_GIT_DEPTH = 20
before_create :set_default_git_depth
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 55f43cd9f7b..d89a449bdd5 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
enable :read_wiki
enable :read_issue
enable :read_label
+ enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
@@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list
enable :read_wiki
enable :read_label
+ enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 15143684b8b..982171b9e34 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -62,7 +62,7 @@
%div
- if show_recaptcha_sign_up?
= recaptcha_tags nonce: content_security_policy_nonce
- .submit-container
+ .submit-container.gl-mt-5
= f.submit button_text, class: 'btn gl-button btn-confirm gl-display-block gl-w-full', data: { qa_selector: 'new_user_register_button' }
= render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index a8275576327..887d07f7a20 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -32,64 +32,5 @@
type_plural: type_plural,
active_tokens: @active_personal_access_tokens,
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
-- if Feature.enabled?(:hide_access_tokens, default_enabled: :yaml)
- #js-tokens-app{ data: { tokens_data: tokens_app_data } }
-- else
- - unless Gitlab::CurrentSettings.disable_feed_token
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0
- = s_('AccessTokens|Feed token')
- %p
- = s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8.feed-token-reset
- = label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
- = text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
- %p.form-text.text-muted
- - reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
- = reset_message.html_safe
- - if incoming_email_token_enabled?
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0
- = s_('AccessTokens|Incoming email token')
- %p
- = s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8.incoming-email-token-reset
- = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
- = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
- %p.form-text.text-muted
- - reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
- = reset_message.html_safe
-
- - if static_objects_external_storage_enabled?
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4
- %h4.gl-mt-0
- = s_('AccessTokens|Static object token')
- %p
- = s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8
- = label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
- = text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
- %p.form-text.text-muted
- - reset_link = url_for [:reset, :static_object_token, :profile]
- - reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- - reset_link_end = '</a>'.html_safe
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
- = reset_message.html_safe
+#js-tokens-app{ data: { tokens_data: tokens_app_data } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 1cb935307af..aa9a3ea61f7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -2,7 +2,7 @@
- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
-- remove_visibility_form_id = 'reduce-visibility-form'
+- reduce_visibility_form_id = 'reduce-visibility-form'
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
@@ -18,11 +18,10 @@
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
.settings-content
- = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: remove_visibility_form_id }, authenticity_token: true do |f|
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
- .js-project-permissions-form
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: visibility_confirm_modal_data(@project, remove_visibility_form_id)
+ .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
diff --git a/app/views/shared/planning_hierarchy.html.haml b/app/views/shared/planning_hierarchy.html.haml
new file mode 100644
index 00000000000..d67ecc6ee48
--- /dev/null
+++ b/app/views/shared/planning_hierarchy.html.haml
@@ -0,0 +1,5 @@
+- page_title _("Planning hierarchy")
+- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
+- has_epics = Gitlab.ee? && @project&.feature_available?(:epics)
+
+#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index 45baa7e2184..c5a03ef4dc1 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -1,22 +1,23 @@
+- sslStatus = hook.enable_ssl_verification ? _('enabled') : _('disabled')
+- sslBadgeText = _('SSL Verification:') + ' ' + sslStatus
+
%li
.row
.col-md-8.col-lg-7
%strong.light-header
= hook.url
- if hook.rate_limited?
- %span.gl-badge.badge-danger.badge-pill.sm= _('Disabled')
+ = gl_badge_tag(_('Disabled'), variant: :danger, size: :sm)
- elsif hook.permanently_disabled?
- %span.gl-badge.badge-danger.badge-pill.sm= s_('Webhooks|Failed to connect')
+ = gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger, size: :sm)
- elsif hook.temporarily_disabled?
- %span.gl-badge.badge-warning.badge-pill.sm= s_('Webhooks|Fails to connect')
+ = gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning, size: :sm)
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
- %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2.deploy-project-label= trigger.to_s.titleize
- %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2
- = _('SSL Verification:')
- = hook.enable_ssl_verification ? _('enabled') : _('disabled')
+ = gl_badge_tag(trigger.to_s.titleize, size: :sm)
+ = gl_badge_tag(sslBadgeText, size: :sm)
.col-md-4.col-lg-5.text-right-md.gl-mt-2
%span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3'