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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
d637f87f
Commit
d637f87f
authored
Feb 09, 2018
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Makes close/reopen issue request to inside the vue app
parent
a4a47cfb
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
206 additions
and
40 deletions
+206
-40
app/assets/javascripts/issue.js
app/assets/javascripts/issue.js
+47
-28
app/assets/javascripts/notes/components/comment_form.vue
app/assets/javascripts/notes/components/comment_form.vue
+16
-12
app/assets/javascripts/notes/index.js
app/assets/javascripts/notes/index.js
+2
-0
app/assets/javascripts/notes/services/notes_service.js
app/assets/javascripts/notes/services/notes_service.js
+3
-0
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+33
-0
app/assets/javascripts/notes/stores/getters.js
app/assets/javascripts/notes/stores/getters.js
+1
-0
app/assets/javascripts/notes/stores/mutation_types.js
app/assets/javascripts/notes/stores/mutation_types.js
+4
-0
app/assets/javascripts/notes/stores/mutations.js
app/assets/javascripts/notes/stores/mutations.js
+8
-0
app/views/projects/issues/_discussion.html.haml
app/views/projects/issues/_discussion.html.haml
+2
-0
changelogs/unreleased/42923-close-issue.yml
changelogs/unreleased/42923-close-issue.yml
+5
-0
spec/javascripts/notes/helpers.js
spec/javascripts/notes/helpers.js
+12
-0
spec/javascripts/notes/mock_data.js
spec/javascripts/notes/mock_data.js
+2
-0
spec/javascripts/notes/stores/actions_spec.js
spec/javascripts/notes/stores/actions_spec.js
+71
-0
No files found.
app/assets/javascripts/issue.js
View file @
d637f87f
...
...
@@ -25,32 +25,29 @@ export default class Issue {
if
(
Issue
.
createMrDropdownWrap
)
{
this
.
createMergeRequestDropdown
=
new
CreateMergeRequestDropdown
(
Issue
.
createMrDropdownWrap
);
}
}
initIssueBtnEventListeners
()
{
const
issueFailMessage
=
'
Unable to update this issue at this time.
'
;
return
$
(
document
).
on
(
'
click
'
,
'
.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen
'
,
(
e
)
=>
{
var
$button
,
shouldSubmit
,
url
;
e
.
preventDefault
();
e
.
stopImmediatePropagation
();
$button
=
$
(
e
.
currentTarget
);
shouldSubmit
=
$button
.
hasClass
(
'
btn-comment
'
);
if
(
shouldSubmit
)
{
Issue
.
submitNoteForm
(
$button
.
closest
(
'
form
'
));
// Listen to state changes in the Vue app
document
.
addEventListener
(
'
issuable_vue_app:change
'
,
(
event
)
=>
{
this
.
updateTopState
(
event
.
detail
.
isClosed
,
event
.
detail
.
data
);
});
}
this
.
disableCloseReopenButton
(
$button
);
url
=
$button
.
attr
(
'
href
'
);
return
axios
.
put
(
url
)
.
then
(({
data
})
=>
{
/**
* This method updates the top area of the issue.
*
* Once the issue state changes, either through a click on the top area (jquery)
* or a click on the bottom area (Vue) we need to update the top area.
*
* @param {Boolean} isClosed
* @param {Array} data
* @param {String} issueFailMessage
*/
updateTopState
(
isClosed
,
data
,
issueFailMessage
=
'
Unable to update this issue at this time.
'
)
{
if
(
'
id
'
in
data
)
{
const
isClosedBadge
=
$
(
'
div.status-box-issue-closed
'
);
const
isOpenBadge
=
$
(
'
div.status-box-open
'
);
const
projectIssuesCounter
=
$
(
'
.issue_counter
'
);
if
(
'
id
'
in
data
)
{
const
isClosed
=
$button
.
hasClass
(
'
btn-close
'
);
isClosedBadge
.
toggleClass
(
'
hidden
'
,
!
isClosed
);
isOpenBadge
.
toggleClass
(
'
hidden
'
,
isClosed
);
...
...
@@ -73,6 +70,28 @@ export default class Issue {
}
else
{
flash
(
issueFailMessage
);
}
}
initIssueBtnEventListeners
()
{
const
issueFailMessage
=
'
Unable to update this issue at this time.
'
;
return
$
(
document
).
on
(
'
click
'
,
'
.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen
'
,
(
e
)
=>
{
var
$button
,
shouldSubmit
,
url
;
e
.
preventDefault
();
e
.
stopImmediatePropagation
();
$button
=
$
(
e
.
currentTarget
);
shouldSubmit
=
$button
.
hasClass
(
'
btn-comment
'
);
if
(
shouldSubmit
)
{
Issue
.
submitNoteForm
(
$button
.
closest
(
'
form
'
));
}
this
.
disableCloseReopenButton
(
$button
);
url
=
$button
.
attr
(
'
href
'
);
return
axios
.
put
(
url
)
.
then
(({
data
})
=>
{
const
isClosed
=
$button
.
hasClass
(
'
btn-close
'
);
this
.
updateTopState
(
isClosed
,
data
);
})
.
catch
(()
=>
flash
(
issueFailMessage
))
.
then
(()
=>
{
...
...
app/assets/javascripts/notes/components/comment_form.vue
View file @
d637f87f
...
...
@@ -2,6 +2,7 @@
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
...
...
@@ -30,9 +31,6 @@
return
{
note
:
''
,
noteType
:
constants
.
COMMENT
,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
issueState
:
this
.
$store
.
getters
.
getNoteableData
.
state
,
isSubmitting
:
false
,
isSubmitButtonDisabled
:
true
,
};
...
...
@@ -43,7 +41,11 @@
'
getUserData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
getIssueState
'
,
]),
issueState
()
{
return
this
.
getIssueState
;
},
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
...
...
@@ -71,8 +73,6 @@
return
{
'
btn-reopen
'
:
!
this
.
isIssueOpen
,
'
btn-close
'
:
this
.
isIssueOpen
,
'
js-note-target-close
'
:
this
.
isIssueOpen
,
'
js-note-target-reopen
'
:
!
this
.
isIssueOpen
,
};
},
markdownDocsPath
()
{
...
...
@@ -105,7 +105,7 @@
mounted
()
{
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$
(
document
).
on
(
'
issuable:change
'
,
(
e
,
isClosed
)
=>
{
this
.
issueState
=
isClosed
?
constants
.
CLOSED
:
constants
.
REOPENED
;
this
.
toggleIssueLocalState
(
isClosed
?
constants
.
CLOSED
:
constants
.
REOPENED
)
;
});
this
.
initAutoSave
();
...
...
@@ -117,6 +117,9 @@
'
stopPolling
'
,
'
restartPolling
'
,
'
removePlaceholderNotes
'
,
'
closeIssue
'
,
'
reopenIssue
'
,
'
toggleIssueLocalState
'
,
]),
setIsSubmitButtonDisabled
(
note
,
isSubmitting
)
{
if
(
!
_
.
isEmpty
(
note
)
&&
!
isSubmitting
)
{
...
...
@@ -185,12 +188,13 @@ Please check your network connection and try again.`;
}
},
toggleIssueState
()
{
this
.
issueState
=
this
.
isIssueOpen
?
constants
.
CLOSED
:
constants
.
REOPENED
;
// This is out of scope for the Notes Vue component.
// It was the shortest path to update the issue state and relevant places.
const
btnClass
=
this
.
isIssueOpen
?
'
btn-reopen
'
:
'
btn-close
'
;
$
(
`.js-btn-issue-action.
${
btnClass
}
:visible`
).
trigger
(
'
click
'
);
if
(
this
.
isIssueOpen
)
{
this
.
closeIssue
()
.
catch
(()
=>
Flash
(
__
(
'
Something went wrong while closing the issue. Please try again later
'
)));
}
else
{
this
.
reopenIssue
()
.
catch
(()
=>
Flash
(
__
(
'
Something went wrong while reopening the issue. Please try again later
'
)));
}
},
discard
(
shouldClear
=
true
)
{
// `blur` is needed to clear slash commands autocomplete cache if event fired.
...
...
app/assets/javascripts/notes/index.js
View file @
d637f87f
...
...
@@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
notesPath
:
notesDataset
.
notesPath
,
markdownDocsPath
:
notesDataset
.
markdownDocsPath
,
quickActionsDocsPath
:
notesDataset
.
quickActionsDocsPath
,
closeIssuePath
:
notesDataset
.
closeIssuePath
,
reopenIssuePath
:
notesDataset
.
reopenIssuePath
,
},
};
},
...
...
app/assets/javascripts/notes/services/notes_service.js
View file @
d637f87f
...
...
@@ -32,4 +32,7 @@ export default {
toggleAward
(
endpoint
,
data
)
{
return
Vue
.
http
.
post
(
endpoint
,
data
,
{
emulateJSON
:
true
});
},
toggleIssueState
(
endpoint
,
data
)
{
return
Vue
.
http
.
put
(
endpoint
,
data
);
},
};
app/assets/javascripts/notes/stores/actions.js
View file @
d637f87f
...
...
@@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export
const
removePlaceholderNotes
=
({
commit
})
=>
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
service
.
toggleIssueState
(
state
.
notesData
.
closeIssuePath
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
CLOSE_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
});
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
service
.
toggleIssueState
(
state
.
notesData
.
reopenIssuePath
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
REOPEN_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
});
export
const
emitStateChangedEvent
=
({
commit
},
data
)
=>
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
data
,
isClosed
:
data
.
state
===
constants
.
CLOSED
,
}
});
document
.
dispatchEvent
(
event
);
};
export
const
toggleIssueLocalState
=
({
commit
},
newState
)
=>
{
if
(
newState
===
constants
.
CLOSED
)
{
commit
(
types
.
CLOSE_ISSUE
);
}
else
if
(
newState
===
constants
.
REOPENED
)
{
commit
(
types
.
REOPEN_ISSUE
);
}
};
export
const
saveNote
=
({
commit
,
dispatch
},
noteData
)
=>
{
const
{
note
}
=
noteData
.
data
.
note
;
let
placeholderText
=
note
;
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
d637f87f
...
...
@@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export
const
getNoteableData
=
state
=>
state
.
noteableData
;
export
const
getNoteableDataByProp
=
state
=>
prop
=>
state
.
noteableData
[
prop
];
export
const
getIssueState
=
state
=>
state
.
noteableData
.
state
;
export
const
getUserData
=
state
=>
state
.
userData
||
{};
export
const
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
...
...
app/assets/javascripts/notes/stores/mutation_types.js
View file @
d637f87f
...
...
@@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export
const
TOGGLE_AWARD
=
'
TOGGLE_AWARD
'
;
export
const
TOGGLE_DISCUSSION
=
'
TOGGLE_DISCUSSION
'
;
export
const
UPDATE_NOTE
=
'
UPDATE_NOTE
'
;
// Issue
export
const
CLOSE_ISSUE
=
'
CLOSE_ISSUE
'
;
export
const
REOPEN_ISSUE
=
'
REOPEN_ISSUE
'
;
app/assets/javascripts/notes/stores/mutations.js
View file @
d637f87f
...
...
@@ -152,4 +152,12 @@ export default {
noteObj
.
notes
.
splice
(
noteObj
.
notes
.
indexOf
(
comment
),
1
,
note
);
}
},
[
types
.
CLOSE_ISSUE
](
state
)
{
Object
.
assign
(
state
.
noteableData
,
{
state
:
constants
.
CLOSED
});
},
[
types
.
REOPEN_ISSUE
](
state
)
{
Object
.
assign
(
state
.
noteableData
,
{
state
:
constants
.
REOPENED
});
},
};
app/views/projects/issues/_discussion.html.haml
View file @
d637f87f
...
...
@@ -12,6 +12,8 @@
markdown_docs_path:
help_page_path
(
'user/markdown'
),
quick_actions_docs_path:
help_page_path
(
'user/project/quick_actions'
),
notes_path:
notes_url
,
close_issue_path:
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
reopen_issue_path:
issue_path
(
@issue
,
issue:
{
state_event: :reopen
},
format:
'json'
),
last_fetched_at:
Time
.
now
.
to_i
,
noteable_data:
serialize_issuable
(
@issue
),
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
,
only_path:
true
).
to_json
}
}
changelogs/unreleased/42923-close-issue.yml
0 → 100644
View file @
d637f87f
---
title
:
Fix close button on issues not working on mobile
merge_request
:
author
:
type
:
fixed
spec/javascripts/notes/helpers.js
0 → 100644
View file @
d637f87f
// eslint-disable-next-line import/prefer-default-export
export
const
resetStore
=
(
store
)
=>
{
store
.
replaceState
({
notes
:
[],
targetNoteHash
:
null
,
lastFetchedAt
:
null
,
notesData
:
{},
userData
:
{},
noteableData
:
{},
});
};
spec/javascripts/notes/mock_data.js
View file @
d637f87f
...
...
@@ -7,6 +7,8 @@ export const notesDataMock = {
notesPath
:
'
/gitlab-org/gitlab-ce/noteable/issue/98/notes
'
,
quickActionsDocsPath
:
'
/help/user/project/quick_actions
'
,
registerPath
:
'
/users/sign_in?redirect_to_referer=yes#register-pane
'
,
closeIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close
'
,
reopenIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen
'
,
};
export
const
userDataMock
=
{
...
...
spec/javascripts/notes/stores/actions_spec.js
View file @
d637f87f
import
Vue
from
'
vue
'
;
import
_
from
'
underscore
'
;
import
*
as
actions
from
'
~/notes/stores/actions
'
;
import
store
from
'
~/notes/stores
'
;
import
testAction
from
'
../../helpers/vuex_action_helper
'
;
import
{
resetStore
}
from
'
../helpers
'
;
import
{
discussionMock
,
notesDataMock
,
userDataMock
,
noteableDataMock
,
individualNote
}
from
'
../mock_data
'
;
describe
(
'
Actions Notes Store
'
,
()
=>
{
afterEach
(()
=>
{
resetStore
(
store
);
});
describe
(
'
setNotesData
'
,
()
=>
{
it
(
'
should set received notes data
'
,
(
done
)
=>
{
testAction
(
actions
.
setNotesData
,
null
,
{
notesData
:
{}
},
[
...
...
@@ -58,4 +66,67 @@ describe('Actions Notes Store', () => {
],
done
);
});
});
describe
(
'
async methods
'
,
()
=>
{
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({}),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
describe
(
'
closeIssue
'
,
()
=>
{
it
(
'
sets state as closed
'
,
(
done
)
=>
{
store
.
dispatch
(
'
closeIssue
'
,
{
notesData
:
{
closeIssuePath
:
''
}
})
.
then
(()
=>
{
expect
(
store
.
state
.
noteableData
.
state
).
toEqual
(
'
closed
'
);
done
();
})
.
catch
(
done
.
fail
);
});
});
describe
(
'
reopenIssue
'
,
()
=>
{
it
(
'
sets state as reopened
'
,
(
done
)
=>
{
store
.
dispatch
(
'
reopenIssue
'
,
{
notesData
:
{
reopenIssuePath
:
''
}
})
.
then
(()
=>
{
expect
(
store
.
state
.
noteableData
.
state
).
toEqual
(
'
reopened
'
);
done
();
})
.
catch
(
done
.
fail
);
});
});
});
describe
(
'
emitStateChangedEvent
'
,
()
=>
{
it
(
'
emits an event on the document
'
,
()
=>
{
document
.
addEventListener
(
'
issuable_vue_app:change
'
,
(
event
)
=>
{
expect
(
event
.
detail
.
data
).
toEqual
({
id
:
'
1
'
,
state
:
'
closed
'
});
expect
(
event
.
detail
.
isClosed
).
toEqual
(
true
);
});
store
.
dispatch
(
'
emitStateChangedEvent
'
,
{
id
:
'
1
'
,
state
:
'
closed
'
});
});
});
describe
(
'
toggleIssueLocalState
'
,
()
=>
{
it
(
'
sets issue state as closed
'
,
(
done
)
=>
{
testAction
(
actions
.
toggleIssueLocalState
,
'
closed
'
,
{},
[
{
type
:
'
CLOSE_ISSUE
'
,
payload
:
'
closed
'
},
],
done
);
});
it
(
'
sets issue state as reopened
'
,
(
done
)
=>
{
testAction
(
actions
.
toggleIssueLocalState
,
'
reopened
'
,
{},
[
{
type
:
'
REOPEN_ISSUE
'
,
payload
:
'
reopened
'
},
],
done
);
});
});
});
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