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
Jérome Perrin
gitlab-ce
Commits
1c783007
Commit
1c783007
authored
Apr 06, 2017
by
Regis Boudinot
Committed by
Jacob Schatz
Apr 06, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Issue title realtime
parent
4e3de96e
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 @
1c783007
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 @
1c783007
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 @
1c783007
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 @
1c783007
/* eslint-disable no-underscore-dangle*/
/* eslint-disable no-underscore-dangle*/
import
'
../../vue_realtime_listener
'
;
import
VueRealtimeListener
from
'
../../vue_realtime_listener
'
;
export
default
class
PipelinesStore
{
export
default
class
PipelinesStore
{
constructor
()
{
constructor
()
{
...
@@ -56,6 +56,6 @@ export default class PipelinesStore {
...
@@ -56,6 +56,6 @@ export default class PipelinesStore {
const
removeIntervals
=
()
=>
clearInterval
(
this
.
timeLoopInterval
);
const
removeIntervals
=
()
=>
clearInterval
(
this
.
timeLoopInterval
);
const
startIntervals
=
()
=>
startTimeLoops
();
const
startIntervals
=
()
=>
startTimeLoops
();
gl
.
VueRealtimeListener
(
removeIntervals
,
startIntervals
);
VueRealtimeListener
(
removeIntervals
,
startIntervals
);
}
}
}
}
app/assets/javascripts/vue_realtime_listener/index.js
View file @
1c783007
/* eslint-disable no-param-reassign */
export
default
(
removeIntervals
,
startIntervals
)
=>
{
((
gl
)
=>
{
gl
.
VueRealtimeListener
=
(
removeIntervals
,
startIntervals
)
=>
{
const
removeAll
=
()
=>
{
removeIntervals
();
window
.
removeEventListener
(
'
beforeunload
'
,
removeIntervals
);
window
.
removeEventListener
(
'
focus
'
,
startIntervals
);
window
.
removeEventListener
(
'
focus
'
,
startIntervals
);
window
.
removeEventListener
(
'
blur
'
,
removeIntervals
);
window
.
removeEventListener
(
'
blur
'
,
removeIntervals
);
document
.
removeEventListener
(
'
beforeunload
'
,
removeAll
);
window
.
removeEventListener
(
'
onbeforeload
'
,
removeIntervals
);
};
window
.
addEventListener
(
'
beforeunload
'
,
removeIntervals
);
window
.
addEventListener
(
'
focus
'
,
startIntervals
);
window
.
addEventListener
(
'
focus
'
,
startIntervals
);
window
.
addEventListener
(
'
blur
'
,
removeIntervals
);
window
.
addEventListener
(
'
blur
'
,
removeIntervals
);
document
.
addEventListener
(
'
beforeunload
'
,
removeAll
);
window
.
addEventListener
(
'
onbeforeload
'
,
removeIntervals
);
};
// 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
=
{}));
app/controllers/projects/issues_controller.rb
View file @
1c783007
...
@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action
:redirect_to_external_issue_tracker
,
only:
[
:index
,
:new
]
before_action
:redirect_to_external_issue_tracker
,
only:
[
:index
,
:new
]
before_action
:module_enabled
before_action
:module_enabled
before_action
:issue
,
only:
[
:edit
,
:update
,
:show
,
:referenced_merge_requests
,
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
# Allow read any issue
before_action
:authorize_read_issue!
,
only:
[
:show
]
before_action
:authorize_read_issue!
,
only:
[
:show
,
:rendered_title
]
# Allow write(create) issue
# Allow write(create) issue
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
...
@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
end
end
def
rendered_title
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
3_000
)
render
json:
{
title:
view_context
.
markdown_field
(
@issue
,
:title
)
}
end
protected
protected
def
issue
def
issue
...
...
app/models/issue.rb
View file @
1c783007
...
@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
...
@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
scope
:include_associations
,
->
{
includes
(
:assignee
,
:labels
,
project: :namespace
)
}
scope
:include_associations
,
->
{
includes
(
:assignee
,
:labels
,
project: :namespace
)
}
after_save
:expire_etag_cache
attr_spammable
:title
,
spam_title:
true
attr_spammable
:title
,
spam_title:
true
attr_spammable
:description
,
spam_description:
true
attr_spammable
:description
,
spam_description:
true
...
@@ -252,4 +254,13 @@ class Issue < ActiveRecord::Base
...
@@ -252,4 +254,13 @@ class Issue < ActiveRecord::Base
def
publicly_visible?
def
publicly_visible?
project
.
public?
&&
!
confidential?
project
.
public?
&&
!
confidential?
end
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
end
app/views/projects/issues/show.html.haml
View file @
1c783007
...
@@ -49,11 +49,12 @@
...
@@ -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
'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'
=
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
.issue-details.issuable-details
.detail-page-description.content-block
{
class:
(
'hide-bottom-border'
unless
@issue
.
description
.
present?
)
}
.detail-page-description.content-block
{
class:
(
'hide-bottom-border'
unless
@issue
.
description
.
present?
)
}
%h2
.title
.issue-title-data.hidden
{
"data"
=>
{
"initial-title"
=>
markdown_field
(
@issue
,
:title
),
=
markdown_field
(
@issue
,
:title
)
"endpoint"
=>
rendered_title_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
}
}
.issue-title-entrypoint
-
if
@issue
.
description
.
present?
-
if
@issue
.
description
.
present?
.description
{
class:
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
}
.description
{
class:
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
}
.wiki
.wiki
...
@@ -77,3 +78,5 @@
...
@@ -77,3 +78,5 @@
=
render
'projects/issues/discussion'
=
render
'projects/issues/discussion'
=
render
'shared/issuable/sidebar'
,
issuable:
@issue
=
render
'shared/issuable/sidebar'
,
issuable:
@issue
=
page_specific_javascript_bundle_tag
(
'issue_show'
)
config/routes/project.rb
View file @
1c783007
...
@@ -250,6 +250,7 @@ constraints(ProjectUrlConstrainer.new) do
...
@@ -250,6 +250,7 @@ constraints(ProjectUrlConstrainer.new) do
get
:referenced_merge_requests
get
:referenced_merge_requests
get
:related_branches
get
:related_branches
get
:can_create_branch
get
:can_create_branch
get
:rendered_title
end
end
collection
do
collection
do
post
:bulk_update
post
:bulk_update
...
...
config/webpack.config.js
View file @
1c783007
...
@@ -46,6 +46,7 @@ var config = {
...
@@ -46,6 +46,7 @@ var config = {
u2f
:
[
'
vendor/u2f
'
],
u2f
:
[
'
vendor/u2f
'
],
users
:
'
./users/users_bundle.js
'
,
users
:
'
./users/users_bundle.js
'
,
vue_pipelines
:
'
./vue_pipelines_index/index.js
'
,
vue_pipelines
:
'
./vue_pipelines_index/index.js
'
,
issue_show
:
'
./issue_show/index.js
'
,
},
},
output
:
{
output
:
{
...
...
lib/gitlab/etag_caching/middleware.rb
View file @
1c783007
...
@@ -3,7 +3,8 @@ module Gitlab
...
@@ -3,7 +3,8 @@ module Gitlab
class
Middleware
class
Middleware
RESERVED_WORDS
=
NamespaceValidator
::
WILDCARD_ROUTES
.
map
{
|
word
|
"/
#{
word
}
/"
}.
join
(
'|'
)
RESERVED_WORDS
=
NamespaceValidator
::
WILDCARD_ROUTES
.
map
{
|
word
|
"/
#{
word
}
/"
}.
join
(
'|'
)
ROUTE_REGEXP
=
Regexp
.
union
(
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
)
def
initialize
(
app
)
...
...
spec/features/gitlab_flavored_markdown_spec.rb
View file @
1c783007
...
@@ -48,7 +48,9 @@ describe "GitLab Flavored Markdown", feature: true do
...
@@ -48,7 +48,9 @@ describe "GitLab Flavored Markdown", feature: true do
end
end
end
end
describe
"for issues"
do
describe
"for issues"
,
feature:
true
,
js:
true
do
include
WaitForVueResource
before
do
before
do
@other_issue
=
create
(
:issue
,
@other_issue
=
create
(
:issue
,
author:
@user
,
author:
@user
,
...
@@ -79,6 +81,14 @@ describe "GitLab Flavored Markdown", feature: true do
...
@@ -79,6 +81,14 @@ describe "GitLab Flavored Markdown", feature: true do
expect
(
page
).
to
have_link
(
fred
.
to_reference
)
expect
(
page
).
to
have_link
(
fred
.
to_reference
)
end
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
end
describe
"for merge requests"
do
describe
"for merge requests"
do
...
...
spec/features/issues/award_emoji_spec.rb
View file @
1c783007
...
@@ -2,6 +2,7 @@ require 'rails_helper'
...
@@ -2,6 +2,7 @@ require 'rails_helper'
describe
'Awards Emoji'
,
feature:
true
do
describe
'Awards Emoji'
,
feature:
true
do
include
WaitForAjax
include
WaitForAjax
include
WaitForVueResource
let!
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)
}
let!
(
:user
)
{
create
(
:user
)
}
...
@@ -22,10 +23,11 @@ describe 'Awards Emoji', feature: true do
...
@@ -22,10 +23,11 @@ describe 'Awards Emoji', feature: true do
# The `heart_tip` emoji is not valid anymore so we need to skip validation
# 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
)
issue
.
award_emoji
.
build
(
user:
user
,
name:
'heart_tip'
).
save!
(
validate:
false
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
wait_for_vue_resource
end
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
# 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
)
expect
(
page
).
to
have_text
(
issue
.
title
)
end
end
end
end
...
@@ -35,6 +37,7 @@ describe 'Awards Emoji', feature: true do
...
@@ -35,6 +37,7 @@ describe 'Awards Emoji', feature: true do
before
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
wait_for_vue_resource
end
end
it
'increments the thumbsdown emoji'
,
js:
true
do
it
'increments the thumbsdown emoji'
,
js:
true
do
...
...
spec/features/issues/move_spec.rb
View file @
1c783007
...
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
...
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
edit_issue
(
issue
)
edit_issue
(
issue
)
end
end
scenario
'moving issue to another project'
do
scenario
'moving issue to another project'
,
js:
true
do
fi
rst
(
'#move_to_project_id'
,
visible:
false
).
set
(
new_project
.
id
)
fi
nd
(
'#move_to_project_id'
,
visible:
false
).
set
(
new_project
.
id
)
click_button
(
'Save changes'
)
click_button
(
'Save changes'
)
expect
(
current_url
).
to
include
project_path
(
new_project
)
expect
(
current_url
).
to
include
project_path
(
new_project
)
...
...
spec/features/issues/spam_issues_spec.rb
View file @
1c783007
require
'rails_helper'
require
'rails_helper'
describe
'New issue'
,
feature:
true
do
describe
'New issue'
,
feature:
true
,
js:
true
do
include
StubENV
include
StubENV
let
(
:project
)
{
create
(
:project
,
:public
)
}
let
(
:project
)
{
create
(
:project
,
:public
)
}
...
...
spec/features/issues_spec.rb
View file @
1c783007
...
@@ -695,4 +695,21 @@ describe 'Issues', feature: true do
...
@@ -695,4 +695,21 @@ describe 'Issues', feature: true do
end
end
end
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
end
spec/javascripts/issue_show/issue_title_spec.js
0 → 100644
View file @
1c783007
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 @
1c783007
...
@@ -64,6 +64,7 @@ if (process.env.BABEL_ENV === 'coverage') {
...
@@ -64,6 +64,7 @@ if (process.env.BABEL_ENV === 'coverage') {
'
./snippet/snippet_bundle.js
'
,
'
./snippet/snippet_bundle.js
'
,
'
./terminal/terminal_bundle.js
'
,
'
./terminal/terminal_bundle.js
'
,
'
./users/users_bundle.js
'
,
'
./users/users_bundle.js
'
,
'
./issue_show/index.js
'
,
];
];
describe
(
'
Uncovered files
'
,
function
()
{
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