Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b8ff7c8f92
commit
15e74f1fdf
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
c76310b9a392e5fe9af1e8fabe10d5ecc17f40bd053bbd5e1578091000263a2b
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.');
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue