diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 15:16:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 15:16:12 +0000 |
commit | 8432be20de0a29f4dde4980efe37d013c9e90034 (patch) | |
tree | af163db5a7c8ac17ca4da59d505c75552452956c /app | |
parent | dd33e917374b611cd5a596c7fa51b47af6e153f6 (diff) | |
download | gitlab-ce-8432be20de0a29f4dde4980efe37d013c9e90034.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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' |