Merge branch 'issue-edit-inline' into issue-edit-inline-confidential
[ci skip]
This commit is contained in:
commit
6963442d67
13 changed files with 398 additions and 6 deletions
|
@ -142,7 +142,8 @@ window.DropzoneInput = (function() {
|
|||
$(child).val(beforeSelection + formattedText + afterSelection);
|
||||
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
return form_textarea.trigger("input");
|
||||
form_textarea.trigger("input");
|
||||
form_textarea.get(0).dispatchEvent(new Event('input'));
|
||||
};
|
||||
getFilename = function(e) {
|
||||
var value;
|
||||
|
|
|
@ -45,6 +45,14 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
markdownPreviewUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
markdownDocs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const store = new Store({
|
||||
|
@ -75,6 +83,7 @@ export default {
|
|||
this.store.formState = {
|
||||
title: this.state.titleText,
|
||||
confidential: this.isConfidential,
|
||||
description: this.state.descriptionText,
|
||||
};
|
||||
},
|
||||
closeForm() {
|
||||
|
@ -155,7 +164,9 @@ export default {
|
|||
<form-component
|
||||
v-if="canUpdate && showForm"
|
||||
:form-state="formState"
|
||||
:can-destroy="canDestroy" />
|
||||
:can-destroy="canDestroy"
|
||||
:markdown-docs="markdownDocs"
|
||||
:markdown-preview-url="markdownPreviewUrl" />
|
||||
<div v-else>
|
||||
<title-component
|
||||
:issuable-ref="issuableRef"
|
||||
|
|
|
@ -18,11 +18,13 @@
|
|||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
taskStatus: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -83,6 +85,7 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="descriptionHtml"
|
||||
class="description"
|
||||
:class="{
|
||||
'js-task-list-container': canUpdate
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
/* global Flash */
|
||||
import markdownField from '../../../vue_shared/components/markdown/field.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
formState: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
markdownPreviewUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
markdownDocs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
markdownField,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-note-form">
|
||||
<label
|
||||
class="sr-only"
|
||||
for="issue-description">
|
||||
Description
|
||||
</label>
|
||||
<markdown-field
|
||||
:markdown-preview-url="markdownPreviewUrl"
|
||||
:markdown-docs="markdownDocs">
|
||||
<textarea
|
||||
id="issue-description"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
data-supports-slash-commands="false"
|
||||
aria-label="Description"
|
||||
v-model="formState.description"
|
||||
ref="textatea"
|
||||
slot="textarea">
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import titleField from './fields/title.vue';
|
||||
import descriptionField from './fields/description.vue';
|
||||
import editActions from './edit_actions.vue';
|
||||
import confidentialCheckbox from './fields/confidential_checkbox.vue';
|
||||
|
||||
|
@ -13,9 +14,18 @@
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
markdownPreviewUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
markdownDocs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
titleField,
|
||||
descriptionField,
|
||||
editActions,
|
||||
confidentialCheckbox,
|
||||
},
|
||||
|
@ -28,6 +38,10 @@
|
|||
:form-state="formState" />
|
||||
<confidential-checkbox
|
||||
:form-state="formState" />
|
||||
<description-field
|
||||
:form-state="formState"
|
||||
:markdown-preview-url="markdownPreviewUrl"
|
||||
:markdown-docs="markdownDocs" />
|
||||
<edit-actions
|
||||
:can-destroy="canDestroy" />
|
||||
</form>
|
||||
|
|
|
@ -26,6 +26,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
endpoint,
|
||||
issuableRef,
|
||||
isConfidential,
|
||||
markdownPreviewUrl,
|
||||
markdownDocs,
|
||||
} = issuableElement.dataset;
|
||||
|
||||
return {
|
||||
|
@ -37,6 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
|
||||
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
|
||||
isConfidential: gl.utils.convertPermissionToBoolean(isConfidential),
|
||||
markdownPreviewUrl,
|
||||
markdownDocs,
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
|
@ -50,6 +54,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initialDescriptionHtml: this.initialDescriptionHtml,
|
||||
initialDescriptionText: this.initialDescriptionText,
|
||||
isConfidential: this.isConfidential,
|
||||
markdownPreviewUrl: this.markdownPreviewUrl,
|
||||
markdownDocs: this.markdownDocs,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ export default class Store {
|
|||
this.formState = {
|
||||
title: '',
|
||||
confidential: false,
|
||||
description: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
107
app/assets/javascripts/vue_shared/components/markdown/field.vue
Normal file
107
app/assets/javascripts/vue_shared/components/markdown/field.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<script>
|
||||
/* global Flash */
|
||||
import markdownHeader from './header.vue';
|
||||
import markdownToolbar from './toolbar.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
markdownPreviewUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
markdownDocs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
markdownPreview: '',
|
||||
markdownPreviewLoading: false,
|
||||
previewMarkdown: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
markdownHeader,
|
||||
markdownToolbar,
|
||||
},
|
||||
methods: {
|
||||
toggleMarkdownPreview() {
|
||||
this.previewMarkdown = !this.previewMarkdown;
|
||||
|
||||
if (!this.previewMarkdown) {
|
||||
this.markdownPreview = '';
|
||||
} else {
|
||||
this.markdownPreviewLoading = true;
|
||||
this.$http.post(
|
||||
this.markdownPreviewUrl,
|
||||
{
|
||||
/*
|
||||
Can't use `$refs` as the component is technically in the parent component
|
||||
so we access the VNode & then get the element
|
||||
*/
|
||||
text: this.$slots.textarea[0].elm.value,
|
||||
},
|
||||
)
|
||||
.then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
this.markdownPreviewLoading = false;
|
||||
this.markdownPreview = data.body;
|
||||
|
||||
this.$nextTick(() => {
|
||||
$(this.$refs['markdown-preview']).renderGFM();
|
||||
});
|
||||
})
|
||||
.catch(() => new Flash('Error loading markdown preview'));
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
/*
|
||||
GLForm class handles all the toolbar buttons
|
||||
*/
|
||||
return new gl.GLForm($(this.$refs['gl-form']));
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="md-area prepend-top-default append-bottom-default"
|
||||
ref="gl-form">
|
||||
<markdown-header
|
||||
:preview-markdown="previewMarkdown"
|
||||
@toggle-markdown="toggleMarkdownPreview" />
|
||||
<div
|
||||
class="md-write-holder"
|
||||
v-show="!previewMarkdown">
|
||||
<div class="zen-backdrop">
|
||||
<slot name="textarea"></slot>
|
||||
<a
|
||||
class="zen-control zen-control-leave js-zen-leave"
|
||||
href="#"
|
||||
aria-label="Enter zen mode">
|
||||
<i
|
||||
class="fa fa-compress"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</a>
|
||||
<markdown-toolbar
|
||||
:markdown-docs="markdownDocs" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="md md-preview-holder md-preview"
|
||||
v-show="previewMarkdown">
|
||||
<div
|
||||
ref="markdown-preview"
|
||||
v-html="markdownPreview">
|
||||
</div>
|
||||
<span v-if="markdownPreviewLoading">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
101
app/assets/javascripts/vue_shared/components/markdown/header.vue
Normal file
101
app/assets/javascripts/vue_shared/components/markdown/header.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<script>
|
||||
import tooltipMixin from '../../mixins/tooltip';
|
||||
import toolbarButton from './toolbar_button.vue';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
props: {
|
||||
previewMarkdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
toolbarButton,
|
||||
},
|
||||
methods: {
|
||||
toggleMarkdownPreview(e) {
|
||||
e.target.blur();
|
||||
|
||||
this.$emit('toggle-markdown');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="md-header">
|
||||
<ul class="nav-links clearfix">
|
||||
<li :class="{ active: !previewMarkdown }">
|
||||
<a
|
||||
href="#md-write-holder"
|
||||
tabindex="-1"
|
||||
@click.prevent="toggleMarkdownPreview($event)">
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ active: previewMarkdown }">
|
||||
<a
|
||||
href="#md-preview-holder"
|
||||
tabindex="-1"
|
||||
@click.prevent="toggleMarkdownPreview($event)">
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="toolbar-group">
|
||||
<toolbar-button
|
||||
tag="**"
|
||||
button-title="Add bold text"
|
||||
icon="bold" />
|
||||
<toolbar-button
|
||||
tag="*"
|
||||
button-title="Add italic text"
|
||||
icon="italic" />
|
||||
<toolbar-button
|
||||
tag="> "
|
||||
:prepend="true"
|
||||
button-title="Insert a quote"
|
||||
icon="quote-right" />
|
||||
<toolbar-button
|
||||
tag="`"
|
||||
tag-block="```"
|
||||
button-title="Insert code"
|
||||
icon="code" />
|
||||
<toolbar-button
|
||||
tag="* "
|
||||
:prepend="true"
|
||||
button-title="Add a bullet list"
|
||||
icon="list-ul" />
|
||||
<toolbar-button
|
||||
tag="1. "
|
||||
:prepend="true"
|
||||
button-title="Add a numbered list"
|
||||
icon="list-ol" />
|
||||
<toolbar-button
|
||||
tag="* [ ] "
|
||||
:prepend="true"
|
||||
button-title="Add a task list"
|
||||
icon="check-square-o" />
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
aria-label="Go full screen"
|
||||
class="toolbar-btn js-zen-enter"
|
||||
data-container="body"
|
||||
tabindex="-1"
|
||||
title="Go full screen"
|
||||
type="button"
|
||||
ref="tooltip">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-arrows-alt fa-fw">
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
markdownDocs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="comment-toolbar clearfix">
|
||||
<div class="toolbar-text">
|
||||
<a
|
||||
:href="markdownDocs"
|
||||
target="_blank"
|
||||
tabindex="-1">
|
||||
Markdown is supported
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
class="toolbar-button markdown-selector"
|
||||
type="button"
|
||||
tabindex="-1">
|
||||
<i
|
||||
class="fa fa-file-image-o toolbar-button-icon"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
Attach a file
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import tooltipMixin from '../../mixins/tooltip';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
props: {
|
||||
buttonTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tagBlock: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
prepend: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconClass() {
|
||||
return `fa-${this.icon}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn js-md hidden-xs"
|
||||
tabindex="-1"
|
||||
ref="tooltip"
|
||||
data-container="body"
|
||||
:data-md-tag="tag"
|
||||
:data-md-block="tagBlock"
|
||||
:data-md-prepend="prepend"
|
||||
:title="buttonTitle"
|
||||
:aria-label="buttonTitle">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-fw"
|
||||
:class="iconClass">
|
||||
</i>
|
||||
</button>
|
||||
</template>
|
|
@ -1,9 +1,17 @@
|
|||
export default {
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
$(this.$refs.tooltip).tooltip();
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
$(this.$refs.tooltip).tooltip('fixTitle');
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
$(this.$refs.tooltip).tooltip('destroy');
|
||||
},
|
||||
};
|
||||
|
|
|
@ -56,6 +56,8 @@
|
|||
"can-destroy" => can?(current_user, :destroy_issue, @issue).to_s,
|
||||
"issuable-ref" => @issue.to_reference,
|
||||
"is-confidential" => @issue.confidential.to_s,
|
||||
"markdown-preview-url" => preview_markdown_path(@project),
|
||||
"markdown-docs" => help_page_path('user/markdown'),
|
||||
} }
|
||||
%h2.title= markdown_field(@issue, :title)
|
||||
- if @issue.description.present?
|
||||
|
|
Loading…
Reference in a new issue