Merge branch 'issue-edit-inline-description-template' into 'issue-edit-inline'
Issue edit inline description template See merge request !11382
This commit is contained in:
commit
d853d82114
8 changed files with 260 additions and 5 deletions
|
@ -46,6 +46,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
issuableTemplates: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
isConfidential: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -58,6 +63,14 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectsAutocompleteUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -186,9 +199,13 @@ export default {
|
|||
:form-state="formState"
|
||||
:can-move="canMove"
|
||||
:can-destroy="canDestroy"
|
||||
:issuable-templates="issuableTemplates"
|
||||
:markdown-docs="markdownDocs"
|
||||
:markdown-preview-url="markdownPreviewUrl"
|
||||
:projects-autocomplete-url="projectsAutocompleteUrl" />
|
||||
:project-path="projectPath"
|
||||
:project-namespace="projectNamespace"
|
||||
:projects-autocomplete-url="projectsAutocompleteUrl"
|
||||
/>
|
||||
<div v-else>
|
||||
<title-component
|
||||
:issuable-ref="issuableRef"
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
formState: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issuableTemplates: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
issuableTemplatesJson() {
|
||||
return JSON.stringify(this.issuableTemplates);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Create the editor for the template
|
||||
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
|
||||
editor.setValue = (val) => {
|
||||
this.formState.description = val;
|
||||
};
|
||||
editor.getValue = () => this.formState.description;
|
||||
|
||||
this.issuableTemplate = new gl.IssuableTemplateSelectors({
|
||||
$dropdowns: $(this.$refs.toggle),
|
||||
editor,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dropdown js-issuable-selector-wrap"
|
||||
data-issuable-type="issue">
|
||||
<button
|
||||
class="dropdown-menu-toggle js-issuable-selector"
|
||||
type="button"
|
||||
ref="toggle"
|
||||
data-field-name="issuable_template"
|
||||
data-selected="null"
|
||||
data-toggle="dropdown"
|
||||
:data-namespace-path="projectNamespace"
|
||||
:data-project-path="projectPath"
|
||||
:data-data="issuableTemplatesJson">
|
||||
<span class="dropdown-toggle-text">
|
||||
Choose a template
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-chevron-down">
|
||||
</i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<div class="dropdown-title">
|
||||
Choose a template
|
||||
<button
|
||||
class="dropdown-title-button dropdown-menu-close"
|
||||
aria-label="Close"
|
||||
type="button">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-times dropdown-menu-close-icon">
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-input">
|
||||
<input
|
||||
type="search"
|
||||
class="dropdown-input-field"
|
||||
placeholder="Filter"
|
||||
autocomplete="off" />
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-search dropdown-input-search">
|
||||
</i>
|
||||
<i
|
||||
role="button"
|
||||
aria-label="Clear templates search input"
|
||||
class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
|
||||
</i>
|
||||
</div>
|
||||
<div class="dropdown-content"></div>
|
||||
<div class="dropdown-footer">
|
||||
<ul class="dropdown-footer-list">
|
||||
<li>
|
||||
<a class="no-template">
|
||||
No template
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="reset-template">
|
||||
Reset template
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -3,6 +3,7 @@
|
|||
import titleField from './fields/title.vue';
|
||||
import descriptionField from './fields/description.vue';
|
||||
import editActions from './edit_actions.vue';
|
||||
import descriptionTemplate from './fields/description_template.vue';
|
||||
import projectMove from './fields/project_move.vue';
|
||||
import confidentialCheckbox from './fields/confidential_checkbox.vue';
|
||||
|
||||
|
@ -20,6 +21,11 @@
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issuableTemplates: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
markdownPreviewUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -28,6 +34,14 @@
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectsAutocompleteUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -37,24 +51,48 @@
|
|||
lockedWarning,
|
||||
titleField,
|
||||
descriptionField,
|
||||
descriptionTemplate,
|
||||
editActions,
|
||||
projectMove,
|
||||
confidentialCheckbox,
|
||||
},
|
||||
computed: {
|
||||
hasIssuableTemplates() {
|
||||
return this.issuableTemplates.length;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<locked-warning v-if="formState.lockedWarningVisible" />
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-sm-4 col-lg-3"
|
||||
v-if="hasIssuableTemplates">
|
||||
<description-template
|
||||
:form-state="formState"
|
||||
:issuable-templates="issuableTemplates"
|
||||
:project-path="projectPath"
|
||||
:project-namespace="projectNamespace" />
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'col-sm-8 col-lg-9': hasIssuableTemplates,
|
||||
'col-xs-12': !hasIssuableTemplates,
|
||||
}">
|
||||
<title-field
|
||||
:form-state="formState" />
|
||||
<confidential-checkbox
|
||||
:form-state="formState" />
|
||||
:form-state="formState"
|
||||
:issuable-templates="issuableTemplates" />
|
||||
</div>
|
||||
</div>
|
||||
<description-field
|
||||
:form-state="formState"
|
||||
:markdown-preview-url="markdownPreviewUrl"
|
||||
:markdown-docs="markdownDocs" />
|
||||
<confidential-checkbox
|
||||
:form-state="formState" />
|
||||
<project-move
|
||||
v-if="canMove"
|
||||
:form-state="formState"
|
||||
|
|
|
@ -4,6 +4,8 @@ import issuableApp from './components/app.vue';
|
|||
import '../vue_shared/vue_resource_interceptor';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
|
||||
const initialData = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"'));
|
||||
$('.issuable-edit').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -44,7 +46,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
isConfidential: gl.utils.convertPermissionToBoolean(isConfidential),
|
||||
markdownPreviewUrl,
|
||||
markdownDocs,
|
||||
projectPath: initialData.project_path,
|
||||
projectNamespace: initialData.namespace_path,
|
||||
projectsAutocompleteUrl,
|
||||
issuableTemplates: initialData.templates,
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
|
@ -58,9 +63,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initialTitle: this.initialTitle,
|
||||
initialDescriptionHtml: this.initialDescriptionHtml,
|
||||
initialDescriptionText: this.initialDescriptionText,
|
||||
issuableTemplates: this.issuableTemplates,
|
||||
isConfidential: this.isConfidential,
|
||||
markdownPreviewUrl: this.markdownPreviewUrl,
|
||||
markdownDocs: this.markdownDocs,
|
||||
projectPath: this.projectPath,
|
||||
projectNamespace: this.projectNamespace,
|
||||
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -199,6 +199,14 @@ module IssuablesHelper
|
|||
issuable_filter_params.any? { |k| params.key?(k) }
|
||||
end
|
||||
|
||||
def issuable_initial_data(issuable)
|
||||
{
|
||||
templates: issuable_templates(issuable),
|
||||
project_path: ref_project.path,
|
||||
namespace_path: ref_project.namespace.full_path
|
||||
}.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
|
||||
.issue-details.issuable-details
|
||||
.detail-page-description.content-block
|
||||
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@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-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import Vue from 'vue';
|
||||
import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
|
||||
import '~/templates/issuable_template_selector';
|
||||
import '~/templates/issuable_template_selectors';
|
||||
|
||||
describe('Issue description template component', () => {
|
||||
let vm;
|
||||
let formState;
|
||||
|
||||
beforeEach((done) => {
|
||||
const Component = Vue.extend(descriptionTemplate);
|
||||
formState = {
|
||||
description: 'test',
|
||||
};
|
||||
|
||||
vm = new Component({
|
||||
propsData: {
|
||||
formState,
|
||||
issuableTemplates: [{ name: 'test' }],
|
||||
projectPath: '/',
|
||||
projectNamespace: '/',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('renders templates as JSON array in data attribute', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'),
|
||||
).toBe('[{"name":"test"}]');
|
||||
});
|
||||
|
||||
it('updates formState when changing template', () => {
|
||||
vm.issuableTemplate.editor.setValue('test new template');
|
||||
|
||||
expect(
|
||||
formState.description,
|
||||
).toBe('test new template');
|
||||
});
|
||||
|
||||
it('returns formState description with editor getValue', () => {
|
||||
formState.description = 'testing new template';
|
||||
|
||||
expect(
|
||||
vm.issuableTemplate.editor.getValue(),
|
||||
).toBe('testing new template');
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import formComponent from '~/issue_show/components/form.vue';
|
||||
import '~/templates/issuable_template_selector';
|
||||
import '~/templates/issuable_template_selectors';
|
||||
|
||||
describe('Inline edit form component', () => {
|
||||
let vm;
|
||||
|
@ -19,12 +21,33 @@ describe('Inline edit form component', () => {
|
|||
markdownPreviewUrl: '/',
|
||||
markdownDocs: '/',
|
||||
projectsAutocompleteUrl: '/',
|
||||
projectPath: '/',
|
||||
projectNamespace: '/',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('does not render template selector if no templates exist', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.js-issuable-selector-wrap'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('renders template selector when templates exists', (done) => {
|
||||
spyOn(gl, 'IssuableTemplateSelectors');
|
||||
vm.issuableTemplates = ['test'];
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.js-issuable-selector-wrap'),
|
||||
).not.toBeNull();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides locked warning by default', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.alert'),
|
||||
|
|
Loading…
Reference in a new issue