Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2025-01-11 00:32:17 +00:00
parent eefff9d8be
commit 64e27d8cbe
26 changed files with 677 additions and 202 deletions

View File

@ -81,6 +81,11 @@ export default {
required: false,
default: '',
},
hideUploadTextOnDragging: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -212,8 +217,15 @@ export default {
class="gl-flex gl-items-center gl-justify-center gl-text-center"
data-testid="dropzone-area"
>
<gl-animated-upload-icon :is-on="animateUploadIcon" :class="iconStyles.class" />
<p class="gl-mb-0" data-testid="upload-text">
<gl-animated-upload-icon
:is-on="animateUploadIcon || hideUploadTextOnDragging"
:class="iconStyles.class"
/>
<p
v-if="!hideUploadTextOnDragging || !dragging"
class="gl-mb-0"
data-testid="upload-text"
>
<slot name="upload-text" :open-file-upload="openFileUpload">
<gl-sprintf
:message="singleFileSelection ? uploadSingleMessage : uploadMultipleMessage"
@ -224,6 +236,9 @@ export default {
</gl-sprintf>
</slot>
</p>
<span v-if="hideUploadTextOnDragging && dragging">
{{ s__('DesignManagement|Drop your images to start the upload.') }}
</span>
</div>
</button>
@ -250,7 +265,7 @@ export default {
<!-- Design Upload Overlay Style for Work Items -->
<template v-if="showUploadDesignOverlay">
<div
v-if="isDragDataValid"
v-if="isDragDataValid && !hideUploadTextOnDragging"
class="gl-absolute gl-bottom-6 gl-flex gl-items-center gl-rounded-base gl-bg-blue-950 gl-px-3 gl-py-2 gl-text-white"
data-testid="design-upload-overlay"
>

View File

@ -346,136 +346,143 @@ export default {
</script>
<template>
<crud-component
v-if="hasDesignsAndVersions"
anchor-name="designs"
anchor-id="designs"
:title="s__('DesignManagement|Designs')"
data-testid="designs-root"
class="gl-relative gl-mt-5"
:body-class="crudBodyClass"
is-collapsible
persist-collapsed-state
>
<template #count>
<design-version-dropdown :all-versions="allVersions" />
</template>
<div>
<slot v-if="!hasDesignsAndVersions" name="empty-state"></slot>
<crud-component
v-if="hasDesignsAndVersions"
anchor-name="designs"
anchor-id="designs"
:title="s__('DesignManagement|Designs')"
data-testid="designs-root"
class="gl-relative gl-mt-5"
:body-class="crudBodyClass"
is-collapsible
persist-collapsed-state
>
<template #count>
<design-version-dropdown :all-versions="allVersions" />
</template>
<template #actions>
<gl-button
v-if="isLatestVersion"
category="tertiary"
size="small"
variant="link"
:disabled="!hasDesigns"
data-testid="select-all-designs-button"
:aria-label="selectAllButtonText"
@click="toggleDesignsSelection"
>
{{ selectAllButtonText }}
</gl-button>
<archive-design-button
v-if="isLatestVersion"
data-testid="archive-button"
button-class="gl-hidden sm:gl-block"
:has-selected-designs="hasSelectedDesigns"
:loading="isArchiving"
@archive-selected-designs="onArchiveDesign"
>
{{ $options.i18n.archiveDesignText }}
</archive-design-button>
<archive-design-button
v-if="isLatestVersion"
v-gl-tooltip.bottom
data-testid="archive-button"
button-class="sm:gl-hidden gl-block"
button-icon="archive"
:title="$options.i18n.archiveDesignText"
:aria-label="$options.i18n.archiveDesignText"
:has-selected-designs="hasSelectedDesigns"
:loading="isArchiving"
@archive-selected-designs="onArchiveDesign"
/>
<gl-button
size="small"
data-testid="add-design"
:disabled="isSaving"
:loading="isSaving"
@click="openDesignUpload"
>{{ __('Add') }}</gl-button
>
<input
ref="fileUpload"
type="file"
name="design_file"
:accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
class="gl-hidden"
multiple
@change="onDesignUploadChange"
/>
</template>
<template #default>
<gl-alert v-if="error || uploadError" :variant="uploadErrorVariant" @dismiss="dismissError()">
{{ error || uploadError }}
</gl-alert>
<design-dropzone
show-upload-design-overlay
validate-design-upload-on-dragover
:accept-design-formats="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
:upload-design-overlay-text="$options.i18n.uploadDesignOverlayText"
@change="$emit('upload', $event)"
@error="$emit('error')"
@dragenter="dismissError"
>
<p v-if="!hasDesigns" class="gl-mb-0 gl-px-5 gl-py-4 gl-text-subtle">
{{ $options.i18n.allDesignsArchived }}
</p>
<vue-draggable
:value="designs"
:disabled="isDraggingDisabled"
v-bind="$options.dragOptions"
:force-fallback="true"
tag="ol"
draggable=".js-design-tile"
filter=".no-drag"
class="list-unstyled row -gl-my-1 gl-flex gl-gap-y-5"
:class="{ 'gl-px-3 gl-py-2': hasDesigns, 'gl-hidden': !hasDesigns }"
@end="onDragEnd"
@change="onDesignsReorder"
@input="onDesignMove"
@pointerup.native="onPointerUp"
<template #actions>
<gl-button
v-if="isLatestVersion"
category="tertiary"
size="small"
variant="link"
:disabled="!hasDesigns"
data-testid="select-all-designs-button"
:aria-label="selectAllButtonText"
@click="toggleDesignsSelection"
>
<li
v-for="design in designs"
:key="design.id"
class="col-md-6 col-lg-3 js-design-tile gl-bg-transparent gl-px-3 gl-shadow-none"
@mousedown="onMouseDown"
@pointerup="onPointerUp"
>
<design
v-bind="design"
class="gl-bg-default"
:is-uploading="false"
:is-dragging="isDraggingDesign"
:work-item-iid="workItemIid"
data-testid="design-item"
@pointerup="onPointerUp"
/>
{{ selectAllButtonText }}
</gl-button>
<archive-design-button
v-if="isLatestVersion"
data-testid="archive-button"
button-class="gl-hidden sm:gl-block"
:has-selected-designs="hasSelectedDesigns"
:loading="isArchiving"
@archive-selected-designs="onArchiveDesign"
>
{{ $options.i18n.archiveDesignText }}
</archive-design-button>
<archive-design-button
v-if="isLatestVersion"
v-gl-tooltip.bottom
data-testid="archive-button"
button-class="sm:gl-hidden gl-block"
button-icon="archive"
:title="$options.i18n.archiveDesignText"
:aria-label="$options.i18n.archiveDesignText"
:has-selected-designs="hasSelectedDesigns"
:loading="isArchiving"
@archive-selected-designs="onArchiveDesign"
/>
<gl-button
size="small"
data-testid="add-design"
:disabled="isSaving"
:loading="isSaving"
@click="openDesignUpload"
>{{ __('Add') }}</gl-button
>
<input
ref="fileUpload"
type="file"
name="design_file"
:accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
class="gl-hidden"
multiple
@change="onDesignUploadChange"
/>
</template>
<gl-form-checkbox
v-if="isLatestVersion"
:id="`design-checkbox-${design.id}`"
:checked="isDesignSelected(design.filename)"
class="no-drag gl-absolute gl-left-5 gl-top-4 gl-ml-2"
data-testid="design-checkbox"
:aria-label="checkboxAriaLabel(design.filename)"
@change="changeSelectedDesigns(design.filename)"
/>
</li>
</vue-draggable>
</design-dropzone>
<router-view :key="$route.fullPath" :all-designs="designs" :all-versions="allVersions" />
</template>
</crud-component>
<template #default>
<gl-alert
v-if="error || uploadError"
:variant="uploadErrorVariant"
@dismiss="dismissError()"
>
{{ error || uploadError }}
</gl-alert>
<design-dropzone
show-upload-design-overlay
validate-design-upload-on-dragover
:accept-design-formats="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
:upload-design-overlay-text="$options.i18n.uploadDesignOverlayText"
@change="$emit('upload', $event)"
@error="$emit('error')"
@dragenter="dismissError"
>
<p v-if="!hasDesigns" class="gl-mb-0 gl-px-5 gl-py-4 gl-text-subtle">
{{ $options.i18n.allDesignsArchived }}
</p>
<vue-draggable
:value="designs"
:disabled="isDraggingDisabled"
v-bind="$options.dragOptions"
:force-fallback="true"
tag="ol"
draggable=".js-design-tile"
filter=".no-drag"
class="list-unstyled row -gl-my-1 gl-flex gl-gap-y-5"
:class="{ 'gl-px-3 gl-py-2': hasDesigns, 'gl-hidden': !hasDesigns }"
@end="onDragEnd"
@change="onDesignsReorder"
@input="onDesignMove"
@pointerup.native="onPointerUp"
>
<li
v-for="design in designs"
:key="design.id"
class="col-md-6 col-lg-3 js-design-tile gl-bg-transparent gl-px-3 gl-shadow-none"
@mousedown="onMouseDown"
@pointerup="onPointerUp"
>
<design
v-bind="design"
class="gl-bg-default"
:is-uploading="false"
:is-dragging="isDraggingDesign"
:work-item-iid="workItemIid"
data-testid="design-item"
@pointerup="onPointerUp"
/>
<gl-form-checkbox
v-if="isLatestVersion"
:id="`design-checkbox-${design.id}`"
:checked="isDesignSelected(design.filename)"
class="no-drag gl-absolute gl-left-5 gl-top-4 gl-ml-2"
data-testid="design-checkbox"
:aria-label="checkboxAriaLabel(design.filename)"
@change="changeSelectedDesigns(design.filename)"
/>
</li>
</vue-draggable>
</design-dropzone>
<router-view :key="$route.fullPath" :all-designs="designs" :all-versions="allVersions" />
</template>
</crud-component>
</div>
</template>

View File

@ -362,6 +362,8 @@ export default {
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
@error="$emit('error', $event)"
@startEditing="$emit('startEditing')"
@stopEditing="$emit('stopEditing')"
/>
<discussion-reply-placeholder
v-else

View File

@ -271,6 +271,11 @@ export default {
if (!this.isSubmitting) {
this.commentText = newText;
updateDraft(this.autosaveKey, this.commentText);
if (this.commentText) {
this.$emit('startEditing');
} else {
this.$emit('stopEditing');
}
}
},
async cancelEditing() {

View File

@ -149,6 +149,7 @@ export default {
this.showForm = false;
this.isExpanded = this.hasReplies;
this.autofocus = false;
this.$emit('cancelEditing');
},
toggleDiscussion() {
this.isExpanded = !this.isExpanded;
@ -235,10 +236,12 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:is-resolving="isResolving"
@startEditing="$emit('startEditing')"
@resolve="resolveDiscussion"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@cancelEditing="$emit('cancelEditing')"
@error="$emit('error', $event)"
/>
<timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
@ -267,8 +270,10 @@ export default {
:is-discussion-resolvable="isDiscussionResolvable"
:is-resolving="isResolving"
@startReplying="showReplyForm"
@startEditing="$emit('startEditing')"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@cancelEditing="$emit('cancelEditing')"
@resolve="resolveDiscussion"
@error="$emit('error', $event)"
/>
@ -300,6 +305,8 @@ export default {
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', reply)"
@reportAbuse="$emit('reportAbuse', reply)"
@startEditing="$emit('startEditing')"
@cancelEditing="$emit('cancelEditing')"
@error="$emit('error', $event)"
/>
</template>
@ -334,6 +341,7 @@ export default {
@replying="onReplying"
@resolve="resolveDiscussion"
@error="$emit('error', $event)"
@startEditing="$emit('startEditing')"
/>
</template>
</discussion-notes-replies-wrapper>

View File

@ -232,8 +232,10 @@ export default {
methods: {
showReplyForm() {
this.$emit('startReplying');
this.$emit('startEditing');
},
startEditing() {
this.$emit('startEditing');
this.isEditing = true;
updateDraft(this.autosaveKey, this.note.body);
},
@ -326,6 +328,10 @@ export default {
Sentry.captureException(error);
}
},
cancelEditing() {
this.isEditing = false;
this.$emit('cancelEditing');
},
},
};
</script>
@ -413,7 +419,7 @@ export default {
:has-replies="hasReplies"
:full-path="fullPath"
class="gl-mt-3"
@cancelEditing="isEditing = false"
@cancelEditing="cancelEditing"
@toggleResolveDiscussion="$emit('resolve')"
@submitForm="updateNote"
/>

View File

@ -1,7 +1,14 @@
<script>
import { isEmpty } from 'lodash';
import { GlAlert, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import {
GlAlert,
GlButton,
GlTooltipDirective,
GlEmptyState,
GlIntersectionObserver,
} from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import { getParameterByName } from '~/lib/utils/url_utility';
@ -50,6 +57,7 @@ import {
designUploadSkippedWarning,
UPLOAD_DESIGN_ERROR_MESSAGE,
ALERT_VARIANTS,
VALID_DESIGN_FILE_MIMETYPE,
} from './design_management/constants';
import WorkItemTree from './work_item_links/work_item_tree.vue';
@ -84,12 +92,15 @@ export default {
GlTooltip: GlTooltipDirective,
},
isLoggedIn: isLoggedIn(),
VALID_DESIGN_FILE_MIMETYPE,
components: {
DesignDropzone,
DesignWidget,
DesignUploadButton,
GlAlert,
GlButton,
GlEmptyState,
GlIntersectionObserver,
WorkItemActions,
TodosToggle,
WorkItemNotificationsWidget,
@ -170,6 +181,11 @@ export default {
designUploadErrorVariant: ALERT_VARIANTS.danger,
workspacePermissions: defaultWorkspacePermissions,
activeChildItem: null,
isEmptyStateVisible: false,
dragCounter: 0,
isDesignUploadButtonInViewport: false,
isDragDataValid: false,
isAddingNotes: false,
};
},
apollo: {
@ -678,6 +694,42 @@ export default {
this.$apollo.queries.workItem.refetch();
this.$emit('workItemTypeChanged', this.workItem);
},
isValidDragDataType({ dataTransfer }) {
this.isDragDataValid = Array.from(dataTransfer.items).some((item) =>
this.$options.VALID_DESIGN_FILE_MIMETYPE.mimetype.includes(item.type),
);
},
onDragEnter(event) {
this.dragCounter += 1;
this.isValidDragDataType(event);
},
onDragOver(event) {
this.isValidDragDataType(event);
if (this.isDesignUploadButtonInViewport) this.isEmptyStateVisible = true;
},
onDragLeave(event) {
const emptyStateDesignDropzone =
this.$refs.emptyStateDesignDropzone?.$el || this.$refs.emptyStateDesignDropzone;
if (!emptyStateDesignDropzone.contains(event.relatedTarget)) {
this.dragCounter -= 1;
}
if (this.dragCounter === 0) {
this.isEmptyStateVisible = false; // Hide dropzone
}
},
onDragLeaveMain(event) {
// Check if the drag is leaving the main container entirely
const mainContainerRef = this.$refs.workItemDetail;
if (!mainContainerRef.contains(event.relatedTarget)) {
this.dragCounter = 0;
this.isEmptyStateVisible = false; // Hide dropzone
}
},
onDrop() {
this.dragCounter = 0; // Reset drag state
this.isEmptyStateVisible = false; // Hide dropzone after drop
},
},
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WORKSPACE_PROJECT,
@ -686,7 +738,15 @@ export default {
</script>
<template>
<div>
<div
ref="workItemDetail"
@dragstart.prevent.stop
@dragend.prevent.stop
@dragenter.prevent.stop="onDragEnter"
@dragover.prevent.stop="onDragOver"
@dragleave.prevent.stop="onDragLeaveMain"
@drop.prevent.stop="onDrop"
>
<work-item-sticky-header
v-if="showIntersectionObserver"
:current-user-todos="currentUserTodos"
@ -870,13 +930,19 @@ export default {
@emoji-updated="$emit('work-item-emoji-updated', $event)"
/>
<div class="gl-flex gl-gap-3">
<design-upload-button
<gl-intersection-observer
v-if="showUploadDesign"
:is-saving="isSaving"
data-testid="design-upload-button"
@upload="onUploadDesign"
@error="onUploadDesignError"
/>
@appear="isDesignUploadButtonInViewport = true"
@disappear="isDesignUploadButtonInViewport = false"
>
<design-upload-button
v-if="showUploadDesign"
:is-saving="isSaving"
data-testid="design-upload-button"
@upload="onUploadDesign"
@error="onUploadDesignError"
/>
</gl-intersection-observer>
<work-item-create-branch-merge-request-split-button
v-if="showCreateBranchMergeRequestSplitButton"
:work-item-id="workItem.id"
@ -915,7 +981,24 @@ export default {
:is-saving="isSaving"
@upload="onUploadDesign"
@dismissError="designUploadError = null"
/>
>
<template #empty-state>
<design-dropzone
v-if="isEmptyStateVisible && !isSaving && isDragDataValid && !isAddingNotes"
ref="emptyStateDesignDropzone"
class="gl-relative gl-mt-5"
show-upload-design-overlay
validate-design-upload-on-dragover
hide-upload-text-on-dragging
:accept-design-formats="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
@change="onUploadDesign"
>
<template #upload-text>
{{ $options.i18n.addDesignEmptyState }}
</template>
</design-dropzone>
</template>
</design-widget>
<work-item-tree
v-if="showWorkItemTree"
@ -971,6 +1054,8 @@ export default {
:parent-id="parentWorkItemId"
@error="updateError = $event"
@openReportAbuse="openReportAbuseModal"
@startEditing="isAddingNotes = true"
@stopEditing="isAddingNotes = false"
/>
</div>
</div>

View File

@ -436,7 +436,12 @@ export default {
<div v-if="someNotesLoaded" class="issuable-discussion gl-mb-5 !gl-clearfix">
<div v-if="formAtTop && !commentsDisabled" class="js-comment-form">
<ul class="notes notes-form timeline">
<work-item-add-note v-bind="workItemCommentFormProps" @error="$emit('error', $event)" />
<work-item-add-note
v-bind="workItemCommentFormProps"
@startEditing="$emit('startEditing')"
@stopEditing="$emit('stopEditing')"
@error="$emit('error', $event)"
/>
</ul>
</div>
<work-item-notes-loading v-if="formAtTop && isLoadingMore" />
@ -466,6 +471,8 @@ export default {
@deleteNote="showDeleteNoteModal($event, discussion)"
@reportAbuse="reportAbuse(true, $event)"
@error="$emit('error', $event)"
@startEditing="$emit('startEditing')"
@cancelEditing="$emit('stopEditing')"
/>
</template>
</template>
@ -478,7 +485,12 @@ export default {
<work-item-notes-loading v-if="!formAtTop && isLoadingMore" />
<div v-if="!formAtTop && !commentsDisabled" class="js-comment-form">
<ul class="notes notes-form timeline">
<work-item-add-note v-bind="workItemCommentFormProps" @error="$emit('error', $event)" />
<work-item-add-note
v-bind="workItemCommentFormProps"
@startEditing="$emit('startEditing')"
@stopEditing="$emit('stopEditing')"
@error="$emit('error', $event)"
/>
</ul>
</div>
</div>

View File

@ -60,6 +60,7 @@ export const i18n = {
"WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
addDesignEmptyState: s__('DesignManagement|Drag images here to add designs.'),
};
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(

View File

@ -6,7 +6,7 @@
%aside.project-page-sidebar{ data: { testid: 'project-page-sidebar' } }
.project-page-sidebar-block.home-panel-home-desc.gl-py-4.gl-border-b.gl-border-b-subtle{ class: '!gl-pt-2' }
%h2.gl-text-base.gl-font-bold.gl-leading-reset.gl-text-strong.gl-m-0.gl-mb-1= s_('ProjectPage|Project information')
%h2.gl-text-base.gl-font-bold.gl-leading-reset.gl-text-heading.gl-m-0.gl-mb-1= s_('ProjectPage|Project information')
-# Project description
- if @project.description.present?
.home-panel-description.gl-break-words

View File

@ -109,7 +109,7 @@ are very appreciative of the work done by translators and proofreaders!
- Pablo Reyes - [GitLab](https://gitlab.com/pabloryst9n), [Crowdin](https://crowdin.com/profile/pabloryst9n)
- Gustavo Román - [GitLab](https://gitlab.com/GustavoStark), [Crowdin](https://crowdin.com/profile/gustavonewton)
- Swedish
- Johannes Nilsson - [GitLab](https://gitlab.com/nlssn), [Crowdin](https://crowdin.com/profile/nlssn)
- Johannes Nilsson - [GitLab](https://gitlab.com/pixelregn), [Crowdin](https://crowdin.com/profile/pixelregn)
- Turkish
- Proofreaders needed.
- Ukrainian

View File

@ -26,6 +26,7 @@ This extension brings the GitLab features you use every day directly into your V
- [Create](#create-a-snippet) and manage snippets.
- [Browse repositories](remote_urls.md#browse-a-repository-in-read-only-mode) without cloning them.
- [View security findings](#view-security-findings).
- [Perform SAST scanning](#perform-sast-scanning).
The GitLab Workflow extension also streamlines your VS Code workflow with AI-assisted features:
@ -229,6 +230,49 @@ To view security findings:
1. Select a desired severity level.
1. Select a finding to open it in a VS Code tab.
## Perform SAST scanning
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab.com
**Status:** Experiment
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/1675) in VS Code extension version 5.31.
Static application security testing (SAST) in VS Code detects vulnerabilities in the active file.
With early detection, you can remediate vulnerabilities before your changes are merged into the
default branch.
When you trigger a SAST scan, the content of the active file is passed to GitLab and checked against
SAST vulnerability rules. Scan results are shown in the primary side bar.
Prerequisites:
- You're using GitLab Workflow version 5.31.0 or later.
- You've [authenticated with GitLab](setup.md#authenticate-with-gitlab).
- You've selected the [**Enable Real-time SAST scan checkbox**](setup.md#code-security).
To perform SAST scanning of a file in VS Code:
1. Open the file.
1. Trigger the SAST scan by either:
- Saving the file (if you have [selected the **Enable scanning on file save** option](setup.md#code-security)).
- Using the Command Palette:
1. Open the Command Palette:
- For macOS, press <kbd>Command</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>.
- For Windows or Linux, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>.
1. Search for **GitLab: Run Remote Scan (SAST)** and press
<kbd>Enter</kbd>.
1. View the results of the SAST scan.
1. View the **Primary Side Bar**.
1. Select GitLab Workflow ({tanuki}) to display the extension sidebar.
<!-- markdownlint-disable MD044 -->
1. Expand the **GITLAB REMOTE SCAN (SAST)** section.
The results of the SAST scan are listed in descending order by severity. To see details of a
finding, select it in the **GITLAB REMOTE SCAN (SAST)** section of the extension sidebar.
<!-- markdownlint-enable MD044 -->
## Search issues and merge requests
To search your project's issues and merge requests directly from VS Code, use filtered search or

View File

@ -59,3 +59,12 @@ To configure settings, go to **Settings > Extensions > GitLab Workflow**.
By default, Code Suggestions and GitLab Duo Chat are turned on, so if you have
the GitLab Duo add-on and a seat assigned, you should have access.
### Code security
To configure the code security settings, go to **Settings > Extensions > GitLab Workflow > Code
Security**.
- To enable SAST scanning of the active file, select the **Enable Real-time SAST scan** checkbox.
- Optional. To enable SAST scanning of the active file when you save it, select the
**Enable scanning on file save** checkbox.

View File

@ -85,7 +85,7 @@ When you've completed your review of a merge request and are ready to [submit yo
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests** and find the merge request you want to review.
1. When you are ready to submit your review, select **Finish review**.
1. Select **Summarize my pending comments**.
1. Select **Add Summary**.
The summary is displayed in the comment box. You can edit and refine the summary prior to submitting your review.

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Gitlab
module Fp
module Settings
class SettingsDependencyResolver
# @param [Array] setting_names An array of setting names
# @param [Hash] dependencies A hash of setting names to array of dependent setting names
# @return [Array] An array of setting names + all recursive dependencies
def self.resolve(setting_names, dependencies)
result = []
visited = Set.new
queue = setting_names.clone
until queue.empty?
setting_name = queue.shift
# Go to the next item in the queue if we've already seen this one
next if visited.include?(setting_name)
visited.add(setting_name)
result.push(setting_name)
setting_dependencies = dependencies[setting_name]
queue.push(*setting_dependencies) if setting_dependencies
end
result
end
end
end
end
end

View File

@ -20,10 +20,13 @@ module WebIde
end
# This value is used when the end-user is accepting the third-party extension marketplace integration.
#
# @return [String] URL of the VSCode Extension Marketplace home
def self.marketplace_home_url
"https://open-vsx.org"
end
# @return [String] URL of the help page for the user preferences for Extensions Marketplace opt-in
def self.help_preferences_url
::Gitlab::Routing.url_helpers.help_page_url('user/profile/preferences.md',
anchor: 'integrate-with-the-extension-marketplace')
@ -34,54 +37,20 @@ module WebIde
#
# - https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/51f9e91f890752596e7a3ef51f436fea07885eff/packages/web-ide-types/src/config.ts#L109
#
# @param [User] user The current user
# @return [Hash]
def self.webide_extensions_gallery_settings(user:)
# TODO: Add instance-level setting for extensions gallery settings.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/451871
settings = Settings.get(
[:vscode_extensions_gallery, :vscode_extensions_gallery_metadata],
Settings.get(
[:vscode_extensions_gallery_view_model],
user: user,
vscode_extensions_marketplace_feature_flag_enabled: feature_enabled?(user: user)
)
settings => {
vscode_extensions_gallery: Hash => vscode_settings,
vscode_extensions_gallery_metadata: Hash => metadata
}
# TODO: Introduce a Service layer and standard ServiceResponse interface,
# and move the following logic either down into the Settings::Main ROP chain
# or up into the Service layer.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/471300
return { enabled: true, vscode_settings: vscode_settings } if metadata.fetch(:enabled)
disabled_reason = metadata.fetch(:disabled_reason)
result = { enabled: false, reason: disabled_reason, help_url: help_url }
result.merge(gallery_disabled_extra_attributes(disabled_reason: disabled_reason, user: user))
end
# rubocop:disable Lint/UnusedMethodArgument -- `user:` param is used in EE
def self.gallery_disabled_extra_attributes(disabled_reason:, user:)
return { user_preferences_url: user_preferences_url } if disabled_reason == :opt_in_unset
return { user_preferences_url: user_preferences_url } if disabled_reason == :opt_in_disabled
{}
end
# rubocop:enable Lint/UnusedMethodArgument
def self.help_url
::Gitlab::Routing.url_helpers.help_page_url('user/project/web_ide/index.md', anchor: 'extension-marketplace')
end
def self.user_preferences_url
# noinspection RubyResolve -- Rubymine is not correctly recognizing indirectly referenced route helper
::Gitlab::Routing.url_helpers.profile_preferences_url(anchor: 'integrations')
).fetch(:vscode_extensions_gallery_view_model)
end
# Returns true if the given flag is enabled for any actor
#
# @param [Symbol] flag
# @return [Boolean]
def self.feature_flag_enabled_for_any_actor?(flag)
# Short circuit if we're globally enabled
return true if Feature.enabled?(flag, nil)
@ -92,7 +61,7 @@ module WebIde
feature && !feature.off?
end
private_class_method :help_url, :user_preferences_url, :feature_flag_enabled_for_any_actor?
private_class_method :feature_flag_enabled_for_any_actor?
end
end

View File

@ -3,10 +3,11 @@
module WebIde
module Settings
class DefaultSettings
UNDEFINED = nil
SETTINGS_DEPENDENCIES = {
vscode_extensions_gallery_view_model: [:vscode_extensions_gallery_metadata, :vscode_extensions_gallery]
}.freeze
# ALL WEB IDE SETTINGS ARE DECLARED HERE.
# See README.md for more details.
# @return [Hash]
def self.default_settings
{
@ -24,7 +25,11 @@ module WebIde
Hash
],
vscode_extensions_gallery_metadata: [
{}, # NOTE: There is no default, the value is always generated by ExtensionsGalleryMetadataGenerator
{ enabled: false, disabled_reason: :instance_disabled },
Hash
],
vscode_extensions_gallery_view_model: [
{ enabled: false, reason: :instance_disabled, help_url: '' },
Hash
]
}

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module WebIde
module Settings
class ExtensionsGalleryViewModelGenerator
# @param [Hash] context
# @return [Hash]
def self.generate(context)
return context unless context.fetch(:requested_setting_names).include?(:vscode_extensions_gallery_view_model)
context[:settings][:vscode_extensions_gallery_view_model] = build_view_model(context)
context
end
# Builds the value for :vscode_extensions_gallery_view_model
#
# @param [Hash] context The settings railway context
# @return [Hash] value for :vscode_extensions_gallery_view_model
def self.build_view_model(context)
context => {
options: {
user: ::User => user
},
settings: {
vscode_extensions_gallery: Hash => vscode_settings,
vscode_extensions_gallery_metadata: Hash => metadata
}
}
return { enabled: true, vscode_settings: vscode_settings } if metadata.fetch(:enabled)
disabled_reason = metadata.fetch(:disabled_reason)
result = { enabled: false, reason: disabled_reason, help_url: help_url }
result.merge(gallery_disabled_extra_attributes(disabled_reason: disabled_reason, user: user))
end
# Returns extra attributes for the view model when the extensions marketplace is disabled
#
# Overridden in EE
#
# @param [Symbol] disabled_reason The reason why the gallery is disabled
# @param [User] user The current user (only used in EE override)
# @return [Hash] Extra attributes for the view model
#
# rubocop:disable Lint/UnusedMethodArgument -- `user:` param is used in EE
def self.gallery_disabled_extra_attributes(disabled_reason:, user:)
return { user_preferences_url: user_preferences_url } if disabled_reason == :opt_in_unset
return { user_preferences_url: user_preferences_url } if disabled_reason == :opt_in_disabled
{}
end
# rubocop:enable Lint/UnusedMethodArgument
# Returns help url for Web IDE extensions marketplace
#
# @return [String]
def self.help_url
::Gitlab::Routing.url_helpers.help_page_url('user/project/web_ide/index.md', anchor: 'extension-marketplace')
end
# Returns user preferences url for changing the user's opt-in status for VSCode extensions marketplace
#
# @return [String]
def self.user_preferences_url
# noinspection RubyResolve -- Rubymine is not correctly recognizing indirectly referenced route helper
::Gitlab::Routing.url_helpers.profile_preferences_url(anchor: 'integrations')
end
private_class_method :build_view_model, :gallery_disabled_extra_attributes, :help_url, :user_preferences_url
end
end
end
WebIde::Settings::ExtensionsGalleryViewModelGenerator.prepend_mod

View File

@ -12,17 +12,19 @@ module WebIde
def self.get_settings(context)
initial_result = Gitlab::Fp::Result.ok(context)
# The order of the chain determines the precedence of settings. I.e., defaults are
# overridden by subsequent steps, and any env vars override previous steps.
# TODO: Add instance-level setting for extensions gallery settings.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/451871
result =
initial_result
.map(SettingsInitializer.method(:init))
.map(ExtensionsGalleryMetadataGenerator.method(:generate))
# NOTE: EnvVarOverrideProcessor is kept as last settings processing step, so it can always be used
# to easily overrideany settings for local or temporary testing, but still before all validators.
# NOTE: EnvVarOverrideProcessor is inserted here to easily override settings for local or temporary testing
# it should happen **before** validators.
.and_then(Gitlab::Fp::Settings::EnvVarOverrideProcessor.method(:process))
.and_then(ExtensionsGalleryValidator.method(:validate))
.and_then(ExtensionsGalleryMetadataValidator.method(:validate))
# NOTE: ViewModel generator happens near the end since it depends on other settings.
.map(ExtensionsGalleryViewModelGenerator.method(:generate))
.map(
# As the final step, return the settings in a SettingsGetSuccessful message
->(context) do

View File

@ -3,12 +3,23 @@
module WebIde
module Settings
class SettingsInitializer
SETTINGS_DEPENDENCIES = {
vscode_extensions_gallery_view_model: [:vscode_extensions_gallery_metadata, :vscode_extensions_gallery]
}.freeze
# @param [Hash] context
# @return [Hash]
# @raise [RuntimeError]
def self.init(context)
context => { requested_setting_names: Array => requested_setting_names }
# NOTE: We override the requested_setting_names to include *all* nested setting dependencies.
requested_setting_names = Gitlab::Fp::Settings::SettingsDependencyResolver.resolve(
requested_setting_names,
SETTINGS_DEPENDENCIES
)
context[:requested_setting_names] = requested_setting_names
context[:settings], context[:setting_types] = Gitlab::Fp::Settings::DefaultSettingsParser.parse(
module_name: "Web IDE",
requested_setting_names: requested_setting_names,

View File

@ -20074,6 +20074,9 @@ msgstr ""
msgid "DesignManagement|Download design"
msgstr ""
msgid "DesignManagement|Drag images here to add designs."
msgstr ""
msgid "DesignManagement|Drop your images to start the upload."
msgstr ""

View File

@ -1,4 +1,4 @@
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -24,6 +24,7 @@ import TodosToggle from '~/work_items/components/shared/todos_toggle.vue';
import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue';
import WorkItemCreateBranchMergeRequestSplitButton from '~/work_items/components/work_item_development/work_item_create_branch_merge_request_split_button.vue';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import uploadDesignMutation from '~/work_items/components/design_management/graphql/upload_design.mutation.graphql';
import { i18n, STATE_CLOSED } from '~/work_items/constants';
import workItemByIdQuery from '~/work_items/graphql/work_item_by_id.query.graphql';
@ -54,6 +55,7 @@ jest.mock('~/work_items/components/design_management/cache_updates');
describe('WorkItemDetail component', () => {
let wrapper;
let glIntersectionObserver;
Vue.use(VueApollo);
@ -127,6 +129,11 @@ describe('WorkItemDetail component', () => {
const findDrawer = () => wrapper.findComponent(WorkItemDrawer);
const findCreateMergeRequestSplitButton = () =>
wrapper.findComponent(WorkItemCreateBranchMergeRequestSplitButton);
const findDesignDropzone = () => wrapper.findComponent(DesignDropzone);
const mockDragEvent = ({ types = ['Files'], files = [], items = [] }) => {
return { dataTransfer: { types, files, items } };
};
const createComponent = ({
isModal = false,
@ -829,6 +836,65 @@ describe('WorkItemDetail component', () => {
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
const fileList = [file];
describe('when designs are not added and no versions exist', () => {
it('renders the design dropzone when valid file is dragged and the Add design button is in viewport', async () => {
createComponent();
await waitForPromises();
glIntersectionObserver = wrapper.findComponent(GlIntersectionObserver);
const dragEvent = mockDragEvent({
types: ['Files', 'image'],
items: [{ type: 'image/png' }],
});
wrapper.trigger('dragenter', dragEvent);
glIntersectionObserver.vm.$emit('appear');
await nextTick();
wrapper.trigger('dragover', dragEvent);
glIntersectionObserver.vm.$emit('appear');
await nextTick();
expect(findDesignDropzone().exists()).toBe(true);
});
it('does not render the design dropzone if add design button is not in viewport', async () => {
createComponent();
await waitForPromises();
glIntersectionObserver = wrapper.findComponent(GlIntersectionObserver);
const dragEvent = mockDragEvent({
types: ['Files', 'image'],
items: [{ type: 'image/png' }],
});
wrapper.trigger('dragenter', dragEvent);
glIntersectionObserver.vm.$emit('disappear');
await nextTick();
wrapper.trigger('dragover', dragEvent);
glIntersectionObserver.vm.$emit('disappear');
await nextTick();
expect(findDesignDropzone().exists()).toBe(false);
});
it('does not render the design dropzone when invalid file is dragged', async () => {
createComponent();
await waitForPromises();
const dragEvent = mockDragEvent({
types: ['Files'],
items: [{ type: 'text/plain' }],
});
wrapper.trigger('dragenter', dragEvent);
await nextTick();
expect(findDesignDropzone().exists()).toBe(false);
});
});
it('does not render if application has no router', async () => {
createComponent({ router: false });
await waitForPromises();

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require "fast_spec_helper"
RSpec.describe Gitlab::Fp::Settings::SettingsDependencyResolver, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
describe '.resolve' do
where(:description, :setting_names, :dependencies, :expected_result) do
'empty' | [] | {} | []
'no dependencies' | [:a] | {} | [:a]
'simple dependency' | [:a] | { a: [:b] } | [:a, :b]
'redundant dependency' | [:a, :b] | { a: [:b] } | [:a, :b]
'nested dependencies' | [:a, :e] | { a: [:b, :c], c: [:d] } | [:a, :e, :b, :c, :d]
'circular dependency' | [:a, :e] | { a: [:b], b: [:a] } | [:a, :e, :b]
'nil dependency' | [:a] | { a: nil } | [:a]
end
with_them do
it 'resolves dependencies correctly' do
result = described_class.resolve(setting_names, dependencies)
expect(result).to eq(expected_result)
end
end
end
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
require "fast_spec_helper"
RSpec.describe WebIde::Settings::ExtensionsGalleryViewModelGenerator, feature_category: :web_ide do
using RSpec::Parameterized::TableSyntax
let(:user_class) { stub_const('User', Class.new) }
let(:user) { user_class.new }
let(:requested_setting_names) { [:vscode_extensions_gallery_view_model] }
let(:vscode_extensions_gallery) { { item_url: 'https://example.com/vscode/is/cooler/than/rubymine' } }
let(:vscode_extensions_gallery_metadata) { { enabled: true } }
let(:context) do
{
requested_setting_names: requested_setting_names,
settings: {
vscode_extensions_gallery: vscode_extensions_gallery,
vscode_extensions_gallery_metadata: vscode_extensions_gallery_metadata
},
options: {
user: user
}
}
end
before do
# why: Stubs necessary for fast_spec_helper. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167495#note_2290309350
# The `spec/lib/web_ide/extensions_marketplace_spec.rb` covers everything in integration, so we should be good.
allow(::Gitlab::Routing).to receive_message_chain(:url_helpers, :profile_preferences_url)
.with(anchor: 'integrations')
.and_return('http://gdk.test/profile_preferences_url#integrations')
allow(::Gitlab::Routing).to receive_message_chain(:url_helpers, :help_page_url)
.with('user/project/web_ide/index.md', anchor: 'extension-marketplace')
.and_return('http://gdk.test/help_url')
end
describe '.generate' do
subject(:settings_result) do
described_class.generate(context).dig(:settings, :vscode_extensions_gallery_view_model)
end
it 'by default, setting is enabled with vscode_settings' do
expect(settings_result).to eq({ enabled: true, vscode_settings: vscode_extensions_gallery })
end
context 'when settings name is not requested' do
let(:requested_setting_names) { [] }
it 'setting is not set' do
expect(settings_result).to be_nil
end
end
context 'when metadata is disabled' do
where(:disabled_reason, :expectation) do
:instance_disabled | {}
:opt_in_unset | { user_preferences_url: 'http://gdk.test/profile_preferences_url#integrations' }
:opt_in_disabled | { user_preferences_url: 'http://gdk.test/profile_preferences_url#integrations' }
end
with_them do
let(:vscode_extensions_gallery_metadata) { { enabled: false, disabled_reason: disabled_reason } }
it 'setting is disabled with attributes for view' do
expect(settings_result).to match({
enabled: false,
reason: disabled_reason,
help_url: 'http://gdk.test/help_url'
}.merge(expectation))
end
end
end
end
end

View File

@ -12,7 +12,8 @@ RSpec.describe WebIde::Settings::Main, :web_ide_fast, feature_category: :web_ide
[WebIde::Settings::ExtensionsGalleryMetadataGenerator, :map],
[Gitlab::Fp::Settings::EnvVarOverrideProcessor, :and_then],
[WebIde::Settings::ExtensionsGalleryValidator, :and_then],
[WebIde::Settings::ExtensionsGalleryMetadataValidator, :and_then]
[WebIde::Settings::ExtensionsGalleryMetadataValidator, :and_then],
[WebIde::Settings::ExtensionsGalleryViewModelGenerator, :map]
]
end

View File

@ -15,7 +15,11 @@ RSpec.describe WebIde::Settings::SettingsInitializer, :web_ide_fast, feature_cat
it "invokes DefaultSettingsParser and sets up necessary values in context for subsequent steps" do
expect(returned_value).to match(
{
requested_setting_names: [:vscode_extensions_gallery, :vscode_extensions_gallery_metadata],
requested_setting_names: [
:vscode_extensions_gallery,
:vscode_extensions_gallery_metadata,
:vscode_extensions_gallery_view_model
],
settings: {
vscode_extensions_gallery: {
control_url: "",
@ -25,11 +29,20 @@ RSpec.describe WebIde::Settings::SettingsInitializer, :web_ide_fast, feature_cat
resource_url_template: 'https://open-vsx.org/vscode/asset/{publisher}/{name}/{version}/Microsoft.VisualStudio.Code.WebResources/{path}',
service_url: "https://open-vsx.org/vscode/gallery"
},
vscode_extensions_gallery_metadata: {}
vscode_extensions_gallery_metadata: {
enabled: false,
disabled_reason: :instance_disabled
},
vscode_extensions_gallery_view_model: {
enabled: false,
reason: :instance_disabled,
help_url: ''
}
},
setting_types: {
vscode_extensions_gallery: Hash,
vscode_extensions_gallery_metadata: Hash
vscode_extensions_gallery_metadata: Hash,
vscode_extensions_gallery_view_model: Hash
},
env_var_prefix: "GITLAB_WEB_IDE",
env_var_failed_message_class: WebIde::Settings::Messages::SettingsEnvironmentVariableOverrideFailed