Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-07 09:10:34 +00:00
parent b8ff7c8f92
commit 15e74f1fdf
26 changed files with 803 additions and 405 deletions

View File

@ -8,7 +8,7 @@ export default () => {
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix');
const filePath = `${editBlobForm.data('blobFilename')}`;
const filePath = editBlobForm.data('blobFilename') && `${editBlobForm.data('blobFilename')}`;
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
const projectPath = editBlobForm.data('project-path');

View File

@ -2,3 +2,4 @@ import { __ } from '~/locale';
export const BLOB_EDITOR_ERROR = __('An error occurred while rendering the editor');
export const BLOB_PREVIEW_ERROR = __('An error occurred previewing the blob');
export const BLOB_EDIT_ERROR = __('An error occurred editing the blob');

View File

@ -7,9 +7,11 @@ import SourceEditor from '~/editor/source_editor';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
import FilepathFormMediator from '~/blob/filepath_form_mediator';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
import { visitUrl } from '~/lib/utils/url_utility';
import Api from '~/api';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR, BLOB_EDIT_ERROR } from './constants';
export default class EditBlob {
// The options object has:
@ -20,18 +22,8 @@ export default class EditBlob {
this.isMarkdown = this.options.isMarkdown;
this.markdownLivePreviewOpened = false;
if (this.isMarkdown) {
this.fetchMarkdownExtension();
}
if (this.options.filePath === '.gitlab/security-policies/policy.yml') {
this.fetchSecurityPolicyExtension(this.options.projectPath);
}
this.initModePanesAndLinks();
this.initFilepathForm();
this.initSoftWrap();
this.editor.focus();
}
async fetchMarkdownExtension() {
@ -72,16 +64,23 @@ export default class EditBlob {
}
}
configureMonacoEditor() {
async configureMonacoEditor() {
const editorEl = document.getElementById('editor');
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
const rootEditor = new SourceEditor();
const { filePath, projectId } = this.options;
const { ref } = editorEl.dataset;
let blobContent = '';
if (filePath) {
const { data } = await Api.getRawFile(projectId, filePath, { ref });
blobContent = String(data);
}
this.editor = rootEditor.createInstance({
el: editorEl,
blobContent: editorEl.innerText,
blobContent,
blobPath: this.options.filePath,
});
this.editor.use([
@ -90,8 +89,31 @@ export default class EditBlob {
{ definition: FileTemplateExtension },
]);
form.addEventListener('submit', () => {
fileContentEl.value = insertFinalNewline(this.editor.getValue());
if (this.isMarkdown) {
this.fetchMarkdownExtension();
}
if (this.options.filePath === '.gitlab/security-policies/policy.yml') {
await this.fetchSecurityPolicyExtension(this.options.projectPath);
}
this.initFilepathForm();
this.editor.focus();
form.addEventListener('submit', async (event) => {
event.preventDefault();
const { formMethod } = form.dataset;
const endpoint = form.action;
const formData = new FormData(form);
formData.set('content', this.editor.getValue());
try {
const { data } = await axios[formMethod](endpoint, Object.fromEntries(formData));
visitUrl(data.filePath);
} catch (error) {
createAlert({ message: BLOB_EDIT_ERROR, captureError: true });
}
});
// onDidChangeModelLanguage is part of the native Monaco API

View File

@ -0,0 +1,196 @@
<script>
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
I18N_WORK_ITEM_ERROR_CREATING,
I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
sprintfWorkItem,
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import groupWorkItemTypesQuery from '../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import WorkItemTitleWithEdit from './work_item_title_with_edit.vue';
export default {
components: {
GlButton,
GlAlert,
GlLoadingIcon,
WorkItemTitleWithEdit,
GlFormSelect,
},
inject: ['fullPath', 'isGroup'],
props: {
initialTitle: {
type: String,
required: false,
default: '',
},
workItemType: {
type: String,
required: false,
default: null,
},
},
data() {
return {
title: this.initialTitle,
editingTitle: false,
error: null,
workItemTypes: [],
selectedWorkItemType: null,
loading: false,
showWorkItemTypeSelect: false,
};
},
apollo: {
workItemTypes: {
query() {
return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
},
variables() {
return {
fullPath: this.fullPath,
name: this.workItemType,
};
},
update(data) {
return data.workspace?.workItemTypes?.nodes.map((node) => ({
value: node.id,
text: capitalizeFirstCharacter(node.name.toLocaleLowerCase(getPreferredLocales()[0])),
}));
},
result() {
if (this.workItemTypes.length === 1) {
this.selectedWorkItemType = this.workItemTypes[0].value;
} else {
this.showWorkItemTypeSelect = true;
}
},
error() {
this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
computed: {
formOptions() {
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
},
isButtonDisabled() {
return this.title.trim().length === 0 || !this.selectedWorkItemType;
},
createErrorText() {
const workItemType = this.workItemTypes.find(
(item) => item.value === this.selectedWorkItemType,
)?.text;
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
},
createWorkItemText() {
const workItemType = this.workItemTypes.find(
(item) => item.value === this.selectedWorkItemType,
)?.text;
return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, workItemType);
},
},
methods: {
async createWorkItem() {
this.loading = true;
try {
const response = await this.$apollo.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
title: this.title,
projectPath: this.fullPath,
workItemTypeId: this.selectedWorkItemType,
},
},
update: (store, { data: { workItemCreate } }) => {
const { workItem } = workItemCreate;
store.writeQuery({
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid: workItem.iid,
},
data: {
workspace: {
__typename: TYPENAME_PROJECT,
id: workItem.namespace.id,
workItems: {
__typename: 'WorkItemConnection',
nodes: [workItem],
},
},
},
});
},
});
this.$emit('workItemCreated', response.data.workItemCreate.workItem);
} catch {
this.error = this.createErrorText;
this.loading = false;
}
},
handleTitleInput(title) {
this.title = title;
},
handleCancelClick() {
this.$emit('cancel');
},
},
};
</script>
<template>
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
<div data-testid="content">
<work-item-title-with-edit
ref="title"
data-testid="title-input"
is-editing
:title="title"
@updateDraft="handleTitleInput"
@updateWorkItem="createWorkItem"
/>
<div>
<gl-loading-icon
v-if="$apollo.queries.workItemTypes.loading"
size="lg"
data-testid="loading-types"
/>
<gl-form-select
v-else-if="showWorkItemTypeSelect"
v-model="selectedWorkItemType"
:options="formOptions"
class="gl-max-w-26"
/>
</div>
</div>
<div class="gl-py-5 gl-mt-4 gl-display-flex gl-justify-content-end gl-gap-3">
<gl-button type="button" data-testid="cancel-button" @click="handleCancelClick">
{{ __('Cancel') }}
</gl-button>
<gl-button
variant="confirm"
:disabled="isButtonDisabled"
:loading="loading"
data-testid="create-button"
type="submit"
>
{{ createWorkItemText }}
</gl-button>
</div>
</form>
</template>

View File

@ -0,0 +1,67 @@
<script>
import { GlButton, GlModal } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import { I18N_NEW_WORK_ITEM_BUTTON_LABEL, sprintfWorkItem } from '../constants';
import CreateWorkItem from './create_work_item.vue';
export default {
components: {
CreateWorkItem,
GlButton,
GlModal,
},
props: {
workItemType: {
type: String,
required: false,
default: null,
},
},
data() {
return {
visible: false,
};
},
computed: {
newWorkItemText() {
return sprintfWorkItem(I18N_NEW_WORK_ITEM_BUTTON_LABEL, this.workItemType);
},
},
methods: {
hideModal() {
this.visible = false;
},
showModal() {
this.visible = true;
},
handleCreation(workItem) {
visitUrl(workItem.webUrl);
},
},
};
</script>
<template>
<div>
<gl-button
category="primary"
variant="confirm"
data-testid="new-epic-button"
@click="showModal"
>{{ newWorkItemText }}</gl-button
>
<gl-modal
modal-id="create-work-item-modal"
:visible="visible"
hide-footer
no-focus-on-show
@hide="hideModal"
>
<create-work-item
:work-item-type="workItemType"
@cancel="hideModal"
@workItemCreated="handleCreation"
/>
</gl-modal>
</div>
</template>

View File

@ -88,6 +88,7 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__(
'WorkItem|Something went wrong while fetching work item award emojis. Please try again.',
);
export const I18N_NEW_WORK_ITEM_BUTTON_LABEL = s__('WorkItem|New %{workItemType}');
export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');

View File

@ -1,7 +1,7 @@
query groupWorkItemTypes($fullPath: ID!) {
query groupWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: group(fullPath: $fullPath) {
id
workItemTypes {
workItemTypes(name: $name) {
nodes {
id
name

View File

@ -1,7 +1,7 @@
query projectWorkItemTypes($fullPath: ID!) {
query projectWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: project(fullPath: $fullPath) {
id
workItemTypes {
workItemTypes(name: $name) {
nodes {
id
name

View File

@ -1,132 +1,15 @@
<script>
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
I18N_WORK_ITEM_ERROR_CREATING,
I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
sprintfWorkItem,
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import groupWorkItemTypesQuery from '../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import CreateWorkItem from '../components/create_work_item.vue';
export default {
name: 'CreateWorkItemPage',
components: {
GlButton,
GlAlert,
GlLoadingIcon,
ItemTitle,
GlFormSelect,
},
inject: ['fullPath', 'isGroup'],
props: {
initialTitle: {
type: String,
required: false,
default: '',
},
},
data() {
return {
title: this.initialTitle,
error: null,
workItemTypes: [],
selectedWorkItemType: null,
loading: false,
};
},
apollo: {
workItemTypes: {
query() {
return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
},
variables() {
return {
fullPath: this.fullPath,
};
},
update(data) {
return data.workspace?.workItemTypes?.nodes.map((node) => ({
value: node.id,
text: capitalizeFirstCharacter(node.name.toLocaleLowerCase(getPreferredLocales()[0])),
}));
},
error() {
this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
computed: {
formOptions() {
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
},
isButtonDisabled() {
return this.title.trim().length === 0 || !this.selectedWorkItemType;
},
createErrorText() {
const workItemType = this.workItemTypes.find(
(item) => item.value === this.selectedWorkItemType,
)?.text;
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
},
CreateWorkItem,
},
methods: {
async createWorkItem() {
this.loading = true;
await this.createStandaloneWorkItem();
this.loading = false;
},
async createStandaloneWorkItem() {
try {
const response = await this.$apollo.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
title: this.title,
projectPath: this.fullPath,
workItemTypeId: this.selectedWorkItemType,
},
},
update: (store, { data: { workItemCreate } }) => {
const { workItem } = workItemCreate;
store.writeQuery({
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid: workItem.iid,
},
data: {
workspace: {
__typename: TYPENAME_PROJECT,
id: workItem.namespace.id,
workItems: {
__typename: 'WorkItemConnection',
nodes: [workItem],
},
},
},
});
},
});
this.$router.push({
name: 'workItem',
params: { id: response.data.workItemCreate.workItem.iid },
});
} catch {
this.error = this.createErrorText;
}
},
handleTitleInput(title) {
this.title = title;
workItemCreated(workItem) {
visitUrl(workItem.webUrl);
},
handleCancelClick() {
this.$router.go(-1);
@ -136,43 +19,5 @@ export default {
</script>
<template>
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
<div data-testid="content">
<item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
<div>
<gl-loading-icon
v-if="$apollo.queries.workItemTypes.loading"
size="lg"
data-testid="loading-types"
/>
<gl-form-select
v-else
v-model="selectedWorkItemType"
:options="formOptions"
class="gl-max-w-26"
/>
</div>
</div>
<div class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4">
<gl-button
variant="confirm"
:disabled="isButtonDisabled"
class="gl-mr-3"
:loading="loading"
data-testid="create-button"
type="submit"
>
{{ s__('WorkItem|Create work item') }}
</gl-button>
<gl-button
type="button"
data-testid="cancel-button"
class="gl-order-n1"
@click="handleCancelClick"
>
{{ __('Cancel') }}
</gl-button>
</div>
</form>
<create-work-item @workItemCreated="workItemCreated" />
</template>

View File

@ -145,7 +145,7 @@ module BlobHelper
@dockerfile_names ||= TemplateFinder.all_template_names(project, :dockerfiles)
end
def blob_editor_paths(project)
def blob_editor_paths(project, method)
{
'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix,
@ -153,7 +153,8 @@ module BlobHelper
'project-id' => project.id,
'project-path': project.full_path,
'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path),
'preview-markdown-path' => preview_markdown_path(project)
'preview-markdown-path' => preview_markdown_path(project),
'form-method' => method
}
end

View File

@ -37,7 +37,7 @@
= _("Soft wrap")
.file-editor.code
.js-edit-mode-pane#editor{ data: { 'editor-loading': true, testid: 'source-editor-preview-container' } }<
.js-edit-mode-pane#editor{ data: { 'editor-loading': true, testid: 'source-editor-preview-container', ref: ref} }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide

View File

@ -26,7 +26,7 @@
= gl_tab_link_to editing_preview_title(@blob.name), '#preview', { data: { 'preview-url': project_preview_blob_path(@project, @id) } }
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths(@project)) do
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths(@project, 'put')) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha

View File

@ -5,7 +5,7 @@
%h1.page-title.blob-new-page-title.gl-font-size-h-display
= _('New file')
.file-editor
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project, 'post')) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"

View File

@ -0,0 +1,13 @@
---
table_name: audit_events_group_streaming_event_type_filters
classes:
- AuditEvents::Group::EventTypeFilter
feature_categories:
- audit_events
description: Stores audit event type filters for external audit event destinations
configurations of top-level groups.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141739
milestone: '16.10'
gitlab_schema: gitlab_main_cell
sharding_key:
namespace_id: namespaces

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class CreateAuditEventsGroupStreamingEventTypeFilters < Gitlab::Database::Migration[2.2]
milestone '16.10'
enable_lock_retries!
INDEX_NAME = 'uniq_audit_group_event_filters_destination_id_and_event_type'
NAMESPACE_INDEX_NAME = 'idx_audit_events_namespace_event_type_filters_on_group_id'
def change
create_table :audit_events_group_streaming_event_type_filters do |t|
t.timestamps_with_timezone null: false
t.references :external_streaming_destination,
null: false,
index: false,
foreign_key: { to_table: 'audit_events_group_external_streaming_destinations', on_delete: :cascade }
t.references :namespace, null: false,
index: { name: NAMESPACE_INDEX_NAME },
foreign_key: { on_delete: :cascade }
t.text :audit_event_type, null: false, limit: 255
t.index [:external_streaming_destination_id, :audit_event_type], unique: true, name: INDEX_NAME
end
end
end

View File

@ -0,0 +1 @@
c76310b9a392e5fe9af1e8fabe10d5ecc17f40bd053bbd5e1578091000263a2b

View File

@ -4697,6 +4697,25 @@ CREATE SEQUENCE audit_events_group_external_streaming_destinations_id_seq
ALTER SEQUENCE audit_events_group_external_streaming_destinations_id_seq OWNED BY audit_events_group_external_streaming_destinations.id;
CREATE TABLE audit_events_group_streaming_event_type_filters (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
external_streaming_destination_id bigint NOT NULL,
namespace_id bigint NOT NULL,
audit_event_type text NOT NULL,
CONSTRAINT check_389708af23 CHECK ((char_length(audit_event_type) <= 255))
);
CREATE SEQUENCE audit_events_group_streaming_event_type_filters_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE audit_events_group_streaming_event_type_filters_id_seq OWNED BY audit_events_group_streaming_event_type_filters.id;
CREATE SEQUENCE audit_events_id_seq
START WITH 1
INCREMENT BY 1
@ -18564,6 +18583,8 @@ ALTER TABLE ONLY audit_events_google_cloud_logging_configurations ALTER COLUMN i
ALTER TABLE ONLY audit_events_group_external_streaming_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_group_external_streaming_destinations_id_seq'::regclass);
ALTER TABLE ONLY audit_events_group_streaming_event_type_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_group_streaming_event_type_filters_id_seq'::regclass);
ALTER TABLE ONLY audit_events_instance_amazon_s3_configurations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_amazon_s3_configurations_id_seq'::regclass);
ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_external_audit_event_destinations_id_seq'::regclass);
@ -20338,6 +20359,9 @@ ALTER TABLE ONLY audit_events_google_cloud_logging_configurations
ALTER TABLE ONLY audit_events_group_external_streaming_destinations
ADD CONSTRAINT audit_events_group_external_streaming_destinations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY audit_events_group_streaming_event_type_filters
ADD CONSTRAINT audit_events_group_streaming_event_type_filters_pkey PRIMARY KEY (id);
ALTER TABLE ONLY audit_events_instance_amazon_s3_configurations
ADD CONSTRAINT audit_events_instance_amazon_s3_configurations_pkey PRIMARY KEY (id);
@ -23601,6 +23625,8 @@ CREATE INDEX idx_approval_project_rules_on_sec_orchestration_config_id ON approv
CREATE INDEX idx_audit_events_group_external_destinations_on_group_id ON audit_events_group_external_streaming_destinations USING btree (group_id);
CREATE INDEX idx_audit_events_namespace_event_type_filters_on_group_id ON audit_events_group_streaming_event_type_filters USING btree (namespace_id);
CREATE INDEX idx_audit_events_part_on_entity_id_desc_author_id_created_at ON ONLY audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
CREATE INDEX idx_award_emoji_on_user_emoji_name_awardable_type_awardable_id ON award_emoji USING btree (user_id, name, awardable_type, awardable_id);
@ -27637,6 +27663,8 @@ CREATE UNIQUE INDEX u_zoekt_indices_zoekt_enabled_namespace_id_and_zoekt_node_id
CREATE UNIQUE INDEX u_zoekt_repositories_zoekt_index_id_and_project_id ON zoekt_repositories USING btree (zoekt_index_id, project_id);
CREATE UNIQUE INDEX uniq_audit_group_event_filters_destination_id_and_event_type ON audit_events_group_streaming_event_type_filters USING btree (external_streaming_destination_id, audit_event_type);
CREATE UNIQUE INDEX uniq_google_cloud_logging_configuration_namespace_id_and_name ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, name);
CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14);
@ -32201,6 +32229,9 @@ ALTER TABLE ONLY bulk_import_export_uploads
ALTER TABLE ONLY vs_code_settings
ADD CONSTRAINT fk_rails_e02b1ed535 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY audit_events_group_streaming_event_type_filters
ADD CONSTRAINT fk_rails_e07e457a27 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY label_priorities
ADD CONSTRAINT fk_rails_e161058b0f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
@ -32303,6 +32334,9 @@ ALTER TABLE ONLY packages_debian_group_distributions
ALTER TABLE ONLY ci_daily_build_group_report_results
ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY audit_events_group_streaming_event_type_filters
ADD CONSTRAINT fk_rails_ee6950967f FOREIGN KEY (external_streaming_destination_id) REFERENCES audit_events_group_external_streaming_destinations(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_debian_group_architectures
ADD CONSTRAINT fk_rails_ef667d1b03 FOREIGN KEY (distribution_id) REFERENCES packages_debian_group_distributions(id) ON DELETE CASCADE;

View File

@ -9,6 +9,9 @@ module Gitlab
# Theme ID used when no `default_theme` configuration setting is provided.
APPLICATION_DEFAULT = 3
# Theme ID previously used for dark mode theme
DEPRECATED_DARK_THEME_ID = 11
# Struct class representing a single Theme
Theme = Struct.new(:id, :name, :css_class, :primary_color)
@ -80,7 +83,7 @@ module Gitlab
end
def self.valid_ids
available_themes.map(&:id)
available_themes.map(&:id) + [DEPRECATED_DARK_THEME_ID]
end
private

View File

@ -5110,6 +5110,9 @@ msgstr ""
msgid "An error occurred creating the new branch."
msgstr ""
msgid "An error occurred editing the blob"
msgstr ""
msgid "An error occurred fetching the approval rules."
msgstr ""
@ -5700,6 +5703,9 @@ msgstr ""
msgid "Analytics|Failed to fetch data"
msgstr ""
msgid "Analytics|Failed to load dashboard"
msgstr ""
msgid "Analytics|Generate with Duo"
msgstr ""
@ -5754,6 +5760,9 @@ msgstr ""
msgid "Analytics|Referer"
msgstr ""
msgid "Analytics|Refresh the page to try again or see %{linkStart}troubleshooting documentation%{linkEnd}."
msgstr ""
msgid "Analytics|Resulting Data"
msgstr ""
@ -56981,9 +56990,6 @@ msgstr ""
msgid "WorkItem|Create %{workItemType}"
msgstr ""
msgid "WorkItem|Create work item"
msgstr ""
msgid "WorkItem|Dates"
msgstr ""

View File

@ -124,19 +124,6 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
expect(page).to have_content('*.rbca')
end
it 'shows loader on commit changes' do
click_link('.gitignore')
edit_in_single_file_editor
# why: We don't want the form to actually submit, so that we can assert the button's changed state
page.execute_script("document.querySelector('.js-edit-blob-form').addEventListener('submit', e => e.preventDefault())")
find('.file-editor', match: :first)
editor_set_value('*.rbca')
click_button('Commit changes')
expect(page).to have_button('Commit changes', disabled: true, class: 'js-commit-button-loading')
end
it 'shows the diff of an edited file' do
click_link('.gitignore')
edit_in_single_file_editor

View File

@ -84,4 +84,23 @@ describe('BlobBundle', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
describe('commit button', () => {
const findCommitButton = () => document.querySelector('.js-commit-button');
const findCommitLoadingButton = () => document.querySelector('.js-commit-button-loading');
it('hides the commit button and displays the loading button when clicked', () => {
setHTMLFixture(
`<div class="js-edit-blob-form">
<button class="js-commit-button"></button>
<button class="js-commit-button-loading gl-display-none"></button>
</div>`,
);
blobBundle();
findCommitButton().click();
expect(findCommitButton().classList).toContain('gl-display-none');
expect(findCommitLoadingButton().classList).not.toContain('gl-display-none');
});
});
});

View File

@ -11,7 +11,13 @@ import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_edito
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import Api from '~/api';
jest.mock('~/api', () => ({ getRawFile: jest.fn().mockResolvedValue({ data: 'raw content' }) }));
jest.mock('~/editor/source_editor');
jest.mock('~/editor/extensions/source_editor_extension_base');
jest.mock('~/editor/extensions/source_editor_file_template_ext');
@ -19,6 +25,8 @@ jest.mock('~/editor/extensions/source_editor_markdown_ext');
jest.mock('~/editor/extensions/source_editor_markdown_livepreview_ext');
jest.mock('~/editor/extensions/source_editor_toolbar_ext');
jest.mock('~/editor/extensions/source_editor_security_policy_schema_ext');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
@ -37,6 +45,8 @@ const markdownExtensions = [
describe('Blob Editing', () => {
let blobInstance;
let mock;
const projectId = '123';
const filePath = 'path/to/file.js';
const useMock = jest.fn(() => markdownExtensions);
const unuseMock = jest.fn();
const emitter = new Emitter();
@ -49,6 +59,7 @@ describe('Blob Editing', () => {
onDidChangeModelLanguage: emitter.event,
updateModelLanguage: jest.fn(),
};
beforeEach(() => {
mock = new MockAdapter(axios);
setHTMLFixture(`
@ -56,7 +67,7 @@ describe('Blob Editing', () => {
<div class="js-edit-mode"><a href="#write">Write</a><a href="#preview">Preview</a></div>
<form class="js-edit-blob-form">
<div id="file_path"></div>
<div id="editor"></div>
<div id="editor" data-ref="main"></div>
<textarea id="file-content"></textarea>
</form>
`);
@ -73,8 +84,9 @@ describe('Blob Editing', () => {
blobInstance = new EditBlob({
isMarkdown,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
filePath: isSecurityPolicy ? '.gitlab/security-policies/policy.yml' : '',
filePath: isSecurityPolicy ? '.gitlab/security-policies/policy.yml' : filePath,
projectPath: 'path/to/project',
projectId,
});
return blobInstance;
};
@ -84,6 +96,21 @@ describe('Blob Editing', () => {
await waitForPromises();
};
describe('file content', () => {
beforeEach(() => initEditor());
it('requests raw file content', () => {
expect(Api.getRawFile).toHaveBeenCalledWith(projectId, filePath, { ref: 'main' });
});
it('creates an editor instance with the raw content', () => {
expect(SourceEditor.prototype.createInstance).toHaveBeenCalledWith(
expect.objectContaining({
blobContent: 'raw content',
}),
);
});
});
it('loads SourceEditorExtension and FileTemplateExtension by default', async () => {
await initEditor();
expect(useMock).toHaveBeenCalledWith(defaultExtensions);
@ -173,14 +200,56 @@ describe('Blob Editing', () => {
);
});
it('adds trailing newline to the blob content on submit', async () => {
const form = document.querySelector('.js-edit-blob-form');
const fileContentEl = document.getElementById('file-content');
describe('submit form', () => {
const findForm = () => document.querySelector('.js-edit-blob-form');
const content = 'some \r\n content \n';
const endpoint = `${TEST_HOST}/some/endpoint`;
await initEditor();
const setupSpec = async (method) => {
setHTMLFixture(`
<form class="js-edit-blob-form" data-form-method="${method}" action="${endpoint}">
<div id="file_path"></div>
<div id="editor"></div>
<button class="js-submit" type="submit">Submit</button>
</form>
`);
form.dispatchEvent(new Event('submit'));
await initEditor();
jest.spyOn(axios, method);
findForm().dispatchEvent(new Event('submit'));
await waitForPromises();
};
expect(fileContentEl.value).toBe('test value\n');
beforeEach(() => {
mockInstance.getValue = jest.fn().mockReturnValue(content);
});
it.each(['post', 'put'])(
'submits a "%s" request without mutating line endings',
async (method) => {
await setupSpec(method);
expect(axios[method]).toHaveBeenCalledWith(endpoint, { content });
},
);
it('redirects to the correct URL', async () => {
mock.onPost(endpoint).reply(HTTP_STATUS_OK, { filePath });
await setupSpec('post');
expect(visitUrl).toHaveBeenCalledWith(filePath);
});
it('creates an alert when an error occurs', async () => {
mock.onPost(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await setupSpec('post');
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: 'An error occurred editing the blob',
captureError: true,
}),
);
});
});
});

View File

@ -0,0 +1,58 @@
import { nextTick } from 'vue';
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
describe('CreateWorkItemModal', () => {
let wrapper;
const findTrigger = () => wrapper.find('[data-testid="new-epic-button"]');
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.findComponent(CreateWorkItem);
const createComponent = ({ workItemType } = {}) => {
wrapper = shallowMount(CreateWorkItemModal, {
propsData: {
workItemType,
},
});
};
it('passes workItemType to CreateWorkItem', () => {
createComponent({ workItemType: 'issue' });
expect(findForm().props('workItemType')).toBe('issue');
});
it('calls visitUrl on workItemCreated', () => {
createComponent();
findForm().vm.$emit('workItemCreated', { webUrl: '/' });
expect(visitUrl).toHaveBeenCalledWith('/');
});
it('opens modal on trigger click', async () => {
createComponent();
findTrigger().vm.$emit('click');
await nextTick();
expect(findModal().props('visible')).toBe(true);
});
it('closes modal on cancel event from form', () => {
createComponent();
findForm().vm.$emit('cancel');
expect(findModal().props('visible')).toBe(false);
});
});

View File

@ -0,0 +1,223 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
import WorkItemTitleWithEdit from '~/work_items/components/work_item_title_with_edit.vue';
import { WORK_ITEM_TYPE_ENUM_EPIC } from '~/work_items/constants';
import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import {
groupWorkItemTypesQueryResponse,
projectWorkItemTypesQueryResponse,
createWorkItemMutationResponse,
} from '../mock_data';
Vue.use(VueApollo);
describe('Create work item component', () => {
let wrapper;
let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const groupQuerySuccessHandler = jest.fn().mockResolvedValue(groupWorkItemTypesQueryResponse);
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(WorkItemTitleWithEdit);
const findSelect = () => wrapper.findComponent(GlFormSelect);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
const createComponent = ({
data = {},
props = {},
isGroup = false,
query = projectWorkItemTypesQuery,
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
} = {}) => {
fakeApollo = createMockApollo(
[
[query, queryHandler],
[createWorkItemMutation, mutationHandler],
],
{},
{ typePolicies: { Project: { merge: true } } },
);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
return {
...data,
};
},
propsData: {
...props,
},
provide: {
fullPath: 'full-path',
isGroup,
},
});
};
it('does not render error by default', () => {
createComponent();
expect(findAlert().exists()).toBe(false);
});
it('renders a disabled Create button when title input is empty', () => {
createComponent();
expect(findCreateButton().props('disabled')).toBe(true);
});
it('emits event on Cancel button click', () => {
createComponent();
findCancelButton().vm.$emit('click');
expect(wrapper.emitted('cancel')).toEqual([[]]);
});
it('emits workItemCreated on successful mutation', async () => {
createComponent();
findTitleInput().vm.$emit('updateDraft', 'Test title');
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(wrapper.emitted('workItemCreated')).toEqual([
[createWorkItemMutationResponse.data.workItemCreate.workItem],
]);
});
it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
expect(findLoadingTypesIcon().exists()).toBe(true);
});
it('displays an alert when work items query is rejected', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem') });
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain('fetching work item types');
});
it('displays a list of project work item types', async () => {
createComponent({
queryHandler: querySuccessHandler,
});
await waitForPromises();
expect(findSelect().attributes('options').split(',')).toHaveLength(6);
});
it('fetches group work item types when isGroup is true', async () => {
createComponent({
isGroup: true,
query: groupWorkItemTypesQuery,
queryHandler: groupQuerySuccessHandler,
});
await waitForPromises();
expect(groupQuerySuccessHandler).toHaveBeenCalled();
expect(findSelect().exists()).toBe(false);
});
it('hides the select field if there is only a single type', async () => {
createComponent({
queryHandler: groupQuerySuccessHandler,
});
await waitForPromises();
expect(findSelect().exists()).toBe(false);
});
it('filters types by workItemType', async () => {
createComponent({
props: {
workItemType: WORK_ITEM_TYPE_ENUM_EPIC,
},
});
await waitForPromises();
expect(querySuccessHandler).toHaveBeenCalledWith({
fullPath: 'full-path',
name: WORK_ITEM_TYPE_ENUM_EPIC,
});
});
it('selects a work item type on click', async () => {
createComponent();
await waitForPromises();
const mockId = 'work-item-1';
findSelect().vm.$emit('input', mockId);
await nextTick();
expect(findSelect().attributes('value')).toBe(mockId);
});
it('hides the alert on dismissing the error', async () => {
createComponent({ data: { error: true } });
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
it('displays an initial title if passed', () => {
const initialTitle = 'Initial Title';
createComponent({
props: { initialTitle },
});
expect(findTitleInput().props('title')).toBe(initialTitle);
});
describe('when title input field has a text', () => {
beforeEach(async () => {
const mockTitle = 'Test title';
createComponent();
await waitForPromises();
findTitleInput().vm.$emit('updateDraft', mockTitle);
});
it('renders a disabled Create button', () => {
expect(findCreateButton().props('disabled')).toBe(true);
});
it('renders a non-disabled Create button when work item type is selected', async () => {
findSelect().vm.$emit('input', 'work-item-1');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(false);
});
});
it('shows an alert on mutation error', async () => {
createComponent({ mutationHandler: errorHandler });
await waitForPromises();
findTitleInput().vm.$emit('updateDraft', 'some title');
findSelect().vm.$emit('input', 'work-item-1');
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().text()).toBe('Something went wrong when creating item. Please try again.');
});
});

View File

@ -1,203 +1,25 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
import CreateWorkItemPage from '~/work_items/pages/create_work_item.vue';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
describe('Create work item component', () => {
describe('Create work item page component', () => {
let wrapper;
let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle);
const findSelect = () => wrapper.findComponent(GlFormSelect);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findContent = () => wrapper.find('[data-testid="content"]');
const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
const createComponent = ({
data = {},
props = {},
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
} = {}) => {
fakeApollo = createMockApollo(
[
[projectWorkItemTypesQuery, queryHandler],
[createWorkItemMutation, mutationHandler],
],
{},
{ typePolicies: { Project: { merge: true } } },
);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
return {
...data,
};
},
propsData: {
...props,
},
mocks: {
$router: {
go: jest.fn(),
push: jest.fn(),
},
},
it('visits work item detail page after create', () => {
wrapper = shallowMount(CreateWorkItemPage, {
provide: {
fullPath: 'full-path',
isGroup: false,
fullPath: 'gitlab-org',
isGroup: true,
},
});
};
afterEach(() => {
fakeApollo = null;
});
wrapper
.findComponent(CreateWorkItem)
.vm.$emit('workItemCreated', { webUrl: '/work_items/1234' });
it('does not render error by default', () => {
createComponent();
expect(findAlert().exists()).toBe(false);
});
it('renders a disabled Create button when title input is empty', () => {
createComponent();
expect(findCreateButton().props('disabled')).toBe(true);
});
describe('when displayed on a separate route', () => {
beforeEach(() => {
createComponent();
});
it('redirects to the previous page on Cancel button click', () => {
findCancelButton().vm.$emit('click');
expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1);
});
it('redirects to the work item page on successful mutation', async () => {
findTitleInput().vm.$emit('title-input', 'Test title');
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
name: 'workItem',
params: { id: '1' },
});
});
it('adds right margin for create button', () => {
expect(findCreateButton().classes()).toContain('gl-mr-3');
});
it('does not add right margin for cancel button', () => {
expect(findCancelButton().classes()).not.toContain('gl-mr-3');
});
it('does not add padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(false);
});
});
it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
expect(findLoadingTypesIcon().exists()).toBe(true);
});
it('displays an alert when work items query is rejected', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem') });
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain('fetching work item types');
});
describe('when work item types are fetched', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('displays a list of work item types', () => {
expect(findSelect().attributes('options').split(',')).toHaveLength(6);
});
it('selects a work item type on click', async () => {
const mockId = 'work-item-1';
findSelect().vm.$emit('input', mockId);
await nextTick();
expect(findSelect().attributes('value')).toBe(mockId);
});
});
it('hides the alert on dismissing the error', async () => {
createComponent({ data: { error: true } });
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
it('displays an initial title if passed', () => {
const initialTitle = 'Initial Title';
createComponent({
props: { initialTitle },
});
expect(findTitleInput().props('title')).toBe(initialTitle);
});
describe('when title input field has a text', () => {
beforeEach(async () => {
const mockTitle = 'Test title';
createComponent();
await waitForPromises();
findTitleInput().vm.$emit('title-input', mockTitle);
});
it('renders a disabled Create button', () => {
expect(findCreateButton().props('disabled')).toBe(true);
});
it('renders a non-disabled Create button when work item type is selected', async () => {
findSelect().vm.$emit('input', 'work-item-1');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(false);
});
});
it('shows an alert on mutation error', async () => {
createComponent({ mutationHandler: errorHandler });
await waitForPromises();
findTitleInput().vm.$emit('title-input', 'some title');
findSelect().vm.$emit('input', 'work-item-1');
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().text()).toBe('Something went wrong when creating item. Please try again.');
expect(visitUrl).toHaveBeenCalledWith('/work_items/1234');
});
});

View File

@ -47,4 +47,10 @@ RSpec.describe Gitlab::Themes, lib: true do
expect(ids).not_to be_empty
end
end
describe '.valid_ids' do
it 'returns array of available_themes ids with DEPRECATED_DARK_THEME_ID' do
expect(described_class.valid_ids).to match_array [1, 6, 4, 7, 5, 8, 9, 10, 2, 3, 11]
end
end
end