summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/issue_show/index.js26
-rw-r--r--app/assets/javascripts/issue_show/issue_title.js78
-rw-r--r--app/assets/javascripts/issue_show/services/index.js10
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js4
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js38
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/models/issue.rb11
-rw-r--r--app/views/projects/issues/show.html.haml9
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/webpack.config.js1
-rw-r--r--lib/gitlab/etag_caching/middleware.rb3
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb12
-rw-r--r--spec/features/issues/award_emoji_spec.rb5
-rw-r--r--spec/features/issues/move_spec.rb4
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues_spec.rb17
-rw-r--r--spec/javascripts/issue_show/issue_title_spec.js22
-rw-r--r--spec/javascripts/test_bundle.js1
18 files changed, 211 insertions, 42 deletions
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
new file mode 100644
index 00000000000..b6ce8e83729
--- /dev/null
+++ b/app/assets/javascripts/issue_show/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import IssueTitle from './issue_title';
+import '../vue_shared/vue_resource_interceptor';
+
+const vueOptions = () => ({
+ el: '.issue-title-entrypoint',
+ components: {
+ IssueTitle,
+ },
+ data() {
+ const issueTitleData = document.querySelector('.issue-title-data').dataset;
+
+ return {
+ initialTitle: issueTitleData.initialTitle,
+ endpoint: issueTitleData.endpoint,
+ };
+ },
+ template: `
+ <IssueTitle
+ :initialTitle="initialTitle"
+ :endpoint="endpoint"
+ />
+ `,
+});
+
+(() => new Vue(vueOptions()))();
diff --git a/app/assets/javascripts/issue_show/issue_title.js b/app/assets/javascripts/issue_show/issue_title.js
new file mode 100644
index 00000000000..1184c8956dc
--- /dev/null
+++ b/app/assets/javascripts/issue_show/issue_title.js
@@ -0,0 +1,78 @@
+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: {
+ fetch() {
+ this.poll.makeRequest();
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ },
+ 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() {
+ this.fetch();
+ },
+ template: `
+ <h2 class='title' v-html='title'></h2>
+ `,
+};
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
new file mode 100644
index 00000000000..c4ab0b1e07a
--- /dev/null
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -0,0 +1,10 @@
+export default class Service {
+ constructor(resource, endpoint) {
+ this.resource = resource;
+ this.endpoint = endpoint;
+ }
+
+ getTitle() {
+ return this.resource.get(this.endpoint);
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index 7ac10086a55..377ec8ba2cc 100644
--- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,5 +1,5 @@
/* eslint-disable no-underscore-dangle*/
-import '../../vue_realtime_listener';
+import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore {
constructor() {
@@ -56,6 +56,6 @@ export default class PipelinesStore {
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
- gl.VueRealtimeListener(removeIntervals, startIntervals);
+ VueRealtimeListener(removeIntervals, startIntervals);
}
}
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js
index 30f6680a673..4ddb2f975b0 100644
--- a/app/assets/javascripts/vue_realtime_listener/index.js
+++ b/app/assets/javascripts/vue_realtime_listener/index.js
@@ -1,29 +1,9 @@
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
- const removeAll = () => {
- removeIntervals();
- window.removeEventListener('beforeunload', removeIntervals);
- window.removeEventListener('focus', startIntervals);
- window.removeEventListener('blur', removeIntervals);
- document.removeEventListener('beforeunload', removeAll);
- };
-
- window.addEventListener('beforeunload', removeIntervals);
- window.addEventListener('focus', startIntervals);
- window.addEventListener('blur', removeIntervals);
- document.addEventListener('beforeunload', removeAll);
-
- // add removeAll methods to stack
- const stack = gl.VueRealtimeListener.reset;
- gl.VueRealtimeListener.reset = () => {
- gl.VueRealtimeListener.reset = stack;
- removeAll();
- stack();
- };
- };
-
- // remove all event listeners and intervals
- gl.VueRealtimeListener.reset = () => undefined; // noop
-})(window.gl || (window.gl = {}));
+export default (removeIntervals, startIntervals) => {
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ window.removeEventListener('onbeforeload', removeIntervals);
+
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ window.addEventListener('onbeforeload', removeIntervals);
+};
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d984e6d3918..3a870ae4241 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
- :related_branches, :can_create_branch]
+ :related_branches, :can_create_branch, :rendered_title]
# Allow read any issue
- before_action :authorize_read_issue!, only: [:show]
+ before_action :authorize_read_issue!, only: [:show, :rendered_title]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
+ def rendered_title
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+ render json: { title: view_context.markdown_field(@issue, :title) }
+ end
+
protected
def issue
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 472796df9df..f9704b0d754 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
+ after_save :expire_etag_cache
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -252,4 +254,13 @@ class Issue < ActiveRecord::Base
def publicly_visible?
project.public? && !confidential?
end
+
+ def expire_etag_cache
+ key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
+ project.namespace,
+ project,
+ self
+ )
+ Gitlab::EtagCaching::Store.new.touch(key)
+ end
end
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 6ac05bf3afe..885795ccb5c 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -49,11 +49,12 @@
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
-
.issue-details.issuable-details
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
- %h2.title
- = markdown_field(@issue, :title)
+ .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
+ "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
+ } }
+ .issue-title-entrypoint
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
@@ -77,3 +78,5 @@
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
+
+= page_specific_javascript_bundle_tag('issue_show')
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7244f851869..62e2e6145fd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -250,6 +250,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
+ get :rendered_title
end
collection do
post :bulk_update
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 9b597f7a04e..69d8c5640f7 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -46,6 +46,7 @@ var config = {
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
+ issue_show: './issue_show/index.js',
},
output: {
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index ab8dfc67880..cd4e318033d 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -3,7 +3,8 @@ module Gitlab
class Middleware
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
ROUTE_REGEXP = Regexp.union(
- %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
+ %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z)
)
def initialize(app)
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 84d73d693bc..876f33dd03e 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -48,7 +48,9 @@ describe "GitLab Flavored Markdown", feature: true do
end
end
- describe "for issues" do
+ describe "for issues", feature: true, js: true do
+ include WaitForVueResource
+
before do
@other_issue = create(:issue,
author: @user,
@@ -79,6 +81,14 @@ describe "GitLab Flavored Markdown", feature: true do
expect(page).to have_link(fred.to_reference)
end
+
+ it "renders updated subject once edited somewhere else in issues#show" do
+ visit namespace_project_issue_path(project.namespace, project, @issue)
+ @issue.update(title: "fix #{@other_issue.to_reference} and update")
+
+ wait_for_vue_resource
+ expect(page).to have_text("fix #{@other_issue.to_reference} and update")
+ end
end
describe "for merge requests" do
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 16e453bc328..8e67ab028d7 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Awards Emoji', feature: true do
include WaitForAjax
+ include WaitForVueResource
let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
@@ -22,10 +23,11 @@ describe 'Awards Emoji', feature: true do
# The `heart_tip` emoji is not valid anymore so we need to skip validation
issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
visit namespace_project_issue_path(project.namespace, project, issue)
+ wait_for_vue_resource
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page' do
+ it 'does not shows a 500 page', js: true do
expect(page).to have_text(issue.title)
end
end
@@ -35,6 +37,7 @@ describe 'Awards Emoji', feature: true do
before do
visit namespace_project_issue_path(project.namespace, project, issue)
+ wait_for_vue_resource
end
it 'increments the thumbsdown emoji', js: true do
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index f89b4db9e62..6c09903a2f6 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
edit_issue(issue)
end
- scenario 'moving issue to another project' do
- first('#move_to_project_id', visible: false).set(new_project.id)
+ scenario 'moving issue to another project', js: true do
+ find('#move_to_project_id', visible: false).set(new_project.id)
click_button('Save changes')
expect(current_url).to include project_path(new_project)
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 4bc9b49f889..6001476d0ca 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'New issue', feature: true do
+describe 'New issue', feature: true, js: true do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 7afceb88cf9..e3213d24f6a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -695,4 +695,21 @@ describe 'Issues', feature: true do
end
end
end
+
+ describe 'title issue#show', js: true do
+ include WaitForVueResource
+
+ it 'updates the title', js: true do
+ issue = create(:issue, author: @user, assignee: @user, project: project, title: 'new title')
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_text("new title")
+
+ issue.update(title: "updated title")
+
+ wait_for_vue_resource
+ expect(page).to have_text("updated title")
+ end
+ end
end
diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js
new file mode 100644
index 00000000000..806d728a874
--- /dev/null
+++ b/spec/javascripts/issue_show/issue_title_spec.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import issueTitle from '~/issue_show/issue_title';
+
+describe('Issue Title', () => {
+ let IssueTitleComponent;
+
+ beforeEach(() => {
+ IssueTitleComponent = Vue.extend(issueTitle);
+ });
+
+ it('should render a title', () => {
+ const component = new IssueTitleComponent({
+ propsData: {
+ initialTitle: 'wow',
+ endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
+ },
+ }).$mount();
+
+ expect(component.$el.classList).toContain('title');
+ expect(component.$el.innerHTML).toContain('wow');
+ });
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index b30c5da8822..07dc51a7815 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -64,6 +64,7 @@ if (process.env.BABEL_ENV === 'coverage') {
'./snippet/snippet_bundle.js',
'./terminal/terminal_bundle.js',
'./users/users_bundle.js',
+ './issue_show/index.js',
];
describe('Uncovered files', function () {