diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-20 12:10:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-20 12:10:35 +0300 |
commit | 0b7d3f810f287523fd4ad72c019d78e8d2983f8a (patch) | |
tree | fdb52060e586a5548f026b6a3ce71c5b676d9e5c /app | |
parent | 51da6793bcbe7295c1f3f881f756b8b60ee06da1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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| |