Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Tatuya Kamada
gitlab-ce
Commits
c25cf77d
Commit
c25cf77d
authored
Apr 06, 2017
by
Jacob Schatz
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'issue-title-vue-2' into 'master'
Issue title realtime Closes #25051 See merge request !10115
parents
4e3de96e
1c783007
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
211 additions
and
42 deletions
+211
-42
app/assets/javascripts/issue_show/index.js
app/assets/javascripts/issue_show/index.js
+26
-0
app/assets/javascripts/issue_show/issue_title.js
app/assets/javascripts/issue_show/issue_title.js
+78
-0
app/assets/javascripts/issue_show/services/index.js
app/assets/javascripts/issue_show/services/index.js
+10
-0
app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
...javascripts/vue_pipelines_index/stores/pipelines_store.js
+2
-2
app/assets/javascripts/vue_realtime_listener/index.js
app/assets/javascripts/vue_realtime_listener/index.js
+9
-29
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+7
-2
app/models/issue.rb
app/models/issue.rb
+11
-0
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+6
-3
config/routes/project.rb
config/routes/project.rb
+1
-0
config/webpack.config.js
config/webpack.config.js
+1
-0
lib/gitlab/etag_caching/middleware.rb
lib/gitlab/etag_caching/middleware.rb
+2
-1
spec/features/gitlab_flavored_markdown_spec.rb
spec/features/gitlab_flavored_markdown_spec.rb
+11
-1
spec/features/issues/award_emoji_spec.rb
spec/features/issues/award_emoji_spec.rb
+4
-1
spec/features/issues/move_spec.rb
spec/features/issues/move_spec.rb
+2
-2
spec/features/issues/spam_issues_spec.rb
spec/features/issues/spam_issues_spec.rb
+1
-1
spec/features/issues_spec.rb
spec/features/issues_spec.rb
+17
-0
spec/javascripts/issue_show/issue_title_spec.js
spec/javascripts/issue_show/issue_title_spec.js
+22
-0
spec/javascripts/test_bundle.js
spec/javascripts/test_bundle.js
+1
-0
No files found.
app/assets/javascripts/issue_show/index.js
0 → 100644
View file @
c25cf77d
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
()))();
app/assets/javascripts/issue_show/issue_title.js
0 → 100644
View file @
c25cf77d
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>
`
,
};
app/assets/javascripts/issue_show/services/index.js
0 → 100644
View file @
c25cf77d
export
default
class
Service
{
constructor
(
resource
,
endpoint
)
{
this
.
resource
=
resource
;
this
.
endpoint
=
endpoint
;
}
getTitle
()
{
return
this
.
resource
.
get
(
this
.
endpoint
);
}
}
app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
View file @
c25cf77d
/* 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
);
}
}
app/assets/javascripts/vue_realtime_listener/index.js
View file @
c25cf77d
/* eslint-disable no-param-reassign */
((
gl
)
=>
{
gl
.
VueRealtimeListener
=
(
removeIntervals
,
startIntervals
)
=>
{
const
removeAll
=
()
=>
{
removeIntervals
();
window
.
removeEventListener
(
'
beforeunload
'
,
removeIntervals
);
export
default
(
removeIntervals
,
startIntervals
)
=>
{
window
.
removeEventListener
(
'
focus
'
,
startIntervals
);
window
.
removeEventListener
(
'
blur
'
,
removeIntervals
);
document
.
removeEventListener
(
'
beforeunload
'
,
removeAll
);
};
window
.
removeEventListener
(
'
onbeforeload
'
,
removeIntervals
);
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
=
{}));
window
.
addEventListener
(
'
onbeforeload
'
,
removeIntervals
);
};
app/controllers/projects/issues_controller.rb
View file @
c25cf77d
...
...
@@ -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
...
...
app/models/issue.rb
View file @
c25cf77d
...
...
@@ -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
app/views/projects/issues/show.html.haml
View file @
c25cf77d
...
...
@@ -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'
)
config/routes/project.rb
View file @
c25cf77d
...
...
@@ -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
...
...
config/webpack.config.js
View file @
c25cf77d
...
...
@@ -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
:
{
...
...
lib/gitlab/etag_caching/middleware.rb
View file @
c25cf77d
...
...
@@ -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
)
...
...
spec/features/gitlab_flavored_markdown_spec.rb
View file @
c25cf77d
...
...
@@ -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
...
...
spec/features/issues/award_emoji_spec.rb
View file @
c25cf77d
...
...
@@ -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
...
...
spec/features/issues/move_spec.rb
View file @
c25cf77d
...
...
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
edit_issue
(
issue
)
end
scenario
'moving issue to another project'
do
fi
rst
(
'#move_to_project_id'
,
visible:
false
).
set
(
new_project
.
id
)
scenario
'moving issue to another project'
,
js:
true
do
fi
nd
(
'#move_to_project_id'
,
visible:
false
).
set
(
new_project
.
id
)
click_button
(
'Save changes'
)
expect
(
current_url
).
to
include
project_path
(
new_project
)
...
...
spec/features/issues/spam_issues_spec.rb
View file @
c25cf77d
require
'rails_helper'
describe
'New issue'
,
feature:
true
do
describe
'New issue'
,
feature:
true
,
js:
true
do
include
StubENV
let
(
:project
)
{
create
(
:project
,
:public
)
}
...
...
spec/features/issues_spec.rb
View file @
c25cf77d
...
...
@@ -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
spec/javascripts/issue_show/issue_title_spec.js
0 → 100644
View file @
c25cf77d
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
'
);
});
});
spec/javascripts/test_bundle.js
View file @
c25cf77d
...
...
@@ -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
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment