Merge branch 'issue-edit-inline' into issue-edit-inline-description-template
[ci skip]
This commit is contained in:
commit
7a21e26f4b
15 changed files with 505 additions and 24 deletions
|
@ -8,6 +8,7 @@ import Store from '../stores';
|
||||||
import titleComponent from './title.vue';
|
import titleComponent from './title.vue';
|
||||||
import descriptionComponent from './description.vue';
|
import descriptionComponent from './description.vue';
|
||||||
import formComponent from './form.vue';
|
import formComponent from './form.vue';
|
||||||
|
import '../../lib/utils/url_utility';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -15,6 +16,10 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
canMove: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
canUpdate: {
|
canUpdate: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -66,6 +71,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectsAutocompleteUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
|
@ -92,23 +101,27 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openForm() {
|
openForm() {
|
||||||
this.showForm = true;
|
if (!this.showForm) {
|
||||||
this.store.formState = {
|
this.showForm = true;
|
||||||
title: this.state.titleText,
|
this.store.formState = {
|
||||||
confidential: this.isConfidential,
|
title: this.state.titleText,
|
||||||
description: this.state.descriptionText,
|
confidential: this.isConfidential,
|
||||||
};
|
description: this.state.descriptionText,
|
||||||
|
move_to_project_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
closeForm() {
|
closeForm() {
|
||||||
this.showForm = false;
|
this.showForm = false;
|
||||||
},
|
},
|
||||||
updateIssuable() {
|
updateIssuable() {
|
||||||
this.service.updateIssuable(this.store.formState)
|
this.service.updateIssuable(this.store.formState)
|
||||||
.then((res) => {
|
.then(res => res.json())
|
||||||
const data = res.json();
|
.then((data) => {
|
||||||
|
if (location.pathname !== data.path) {
|
||||||
if (data.confidential !== this.isConfidential) {
|
gl.utils.visitUrl(data.path);
|
||||||
location.reload();
|
} else if (data.confidential !== this.isConfidential) {
|
||||||
|
gl.utils.visitUrl(location.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventHub.$emit('close.form');
|
eventHub.$emit('close.form');
|
||||||
|
@ -177,12 +190,15 @@ export default {
|
||||||
<form-component
|
<form-component
|
||||||
v-if="canUpdate && showForm"
|
v-if="canUpdate && showForm"
|
||||||
:form-state="formState"
|
:form-state="formState"
|
||||||
|
:can-move="canMove"
|
||||||
:can-destroy="canDestroy"
|
:can-destroy="canDestroy"
|
||||||
:issuable-templates="issuableTemplates"
|
:issuable-templates="issuableTemplates"
|
||||||
:markdown-docs="markdownDocs"
|
:markdown-docs="markdownDocs"
|
||||||
:markdown-preview-url="markdownPreviewUrl"
|
:markdown-preview-url="markdownPreviewUrl"
|
||||||
:project-path="projectPath"
|
:project-path="projectPath"
|
||||||
:project-namespace="projectNamespace" />
|
:project-namespace="projectNamespace"
|
||||||
|
:projects-autocomplete-url="projectsAutocompleteUrl"
|
||||||
|
/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<title-component
|
<title-component
|
||||||
:issuable-ref="issuableRef"
|
:issuable-ref="issuableRef"
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
components: {
|
components: {
|
||||||
markdownField,
|
markdownField,
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.textarea.focus();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -39,7 +42,7 @@
|
||||||
data-supports-slash-commands="false"
|
data-supports-slash-commands="false"
|
||||||
aria-label="Description"
|
aria-label="Description"
|
||||||
v-model="formState.description"
|
v-model="formState.description"
|
||||||
ref="textatea"
|
ref="textarea"
|
||||||
slot="textarea">
|
slot="textarea">
|
||||||
</textarea>
|
</textarea>
|
||||||
</markdown-field>
|
</markdown-field>
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script>
|
||||||
|
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [
|
||||||
|
tooltipMixin,
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
formState: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
projectsAutocompleteUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const $moveDropdown = $(this.$refs['move-dropdown']);
|
||||||
|
|
||||||
|
$moveDropdown.select2({
|
||||||
|
ajax: {
|
||||||
|
url: this.projectsAutocompleteUrl,
|
||||||
|
quietMillis: 125,
|
||||||
|
data(term, page, context) {
|
||||||
|
return {
|
||||||
|
search: term,
|
||||||
|
offset_id: context,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
results(data) {
|
||||||
|
const more = data.length >= 50;
|
||||||
|
const context = data[data.length - 1] ? data[data.length - 1].id : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: data,
|
||||||
|
more,
|
||||||
|
context,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formatResult(project) {
|
||||||
|
return project.name_with_namespace;
|
||||||
|
},
|
||||||
|
formatSelection(project) {
|
||||||
|
return project.name_with_namespace;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.on('change', (e) => {
|
||||||
|
this.formState.move_to_project_id = parseInt(e.target.value, 10);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
$(this.$refs['move-dropdown']).select2('destroy');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<fieldset>
|
||||||
|
<label
|
||||||
|
for="issuable-move"
|
||||||
|
class="sr-only">
|
||||||
|
Move
|
||||||
|
</label>
|
||||||
|
<div class="issuable-form-select-holder append-right-5">
|
||||||
|
<input
|
||||||
|
ref="move-dropdown"
|
||||||
|
type="hidden"
|
||||||
|
id="issuable-move"
|
||||||
|
data-placeholder="Move to a different project" />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
data-placement="auto top"
|
||||||
|
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
|
||||||
|
ref="tooltip">
|
||||||
|
<i
|
||||||
|
class="fa fa-question-circle"
|
||||||
|
aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
|
@ -2,11 +2,19 @@
|
||||||
import titleField from './fields/title.vue';
|
import titleField from './fields/title.vue';
|
||||||
import descriptionField from './fields/description.vue';
|
import descriptionField from './fields/description.vue';
|
||||||
import editActions from './edit_actions.vue';
|
import editActions from './edit_actions.vue';
|
||||||
|
<<<<<<< HEAD
|
||||||
import descriptionTemplate from './fields/description_template.vue';
|
import descriptionTemplate from './fields/description_template.vue';
|
||||||
|
=======
|
||||||
|
import projectMove from './fields/project_move.vue';
|
||||||
|
>>>>>>> issue-edit-inline
|
||||||
import confidentialCheckbox from './fields/confidential_checkbox.vue';
|
import confidentialCheckbox from './fields/confidential_checkbox.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
canMove: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
canDestroy: {
|
canDestroy: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -36,12 +44,17 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectsAutocompleteUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
titleField,
|
titleField,
|
||||||
descriptionField,
|
descriptionField,
|
||||||
descriptionTemplate,
|
descriptionTemplate,
|
||||||
editActions,
|
editActions,
|
||||||
|
projectMove,
|
||||||
confidentialCheckbox,
|
confidentialCheckbox,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -80,6 +93,10 @@
|
||||||
:markdown-docs="markdownDocs" />
|
:markdown-docs="markdownDocs" />
|
||||||
<confidential-checkbox
|
<confidential-checkbox
|
||||||
:form-state="formState" />
|
:form-state="formState" />
|
||||||
|
<project-move
|
||||||
|
v-if="canMove"
|
||||||
|
:form-state="formState"
|
||||||
|
:projects-autocomplete-url="projectsAutocompleteUrl" />
|
||||||
<edit-actions
|
<edit-actions
|
||||||
:form-state="formState"
|
:form-state="formState"
|
||||||
:can-destroy="canDestroy" />
|
:can-destroy="canDestroy" />
|
||||||
|
|
|
@ -25,16 +25,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const {
|
const {
|
||||||
canUpdate,
|
canUpdate,
|
||||||
canDestroy,
|
canDestroy,
|
||||||
|
canMove,
|
||||||
endpoint,
|
endpoint,
|
||||||
issuableRef,
|
issuableRef,
|
||||||
isConfidential,
|
isConfidential,
|
||||||
markdownPreviewUrl,
|
markdownPreviewUrl,
|
||||||
markdownDocs,
|
markdownDocs,
|
||||||
|
projectsAutocompleteUrl,
|
||||||
} = issuableElement.dataset;
|
} = issuableElement.dataset;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
|
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
|
||||||
canDestroy: gl.utils.convertPermissionToBoolean(canDestroy),
|
canDestroy: gl.utils.convertPermissionToBoolean(canDestroy),
|
||||||
|
canMove: gl.utils.convertPermissionToBoolean(canMove),
|
||||||
endpoint,
|
endpoint,
|
||||||
issuableRef,
|
issuableRef,
|
||||||
initialTitle: issuableTitleElement.innerHTML,
|
initialTitle: issuableTitleElement.innerHTML,
|
||||||
|
@ -45,6 +48,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
markdownDocs,
|
markdownDocs,
|
||||||
projectPath: initialData.project_path,
|
projectPath: initialData.project_path,
|
||||||
projectNamespace: initialData.namespace_path,
|
projectNamespace: initialData.namespace_path,
|
||||||
|
projectsAutocompleteUrl,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
|
@ -52,6 +56,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
props: {
|
props: {
|
||||||
canUpdate: this.canUpdate,
|
canUpdate: this.canUpdate,
|
||||||
canDestroy: this.canDestroy,
|
canDestroy: this.canDestroy,
|
||||||
|
canMove: this.canMove,
|
||||||
endpoint: this.endpoint,
|
endpoint: this.endpoint,
|
||||||
issuableRef: this.issuableRef,
|
issuableRef: this.issuableRef,
|
||||||
initialTitle: this.initialTitle,
|
initialTitle: this.initialTitle,
|
||||||
|
@ -63,6 +68,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
markdownDocs: this.markdownDocs,
|
markdownDocs: this.markdownDocs,
|
||||||
projectPath: this.projectPath,
|
projectPath: this.projectPath,
|
||||||
projectNamespace: this.projectNamespace,
|
projectNamespace: this.projectNamespace,
|
||||||
|
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default class Service {
|
||||||
constructor(endpoint) {
|
constructor(endpoint) {
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
|
|
||||||
this.resource = Vue.resource(this.endpoint, {}, {
|
this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
|
||||||
realtimeChanges: {
|
realtimeChanges: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${this.endpoint}/realtime_changes`,
|
url: `${this.endpoint}/realtime_changes`,
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default class Store {
|
||||||
title: '',
|
title: '',
|
||||||
confidential: false,
|
confidential: false,
|
||||||
description: '',
|
description: '',
|
||||||
|
move_to_project_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
if @issue.valid?
|
if @issue.valid?
|
||||||
render json: @issue.to_json(methods: [:task_status, :task_status_short],
|
render json: IssueSerializer.new.represent(@issue)
|
||||||
include: { milestone: {},
|
|
||||||
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
|
|
||||||
labels: { methods: :text_color } })
|
|
||||||
else
|
else
|
||||||
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
class IssueEntity < IssuableEntity
|
class IssueEntity < IssuableEntity
|
||||||
|
include RequestAwareEntity
|
||||||
|
|
||||||
expose :branch_name
|
expose :branch_name
|
||||||
expose :confidential
|
expose :confidential
|
||||||
expose :assignees, using: API::Entities::UserBasic
|
expose :assignees, using: API::Entities::UserBasic
|
||||||
|
@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
|
||||||
expose :project_id
|
expose :project_id
|
||||||
expose :milestone, using: API::Entities::Milestone
|
expose :milestone, using: API::Entities::Milestone
|
||||||
expose :labels, using: LabelEntity
|
expose :labels, using: LabelEntity
|
||||||
|
|
||||||
|
expose :path do |issue|
|
||||||
|
namespace_project_issue_path(issue.project.namespace, issue.project, issue)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,10 +55,12 @@
|
||||||
#js-issuable-app{ "data" => { "endpoint" => namespace_project_issue_path(@project.namespace, @project, @issue),
|
#js-issuable-app{ "data" => { "endpoint" => namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||||
"can-update" => can?(current_user, :update_issue, @issue).to_s,
|
"can-update" => can?(current_user, :update_issue, @issue).to_s,
|
||||||
"can-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
|
"can-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
|
||||||
|
"can-move" => @issue.can_move?(current_user).to_s,
|
||||||
"issuable-ref" => @issue.to_reference,
|
"issuable-ref" => @issue.to_reference,
|
||||||
"is-confidential" => @issue.confidential.to_s,
|
"is-confidential" => @issue.confidential.to_s,
|
||||||
"markdown-preview-url" => preview_markdown_path(@project),
|
"markdown-preview-url" => preview_markdown_path(@project),
|
||||||
"markdown-docs" => help_page_path('user/markdown'),
|
"markdown-docs" => help_page_path('user/markdown'),
|
||||||
|
"projects-autocomplete-url" => autocomplete_projects_path(project_id: @project.id),
|
||||||
} }
|
} }
|
||||||
%h2.title= markdown_field(@issue, :title)
|
%h2.title= markdown_field(@issue, :title)
|
||||||
- if @issue.description.present?
|
- if @issue.description.present?
|
||||||
|
|
|
@ -29,12 +29,15 @@ describe('Issuable output', () => {
|
||||||
propsData: {
|
propsData: {
|
||||||
canUpdate: true,
|
canUpdate: true,
|
||||||
canDestroy: true,
|
canDestroy: true,
|
||||||
|
canMove: true,
|
||||||
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
|
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
|
||||||
issuableRef: '#1',
|
issuableRef: '#1',
|
||||||
initialTitle: '',
|
initialTitle: '',
|
||||||
initialDescriptionHtml: '',
|
initialDescriptionHtml: '',
|
||||||
initialDescriptionText: '',
|
initialDescriptionText: '',
|
||||||
showForm: false,
|
markdownPreviewUrl: '/',
|
||||||
|
markdownDocs: '/',
|
||||||
|
projectsAutocompleteUrl: '/',
|
||||||
isConfidential: false,
|
isConfidential: false,
|
||||||
},
|
},
|
||||||
}).$mount();
|
}).$mount();
|
||||||
|
@ -89,7 +92,47 @@ describe('Issuable output', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not update formState if form is already open', (done) => {
|
||||||
|
vm.openForm();
|
||||||
|
|
||||||
|
vm.state.titleText = 'testing 123';
|
||||||
|
|
||||||
|
vm.openForm();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
vm.store.formState.title,
|
||||||
|
).not.toBe('testing 123');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateIssuable', () => {
|
describe('updateIssuable', () => {
|
||||||
|
it('reloads the page if the confidential status has changed', (done) => {
|
||||||
|
spyOn(gl.utils, 'visitUrl');
|
||||||
|
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
json() {
|
||||||
|
return {
|
||||||
|
confidential: true,
|
||||||
|
path: location.pathname,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
vm.updateIssuable();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(
|
||||||
|
gl.utils.visitUrl,
|
||||||
|
).toHaveBeenCalledWith(location.pathname);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('correctly updates issuable data', (done) => {
|
it('correctly updates issuable data', (done) => {
|
||||||
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -109,13 +152,14 @@ describe('Issuable output', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reloads the page if the confidential status has changed', (done) => {
|
it('does not redirect if issue has not moved', (done) => {
|
||||||
spyOn(window.location, 'reload');
|
spyOn(gl.utils, 'visitUrl');
|
||||||
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
||||||
resolve({
|
resolve({
|
||||||
json() {
|
json() {
|
||||||
return {
|
return {
|
||||||
confidential: true,
|
path: location.pathname,
|
||||||
|
confidential: vm.isConfidential,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -125,8 +169,32 @@ describe('Issuable output', () => {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(
|
expect(
|
||||||
window.location.reload,
|
gl.utils.visitUrl,
|
||||||
).toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects if issue is moved', (done) => {
|
||||||
|
spyOn(gl.utils, 'visitUrl');
|
||||||
|
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
json() {
|
||||||
|
return {
|
||||||
|
path: '/testing-issue-move',
|
||||||
|
confidential: vm.isConfidential,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
vm.updateIssuable();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(
|
||||||
|
gl.utils.visitUrl,
|
||||||
|
).toHaveBeenCalledWith('/testing-issue-move');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Store from '~/issue_show/stores';
|
||||||
|
import descriptionField from '~/issue_show/components/fields/description.vue';
|
||||||
|
|
||||||
|
describe('Description field component', () => {
|
||||||
|
let vm;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
const Component = Vue.extend(descriptionField);
|
||||||
|
const el = document.createElement('div');
|
||||||
|
store = new Store({
|
||||||
|
titleHtml: '',
|
||||||
|
descriptionHtml: '',
|
||||||
|
issuableRef: '',
|
||||||
|
});
|
||||||
|
store.formState.description = 'test';
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
vm = new Component({
|
||||||
|
el,
|
||||||
|
propsData: {
|
||||||
|
markdownPreviewUrl: '/',
|
||||||
|
markdownDocs: '/',
|
||||||
|
formState: store.formState,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders markdown field with description', () => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.md-area textarea').value,
|
||||||
|
).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders markdown field with a markdown description', (done) => {
|
||||||
|
store.formState.description = '**test**';
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.md-area textarea').value,
|
||||||
|
).toBe('**test**');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses field when mounted', () => {
|
||||||
|
expect(
|
||||||
|
document.activeElement,
|
||||||
|
).toBe(vm.$refs.textarea);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import projectMove from '~/issue_show/components/fields/project_move.vue';
|
||||||
|
|
||||||
|
describe('Project move field component', () => {
|
||||||
|
let vm;
|
||||||
|
let formState;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
const Component = Vue.extend(projectMove);
|
||||||
|
|
||||||
|
formState = {
|
||||||
|
move_to_project_id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm = new Component({
|
||||||
|
propsData: {
|
||||||
|
formState,
|
||||||
|
projectsAutocompleteUrl: '/autocomplete',
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mounts select2 element', () => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.select2-container'),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates formState on change', () => {
|
||||||
|
$(vm.$refs['move-dropdown']).val(2).trigger('change');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
formState.move_to_project_id,
|
||||||
|
).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
121
spec/javascripts/vue_shared/components/markdown/field_spec.js
Normal file
121
spec/javascripts/vue_shared/components/markdown/field_spec.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
|
||||||
|
|
||||||
|
describe('Markdown field component', () => {
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vm = new Vue({
|
||||||
|
render(createElement) {
|
||||||
|
return createElement(
|
||||||
|
fieldComponent,
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
markdownPreviewUrl: '/preview',
|
||||||
|
markdownDocs: '/docs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createElement('textarea', {
|
||||||
|
slot: 'textarea',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a new instance of GL form', (done) => {
|
||||||
|
spyOn(gl, 'GLForm');
|
||||||
|
vm.$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
gl.GLForm,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mounted', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
vm.$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders textarea inside backdrop', () => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.zen-backdrop textarea'),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markdown preview', () => {
|
||||||
|
let previewLink;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
json() {
|
||||||
|
return {
|
||||||
|
body: '<p>markdown preview</p>',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets preview link as active', (done) => {
|
||||||
|
previewLink.click();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
previewLink.parentNode.classList.contains('active'),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows preview loading text', (done) => {
|
||||||
|
previewLink.click();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.md-preview').textContent.trim(),
|
||||||
|
).toContain('Loading...');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders markdown preview', (done) => {
|
||||||
|
previewLink.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('.md-preview').innerHTML,
|
||||||
|
).toContain('<p>markdown preview</p>');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders GFM with jQuery', (done) => {
|
||||||
|
spyOn($.fn, 'renderGFM');
|
||||||
|
previewLink.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(
|
||||||
|
$.fn.renderGFM,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import headerComponent from '~/vue_shared/components/markdown/header.vue';
|
||||||
|
|
||||||
|
describe('Markdown field header component', () => {
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
const Component = Vue.extend(headerComponent);
|
||||||
|
|
||||||
|
vm = new Component({
|
||||||
|
propsData: {
|
||||||
|
previewMarkdown: false,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders markdown buttons', () => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelectorAll('.js-md').length,
|
||||||
|
).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `write` link as active when previewMarkdown is false', () => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('li:nth-child(1)').classList.contains('active'),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `preview` link as active when previewMarkdown is true', (done) => {
|
||||||
|
vm.previewMarkdown = true;
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
vm.$el.querySelector('li:nth-child(2)').classList.contains('active'),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits toggle markdown event when clicking preview', () => {
|
||||||
|
spyOn(vm, '$emit');
|
||||||
|
|
||||||
|
vm.$el.querySelector('li:nth-child(2) a').click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
vm.$emit,
|
||||||
|
).toHaveBeenCalledWith('toggle-markdown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blurs preview link after click', (done) => {
|
||||||
|
const link = vm.$el.querySelector('li:nth-child(2) a');
|
||||||
|
spyOn(HTMLElement.prototype, 'blur');
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(
|
||||||
|
link.blur,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue