mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-25 16:03:48 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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__(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
33
lib/gitlab/fp/settings/settings_dependency_resolver.rb
Normal file
33
lib/gitlab/fp/settings/settings_dependency_resolver.rb
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
]
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user