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) {
|
if (editBlobForm.length) {
|
||||||
const urlRoot = editBlobForm.data('relativeUrlRoot');
|
const urlRoot = editBlobForm.data('relativeUrlRoot');
|
||||||
const assetsPath = editBlobForm.data('assetsPrefix');
|
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 currentAction = $('.js-file-title').data('currentAction');
|
||||||
const projectId = editBlobForm.data('project-id');
|
const projectId = editBlobForm.data('project-id');
|
||||||
const projectPath = editBlobForm.data('project-path');
|
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_EDITOR_ERROR = __('An error occurred while rendering the editor');
|
||||||
export const BLOB_PREVIEW_ERROR = __('An error occurred previewing the blob');
|
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 { createAlert } from '~/alert';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
|
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
|
||||||
import { insertFinalNewline } from '~/lib/utils/text_utility';
|
|
||||||
import FilepathFormMediator from '~/blob/filepath_form_mediator';
|
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 {
|
export default class EditBlob {
|
||||||
// The options object has:
|
// The options object has:
|
||||||
|
|
@ -20,18 +22,8 @@ export default class EditBlob {
|
||||||
this.isMarkdown = this.options.isMarkdown;
|
this.isMarkdown = this.options.isMarkdown;
|
||||||
this.markdownLivePreviewOpened = false;
|
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.initModePanesAndLinks();
|
||||||
this.initFilepathForm();
|
|
||||||
this.initSoftWrap();
|
this.initSoftWrap();
|
||||||
this.editor.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMarkdownExtension() {
|
async fetchMarkdownExtension() {
|
||||||
|
|
@ -72,16 +64,23 @@ export default class EditBlob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configureMonacoEditor() {
|
async configureMonacoEditor() {
|
||||||
const editorEl = document.getElementById('editor');
|
const editorEl = document.getElementById('editor');
|
||||||
const fileContentEl = document.getElementById('file-content');
|
|
||||||
const form = document.querySelector('.js-edit-blob-form');
|
const form = document.querySelector('.js-edit-blob-form');
|
||||||
|
|
||||||
const rootEditor = new SourceEditor();
|
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({
|
this.editor = rootEditor.createInstance({
|
||||||
el: editorEl,
|
el: editorEl,
|
||||||
blobContent: editorEl.innerText,
|
blobContent,
|
||||||
blobPath: this.options.filePath,
|
blobPath: this.options.filePath,
|
||||||
});
|
});
|
||||||
this.editor.use([
|
this.editor.use([
|
||||||
|
|
@ -90,8 +89,31 @@ export default class EditBlob {
|
||||||
{ definition: FileTemplateExtension },
|
{ definition: FileTemplateExtension },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
form.addEventListener('submit', () => {
|
if (this.isMarkdown) {
|
||||||
fileContentEl.value = insertFinalNewline(this.editor.getValue());
|
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
|
// 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.',
|
'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_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_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
|
||||||
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
|
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) {
|
workspace: group(fullPath: $fullPath) {
|
||||||
id
|
id
|
||||||
workItemTypes {
|
workItemTypes(name: $name) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
query projectWorkItemTypes($fullPath: ID!) {
|
query projectWorkItemTypes($fullPath: ID!, $name: IssueType) {
|
||||||
workspace: project(fullPath: $fullPath) {
|
workspace: project(fullPath: $fullPath) {
|
||||||
id
|
id
|
||||||
workItemTypes {
|
workItemTypes(name: $name) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
|
import { visitUrl } from '~/lib/utils/url_utility';
|
||||||
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
|
import CreateWorkItem from '../components/create_work_item.vue';
|
||||||
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';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'CreateWorkItemPage',
|
||||||
components: {
|
components: {
|
||||||
GlButton,
|
CreateWorkItem,
|
||||||
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);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async createWorkItem() {
|
workItemCreated(workItem) {
|
||||||
this.loading = true;
|
visitUrl(workItem.webUrl);
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
handleCancelClick() {
|
handleCancelClick() {
|
||||||
this.$router.go(-1);
|
this.$router.go(-1);
|
||||||
|
|
@ -136,43 +19,5 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="createWorkItem">
|
<create-work-item @workItemCreated="workItemCreated" />
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ module BlobHelper
|
||||||
@dockerfile_names ||= TemplateFinder.all_template_names(project, :dockerfiles)
|
@dockerfile_names ||= TemplateFinder.all_template_names(project, :dockerfiles)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blob_editor_paths(project)
|
def blob_editor_paths(project, method)
|
||||||
{
|
{
|
||||||
'relative-url-root' => Rails.application.config.relative_url_root,
|
'relative-url-root' => Rails.application.config.relative_url_root,
|
||||||
'assets-prefix' => Gitlab::Application.config.assets.prefix,
|
'assets-prefix' => Gitlab::Application.config.assets.prefix,
|
||||||
|
|
@ -153,7 +153,8 @@ module BlobHelper
|
||||||
'project-id' => project.id,
|
'project-id' => project.id,
|
||||||
'project-path': project.full_path,
|
'project-path': project.full_path,
|
||||||
'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.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
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
= _("Soft wrap")
|
= _("Soft wrap")
|
||||||
|
|
||||||
.file-editor.code
|
.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]
|
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
|
||||||
- if local_assigns[:path]
|
- if local_assigns[:path]
|
||||||
.js-edit-mode-pane#preview.hide
|
.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) } }
|
= 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 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
|
||||||
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
|
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
|
||||||
= hidden_field_tag 'last_commit_sha', @last_commit_sha
|
= 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
|
%h1.page-title.blob-new-page-title.gl-font-size-h-display
|
||||||
= _('New file')
|
= _('New file')
|
||||||
.file-editor
|
.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 'projects/blob/editor', ref: @ref
|
||||||
= render 'shared/new_commit_form', placeholder: "Add new file"
|
= 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;
|
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
|
CREATE SEQUENCE audit_events_id_seq
|
||||||
START WITH 1
|
START WITH 1
|
||||||
INCREMENT BY 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_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_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);
|
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
|
ALTER TABLE ONLY audit_events_group_external_streaming_destinations
|
||||||
ADD CONSTRAINT audit_events_group_external_streaming_destinations_pkey PRIMARY KEY (id);
|
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
|
ALTER TABLE ONLY audit_events_instance_amazon_s3_configurations
|
||||||
ADD CONSTRAINT audit_events_instance_amazon_s3_configurations_pkey PRIMARY KEY (id);
|
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_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_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);
|
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 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_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);
|
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
|
ALTER TABLE ONLY vs_code_settings
|
||||||
ADD CONSTRAINT fk_rails_e02b1ed535 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY label_priorities
|
||||||
ADD CONSTRAINT fk_rails_e161058b0f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
|
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
|
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;
|
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
|
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;
|
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.
|
# Theme ID used when no `default_theme` configuration setting is provided.
|
||||||
APPLICATION_DEFAULT = 3
|
APPLICATION_DEFAULT = 3
|
||||||
|
|
||||||
|
# Theme ID previously used for dark mode theme
|
||||||
|
DEPRECATED_DARK_THEME_ID = 11
|
||||||
|
|
||||||
# Struct class representing a single Theme
|
# Struct class representing a single Theme
|
||||||
Theme = Struct.new(:id, :name, :css_class, :primary_color)
|
Theme = Struct.new(:id, :name, :css_class, :primary_color)
|
||||||
|
|
||||||
|
|
@ -80,7 +83,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.valid_ids
|
def self.valid_ids
|
||||||
available_themes.map(&:id)
|
available_themes.map(&:id) + [DEPRECATED_DARK_THEME_ID]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -5110,6 +5110,9 @@ msgstr ""
|
||||||
msgid "An error occurred creating the new branch."
|
msgid "An error occurred creating the new branch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occurred editing the blob"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred fetching the approval rules."
|
msgid "An error occurred fetching the approval rules."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -5700,6 +5703,9 @@ msgstr ""
|
||||||
msgid "Analytics|Failed to fetch data"
|
msgid "Analytics|Failed to fetch data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Analytics|Failed to load dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Generate with Duo"
|
msgid "Analytics|Generate with Duo"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -5754,6 +5760,9 @@ msgstr ""
|
||||||
msgid "Analytics|Referer"
|
msgid "Analytics|Referer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Analytics|Refresh the page to try again or see %{linkStart}troubleshooting documentation%{linkEnd}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Resulting Data"
|
msgid "Analytics|Resulting Data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -56981,9 +56990,6 @@ msgstr ""
|
||||||
msgid "WorkItem|Create %{workItemType}"
|
msgid "WorkItem|Create %{workItemType}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "WorkItem|Create work item"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "WorkItem|Dates"
|
msgid "WorkItem|Dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,19 +124,6 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
||||||
expect(page).to have_content('*.rbca')
|
expect(page).to have_content('*.rbca')
|
||||||
end
|
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
|
it 'shows the diff of an edited file' do
|
||||||
click_link('.gitignore')
|
click_link('.gitignore')
|
||||||
edit_in_single_file_editor
|
edit_in_single_file_editor
|
||||||
|
|
|
||||||
|
|
@ -84,4 +84,23 @@ describe('BlobBundle', () => {
|
||||||
expect(createAlert).toHaveBeenCalledWith({ message });
|
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 { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
|
||||||
import SourceEditor from '~/editor/source_editor';
|
import SourceEditor from '~/editor/source_editor';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
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/source_editor');
|
||||||
jest.mock('~/editor/extensions/source_editor_extension_base');
|
jest.mock('~/editor/extensions/source_editor_extension_base');
|
||||||
jest.mock('~/editor/extensions/source_editor_file_template_ext');
|
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_markdown_livepreview_ext');
|
||||||
jest.mock('~/editor/extensions/source_editor_toolbar_ext');
|
jest.mock('~/editor/extensions/source_editor_toolbar_ext');
|
||||||
jest.mock('~/editor/extensions/source_editor_security_policy_schema_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 PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
|
||||||
const defaultExtensions = [
|
const defaultExtensions = [
|
||||||
|
|
@ -37,6 +45,8 @@ const markdownExtensions = [
|
||||||
describe('Blob Editing', () => {
|
describe('Blob Editing', () => {
|
||||||
let blobInstance;
|
let blobInstance;
|
||||||
let mock;
|
let mock;
|
||||||
|
const projectId = '123';
|
||||||
|
const filePath = 'path/to/file.js';
|
||||||
const useMock = jest.fn(() => markdownExtensions);
|
const useMock = jest.fn(() => markdownExtensions);
|
||||||
const unuseMock = jest.fn();
|
const unuseMock = jest.fn();
|
||||||
const emitter = new Emitter();
|
const emitter = new Emitter();
|
||||||
|
|
@ -49,6 +59,7 @@ describe('Blob Editing', () => {
|
||||||
onDidChangeModelLanguage: emitter.event,
|
onDidChangeModelLanguage: emitter.event,
|
||||||
updateModelLanguage: jest.fn(),
|
updateModelLanguage: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock = new MockAdapter(axios);
|
mock = new MockAdapter(axios);
|
||||||
setHTMLFixture(`
|
setHTMLFixture(`
|
||||||
|
|
@ -56,7 +67,7 @@ describe('Blob Editing', () => {
|
||||||
<div class="js-edit-mode"><a href="#write">Write</a><a href="#preview">Preview</a></div>
|
<div class="js-edit-mode"><a href="#write">Write</a><a href="#preview">Preview</a></div>
|
||||||
<form class="js-edit-blob-form">
|
<form class="js-edit-blob-form">
|
||||||
<div id="file_path"></div>
|
<div id="file_path"></div>
|
||||||
<div id="editor"></div>
|
<div id="editor" data-ref="main"></div>
|
||||||
<textarea id="file-content"></textarea>
|
<textarea id="file-content"></textarea>
|
||||||
</form>
|
</form>
|
||||||
`);
|
`);
|
||||||
|
|
@ -73,8 +84,9 @@ describe('Blob Editing', () => {
|
||||||
blobInstance = new EditBlob({
|
blobInstance = new EditBlob({
|
||||||
isMarkdown,
|
isMarkdown,
|
||||||
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
|
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
|
||||||
filePath: isSecurityPolicy ? '.gitlab/security-policies/policy.yml' : '',
|
filePath: isSecurityPolicy ? '.gitlab/security-policies/policy.yml' : filePath,
|
||||||
projectPath: 'path/to/project',
|
projectPath: 'path/to/project',
|
||||||
|
projectId,
|
||||||
});
|
});
|
||||||
return blobInstance;
|
return blobInstance;
|
||||||
};
|
};
|
||||||
|
|
@ -84,6 +96,21 @@ describe('Blob Editing', () => {
|
||||||
await waitForPromises();
|
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 () => {
|
it('loads SourceEditorExtension and FileTemplateExtension by default', async () => {
|
||||||
await initEditor();
|
await initEditor();
|
||||||
expect(useMock).toHaveBeenCalledWith(defaultExtensions);
|
expect(useMock).toHaveBeenCalledWith(defaultExtensions);
|
||||||
|
|
@ -173,14 +200,56 @@ describe('Blob Editing', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds trailing newline to the blob content on submit', async () => {
|
describe('submit form', () => {
|
||||||
const form = document.querySelector('.js-edit-blob-form');
|
const findForm = () => document.querySelector('.js-edit-blob-form');
|
||||||
const fileContentEl = document.getElementById('file-content');
|
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 { shallowMount } from '@vue/test-utils';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import CreateWorkItemPage from '~/work_items/pages/create_work_item.vue';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
|
||||||
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
|
import { visitUrl } from '~/lib/utils/url_utility';
|
||||||
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';
|
|
||||||
|
|
||||||
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
|
jest.mock('~/lib/utils/url_utility');
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
describe('Create work item page component', () => {
|
||||||
|
|
||||||
describe('Create work item component', () => {
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let fakeApollo;
|
|
||||||
|
|
||||||
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
|
it('visits work item detail page after create', () => {
|
||||||
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
|
wrapper = shallowMount(CreateWorkItemPage, {
|
||||||
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(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provide: {
|
provide: {
|
||||||
fullPath: 'full-path',
|
fullPath: 'gitlab-org',
|
||||||
isGroup: false,
|
isGroup: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
wrapper
|
||||||
fakeApollo = null;
|
.findComponent(CreateWorkItem)
|
||||||
});
|
.vm.$emit('workItemCreated', { webUrl: '/work_items/1234' });
|
||||||
|
|
||||||
it('does not render error by default', () => {
|
expect(visitUrl).toHaveBeenCalledWith('/work_items/1234');
|
||||||
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.');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,10 @@ RSpec.describe Gitlab::Themes, lib: true do
|
||||||
expect(ids).not_to be_empty
|
expect(ids).not_to be_empty
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue