summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issue_show/index.js6
-rw-r--r--app/assets/javascripts/issue_show/issue_title.vue80
-rw-r--r--app/assets/javascripts/issue_show/issue_title_description.vue158
-rw-r--r--app/assets/stylesheets/pages/issues.scss9
-rw-r--r--app/controllers/projects/issues_controller.rb8
-rw-r--r--app/views/projects/issues/show.html.haml11
6 files changed, 180 insertions, 92 deletions
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 4d491e70d83..db1cdb6d498 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,16 +1,16 @@
import Vue from 'vue';
-import IssueTitle from './issue_title.vue';
+import IssueTitle from './issue_title_description.vue';
import '../vue_shared/vue_resource_interceptor';
(() => {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
- const { initialTitle, endpoint } = issueTitleData;
+ const { candescription, endpoint } = issueTitleData;
const vm = new Vue({
el: '.issue-title-entrypoint',
render: createElement => createElement(IssueTitle, {
props: {
- initialTitle,
+ candescription,
endpoint,
},
}),
diff --git a/app/assets/javascripts/issue_show/issue_title.vue b/app/assets/javascripts/issue_show/issue_title.vue
deleted file mode 100644
index 00b0e56030a..00000000000
--- a/app/assets/javascripts/issue_show/issue_title.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-import Visibility from 'visibilityjs';
-import Poll from './../lib/utils/poll';
-import Service from './services/index';
-
-export default {
- props: {
- initialTitle: { required: true, type: String },
- endpoint: { required: true, type: String },
- },
- data() {
- const resource = new Service(this.$http, this.endpoint);
-
- const poll = new Poll({
- resource,
- method: 'getTitle',
- successCallback: (res) => {
- this.renderResponse(res);
- },
- errorCallback: (err) => {
- if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line no-console
- console.error('ISSUE SHOW TITLE REALTIME ERROR', err);
- } else {
- throw new Error(err);
- }
- },
- });
-
- return {
- poll,
- timeoutId: null,
- title: this.initialTitle,
- };
- },
- methods: {
- renderResponse(res) {
- const body = JSON.parse(res.body);
- this.triggerAnimation(body);
- },
- triggerAnimation(body) {
- const { title } = body;
-
- /**
- * since opacity is changed, even if there is no diff for Vue to update
- * we must check the title even on a 304 to ensure no visual change
- */
- if (this.title === title) return;
-
- this.$el.style.opacity = 0;
-
- this.timeoutId = setTimeout(() => {
- this.title = title;
-
- this.$el.style.transition = 'opacity 0.2s ease';
- this.$el.style.opacity = 1;
-
- clearTimeout(this.timeoutId);
- }, 100);
- },
- },
- created() {
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- },
-};
-</script>
-
-<template>
- <h2 class="title" v-html="title"></h2>
-</template>
diff --git a/app/assets/javascripts/issue_show/issue_title_description.vue b/app/assets/javascripts/issue_show/issue_title_description.vue
new file mode 100644
index 00000000000..e21667f2ac7
--- /dev/null
+++ b/app/assets/javascripts/issue_show/issue_title_description.vue
@@ -0,0 +1,158 @@
+<script>
+import Visibility from 'visibilityjs';
+import Poll from './../lib/utils/poll';
+import Service from './services/index';
+
+export default {
+ props: {
+ endpoint: { required: true, type: String },
+ candescription: { required: true, type: String },
+ },
+ data() {
+ const resource = new Service(this.$http, this.endpoint);
+
+ const poll = new Poll({
+ resource,
+ method: 'getTitle',
+ successCallback: (res) => {
+ this.renderResponse(res);
+ },
+ errorCallback: (err) => {
+ if (process.env.NODE_ENV !== 'production') {
+ // eslint-disable-next-line no-console
+ console.error('ISSUE SHOW REALTIME ERROR', err);
+ } else {
+ throw new Error(err);
+ }
+ },
+ });
+
+ return {
+ poll,
+ timeoutId: null,
+ title: '<span></span>',
+ description: '<span></span>',
+ descriptionText: '',
+ descriptionChange: false,
+ taskStatus: '',
+ };
+ },
+ methods: {
+ renderResponse(res) {
+ const data = JSON.parse(res.body);
+ this.triggerAnimation(data);
+ },
+ updateTaskHTML(data) {
+ this.taskStatus = data.task_status;
+ document.querySelector('#task_status').innerText = this.taskStatus;
+ },
+ elementsToVisualize(noTitleChange, noDescriptionChange) {
+ const elementStack = [];
+
+ if (!noTitleChange) {
+ elementStack.push(this.$el.querySelector('.title'));
+ }
+
+ if (!noDescriptionChange) {
+ // only change to true when we need to bind TaskLists the html of description
+ this.descriptionChange = true;
+ elementStack.push(this.$el.querySelector('.wiki'));
+ }
+
+ elementStack.forEach((element) => {
+ element.classList.remove('issue-realtime-trigger-pulse');
+ element.classList.add('issue-realtime-pre-pulse');
+ });
+
+ return elementStack;
+ },
+ animate(title, description, elementsToVisualize) {
+ this.timeoutId = setTimeout(() => {
+ this.title = title;
+ this.description = description;
+ document.querySelector('title').innerText = title;
+
+ elementsToVisualize.forEach((element) => {
+ element.classList.remove('issue-realtime-pre-pulse');
+ element.classList.add('issue-realtime-trigger-pulse');
+ });
+
+ clearTimeout(this.timeoutId);
+ }, 0);
+ },
+ triggerAnimation(data) {
+ // always reset to false before checking the change
+ this.descriptionChange = false;
+
+ const { title, description } = data;
+ this.descriptionText = data.description_text;
+ this.updateTaskHTML(data);
+ /**
+ * since opacity is changed, even if there is no diff for Vue to update
+ * we must check the title/description even on a 304 to ensure no visual change
+ */
+ const noTitleChange = this.title === title;
+ const noDescriptionChange = this.description === description;
+
+ if (noTitleChange && noDescriptionChange) return;
+
+ const elementsToVisualize = this.elementsToVisualize(
+ noTitleChange,
+ noDescriptionChange,
+ );
+
+ this.animate(title, description, elementsToVisualize);
+ },
+ },
+ computed: {
+ descriptionClass() {
+ return `description ${this.candescription} is-task-list-enabled`;
+ },
+ },
+ created() {
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ },
+ updated() {
+ // if new html is injected (description changed) - bind TaskList and call renderGFM
+ if (this.descriptionChange) {
+ const tl = new gl.TaskList({
+ dataType: 'issue',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ });
+
+ $(this.$refs['issue-content-container-gfm-entry']).renderGFM();
+ return tl;
+ }
+ return null;
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h2 class="title issue-realtime-trigger-pulse" v-html="title"></h2>
+ <div
+ :class="descriptionClass"
+ v-if="description"
+ >
+ <div
+ class="wiki issue-realtime-trigger-pulse"
+ v-html="description"
+ ref="issue-content-container-gfm-entry"
+ >
+ </div>
+ <textarea class="hidden js-task-list-field" v-if="descriptionText">{{descriptionText}}</textarea>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 2aa52986e0a..f109a01cfa0 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -18,6 +18,15 @@
}
}
+.issue-realtime-pre-pulse {
+ opacity: 0;
+}
+
+.issue-realtime-trigger-pulse {
+ transition: opacity 0.2s ease;
+ opacity: 1;
+}
+
.check-all-holder {
line-height: 36px;
float: left;
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cbf67137261..e5c1505ece6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -198,7 +198,13 @@ class Projects::IssuesController < Projects::ApplicationController
def rendered_title
Gitlab::PollingInterval.set_header(response, interval: 3_000)
- render json: { title: view_context.markdown_field(@issue, :title) }
+
+ render json: {
+ title: view_context.markdown_field(@issue, :title),
+ description: view_context.markdown_field(@issue, :description),
+ description_text: @issue.description,
+ task_status: @issue.task_status,
+ }
end
protected
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2a871966aa8..3971ea44ef3 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -51,16 +51,11 @@
.issue-details.issuable-details
.detail-page-description.content-block
- .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
- "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
+ .issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
+ "canDescription" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
} }
.issue-title-entrypoint
- - if @issue.description.present?
- .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
- .wiki
- = markdown_field(@issue, :description)
- %textarea.hidden.js-task-list-field
- = @issue.description
+
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }