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
Boxiang Sun
gitlab-ce
Commits
07c984d8
Commit
07c984d8
authored
May 18, 2017
by
Luke "Jared" Bennett
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Port fix-realtime-edited-text-for-issues 9-2-stable fix to master.
parent
228926da
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
241 additions
and
28 deletions
+241
-28
app/assets/javascripts/issue_show/components/app.vue
app/assets/javascripts/issue_show/components/app.vue
+26
-1
app/assets/javascripts/issue_show/components/description.vue
app/assets/javascripts/issue_show/components/description.vue
+0
-11
app/assets/javascripts/issue_show/components/edited.vue
app/assets/javascripts/issue_show/components/edited.vue
+56
-0
app/assets/javascripts/issue_show/index.js
app/assets/javascripts/issue_show/index.js
+10
-0
app/assets/javascripts/issue_show/stores/index.js
app/assets/javascripts/issue_show/stores/index.js
+8
-1
app/assets/stylesheets/framework/mobile.scss
app/assets/stylesheets/framework/mobile.scss
+0
-5
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+10
-3
app/helpers/application_helper.rb
app/helpers/application_helper.rb
+1
-1
app/helpers/editable_helper.rb
app/helpers/editable_helper.rb
+13
-0
app/models/concerns/editable.rb
app/models/concerns/editable.rb
+7
-0
app/models/concerns/issuable.rb
app/models/concerns/issuable.rb
+1
-0
app/models/note.rb
app/models/note.rb
+1
-0
app/models/snippet.rb
app/models/snippet.rb
+1
-0
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+2
-2
spec/helpers/editable_helper_spec.rb
spec/helpers/editable_helper_spec.rb
+22
-0
spec/javascripts/issue_show/components/app_spec.js
spec/javascripts/issue_show/components/app_spec.js
+14
-1
spec/javascripts/issue_show/components/edited_spec.js
spec/javascripts/issue_show/components/edited_spec.js
+49
-0
spec/javascripts/issue_show/mock_data.js
spec/javascripts/issue_show/mock_data.js
+9
-3
spec/models/concerns/editable_spec.rb
spec/models/concerns/editable_spec.rb
+11
-0
No files found.
app/assets/javascripts/issue_show/components/app.vue
View file @
07c984d8
...
...
@@ -5,6 +5,7 @@ import Service from '../services/index';
import
Store
from
'
../stores
'
;
import
titleComponent
from
'
./title.vue
'
;
import
descriptionComponent
from
'
./description.vue
'
;
import
editedComponent
from
'
./edited.vue
'
;
export
default
{
props
:
{
...
...
@@ -34,12 +35,30 @@ export default {
required
:
false
,
default
:
''
,
},
updatedAt
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
updatedByName
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
updatedByPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
const
store
=
new
Store
({
titleHtml
:
this
.
initialTitle
,
descriptionHtml
:
this
.
initialDescriptionHtml
,
descriptionText
:
this
.
initialDescriptionText
,
updatedAt
:
this
.
updatedAt
,
updatedByName
:
this
.
updatedByName
,
updatedByPath
:
this
.
updatedByPath
,
});
return
{
...
...
@@ -50,6 +69,7 @@ export default {
components
:
{
descriptionComponent
,
titleComponent
,
editedComponent
,
},
created
()
{
const
resource
=
new
Service
(
this
.
endpoint
);
...
...
@@ -90,7 +110,12 @@ export default {
:can-update=
"canUpdate"
:description-html=
"state.descriptionHtml"
:description-text=
"state.descriptionText"
:updated-at=
"state.updatedAt"
:task-status=
"state.taskStatus"
/>
<edited-component
v-if=
"!!state.updatedAt"
:updated-at=
"state.updatedAt"
:updated-by-name=
"state.updatedByName"
:updated-by-path=
"state.updatedByPath"
/>
</div>
</
template
>
app/assets/javascripts/issue_show/components/description.vue
View file @
07c984d8
...
...
@@ -16,10 +16,6 @@
type
:
String
,
required
:
true
,
},
updatedAt
:
{
type
:
String
,
required
:
true
,
},
taskStatus
:
{
type
:
String
,
required
:
true
,
...
...
@@ -29,7 +25,6 @@
return
{
preAnimation
:
false
,
pulseAnimation
:
false
,
timeAgoEl
:
$
(
'
.js-issue-edited-ago
'
),
};
},
watch
:
{
...
...
@@ -37,12 +32,6 @@
this
.
animateChange
();
this
.
$nextTick
(()
=>
{
const
toolTipTime
=
gl
.
utils
.
formatDate
(
this
.
updatedAt
);
this
.
timeAgoEl
.
attr
(
'
datetime
'
,
this
.
updatedAt
)
.
attr
(
'
title
'
,
toolTipTime
)
.
tooltip
(
'
fixTitle
'
);
this
.
renderGFM
();
});
},
...
...
app/assets/javascripts/issue_show/components/edited.vue
0 → 100644
View file @
07c984d8
<
script
>
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
props
:
{
updatedAt
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
updatedByName
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
updatedByPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
components
:
{
timeAgoTooltip
,
},
computed
:
{
hasUpdatedBy
()
{
return
this
.
updatedByName
&&
this
.
updatedByPath
;
},
},
};
</
script
>
<
template
>
<small
class=
"edited-text"
>
Edited
<time-ago-tooltip
v-if=
"updatedAt"
placement=
"bottom"
:time=
"updatedAt"
/>
<span
v-if=
"hasUpdatedBy"
>
by
<a
class=
"author_link"
:href=
"updatedByPath"
>
<span>
{{
updatedByName
}}
</span>
</a>
</span>
</small>
</
template
>
app/assets/javascripts/issue_show/index.js
View file @
07c984d8
...
...
@@ -12,10 +12,14 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const
issuableTitleElement
=
issuableElement
.
querySelector
(
'
.title
'
);
const
issuableDescriptionElement
=
issuableElement
.
querySelector
(
'
.wiki
'
);
const
issuableDescriptionTextarea
=
issuableElement
.
querySelector
(
'
.js-task-list-field
'
);
const
{
canUpdate
,
endpoint
,
issuableRef
,
updatedAt
,
updatedByName
,
updatedByPath
,
}
=
issuableElement
.
dataset
;
return
{
...
...
@@ -25,6 +29,9 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
initialTitle
:
issuableTitleElement
.
innerHTML
,
initialDescriptionHtml
:
issuableDescriptionElement
?
issuableDescriptionElement
.
innerHTML
:
''
,
initialDescriptionText
:
issuableDescriptionTextarea
?
issuableDescriptionTextarea
.
textContent
:
''
,
updatedAt
,
updatedByName
,
updatedByPath
,
};
},
render
(
createElement
)
{
...
...
@@ -36,6 +43,9 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
initialTitle
:
this
.
initialTitle
,
initialDescriptionHtml
:
this
.
initialDescriptionHtml
,
initialDescriptionText
:
this
.
initialDescriptionText
,
updatedAt
:
this
.
updatedAt
,
updatedByName
:
this
.
updatedByName
,
updatedByPath
:
this
.
updatedByPath
,
},
});
},
...
...
app/assets/javascripts/issue_show/stores/index.js
View file @
07c984d8
...
...
@@ -3,6 +3,9 @@ export default class Store {
titleHtml
,
descriptionHtml
,
descriptionText
,
updatedAt
,
updatedByName
,
updatedByPath
,
})
{
this
.
state
=
{
titleHtml
,
...
...
@@ -10,7 +13,9 @@ export default class Store {
descriptionHtml
,
descriptionText
,
taskStatus
:
''
,
updatedAt
:
''
,
updatedAt
,
updatedByName
,
updatedByPath
,
};
}
...
...
@@ -21,5 +26,7 @@ export default class Store {
this
.
state
.
descriptionText
=
data
.
description_text
;
this
.
state
.
taskStatus
=
data
.
task_status
;
this
.
state
.
updatedAt
=
data
.
updated_at
;
this
.
state
.
updatedByName
=
data
.
updated_by_name
;
this
.
state
.
updatedByPath
=
data
.
updated_by_path
;
}
}
app/assets/stylesheets/framework/mobile.scss
View file @
07c984d8
...
...
@@ -112,11 +112,6 @@
}
}
.issue-edited-ago
,
.note_edited_ago
{
display
:
none
;
}
aside
:not
(
.right-sidebar
)
{
display
:
none
;
}
...
...
app/controllers/projects/issues_controller.rb
View file @
07c984d8
...
...
@@ -202,14 +202,21 @@ class Projects::IssuesController < Projects::ApplicationController
def
realtime_changes
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
3_000
)
re
nder
json:
{
re
sponse
=
{
title:
view_context
.
markdown_field
(
@issue
,
:title
),
title_text:
@issue
.
title
,
description:
view_context
.
markdown_field
(
@issue
,
:description
),
description_text:
@issue
.
description
,
task_status:
@issue
.
task_status
,
updated_at:
@issue
.
updated_at
task_status:
@issue
.
task_status
}
if
@issue
.
is_edited?
response
[
:updated_at
]
=
@issue
.
updated_at
response
[
:updated_by_name
]
=
@issue
.
last_edited_by
.
name
response
[
:updated_by_path
]
=
user_path
(
@issue
.
last_edited_by
)
end
render
json:
response
end
def
create_merge_request
...
...
app/helpers/application_helper.rb
View file @
07c984d8
...
...
@@ -181,7 +181,7 @@ module ApplicationHelper
end
def
edited_time_ago_with_tooltip
(
object
,
placement:
'top'
,
html_class:
'time_ago'
,
exclude_author:
false
)
return
if
object
.
last_edited_at
==
object
.
created_at
||
object
.
last_edited_at
.
blank
?
return
unless
object
.
is_edited
?
content_tag
:small
,
class:
'edited-text'
do
output
=
content_tag
(
:span
,
'Edited '
)
...
...
app/helpers/editable_helper.rb
0 → 100644
View file @
07c984d8
module
EditableHelper
def
updated_at_by
(
editable
)
return
nil
unless
editable
.
is_edited?
{
updated_at:
editable
.
updated_at
,
updated_by:
{
name:
editable
.
last_edited_by
.
name
,
path:
user_path
(
editable
.
last_edited_by
)
}
}
end
end
app/models/concerns/editable.rb
0 → 100644
View file @
07c984d8
module
Editable
extend
ActiveSupport
::
Concern
def
is_edited?
last_edited_at
.
present?
&&
last_edited_at
!=
created_at
end
end
app/models/concerns/issuable.rb
View file @
07c984d8
...
...
@@ -15,6 +15,7 @@ module Issuable
include
Taskable
include
TimeTrackable
include
Importable
include
Editable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
...
...
app/models/note.rb
View file @
07c984d8
...
...
@@ -13,6 +13,7 @@ class Note < ActiveRecord::Base
include
AfterCommitQueue
include
ResolvableNote
include
IgnorableColumn
include
Editable
ignore_column
:original_discussion_id
...
...
app/models/snippet.rb
View file @
07c984d8
...
...
@@ -8,6 +8,7 @@ class Snippet < ActiveRecord::Base
include
Awardable
include
Mentionable
include
Spammable
include
Editable
cache_markdown_field
:title
,
pipeline: :single_line
cache_markdown_field
:content
...
...
app/views/projects/issues/show.html.haml
View file @
07c984d8
...
...
@@ -58,14 +58,14 @@
#js-issuable-app
{
"data"
=>
{
"endpoint"
=>
realtime_changes_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
"can-update"
=>
can?
(
current_user
,
:update_issue
,
@issue
).
to_s
,
"issuable-ref"
=>
@issue
.
to_reference
,
}
}
}
.
merge
(
updated_at_by
(
@issue
))
}
%h2
.title
=
markdown_field
(
@issue
,
:title
)
-
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 js-issue-edited-ago'
)
=
edited_time_ago_with_tooltip
(
@issue
,
placement:
'bottom'
,
html_class:
'issue-edited-ago js-issue-edited-ago'
)
#merge-requests
{
data:
{
url:
referenced_merge_requests_namespace_project_issue_url
(
@project
.
namespace
,
@project
,
@issue
)
}
}
// This element is filled in using JavaScript.
...
...
spec/helpers/editable_helper_spec.rb
0 → 100644
View file @
07c984d8
require
'spec_helper'
describe
EditableHelper
do
describe
'#updated_at_by'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:unedited_editable
)
{
create
(
:issue
)
}
let
(
:edited_editable
)
{
create
(
:issue
,
last_edited_by:
user
,
created_at:
3
.
days
.
ago
,
updated_at:
2
.
days
.
ago
,
last_edited_at:
2
.
days
.
ago
)
}
let
(
:edited_updated_at_by
)
do
{
updated_at:
edited_editable
.
updated_at
,
updated_by:
{
name:
user
.
name
,
path:
user_path
(
user
)
}
}
end
it
{
expect
(
helper
.
updated_at_by
(
unedited_editable
)).
to
eq
(
nil
)
}
it
{
expect
(
helper
.
updated_at_by
(
edited_editable
)).
to
eq
(
edited_updated_at_by
)
}
end
end
spec/javascripts/issue_show/components/app_spec.js
View file @
07c984d8
...
...
@@ -13,6 +13,10 @@ const issueShowInterceptor = data => (request, next) => {
}));
};
function
formatText
(
text
)
{
return
text
.
trim
().
replace
(
/
\s\s
+/g
,
'
'
);
}
describe
(
'
Issuable output
'
,
()
=>
{
document
.
body
.
innerHTML
=
'
<span id="task_status"></span>
'
;
...
...
@@ -38,12 +42,17 @@ describe('Issuable output', () => {
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
issueShowInterceptor
);
});
it
(
'
should render a title/description
and update title/description
on update
'
,
(
done
)
=>
{
it
(
'
should render a title/description
/edited and update title/description/edited
on update
'
,
(
done
)
=>
{
setTimeout
(()
=>
{
const
editedText
=
vm
.
$el
.
querySelector
(
'
.edited-text
'
);
expect
(
document
.
querySelector
(
'
title
'
).
innerText
).
toContain
(
'
this is a title (#1)
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>this is a title</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>this is a description!</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
value
).
toContain
(
'
this is a description
'
);
expect
(
formatText
(
editedText
.
innerText
)).
toMatch
(
/Edited
[\s\S]
+
?
by Some User/
);
expect
(
editedText
.
querySelector
(
'
.author_link
'
).
href
).
toMatch
(
/
\/
some_user$/
);
expect
(
editedText
.
querySelector
(
'
time
'
)).
toBeTruthy
();
Vue
.
http
.
interceptors
.
push
(
issueShowInterceptor
(
issueShowData
.
secondRequest
));
...
...
@@ -52,6 +61,10 @@ describe('Issuable output', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
).
toContain
(
'
<p>2</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.wiki
'
).
innerHTML
).
toContain
(
'
<p>42</p>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-task-list-field
'
).
value
).
toContain
(
'
42
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.edited-text
'
)).
toBeTruthy
();
expect
(
formatText
(
vm
.
$el
.
querySelector
(
'
.edited-text
'
).
innerText
)).
toMatch
(
/Edited
[\s\S]
+
?
by Other User/
);
expect
(
editedText
.
querySelector
(
'
.author_link
'
).
href
).
toMatch
(
/
\/
other_user$/
);
expect
(
editedText
.
querySelector
(
'
time
'
)).
toBeTruthy
();
done
();
});
...
...
spec/javascripts/issue_show/components/edited_spec.js
0 → 100644
View file @
07c984d8
import
Vue
from
'
vue
'
;
import
edited
from
'
~/issue_show/components/edited.vue
'
;
function
formatText
(
text
)
{
return
text
.
trim
().
replace
(
/
\s\s
+/g
,
'
'
);
}
describe
(
'
edited
'
,
()
=>
{
const
EditedComponent
=
Vue
.
extend
(
edited
);
it
(
'
should render an edited at+by string
'
,
()
=>
{
const
editedComponent
=
new
EditedComponent
({
propsData
:
{
updatedAt
:
'
2017-05-15T12:31:04.428Z
'
,
updatedByName
:
'
Some User
'
,
updatedByPath
:
'
/some_user
'
,
},
}).
$mount
();
expect
(
formatText
(
editedComponent
.
$el
.
innerText
)).
toMatch
(
/Edited
[\s\S]
+
?
by Some User/
);
expect
(
editedComponent
.
$el
.
querySelector
(
'
.author_link
'
).
href
).
toMatch
(
/
\/
some_user$/
);
expect
(
editedComponent
.
$el
.
querySelector
(
'
time
'
)).
toBeTruthy
();
});
it
(
'
if no updatedAt is provided, no time element will be rendered
'
,
()
=>
{
const
editedComponent
=
new
EditedComponent
({
propsData
:
{
updatedByName
:
'
Some User
'
,
updatedByPath
:
'
/some_user
'
,
},
}).
$mount
();
expect
(
formatText
(
editedComponent
.
$el
.
innerText
)).
toMatch
(
/Edited by Some User/
);
expect
(
editedComponent
.
$el
.
querySelector
(
'
.author_link
'
).
href
).
toMatch
(
/
\/
some_user$/
);
expect
(
editedComponent
.
$el
.
querySelector
(
'
time
'
)).
toBeFalsy
();
});
it
(
'
if no updatedByName and updatedByPath is provided, no user element will be rendered
'
,
()
=>
{
const
editedComponent
=
new
EditedComponent
({
propsData
:
{
updatedAt
:
'
2017-05-15T12:31:04.428Z
'
,
},
}).
$mount
();
expect
(
formatText
(
editedComponent
.
$el
.
innerText
)).
not
.
toMatch
(
/by Some User/
);
expect
(
editedComponent
.
$el
.
querySelector
(
'
.author_link
'
)).
toBeFalsy
();
expect
(
editedComponent
.
$el
.
querySelector
(
'
time
'
)).
toBeTruthy
();
});
});
spec/javascripts/issue_show/mock_data.js
View file @
07c984d8
...
...
@@ -5,7 +5,9 @@ export default {
description
:
'
<p>this is a description!</p>
'
,
description_text
:
'
this is a description
'
,
task_status
:
'
2 of 4 completed
'
,
updated_at
:
new
Date
().
toString
(),
updated_at
:
'
2015-05-15T12:31:04.428Z
'
,
updated_by_name
:
'
Some User
'
,
updated_by_path
:
'
/some_user
'
,
},
secondRequest
:
{
title
:
'
<p>2</p>
'
,
...
...
@@ -13,7 +15,9 @@ export default {
description
:
'
<p>42</p>
'
,
description_text
:
'
42
'
,
task_status
:
'
0 of 0 completed
'
,
updated_at
:
new
Date
().
toString
(),
updated_at
:
'
2016-05-15T12:31:04.428Z
'
,
updated_by_name
:
'
Other User
'
,
updated_by_path
:
'
/other_user
'
,
},
issueSpecRequest
:
{
title
:
'
<p>this is a title</p>
'
,
...
...
@@ -21,6 +25,8 @@ export default {
description
:
'
<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>
'
,
description_text
:
'
- [ ] Task List Item
'
,
task_status
:
'
0 of 1 completed
'
,
updated_at
:
new
Date
().
toString
(),
updated_at
:
'
2017-05-15T12:31:04.428Z
'
,
updated_by_name
:
'
Last User
'
,
updated_by_path
:
'
/last_user
'
,
},
};
spec/models/concerns/editable_spec.rb
0 → 100644
View file @
07c984d8
require
'spec_helper'
describe
Editable
do
describe
'#is_edited?'
do
let
(
:issue
)
{
create
(
:issue
,
last_edited_at:
nil
)
}
let
(
:edited_issue
)
{
create
(
:issue
,
created_at:
3
.
days
.
ago
,
last_edited_at:
2
.
days
.
ago
)
}
it
{
expect
(
issue
.
is_edited?
).
to
eq
(
false
)
}
it
{
expect
(
edited_issue
.
is_edited?
).
to
eq
(
true
)
}
end
end
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