Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-20 12:10:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-20 12:10:35 +0300
commit0b7d3f810f287523fd4ad72c019d78e8d2983f8a (patch)
treefdb52060e586a5548f026b6a3ce71c5b676d9e5c /app
parent51da6793bcbe7295c1f3f881f756b8b60ee06da1 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue2
-rw-r--r--app/assets/javascripts/organizations/mock_data.js6
-rw-r--r--app/assets/javascripts/organizations/shared/components/groups_view.vue12
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue107
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue84
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue90
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title_with_edit.vue43
-rw-r--r--app/assets/stylesheets/framework/typography.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss9
-rw-r--r--app/assets/stylesheets/utilities.scss10
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/users/in_product_marketing_email.rb75
-rw-r--r--app/services/work_items/callbacks/assignees.rb36
-rw-r--r--app/services/work_items/widgets/assignees_service/update_service.rb38
-rw-r--r--app/views/projects/_sidebar.html.haml69
-rw-r--r--app/views/projects/empty.html.haml2
23 files changed, 451 insertions, 264 deletions
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
index 4d8612aeeff..92edd286c76 100644
--- a/app/assets/javascripts/lib/utils/secret_detection.js
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -32,6 +32,10 @@ export const containsSensitiveToken = (message) => {
name: 'GitLab Deploy Token',
regex: `gldt-[0-9a-zA-Z_-]{20}`,
},
+ {
+ name: 'GitLab SCIM OAuth Access Token',
+ regex: `glsoat-[0-9a-zA-Z_-]{20}`,
+ },
];
for (const rule of sensitiveDataPatterns) {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index dba738de5e1..ebe69925491 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -172,6 +172,6 @@ export default {
</div>
</div>
</div>
- <component :is="routerView" />
+ <component :is="routerView" list-item-class="gl-px-5" />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 0c363cf7c7f..92381087917 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -39,14 +39,14 @@ export const organizationProjects = {
id: 'gid://gitlab/Project/8',
nameWithNamespace: 'Twitter / Typeahead.Js',
webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
- topics: ['JavaScript', 'Vue.js'],
+ topics: ['JavaScript', 'Vue.js', 'GraphQL', 'Jest', 'CSS', 'HTML'],
forksCount: 4,
avatarUrl: null,
starCount: 0,
visibility: 'public',
openIssuesCount: 48,
descriptionHtml:
- '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi. Sed sit amet iaculis neque. Morbi vel convallis elit. Aliquam vitae arcu orci. Aenean sem velit, dapibus eget enim id, tempor lobortis orci. Pellentesque dignissim nec velit eget sagittis. Maecenas lectus sapien, tincidunt ac cursus a, aliquam eu ipsum. Aliquam posuere maximus augue, ut vehicula elit vulputate condimentum. In libero leo, vehicula nec risus in, ullamcorper convallis risus. Phasellus sit amet lectus sit amet sem volutpat cursus. Nullam facilisis nulla nec lacus pretium, in pretium ex aliquam.</p>',
issuesAccessLevel: 'enabled',
forkingAccessLevel: 'enabled',
isForked: true,
@@ -141,7 +141,7 @@ export const organizationGroups = {
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/Commit451',
descriptionHtml:
- '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
+ '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse libero sem, congue ut sem id, semper pharetra ante. Sed at dui ac nunc pellentesque congue. Phasellus posuere, nisl non pellentesque dignissim, lacus nisi molestie ex, vel placerat neque ligula non libero. Curabitur ipsum enim, pretium eu dignissim vitae, euismod nec nibh. Praesent eget ipsum eleifend, pellentesque tortor vel, consequat neque. Etiam suscipit dolor massa, sed consectetur nunc tincidunt ut. Suspendisse posuere malesuada finibus. Maecenas iaculis et diam eu iaculis. Proin at nulla sit amet erat sollicitudin suscipit sit amet a libero.</p>',
avatarUrl: null,
descendantGroupsCount: 0,
projectsCount: 3,
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
index eaa3017ef97..87bc79a5405 100644
--- a/app/assets/javascripts/organizations/shared/components/groups_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -77,6 +82,11 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
+ <groups-list
+ v-else-if="groups.length"
+ :groups="groups"
+ show-group-icon
+ :list-item-class="listItemClass"
+ />
<gl-empty-state v-else v-bind="emptyStateProps" />
</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
index 9bf4e597884..323a8895821 100644
--- a/app/assets/javascripts/organizations/shared/components/projects_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -81,6 +86,11 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
+ <projects-list
+ v-else-if="projects.length"
+ :projects="projects"
+ show-project-icon
+ :list-item-class="listItemClass"
+ />
<gl-empty-state v-else v-bind="emptyStateProps" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
index a375a167c68..e84a810c0b0 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
@@ -13,6 +13,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -24,6 +29,7 @@ export default {
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"
+ :class="listItemClass"
@delete="$emit('delete', $event)"
/>
</ul>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index ca1e7400f2d..ace3846723c 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -1,10 +1,9 @@
<script>
-import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
+import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText, GlBadge } from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -20,15 +19,12 @@ export default {
showMore: __('Show more'),
showLess: __('Show less'),
},
- avatarSize: { default: 32, md: 48 },
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
+ truncateTextToggleButtonProps: { class: 'gl-font-sm!' },
components: {
GlAvatarLabeled,
GlIcon,
- UserAccessRoleBadge,
GlTruncateText,
+ GlBadge,
ListActions,
DangerConfirmModal,
},
@@ -76,7 +72,7 @@ export default {
return this.group.parent ? 'subgroup' : 'group';
},
statsPadding() {
- return this.showGroupIcon ? 'gl-pl-11' : 'gl-pl-8';
+ return this.showGroupIcon ? 'gl-pl-12' : 'gl-pl-10';
},
descendantGroupsCount() {
return numberToMetricPrefix(this.group.descendantGroupsCount);
@@ -113,21 +109,22 @@ export default {
</script>
<template>
- <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
- <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex">
+ <div class="gl-md-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
+ <div
v-if="showGroupIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- :name="groupIconName"
- />
+ class="gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-h-9 gl-mr-3"
+ >
+ <gl-icon class="gl-text-secondary" :name="groupIconName" />
+ </div>
<gl-avatar-labeled
:entity-id="group.id"
:entity-name="group.fullName"
:label="group.fullName"
:label-link="group.webUrl"
shape="rect"
- :size="$options.avatarSize"
+ :size="48"
>
<template #meta>
<div class="gl-px-2">
@@ -141,9 +138,9 @@ export default {
/>
</div>
<div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
- }}</user-access-role-badge>
+ }}</gl-badge>
</div>
</div>
</div>
@@ -154,57 +151,57 @@ export default {
:mobile-lines="2"
:show-more-text="$options.i18n.showMore"
:show-less-text="$options.i18n.showLess"
- class="gl-mt-2"
+ :toggle-button-props="$options.truncateTextToggleButtonProps"
+ class="gl-mt-2 gl-max-w-88"
>
<div
- v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
- class="gl-font-sm md"
+ v-safe-html="group.descriptionHtml"
+ class="gl-font-sm gl-text-secondary md"
data-testid="group-description"
></div>
</gl-truncate-text>
</gl-avatar-labeled>
</div>
<div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
+ class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3 gl-md-h-9"
:class="statsPadding"
>
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
- <div
- v-gl-tooltip="$options.i18n.subgroups"
- :aria-label="$options.i18n.subgroups"
- class="gl-text-secondary"
- data-testid="subgroups-count"
- >
- <gl-icon name="subgroup" />
- <span>{{ descendantGroupsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.projects"
- :aria-label="$options.i18n.projects"
- class="gl-text-secondary"
- data-testid="projects-count"
- >
- <gl-icon name="project" />
- <span>{{ projectsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.directMembers"
- :aria-label="$options.i18n.directMembers"
- class="gl-text-secondary"
- data-testid="members-count"
- >
- <gl-icon name="users" />
- <span>{{ groupMembersCount }}</span>
- </div>
+ <div
+ v-gl-tooltip="$options.i18n.subgroups"
+ :aria-label="$options.i18n.subgroups"
+ class="gl-text-secondary"
+ data-testid="subgroups-count"
+ >
+ <gl-icon name="subgroup" />
+ <span>{{ descendantGroupsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.projects"
+ :aria-label="$options.i18n.projects"
+ class="gl-text-secondary"
+ data-testid="projects-count"
+ >
+ <gl-icon name="project" />
+ <span>{{ projectsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.directMembers"
+ :aria-label="$options.i18n.directMembers"
+ class="gl-text-secondary"
+ data-testid="members-count"
+ >
+ <gl-icon name="users" />
+ <span>{{ groupMembersCount }}</span>
</div>
</div>
</div>
- <list-actions
- v-if="hasActions"
- class="gl-ml-3 gl-md-align-self-center"
- :actions="actions"
- :available-actions="group.availableActions"
- />
+ <div class="gl-display-flex gl-align-items-center gl-h-9 gl-ml-3">
+ <list-actions
+ v-if="hasActions"
+ :actions="actions"
+ :available-actions="group.availableActions"
+ />
+ </div>
<danger-confirm-modal
v-if="hasActionDelete"
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
index 3a4da54c84c..d06024638fa 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
@@ -35,6 +35,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -46,6 +51,7 @@ export default {
:key="project.id"
:project="project"
:show-project-icon="showProjectIcon"
+ :class="listItemClass"
@delete="$emit('delete', $event)"
/>
</ul>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index ce75e305473..3a077d09e40 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,13 +7,13 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
+ GlTruncateText,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_ENABLED } from '~/featurable/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
@@ -37,19 +37,18 @@ export default {
moreTopics: __('More topics'),
updated: __('Updated'),
actions: __('Actions'),
+ showMore: __('Show more'),
+ showLess: __('Show less'),
},
- avatarSize: { default: 32, md: 48 },
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
+ truncateTextToggleButtonProps: { class: 'gl-font-sm!' },
components: {
GlAvatarLabeled,
GlIcon,
- UserAccessRoleBadge,
GlLink,
GlBadge,
GlPopover,
GlSprintf,
+ GlTruncateText,
TimeAgoTooltip,
DeleteModal,
ListActions,
@@ -203,21 +202,22 @@ export default {
</script>
<template>
- <li class="projects-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
- <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <li class="projects-list-item gl-py-5 gl-border-b gl-display-flex">
+ <div class="gl-md-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
+ <div
v-if="showProjectIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- name="project"
- />
+ class="gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-h-9 gl-mr-3"
+ >
+ <gl-icon class="gl-text-secondary" name="project" />
+ </div>
<gl-avatar-labeled
:entity-id="project.id"
:entity-name="project.name"
:label="project.name"
:label-link="project.webUrl"
shape="rect"
- :size="$options.avatarSize"
+ :size="48"
>
<template #meta>
<div class="gl-px-2">
@@ -231,33 +231,46 @@ export default {
/>
</div>
<div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
- }}</user-access-role-badge>
+ }}</gl-badge>
</div>
</div>
</div>
</template>
- <div
+ <gl-truncate-text
v-if="project.descriptionHtml"
- v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml"
- class="gl-font-sm gl-overflow-hidden gl-line-height-20 description md"
- data-testid="project-description"
- ></div>
+ :lines="2"
+ :mobile-lines="2"
+ :show-more-text="$options.i18n.showMore"
+ :show-less-text="$options.i18n.showLess"
+ :toggle-button-props="$options.truncateTextToggleButtonProps"
+ class="gl-mt-2 gl-max-w-88"
+ >
+ <div
+ v-safe-html="project.descriptionHtml"
+ class="gl-font-sm gl-text-secondary md"
+ data-testid="project-description"
+ ></div>
+ </gl-truncate-text>
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
>
- <span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span>
+ <span class="gl-p-2 gl-font-sm gl-text-secondary">{{ $options.i18n.topics }}:</span>
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
- <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ <gl-badge
+ v-gl-tooltip="topicTooltipTitle(topic)"
+ size="sm"
+ :href="topicPath(topic)"
+ >
{{ topicTitle(topic) }}
</gl-badge>
</div>
<template v-if="popoverTopics.length">
<div
:id="topicsPopoverTarget"
- class="gl-p-2 gl-text-secondary"
+ class="gl-p-2 gl-font-sm gl-text-secondary"
role="button"
tabindex="0"
>
@@ -272,7 +285,11 @@ export default {
:key="topic"
class="gl-p-2 gl-display-inline-block"
>
- <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ <gl-badge
+ v-gl-tooltip="topicTooltipTitle(topic)"
+ size="sm"
+ :href="topicPath(topic)"
+ >
{{ topicTitle(topic) }}
</gl-badge>
</div>
@@ -285,9 +302,9 @@ export default {
</div>
<div
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0"
- :class="showProjectIcon ? 'gl-pl-11' : 'gl-pl-8'"
+ :class="showProjectIcon ? 'gl-pl-12' : 'gl-pl-10'"
>
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-md-h-9">
<gl-badge v-if="project.archived" variant="warning">{{
$options.i18n.archived
}}</gl-badge>
@@ -323,19 +340,20 @@ export default {
</div>
<div
v-if="project.updatedAt"
- class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3"
+ class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3 gl-md-mt-0"
>
<span>{{ $options.i18n.updated }}</span>
<time-ago-tooltip :time="project.updatedAt" />
</div>
</div>
</div>
- <list-actions
- v-if="hasActions"
- class="gl-ml-3 gl-md-align-self-center"
- :actions="actions"
- :available-actions="project.availableActions"
- />
+ <div class="gl-display-flex gl-align-items-center gl-h-9 gl-ml-3">
+ <list-actions
+ v-if="hasActions"
+ :actions="actions"
+ :available-actions="project.availableActions"
+ />
+ </div>
<delete-modal
v-if="hasActionDelete"
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 77c573b47e4..4301dcca30b 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -40,12 +40,27 @@ export default {
type: String,
required: true,
},
+ disableInlineEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ editMode: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
markdownDocsPath: helpPagePath('user/markdown'),
data() {
return {
workItem: {},
- isEditing: false,
+ isEditing: this.editMode,
isSubmitting: false,
isSubmittingWithKeydown: false,
descriptionText: '',
@@ -126,6 +141,26 @@ export default {
autocompleteDataSources() {
return autocompleteDataSources(this.fullPath, this.workItem.iid);
},
+ saveButtonText() {
+ return this.editMode ? __('Save changes') : __('Save');
+ },
+ formGroupClass() {
+ return {
+ 'gl-border-t gl-pt-6': !this.disableInlineEditing,
+ 'gl-mb-5 common-note-form': true,
+ };
+ },
+ },
+ watch: {
+ updateInProgress(newValue) {
+ this.isSubmitting = newValue;
+ },
+ editMode(newValue) {
+ this.isEditing = newValue;
+ if (newValue) {
+ this.startEditing();
+ }
+ },
},
methods: {
checkForConflicts() {
@@ -159,6 +194,7 @@ export default {
}
this.isEditing = false;
+ this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
onInput() {
@@ -175,6 +211,11 @@ export default {
this.isSubmittingWithKeydown = true;
}
+ if (this.disableInlineEditing) {
+ this.$emit('updateWorkItem');
+ return;
+ }
+
this.isSubmitting = true;
try {
@@ -210,6 +251,9 @@ export default {
},
setDescriptionText(newText) {
this.descriptionText = newText;
+ if (this.disableInlineEditing) {
+ this.$emit('updateDraft', this.descriptionText);
+ }
updateDraft(this.autosaveKey, this.descriptionText);
},
handleDescriptionTextUpdated(newText) {
@@ -224,12 +268,13 @@ export default {
<div>
<gl-form v-if="isEditing" @submit.prevent="updateWorkItem" @reset.prevent="cancelEditing">
<gl-form-group
- class="gl-mb-5 gl-border-t gl-pt-6 common-note-form"
+ :class="formGroupClass"
:label="__('Description')"
+ :label-sr-only="disableInlineEditing"
label-for="work-item-description"
>
<markdown-editor
- class="gl-my-5"
+ class="gl-mb-5"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
@@ -285,9 +330,9 @@ export default {
:loading="isSubmitting"
data-testid="save-description"
type="submit"
- >{{ __('Save') }}
+ >{{ saveButtonText }}
</gl-button>
- <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" type="reset"
+ <gl-button category="secondary" class="gl-ml-3" data-testid="cancel" type="reset"
>{{ __('Cancel') }}
</gl-button>
</template>
@@ -296,13 +341,14 @@ export default {
</gl-form>
<work-item-description-rendered
v-else
+ :disable-inline-editing="disableInlineEditing"
:work-item-description="workItemDescription"
:can-edit="canEdit"
@startEditing="startEditing"
@descriptionUpdated="handleDescriptionTextUpdated"
/>
<edited-at
- v-if="lastEditedAt"
+ v-if="lastEditedAt && !editMode"
:updated-at="lastEditedAt"
:updated-by-name="lastEditedByName"
:updated-by-path="lastEditedByPath"
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 124e05db431..1699f6c419e 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -22,6 +22,16 @@ export default {
type: Boolean,
required: true,
},
+ disableInlineEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ checkboxes: [],
+ };
},
computed: {
descriptionText() {
@@ -33,6 +43,12 @@ export default {
descriptionEmpty() {
return this.descriptionHtml?.trim() === '';
},
+ showEmptyDescription() {
+ return this.descriptionEmpty && !this.disableInlineEditing;
+ },
+ showEditButton() {
+ return this.canEdit && !this.disableInlineEditing;
+ },
},
watch: {
descriptionHtml: {
@@ -96,9 +112,11 @@ export default {
<template>
<div class="gl-mb-5">
<div class="gl-display-inline-flex gl-align-items-center gl-mb-3">
- <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
+ <label v-if="!disableInlineEditing" class="d-block col-form-label gl-mr-5">{{
+ __('Description')
+ }}</label>
<gl-button
- v-if="canEdit"
+ v-if="showEditButton"
v-gl-tooltip
class="gl-ml-auto"
icon="pencil"
@@ -109,9 +127,9 @@ export default {
/>
</div>
- <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
+ <div v-if="showEmptyDescription" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
<div
- v-else
+ v-else-if="!descriptionEmpty"
ref="gfm-content"
v-safe-html="descriptionHtml"
class="md gl-mb-5 gl-min-h-8 gl-clearfix"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index b74cbc85379..93f552bfa4c 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -2,6 +2,7 @@
import { isEmpty } from 'lodash';
import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -41,6 +42,7 @@ import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemStickyHeader from './work_item_sticky_header.vue';
import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
+import WorkItemTitleWithEdit from './work_item_title_with_edit.vue';
export default {
i18n,
@@ -67,6 +69,7 @@ export default {
WorkItemRelationships,
WorkItemStickyHeader,
WorkItemAncestors,
+ WorkItemTitleWithEdit,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
@@ -94,6 +97,8 @@ export default {
reportedUrl: '',
reportedUserId: 0,
isStickyHeaderShowing: false,
+ editMode: false,
+ draftData: {},
};
},
apollo: {
@@ -219,7 +224,7 @@ export default {
};
},
showIntersectionObserver() {
- return !this.isModal && this.workItemsMvc2Enabled;
+ return !this.isModal && this.workItemsMvc2Enabled && !this.editMode;
},
hasLinkedWorkItems() {
return this.glFeatures.linkedWorkItems;
@@ -233,13 +238,15 @@ export default {
titleClassHeader() {
return {
'gl-sm-display-none!': this.parentWorkItem,
- 'gl-w-full': !this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem && !this.editMode,
+ 'editable-wi-title': this.editMode && !this.parentWorkItem,
};
},
titleClassComponent() {
return {
'gl-sm-display-block!': !this.parentWorkItem,
'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ 'gl-mt-3 editable-wi-title': this.workItemsMvc2Enabled,
};
},
headerWrapperClass() {
@@ -258,6 +265,9 @@ export default {
}
},
methods: {
+ enableEditMode() {
+ this.editMode = true;
+ },
isWidgetPresent(type) {
return this.workItem.widgets?.find((widget) => widget.type === type);
},
@@ -349,6 +359,45 @@ export default {
this.isStickyHeaderShowing = true;
}
},
+ updateDraft(type, value) {
+ this.draftData[type] = value;
+ },
+ async updateWorkItem() {
+ this.updateInProgress = true;
+ try {
+ const {
+ data: { workItemUpdate },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItem.id,
+ title: this.draftData.title,
+ descriptionWidget: {
+ description: this.draftData.description,
+ },
+ },
+ },
+ });
+
+ const { errors } = workItemUpdate;
+
+ if (errors?.length) {
+ this.updateError = errors.join('\n');
+ throw new Error(this.updateError);
+ }
+
+ this.editMode = false;
+ } catch (error) {
+ Sentry.captureException(error);
+ } finally {
+ this.updateInProgress = false;
+ }
+ },
+ cancelEditing() {
+ this.draftData = {};
+ this.editMode = false;
+ },
},
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WORKSPACE_PROJECT,
@@ -388,8 +437,16 @@ export default {
:class="titleClassHeader"
data-testid="work-item-type"
>
+ <work-item-title-with-edit
+ v-if="workItem.title && workItemsMvc2Enabled"
+ ref="title"
+ class="gl-mt-3 gl-sm-display-block!"
+ :is-editing="editMode"
+ :title="workItem.title"
+ @updateDraft="updateDraft('title', $event)"
+ />
<work-item-title
- v-if="workItem.title"
+ v-else-if="workItem.title"
ref="title"
class="gl-sm-display-block!"
:work-item-id="workItem.id"
@@ -402,6 +459,14 @@ export default {
<div
class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3"
>
+ <gl-button
+ v-if="workItemsMvc2Enabled && !editMode"
+ category="secondary"
+ data-testid="work-item-edit-form-button"
+ @click="enableEditMode"
+ >
+ {{ __('Edit') }}
+ </gl-button>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
@@ -441,8 +506,16 @@ export default {
/>
</div>
<div>
+ <work-item-title-with-edit
+ v-if="workItem.title && workItemsMvc2Enabled && parentWorkItem"
+ ref="title"
+ :is-editing="editMode"
+ :class="titleClassComponent"
+ :title="workItem.title"
+ @updateDraft="updateDraft('title', $event)"
+ />
<work-item-title
- v-if="workItem.title && parentWorkItem"
+ v-else-if="workItem.title && parentWorkItem"
ref="title"
:class="titleClassComponent"
:work-item-id="workItem.id"
@@ -453,6 +526,7 @@ export default {
@error="updateError = $event"
/>
<work-item-created-updated
+ v-if="!editMode"
:full-path="fullPath"
:work-item-iid="workItemIid"
:update-in-progress="updateInProgress"
@@ -490,10 +564,16 @@ export default {
/>
<work-item-description
v-if="hasDescriptionWidget"
+ :class="workItemsMvc2Enabled ? '' : 'gl-pt-5'"
+ :disable-inline-editing="workItemsMvc2Enabled"
+ :edit-mode="editMode"
:full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
- class="gl-pt-5"
+ :update-in-progress="updateInProgress"
+ @updateWorkItem="updateWorkItem"
+ @updateDraft="updateDraft('description', $event)"
+ @cancelEditing="cancelEditing"
@error="updateError = $event"
/>
<work-item-award-emoji
diff --git a/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue
new file mode 100644
index 00000000000..02ed25f98e4
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ i18n: {
+ titleLabel: __('Title (required)'),
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group v-if="isEditing" :label="$options.i18n.titleLabel" label-for="work-item-title">
+ <gl-form-input
+ id="work-item-title"
+ class="gl-w-full"
+ :value="title"
+ @change="$emit('updateDraft', $event)"
+ />
+ </gl-form-group>
+ <h1
+ v-else
+ data-testid="work-item-title"
+ class="gl-w-full gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-font-size-h-display"
+ >
+ {{ title }}
+ </h1>
+</template>
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index eefdbda8f4f..8153c4d4717 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -602,6 +602,20 @@
}
@include email-code-block;
+
+ &.gl-text-secondary {
+ color: $gl-text-color-secondary;
+
+ p,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ table:not(.code) {
+ color: $gl-text-color-secondary;
+ }
+ }
}
/**
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index c2ecf3702f9..bd24d991c8d 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -189,10 +189,6 @@
.project-page-sidebar-block {
width: $right-sidebar-width - 1px;
-
- &:first-of-type {
- padding-top: $gl-spacing-scale-1;
- }
}
.nav {
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index b9ab2450ff9..5b354f3575c 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -4,6 +4,7 @@
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
$work-item-overview-right-sidebar-width: 23rem;
$work-item-sticky-header-height: 52px;
+$work-item-overview-gap-width: 2rem;
.gl-token-selector-token-container {
display: flex;
@@ -146,7 +147,7 @@ $work-item-sticky-header-height: 52px;
@include media-breakpoint-up(md) {
display: grid;
grid-template-columns: 1fr $work-item-overview-right-sidebar-width;
- gap: 2rem;
+ gap: $work-item-overview-gap-width;
}
}
@@ -216,6 +217,12 @@ $work-item-sticky-header-height: 52px;
}
}
+.editable-wi-title {
+ width: 100%;
+ @include media-breakpoint-up(md) {
+ width: calc(100% - #{$work-item-overview-right-sidebar-width} - #{$work-item-overview-gap-width});
+ }
+}
// Disclosure hierarchy component, used for Ancestors widget
$disclosure-hierarchy-chevron-dimension: 1.2rem;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 79ea8d3cc70..7ae17f4c191 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -136,3 +136,13 @@
.gl-last-of-type-border-b-0:last-of-type {
@include gl-border-b-0;
}
+
+.gl-md-h-9 {
+ @include gl-media-breakpoint-up(md) {
+ height: $gl-spacing-scale-9;
+ }
+}
+
+.gl-pl-12 {
+ padding-left: $gl-spacing-scale-12;
+}
diff --git a/app/models/user.rb b/app/models/user.rb
index 7e6eb3529d9..8ae89d60b0b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -284,8 +284,6 @@ class User < MainClusterwide::ApplicationRecord
has_many :reviews, foreign_key: :author_id, inverse_of: :author
- has_many :in_product_marketing_emails, class_name: '::Users::InProductMarketingEmail'
-
has_many :timelogs
has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
deleted file mode 100644
index 5362a726ff5..00000000000
--- a/app/models/users/in_product_marketing_email.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class InProductMarketingEmail < ApplicationRecord
- include BulkInsertSafe
-
- belongs_to :user
-
- validates :user, presence: true
- validates :track, presence: true
- validates :series, presence: true
-
- validates :user_id, uniqueness: {
- scope: [:track, :series],
- message: 'track series email has already been sent'
- }, if: -> { track.present? }
-
- enum track: {
- create: 0,
- verify: 1,
- trial: 2,
- team: 3,
- experience: 4,
- team_short: 5,
- trial_short: 6,
- admin_verify: 7,
- invite_team: 8
- }, _suffix: true
-
- # Tracks we don't send emails for (e.g. unsuccessful experiment). These
- # are kept since we already have DB records that use the enum value.
- INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze
- ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
-
- scope :for_user_with_track_and_series, ->(user, track, series) do
- where(user: user, track: track, series: series)
- end
-
- scope :without_track_and_series, ->(track, series) do
- join_condition = for_user.and(for_track_and_series(track, series))
- users_without_records(join_condition)
- end
-
- def self.users_table
- User.arel_table
- end
-
- def self.distinct_users_sql
- name = users_table.name
- Arel.sql("DISTINCT ON(#{name}.id) #{name}.*")
- end
-
- def self.users_without_records(condition)
- arel_join = users_table.join(arel_table, Arel::Nodes::OuterJoin).on(condition)
- joins(arel_join.join_sources)
- .where(in_product_marketing_emails: { id: nil })
- .select(distinct_users_sql)
- end
-
- def self.for_user
- arel_table[:user_id].eq(users_table[:id])
- end
-
- def self.for_track_and_series(track, series)
- arel_table[:track].eq(ACTIVE_TRACKS[track])
- .and(arel_table[:series]).eq(series)
- end
-
- def self.save_cta_click(user, track, series)
- email = for_user_with_track_and_series(user, track, series).take
-
- email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
- end
- end
-end
diff --git a/app/services/work_items/callbacks/assignees.rb b/app/services/work_items/callbacks/assignees.rb
new file mode 100644
index 00000000000..14755ff0b46
--- /dev/null
+++ b/app/services/work_items/callbacks/assignees.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Callbacks
+ class Assignees < Base
+ def before_update
+ params[:assignee_ids] = [] if excluded_in_new_type?
+
+ return unless params.present? && params.has_key?(:assignee_ids)
+ return unless has_permission?(:set_work_item_metadata)
+
+ assignee_ids = filter_assignees_count(params[:assignee_ids])
+ assignee_ids = filter_assignee_permissions(assignee_ids)
+
+ return if assignee_ids.sort == work_item.assignee_ids.sort
+
+ work_item.assignee_ids = assignee_ids
+ work_item.touch
+ end
+
+ private
+
+ def filter_assignees_count(assignee_ids)
+ return assignee_ids if work_item.allows_multiple_assignees?
+
+ assignee_ids.first(1)
+ end
+
+ def filter_assignee_permissions(assignee_ids)
+ assignees = User.id_in(assignee_ids)
+
+ assignees.select { |assignee| assignee.can?(:read_work_item, work_item) }.map(&:id)
+ end
+ end
+ end
+end
diff --git a/app/services/work_items/widgets/assignees_service/update_service.rb b/app/services/work_items/widgets/assignees_service/update_service.rb
deleted file mode 100644
index 7a084917ea7..00000000000
--- a/app/services/work_items/widgets/assignees_service/update_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- module AssigneesService
- class UpdateService < WorkItems::Widgets::BaseService
- def before_update_in_transaction(params:)
- params[:assignee_ids] = [] if new_type_excludes_widget?
-
- return unless params.present? && params.has_key?(:assignee_ids)
- return unless has_permission?(:set_work_item_metadata)
-
- assignee_ids = filter_assignees_count(params[:assignee_ids])
- assignee_ids = filter_assignee_permissions(assignee_ids)
-
- return if assignee_ids.sort == work_item.assignee_ids.sort
-
- work_item.assignee_ids = assignee_ids
- work_item.touch
- end
-
- private
-
- def filter_assignees_count(assignee_ids)
- return assignee_ids if work_item.allows_multiple_assignees?
-
- assignee_ids.first(1)
- end
-
- def filter_assignee_permissions(assignee_ids)
- assignees = User.id_in(assignee_ids)
-
- assignees.select { |assignee| assignee.can?(:read_work_item, work_item) }.map(&:id)
- end
- end
- end
- end
-end
diff --git a/app/views/projects/_sidebar.html.haml b/app/views/projects/_sidebar.html.haml
index 565f14d01d9..7cb2f622788 100644
--- a/app/views/projects/_sidebar.html.haml
+++ b/app/views/projects/_sidebar.html.haml
@@ -2,43 +2,44 @@
- show_auto_devops_callout = show_auto_devops_callout?(@project)
%aside.project-page-sidebar
- - if @project.description.present? || @project.badges.present?
- .project-page-sidebar-block.home-panel-home-desc.gl-py-4.gl-border-b.gl-border-gray-50
- -# Project description
- - if @project.description.present?
- .gl-display-flex.gl-justify-content-space-between.gl-mt-1.gl-pr-2
- %p.gl-font-weight-bold.gl-text-gray-900.gl-m-0= s_('ProjectPage|Project information')
- = render Pajamas::ButtonComponent.new(href: edit_project_path(@project),
- category: :tertiary,
- icon: 'settings',
- size: :small,
- button_options: { class: 'has-tooltip', title: s_('ProjectPage|Project settings'), 'aria-label' => s_('ProjectPage|Project settings') })
- .home-panel-description.text-break
- .home-panel-description-markdown{ itemprop: 'description' }
- = markdown_field(@project, :description)
+ .project-page-sidebar-block.home-panel-home-desc.gl-py-4.gl-border-b.gl-border-gray-50{ class: 'gl-pt-2!' }
+ .gl-display-flex.gl-justify-content-space-between
+ %p.gl-font-weight-bold.gl-text-gray-900.gl-m-0.gl-mb-1= s_('ProjectPage|Project information')
+ -# Project settings
+ - if can?(current_user, :admin_project, @project)
+ = render Pajamas::ButtonComponent.new(href: edit_project_path(@project),
+ category: :tertiary,
+ icon: 'settings',
+ size: :small,
+ button_options: { class: 'has-tooltip gl-ml-2 gl-sm-mr-3', title: s_('ProjectPage|Project settings'), 'aria-label' => s_('ProjectPage|Project settings'), 'data-testid': 'project-settings-button' })
+ -# Project description
+ - if @project.description.present?
+ .home-panel-description.text-break
+ .home-panel-description-markdown{ itemprop: 'description' }
+ = markdown_field(@project, :description)
- -# Topics
- - if @project.topics.present?
- .gl-mb-5
- = render "shared/projects/topics", project: @project
+ -# Topics
+ - if @project.topics.present?
+ .gl-mb-5
+ = render "shared/projects/topics", project: @project
- -# Programming languages
- - if can?(current_user, :read_code, @project) && @project.repository_languages.present?
- .gl-mb-2{ class: ('gl-mb-4!' if @project.badges.present?) }
- = repository_languages_bar(@project.repository_languages)
+ -# Programming languages
+ - if can?(current_user, :read_code, @project) && @project.repository_languages.present?
+ .gl-mb-2{ class: [('gl-mb-4!' if @project.badges.present?), ('gl-mt-3' if !@project.description.present?)] }
+ = repository_languages_bar(@project.repository_languages)
- -# Badges
- - if @project.badges.present?
- .project-badges.gl-mb-2{ data: { testid: 'project-badges-content' } }
- - @project.badges.each do |badge|
- - badge_link_url = badge.rendered_link_url(@project)
- %a.gl-mr-3{ href: badge_link_url,
- target: '_blank',
- rel: 'noopener noreferrer',
- data: { testid: 'badge-image-link', qa_link_url: badge_link_url } }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: 'Project badge' }>
+ -# Badges
+ - if @project.badges.present?
+ .project-badges.gl-mb-2{ data: { testid: 'project-badges-content' } }
+ - @project.badges.each do |badge|
+ - badge_link_url = badge.rendered_link_url(@project)
+ %a.gl-mr-3{ href: badge_link_url,
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ data: { testid: 'badge-image-link', qa_link_url: badge_link_url } }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
-# Invite members
- if @project.empty_repo?
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 684ea8242f7..ac3b67d6157 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -20,7 +20,7 @@
.project-clone-holder.d-block.d-sm-none
= render "shared/mobile_clone_panel"
- .project-clone-holder.gl-display-none.gl-sm-display-flex.gl-justify-content-end.gl-w-full.gl-mt-2
+ .project-clone-holder.gl-display-none.gl-sm-display-flex.gl-justify-content-end.gl-w-full
= render "projects/buttons/code", ref: @ref
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-new-card-body gl-bg-gray-10 gl-p-5' }) do |c|