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
05728e78
Commit
05728e78
authored
Oct 26, 2017
by
Phil Hughes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[WIP] Move multi-file editor store to Vuex
parent
95e56a61
Changes
33
Show whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
910 additions
and
584 deletions
+910
-584
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+1
-0
app/assets/javascripts/repo/components/new_branch_form.vue
app/assets/javascripts/repo/components/new_branch_form.vue
+21
-28
app/assets/javascripts/repo/components/repo.vue
app/assets/javascripts/repo/components/repo.vue
+18
-60
app/assets/javascripts/repo/components/repo_commit_section.vue
...ssets/javascripts/repo/components/repo_commit_section.vue
+61
-106
app/assets/javascripts/repo/components/repo_edit_button.vue
app/assets/javascripts/repo/components/repo_edit_button.vue
+41
-34
app/assets/javascripts/repo/components/repo_editor.vue
app/assets/javascripts/repo/components/repo_editor.vue
+54
-93
app/assets/javascripts/repo/components/repo_file.vue
app/assets/javascripts/repo/components/repo_file.vue
+10
-9
app/assets/javascripts/repo/components/repo_file_buttons.vue
app/assets/javascripts/repo/components/repo_file_buttons.vue
+15
-30
app/assets/javascripts/repo/components/repo_loading_file.vue
app/assets/javascripts/repo/components/repo_loading_file.vue
+6
-4
app/assets/javascripts/repo/components/repo_prev_directory.vue
...ssets/javascripts/repo/components/repo_prev_directory.vue
+12
-16
app/assets/javascripts/repo/components/repo_preview.vue
app/assets/javascripts/repo/components/repo_preview.vue
+10
-23
app/assets/javascripts/repo/components/repo_sidebar.vue
app/assets/javascripts/repo/components/repo_sidebar.vue
+25
-91
app/assets/javascripts/repo/components/repo_tab.vue
app/assets/javascripts/repo/components/repo_tab.vue
+9
-15
app/assets/javascripts/repo/components/repo_tabs.vue
app/assets/javascripts/repo/components/repo_tabs.vue
+6
-6
app/assets/javascripts/repo/helpers/monaco_loader_helper.js
app/assets/javascripts/repo/helpers/monaco_loader_helper.js
+0
-4
app/assets/javascripts/repo/index.js
app/assets/javascripts/repo/index.js
+42
-40
app/assets/javascripts/repo/mixins/repo_mixin.js
app/assets/javascripts/repo/mixins/repo_mixin.js
+0
-17
app/assets/javascripts/repo/services/index.js
app/assets/javascripts/repo/services/index.js
+28
-0
app/assets/javascripts/repo/stores/actions.js
app/assets/javascripts/repo/stores/actions.js
+94
-0
app/assets/javascripts/repo/stores/actions/branch.js
app/assets/javascripts/repo/stores/actions/branch.js
+20
-0
app/assets/javascripts/repo/stores/actions/file.js
app/assets/javascripts/repo/stores/actions/file.js
+50
-0
app/assets/javascripts/repo/stores/actions/tree.js
app/assets/javascripts/repo/stores/actions/tree.js
+70
-0
app/assets/javascripts/repo/stores/getters.js
app/assets/javascripts/repo/stores/getters.js
+39
-0
app/assets/javascripts/repo/stores/index.js
app/assets/javascripts/repo/stores/index.js
+15
-0
app/assets/javascripts/repo/stores/mutation_types.js
app/assets/javascripts/repo/stores/mutation_types.js
+26
-0
app/assets/javascripts/repo/stores/mutations.js
app/assets/javascripts/repo/stores/mutations.js
+54
-0
app/assets/javascripts/repo/stores/mutations/branch.js
app/assets/javascripts/repo/stores/mutations/branch.js
+9
-0
app/assets/javascripts/repo/stores/mutations/file.js
app/assets/javascripts/repo/stores/mutations/file.js
+50
-0
app/assets/javascripts/repo/stores/mutations/tree.js
app/assets/javascripts/repo/stores/mutations/tree.js
+27
-0
app/assets/javascripts/repo/stores/repo_store.js
app/assets/javascripts/repo/stores/repo_store.js
+0
-7
app/assets/javascripts/repo/stores/state.js
app/assets/javascripts/repo/stores/state.js
+21
-0
app/assets/javascripts/repo/stores/utils.js
app/assets/javascripts/repo/stores/utils.js
+73
-0
app/views/shared/repo/_repo.html.haml
app/views/shared/repo/_repo.html.haml
+3
-1
No files found.
app/assets/javascripts/api.js
View file @
05728e78
...
...
@@ -16,6 +16,7 @@ const Api = {
usersPath
:
'
/api/:version/users.json
'
,
commitPath
:
'
/api/:version/projects/:id/repository/commits
'
,
branchSinglePath
:
'
/api/:version/projects/:id/repository/branches/:branch
'
,
createBranchPath
:
'
/api/:version/projects/:id/repository/branches
'
,
group
(
groupId
,
callback
)
{
const
url
=
Api
.
buildUrl
(
Api
.
groupPath
)
...
...
app/assets/javascripts/repo/components/new_branch_form.vue
View file @
05728e78
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
flash
,
{
hideFlash
}
from
'
../../flash
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
eventHub
from
'
../event_hub
'
;
export
default
{
components
:
{
loadingIcon
,
},
props
:
{
currentBranch
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
branchName
:
''
,
...
...
@@ -20,11 +14,17 @@
};
},
computed
:
{
...
mapState
([
'
currentBranch
'
,
]),
btnDisabled
()
{
return
this
.
loading
||
this
.
branchName
===
''
;
},
},
methods
:
{
...
mapActions
([
'
createNewBranch
'
,
]),
toggleDropdown
()
{
this
.
$dropdown
.
dropdown
(
'
toggle
'
);
},
...
...
@@ -38,19 +38,21 @@
hideFlash
(
flashEl
,
false
);
}
eventHub
.
$emit
(
'
createNewBranch
'
,
this
.
branchName
);
},
showErrorMessage
(
message
)
{
this
.
loading
=
false
;
flash
(
message
,
'
alert
'
,
this
.
$el
);
},
createdNewBranch
(
newBranchName
)
{
this
.
createNewBranch
(
this
.
branchName
)
.
then
(()
=>
{
this
.
loading
=
false
;
this
.
branchName
=
''
;
if
(
this
.
dropdownText
)
{
this
.
dropdownText
.
textContent
=
newBranchName
;
this
.
dropdownText
.
textContent
=
this
.
currentBranch
;
}
this
.
toggleDropdown
();
})
.
catch
(
res
=>
res
.
json
().
then
((
data
)
=>
{
this
.
loading
=
false
;
flash
(
data
.
message
,
'
alert
'
,
this
.
$el
);
}));
},
},
created
()
{
...
...
@@ -59,15 +61,6 @@
// text element is outside Vue app
this
.
dropdownText
=
document
.
querySelector
(
'
.project-refs-form .dropdown-toggle-text
'
);
eventHub
.
$on
(
'
createNewBranchSuccess
'
,
this
.
createdNewBranch
);
eventHub
.
$on
(
'
createNewBranchError
'
,
this
.
showErrorMessage
);
eventHub
.
$on
(
'
toggleNewBranchDropdown
'
,
this
.
toggleDropdown
);
},
destroyed
()
{
eventHub
.
$off
(
'
createNewBranchSuccess
'
,
this
.
createdNewBranch
);
eventHub
.
$off
(
'
toggleNewBranchDropdown
'
,
this
.
toggleDropdown
);
eventHub
.
$off
(
'
createNewBranchError
'
,
this
.
showErrorMessage
);
},
};
</
script
>
...
...
app/assets/javascripts/repo/components/repo.vue
View file @
05728e78
<
script
>
import
{
mapState
,
mapGetters
}
from
'
vuex
'
;
import
RepoSidebar
from
'
./repo_sidebar.vue
'
;
import
RepoCommitSection
from
'
./repo_commit_section.vue
'
;
import
RepoTabs
from
'
./repo_tabs.vue
'
;
import
RepoFileButtons
from
'
./repo_file_buttons.vue
'
;
import
RepoPreview
from
'
./repo_preview.vue
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
import
PopupDialog
from
'
../../vue_shared/components/popup_dialog.vue
'
;
import
Store
from
'
../stores/repo_store
'
;
import
Helper
from
'
../helpers/repo_helper
'
;
import
Service
from
'
../services/repo_service
'
;
import
MonacoLoaderHelper
from
'
../helpers/monaco_loader_helper
'
;
import
eventHub
from
'
../event_hub
'
;
export
default
{
data
()
{
return
Store
;
computed
:
{
...
mapState
([
'
currentBlobView
'
,
'
editMode
'
,
]),
...
mapGetters
([
'
isMini
'
,
'
changedFiles
'
,
]),
},
mixins
:
[
RepoMixin
],
components
:
{
RepoSidebar
,
RepoTabs
,
RepoFileButtons
,
'
repo-editor
'
:
MonacoLoaderHelper
.
repoEditorLoader
,
RepoCommitSection
,
PopupDialog
,
RepoPreview
,
},
created
()
{
eventHub
.
$on
(
'
createNewBranch
'
,
this
.
createNewBranch
);
},
mounted
()
{
Helper
.
getContent
().
catch
(
Helper
.
loadingError
);
},
destroyed
()
{
eventHub
.
$off
(
'
createNewBranch
'
,
this
.
createNewBranch
);
},
methods
:
{
getCurrentLocation
()
{
return
location
.
href
;
},
toggleDialogOpen
(
toggle
)
{
this
.
dialog
.
open
=
toggle
;
},
window
.
onbeforeunload
=
(
e
)
=>
{
const
event
=
e
||
window
.
event
;
dialogSubmitted
(
status
)
{
this
.
toggleDialogOpen
(
false
);
this
.
dialog
.
status
=
status
;
if
(
!
this
.
changedFiles
.
length
)
return
undefined
;
// remove tmp files
Helper
.
removeAllTmpFiles
(
'
openedFiles
'
);
Helper
.
removeAllTmpFiles
(
'
files
'
);
},
toggleBlobView
:
Store
.
toggleBlobView
,
createNewBranch
(
branch
)
{
Service
.
createBranch
({
branch
,
ref
:
Store
.
currentBranch
,
}).
then
((
res
)
=>
{
const
newBranchName
=
res
.
data
.
name
;
const
newUrl
=
this
.
getCurrentLocation
().
replace
(
Store
.
currentBranch
,
newBranchName
);
Store
.
currentBranch
=
newBranchName
;
if
(
event
)
event
.
returnValue
=
'
Are you sure you want to lose unsaved changes?
'
;
history
.
pushState
({
key
:
Helper
.
key
},
''
,
newUrl
);
eventHub
.
$emit
(
'
createNewBranchSuccess
'
,
newBranchName
);
eventHub
.
$emit
(
'
toggleNewBranchDropdown
'
);
}).
catch
((
err
)
=>
{
eventHub
.
$emit
(
'
createNewBranchError
'
,
err
.
response
.
data
.
message
);
});
},
// For Safari
return
'
Are you sure you want to lose unsaved changes?
'
;
};
},
};
</
script
>
...
...
@@ -88,15 +55,6 @@ export default {
<repo-file-buttons/>
</div>
</div>
<repo-commit-section/>
<popup-dialog
v-show=
"dialog.open"
:primary-button-label=
"__('Discard changes')"
kind=
"warning"
:title=
"__('Are you sure?')"
:text=
"__('Are you sure you want to discard your changes?')"
@
toggle=
"toggleDialogOpen"
@
submit=
"dialogSubmitted"
/>
<repo-commit-section
v-if=
"changedFiles.length"
/>
</div>
</
template
>
app/assets/javascripts/repo/components/repo_commit_section.vue
View file @
05728e78
<
script
>
import
Flash
from
'
../../flash
'
;
import
Store
from
'
../stores/repo_store
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
import
Service
from
'
../services/repo_service
'
;
import
{
mapGetters
,
mapState
,
mapActions
}
from
'
vuex
'
;
import
PopupDialog
from
'
../../vue_shared/components/popup_dialog.vue
'
;
import
{
visitUrl
}
from
'
../../lib/utils/url_utility
'
;
import
{
n__
}
from
'
../../locale
'
;
export
default
{
mixins
:
[
RepoMixin
],
data
()
{
return
Store
;
},
components
:
{
PopupDialog
,
},
computed
:
{
showCommitable
()
{
return
this
.
isCommitable
&&
this
.
changedFiles
.
length
;
},
branchPaths
()
{
return
this
.
changedFiles
.
map
(
f
=>
f
.
path
);
data
()
{
return
{
showNewBranchDialog
:
false
,
submitCommitsLoading
:
false
,
startNewMR
:
false
,
commitMessage
:
''
,
};
},
cantCommitYet
()
{
computed
:
{
...
mapState
([
'
currentBranch
'
,
]),
...
mapGetters
([
'
changedFiles
'
,
]),
commitButtonDisabled
()
{
return
!
this
.
commitMessage
||
this
.
submitCommitsLoading
;
},
filePluralize
()
{
return
this
.
changedFiles
.
length
>
1
?
'
files
'
:
'
file
'
;
commitButtonText
()
{
return
n__
(
'
Commit %d file
'
,
'
Commit %d files
'
,
this
.
changedFiles
.
length
);
},
},
methods
:
{
commitToNewBranch
(
status
)
{
if
(
status
)
{
this
.
showNewBranchDialog
=
false
;
this
.
tryCommit
(
null
,
true
,
true
);
}
else
{
// reset the state
}
},
...
mapActions
([
'
checkCommitStatus
'
,
'
commitChanges
'
,
]),
makeCommit
(
newBranch
=
false
)
{
const
createNewBranch
=
newBranch
||
this
.
startNewMR
;
makeCommit
(
newBranch
)
{
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const
commitMessage
=
this
.
commitMessage
;
const
actions
=
this
.
changedFiles
.
map
(
f
=>
({
const
payload
=
{
branch
:
createNewBranch
?
`
${
this
.
currentBranch
}
-
${
new
Date
().
getTime
().
toString
()}
`
:
this
.
currentBranch
,
commit_message
:
this
.
commitMessage
,
actions
:
this
.
changedFiles
.
map
(
f
=>
({
action
:
f
.
tempFile
?
'
create
'
:
'
update
'
,
file_path
:
f
.
path
,
content
:
f
.
newContent
,
}));
const
branch
=
newBranch
?
`
${
this
.
currentBranch
}
-
${
this
.
currentShortHash
}
`
:
this
.
currentBranch
;
const
payload
=
{
branch
,
commit_message
:
commitMessage
,
actions
,
content
:
f
.
content
,
})),
start_branch
:
createNewBranch
?
this
.
currentBranch
:
undefined
,
};
if
(
newBranch
)
{
payload
.
start_branch
=
this
.
currentBranch
;
}
Service
.
commitFiles
(
payload
)
this
.
showNewBranchDialog
=
false
;
this
.
submitCommitsLoading
=
true
;
this
.
commitChanges
({
payload
,
newMr
:
this
.
startNewMR
})
.
then
(()
=>
{
this
.
resetCommitState
();
if
(
this
.
startNewMR
)
{
this
.
redirectToNewMr
(
branch
);
}
else
{
this
.
redirectToBranch
(
branch
);
}
this
.
submitCommitsLoading
=
false
;
})
.
catch
(()
=>
{
Flash
(
'
An error occurred while committing your changes
'
)
;
this
.
submitCommitsLoading
=
false
;
});
},
tryCommit
(
e
,
skipBranchCheck
=
false
,
newBranch
=
false
)
{
tryCommit
()
{
this
.
submitCommitsLoading
=
true
;
if
(
skipBranchCheck
)
{
this
.
makeCommit
(
newBranch
);
this
.
checkCommitStatus
()
.
then
((
branchChanged
)
=>
{
if
(
branchChanged
)
{
this
.
showNewBranchDialog
=
true
;
}
else
{
Store
.
setBranchHash
()
.
then
(()
=>
{
if
(
Store
.
branchChanged
)
{
Store
.
showNewBranchDialog
=
true
;
return
;
this
.
makeCommit
();
}
this
.
makeCommit
(
newBranch
);
})
.
catch
(()
=>
{
this
.
submitCommitsLoading
=
false
;
Flash
(
'
An error occurred while committing your changes
'
);
});
}
},
redirectToNewMr
(
branch
)
{
visitUrl
(
this
.
newMrTemplateUrl
.
replace
(
'
{{source_branch}}
'
,
branch
));
},
redirectToBranch
(
branch
)
{
visitUrl
(
this
.
customBranchURL
.
replace
(
'
{{branch}}
'
,
branch
));
},
resetCommitState
()
{
this
.
submitCommitsLoading
=
false
;
this
.
openedFiles
=
this
.
openedFiles
.
map
((
file
)
=>
{
const
f
=
file
;
f
.
changed
=
false
;
return
f
;
});
this
.
changedFiles
=
[];
this
.
commitMessage
=
''
;
this
.
editMode
=
false
;
window
.
scrollTo
(
0
,
0
);
},
},
};
</
script
>
<
template
>
<div
v-if=
"showCommitable"
id=
"commit-area"
>
<div
id=
"commit-area"
>
<popup-dialog
v-if=
"showNewBranchDialog"
:primary-button-label=
"__('Create new branch')"
kind=
"primary"
:title=
"__('Branch has changed')"
:text=
"__('This branch has changed since you started editing. Would you like to create a new branch?')"
@
submit=
"commitToNewBranch"
@
toggle=
"showNewBranchDialog = false"
@
submit=
"makeCommit(true)"
/>
<form
class=
"form-horizontal"
@
submit.prevent=
"tryCommit"
>
@
submit.prevent=
"tryCommit
()
"
>
<fieldset>
<div
class=
"form-group"
>
<label
class=
"col-md-4 control-label staged-files"
>
...
...
@@ -144,10 +100,10 @@ export default {
<div
class=
"col-md-6"
>
<ul
class=
"list-unstyled changed-files"
>
<li
v-for=
"
branchPath in branchPath
s"
:key=
"
branchPath
"
>
v-for=
"
(file, index) in changedFile
s"
:key=
"
index
"
>
<span
class=
"help-block"
>
{{
branchPath
}}
{{
file
.
path
}}
</span>
</li>
</ul>
...
...
@@ -182,9 +138,8 @@ export default {
</div>
<div
class=
"col-md-offset-4 col-md-6"
>
<button
ref=
"submitCommit"
type=
"submit"
:disabled=
"c
antCommitYet
"
:disabled=
"c
ommitButtonDisabled
"
class=
"btn btn-success"
>
<i
v-if=
"submitCommitsLoading"
...
...
@@ -193,7 +148,7 @@ export default {
aria-label=
"loading"
>
</i>
<span
class=
"commit-summary"
>
Commit
{{
changedFiles
.
length
}}
{{
filePluralize
}}
{{
commitButtonText
}}
</span>
</button>
</div>
...
...
app/assets/javascripts/repo/components/repo_edit_button.vue
View file @
05728e78
<
script
>
import
Store
from
'
../stores/repo_store
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
import
{
mapGetters
,
mapActions
,
mapState
}
from
'
vuex
'
;
import
popupDialog
from
'
../../vue_shared/components/popup_dialog.vue
'
;
export
default
{
data
()
{
return
Store
;
components
:
{
popupDialog
,
},
mixins
:
[
RepoMixin
],
computed
:
{
...
mapState
([
'
editMode
'
,
'
discardPopupOpen
'
,
]),
...
mapGetters
([
'
canEditFile
'
,
]),
buttonLabel
()
{
return
this
.
editMode
?
this
.
__
(
'
Cancel edit
'
)
:
this
.
__
(
'
Edit
'
);
},
showButton
()
{
return
this
.
isCommitable
&&
!
this
.
activeFile
.
render_error
&&
!
this
.
binary
&&
this
.
openedFiles
.
length
;
},
},
methods
:
{
editCancelClicked
()
{
if
(
this
.
changedFiles
.
length
)
{
this
.
dialog
.
open
=
true
;
return
;
}
this
.
editMode
=
!
this
.
editMode
;
Store
.
toggleBlobView
();
},
...
mapActions
([
'
toggleEditMode
'
,
'
closeDiscardPopup
'
,
]),
},
};
</
script
>
<
template
>
<button
v-if=
"showButton"
<div>
<button
v-if=
"canEditFile"
class=
"btn btn-default"
type=
"button"
@
click.prevent=
"editCancelClicked
"
>
@
click.prevent=
"toggleEditMode()
"
>
<i
v-if=
"!editMode"
class=
"fa fa-pencil"
...
...
@@ -46,5 +42,16 @@ export default {
<span>
{{
buttonLabel
}}
</span>
</button>
</button>
<popup-dialog
v-if=
"discardPopupOpen"
class=
"text-left"
:primary-button-label=
"__('Discard changes')"
kind=
"warning"
:title=
"__('Are you sure?')"
:text=
"__('Are you sure you want to discard your changes?')"
@
toggle=
"closeDiscardPopup"
@
submit=
"toggleEditMode(true)"
/>
</div>
</
template
>
app/assets/javascripts/repo/components/repo_editor.vue
View file @
05728e78
<
script
>
/* global monaco */
import
Store
from
'
../stores/repo_store
'
;
import
Service
from
'
../services/repo_service
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
Helper
from
'
../helpers/repo_helper
'
;
import
flash
from
'
../../flash
'
;
const
RepoEditor
=
{
data
()
{
return
Store
;
},
export
default
{
destroyed
()
{
if
(
Helper
.
monacoInstance
)
{
Helper
.
monacoInstance
.
destroy
();
if
(
this
.
monacoInstance
)
{
this
.
monacoInstance
.
destroy
();
}
},
mounted
()
{
Service
.
getRaw
(
this
.
activeFile
)
.
then
((
rawResponse
)
=>
{
Store
.
blobRaw
=
rawResponse
.
data
;
Store
.
activeFile
.
plain
=
rawResponse
.
data
;
this
.
initMonaco
();
},
methods
:
{
...
mapActions
([
'
getRawFileData
'
,
'
changeFileContent
'
,
]),
initMonaco
()
{
if
(
this
.
monacoInstance
)
{
this
.
monacoInstance
.
setModel
(
null
);
}
const
monacoInstance
=
Helper
.
monaco
.
editor
.
create
(
this
.
$el
,
{
this
.
getRawFileData
(
this
.
activeFile
)
.
then
(()
=>
{
if
(
!
this
.
monacoInstance
)
{
this
.
monacoInstance
=
Helper
.
monaco
.
editor
.
create
(
this
.
$el
,
{
model
:
null
,
readOnly
:
false
,
contextmenu
:
true
,
scrollBeyondLastLine
:
false
,
});
Helper
.
monacoInstance
=
monacoInstance
;
this
.
languages
=
Helper
.
monaco
.
languages
.
getLanguages
()
;
this
.
addMonacoEvents
();
}
this
.
setupEditor
();
})
.
catch
(
Helper
.
loadingError
);
.
catch
(()
=>
flash
(
'
Error setting up monaco. Please try again.
'
)
);
},
methods
:
{
setupEditor
()
{
this
.
showHide
();
const
foundLang
=
this
.
languages
.
find
(
lang
=>
lang
.
extensions
&&
lang
.
extensions
.
indexOf
(
this
.
activeFileExtension
)
===
0
,
);
const
newModel
=
Helper
.
monaco
.
editor
.
createModel
(
this
.
activeFile
.
raw
,
foundLang
?
foundLang
.
id
:
'
plaintext
'
,
);
Helper
.
setMonacoModelFromLanguage
(
);
this
.
monacoInstance
.
setModel
(
newModel
);
},
showHide
()
{
if
(
!
this
.
openedFiles
.
length
||
(
this
.
binary
&&
!
this
.
activeFile
.
raw
))
{
this
.
$el
.
style
.
display
=
'
none
'
;
}
else
{
this
.
$el
.
style
.
display
=
'
inline-block
'
;
}
},
addMonacoEvents
()
{
Helper
.
monacoInstance
.
onMouseUp
(
this
.
onMonacoEditorMouseUp
);
Helper
.
monacoInstance
.
onKeyUp
(
this
.
onMonacoEditorKeysPressed
.
bind
(
this
));
},
onMonacoEditorKeysPressed
()
{
Store
.
setActiveFileContents
(
Helper
.
monacoInstance
.
getValue
());
},
onMonacoEditorMouseUp
(
e
)
{
if
(
!
e
.
target
.
position
)
return
;
const
lineNumber
=
e
.
target
.
position
.
lineNumber
;
if
(
e
.
target
.
element
.
classList
.
contains
(
'
line-numbers
'
))
{
location
.
hash
=
`L
${
lineNumber
}
`
;
Store
.
setActiveLine
(
lineNumber
);
}
},
},
watch
:
{
dialog
:
{
handler
(
obj
)
{
const
newObj
=
obj
;
if
(
newObj
.
status
)
{
newObj
.
status
=
false
;
this
.
openedFiles
=
this
.
openedFiles
.
map
((
file
)
=>
{
const
f
=
file
;
if
(
f
.
active
)
{
this
.
blobRaw
=
f
.
plain
;
}
f
.
changed
=
false
;
delete
f
.
newContent
;
return
f
;
this
.
monacoInstance
.
onKeyUp
(()
=>
{
this
.
changeFileContent
({
file
:
this
.
activeFile
,
content
:
this
.
monacoInstance
.
getValue
(),
});
});
this
.
editMode
=
false
;
Store
.
toggleBlobView
();
}
},
deep
:
true
,
},
blobRaw
()
{
if
(
Helper
.
monacoInstance
)
{
this
.
setupEditor
();
}
},
activeLine
()
{
if
(
Helper
.
monacoInstance
)
{
Helper
.
monacoInstance
.
setPosition
({
lineNumber
:
this
.
activeLine
,
column
:
1
,
});
watch
:
{
activeFile
(
oldVal
,
newVal
)
{
if
(
newVal
.
active
)
{
this
.
initMonaco
();
}
},
},
computed
:
{
...
mapGetters
([
'
activeFile
'
,
'
activeFileExtension
'
,
]),
shouldHideEditor
()
{
return
!
this
.
openedFiles
.
length
||
(
this
.
binary
&&
!
this
.
activeFile
.
raw
)
;
return
this
.
activeFile
.
binary
&&
!
this
.
activeFile
.
raw
;
},
},
};
export
default
RepoEditor
;
</
script
>
<
template
>
...
...
app/assets/javascripts/repo/components/repo_file.vue
View file @
05728e78
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
timeAgoMixin
from
'
../../vue_shared/mixins/timeago
'
;
import
eventHub
from
'
../event_hub
'
;
import
repoMixin
from
'
../mixins/repo_mixin
'
;
export
default
{
mixins
:
[
repoMixin
,
timeAgoMixin
,
],
props
:
{
...
...
@@ -15,13 +13,15 @@
},
},
computed
:
{
...
mapGetters
([
'
isMini
'
,
]),
fileIcon
()
{
const
classObj
=
{
return
{
'
fa-spinner fa-spin
'
:
this
.
file
.
loading
,
[
this
.
file
.
icon
]:
!
this
.
file
.
loading
,
'
fa-folder-open
'
:
!
this
.
file
.
loading
&&
this
.
file
.
opened
,
};
return
classObj
;
},
levelIndentation
()
{
return
{
...
...
@@ -33,9 +33,10 @@
},
},
methods
:
{
linkClicked
(
file
)
{
eventHub
.
$emit
(
'
fileNameClicked
'
,
file
);
},
...
mapActions
([
'
getTreeData
'
,
'
clickedTreeRow
'
,
]),
},
};
</
script
>
...
...
@@ -43,7 +44,7 @@
<
template
>
<tr
class=
"file"
@
click.prevent=
"
linkClicked
(file)"
>
@
click.prevent=
"
clickedTreeRow
(file)"
>
<td>
<i
class=
"fa fa-fw file-icon"
...
...
app/assets/javascripts/repo/components/repo_file_buttons.vue
View file @
05728e78
<
script
>
import
Store
from
'
../stores/repo_store
'
;
import
Helper
from
'
../helpers/repo_helper
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
const
RepoFileButtons
=
{
data
()
{
return
Store
;
},
mixins
:
[
RepoMixin
],
import
{
mapGetters
}
from
'
vuex
'
;
export
default
{
computed
:
{
...
mapGetters
([
'
activeFile
'
,
]),
showButtons
()
{
return
this
.
activeFile
.
raw
_p
ath
||
this
.
activeFile
.
blame
_p
ath
||
this
.
activeFile
.
commits
_p
ath
||
return
this
.
activeFile
.
raw
P
ath
||
this
.
activeFile
.
blame
P
ath
||
this
.
activeFile
.
commits
P
ath
||
this
.
activeFile
.
permalink
;
},
rawDownloadButtonLabel
()
{
return
this
.
binary
?
'
Download
'
:
'
Raw
'
;
},
canPreview
()
{
return
Helper
.
isRenderable
();
return
this
.
activeFile
.
binary
?
'
Download
'
:
'
Raw
'
;
},
},
methods
:
{
rawPreviewToggle
:
Store
.
toggleRawPreview
,
},
};
export
default
RepoFileButtons
;
</
script
>
<
template
>
...
...
@@ -40,11 +25,11 @@ export default RepoFileButtons;
class=
"repo-file-buttons"
>
<a
:href=
"activeFile.raw
_p
ath"
:href=
"activeFile.raw
P
ath"
target=
"_blank"
class=
"btn btn-default raw"
rel=
"noopener noreferrer"
>
{{
rawDownloadButtonLabel
}}
{{
rawDownloadButtonLabel
}}
</a>
<div
...
...
@@ -52,12 +37,12 @@ export default RepoFileButtons;
role=
"group"
aria-label=
"File actions"
>
<a
:href=
"activeFile.blame
_p
ath"
:href=
"activeFile.blame
P
ath"
class=
"btn btn-default blame"
>
Blame
</a>
<a
:href=
"activeFile.commits
_p
ath"
:href=
"activeFile.commits
P
ath"
class=
"btn btn-default history"
>
History
</a>
...
...
@@ -68,12 +53,12 @@ export default RepoFileButtons;
</a>
</div>
<a
<
!--
<
a
v-if=
"canPreview"
href=
"#"
@
click.prevent=
"rawPreviewToggle"
class=
"btn btn-default preview"
>
{{
activeFileLabel
}}
</a>
</a>
-->
</div>
</
template
>
app/assets/javascripts/repo/components/repo_loading_file.vue
View file @
05728e78
<
script
>
import
repoMixin
from
'
../mixins/repo_mixin
'
;
import
{
mapGetters
}
from
'
vuex
'
;
export
default
{
mixins
:
[
repoMixin
,
],
computed
:
{
...
mapGetters
([
'
isMini
'
,
]),
},
methods
:
{
lineOfCode
(
n
)
{
return
`skeleton-line-
${
n
}
`
;
...
...
app/assets/javascripts/repo/components/repo_prev_directory.vue
View file @
05728e78
<
script
>
import
eventHub
from
'
../event_hub
'
;
import
repoMixin
from
'
../mixins/repo_mixin
'
;
import
{
mapGetters
,
mapState
,
mapActions
}
from
'
vuex
'
;
export
default
{
mixins
:
[
repoMixin
,
],
props
:
{
prevUrl
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
parentTreeUrl
'
,
]),
...
mapGetters
([
'
isMini
'
,
]),
colSpanCondition
()
{
return
this
.
isMini
?
undefined
:
3
;
},
},
methods
:
{
linkClicked
(
file
)
{
eventHub
.
$emit
(
'
goToPreviousDirectoryClicked
'
,
file
);
}
,
...
mapActions
([
'
getTreeData
'
,
])
,
},
};
</
script
>
...
...
@@ -30,9 +26,9 @@
<td
:colspan=
"colSpanCondition"
class=
"table-cell"
@
click.prevent=
"
linkClicked(prevUrl
)"
@
click.prevent=
"
getTreeData(
{ endpoint: parentTreeUrl }
)"
>
<a
:href=
"p
rev
Url"
>
...
</a>
<a
:href=
"p
arentTree
Url"
>
...
</a>
</td>
</tr>
</
template
>
app/assets/javascripts/repo/components/repo_preview.vue
View file @
05728e78
<
script
>
/* global LineHighlighter */
import
Store
from
'
../stores/repo_store
'
;
import
{
mapGetters
}
from
'
vuex
'
;
export
default
{
data
()
{
return
Store
;
},
computed
:
{
html
()
{
return
this
.
activeFile
.
html
;
}
,
...
mapGetters
([
'
activeFile
'
,
])
,
},
methods
:
{
highlightFile
()
{
$
(
this
.
$el
).
find
(
'
.file-content
'
).
syntaxHighlight
();
},
highlightLine
()
{
if
(
Store
.
activeLine
>
-
1
)
{
this
.
lineHighlighter
.
highlightHash
(
`#L
${
Store
.
activeLine
}
`
);
}
},
},
mounted
()
{
this
.
highlightFile
();
// TODO: get this to work across different files
this
.
lineHighlighter
=
new
LineHighlighter
({
fileHolderSelector
:
'
.blob-viewer-container
'
,
scrollFileHolder
:
true
,
});
},
watch
:
{
html
()
{
updated
()
{
this
.
$nextTick
(()
=>
{
this
.
highlightFile
();
this
.
highlightLine
();
});
},
activeLine
()
{
this
.
highlightLine
();
},
},
};
</
script
>
...
...
app/assets/javascripts/repo/components/repo_sidebar.vue
View file @
05728e78
<
script
>
import
_
from
'
underscore
'
;
import
Service
from
'
../services/repo_service
'
;
import
Helper
from
'
../helpers/repo_helper
'
;
import
Store
from
'
../stores/repo_store
'
;
import
eventHub
from
'
../event_hub
'
;
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
RepoPreviousDirectory
from
'
./repo_prev_directory.vue
'
;
import
RepoFile
from
'
./repo_file.vue
'
;
import
RepoLoadingFile
from
'
./repo_loading_file.vue
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
export
default
{
mixins
:
[
RepoMixin
],
components
:
{
'
repo-previous-directory
'
:
RepoPreviousDirectory
,
'
repo-file
'
:
RepoFile
,
'
repo-loading-file
'
:
RepoLoadingFile
,
},
created
()
{
window
.
addEventListener
(
'
popstate
'
,
this
.
checkHistory
);
window
.
addEventListener
(
'
popstate
'
,
this
.
popHistoryState
);
},
destroyed
()
{
eventHub
.
$off
(
'
fileNameClicked
'
,
this
.
fileClicked
);
eventHub
.
$off
(
'
goToPreviousDirectoryClicked
'
,
this
.
goToPreviousDirectoryClicked
);
window
.
removeEventListener
(
'
popstate
'
,
this
.
checkHistory
);
window
.
removeEventListener
(
'
popstate
'
,
this
.
popHistoryState
);
},
mounted
()
{
eventHub
.
$on
(
'
fileNameClicked
'
,
this
.
fileClicked
);
eventHub
.
$on
(
'
goToPreviousDirectoryClicked
'
,
this
.
goToPreviousDirectoryClicked
);
},
data
()
{
return
Store
;
this
.
getTreeData
();
},
computed
:
{
flattendFiles
()
{
const
mapFiles
=
arr
=>
(
!
arr
.
files
.
length
?
[]
:
_
.
map
(
arr
.
files
,
a
=>
[
a
,
mapFiles
(
a
)]));
return
_
.
chain
(
this
.
files
)
.
map
(
arr
=>
[
arr
,
mapFiles
(
arr
)])
.
flatten
()
.
value
()
;
...
mapState
([
'
loading
'
,
'
isRoot
'
,
]),
...
mapState
({
projectName
(
state
)
{
return
state
.
project
.
name
;
},
}),
...
mapGetters
([
'
treeList
'
,
'
isMini
'
,
]),
},
methods
:
{
checkHistory
()
{
let
selectedFile
=
this
.
files
.
find
(
file
=>
location
.
pathname
.
indexOf
(
file
.
url
)
>
-
1
);
if
(
!
selectedFile
)
{
// Maybe it is not in the current tree but in the opened tabs
selectedFile
=
Helper
.
getFileFromPath
(
location
.
pathname
);
}
let
lineNumber
=
null
;
if
(
location
.
hash
.
indexOf
(
'
#L
'
)
>
-
1
)
lineNumber
=
Number
(
location
.
hash
.
substr
(
2
));
if
(
selectedFile
)
{
if
(
selectedFile
.
url
!==
this
.
activeFile
.
url
)
{
this
.
fileClicked
(
selectedFile
,
lineNumber
);
}
else
{
Store
.
setActiveLine
(
lineNumber
);
}
}
else
{
// Not opened at all lets open new tab
this
.
fileClicked
({
url
:
location
.
href
,
},
lineNumber
);
}
},
fileClicked
(
clickedFile
,
lineNumber
)
{
const
file
=
clickedFile
;
if
(
file
.
loading
)
return
;
if
(
file
.
type
===
'
tree
'
&&
file
.
opened
)
{
Helper
.
setDirectoryToClosed
(
file
);
Store
.
setActiveLine
(
lineNumber
);
}
else
if
(
file
.
type
===
'
submodule
'
)
{
file
.
loading
=
true
;
gl
.
utils
.
visitUrl
(
file
.
url
);
}
else
{
const
openFile
=
Helper
.
getFileFromPath
(
file
.
url
);
if
(
openFile
)
{
Store
.
setActiveFiles
(
openFile
);
Store
.
setActiveLine
(
lineNumber
);
}
else
{
file
.
loading
=
true
;
Service
.
url
=
file
.
url
;
Helper
.
getContent
(
file
)
.
then
(()
=>
{
file
.
loading
=
false
;
Helper
.
scrollTabsRight
();
Store
.
setActiveLine
(
lineNumber
);
})
.
catch
(
Helper
.
loadingError
);
}
}
},
goToPreviousDirectoryClicked
(
prevURL
)
{
Service
.
url
=
prevURL
;
Helper
.
getContent
(
null
,
true
)
.
then
(()
=>
Helper
.
scrollTabsRight
())
.
catch
(
Helper
.
loadingError
);
},
...
mapActions
([
'
getTreeData
'
,
'
popHistoryState
'
,
]),
},
};
</
script
>
...
...
@@ -136,17 +71,16 @@ export default {
</thead>
<tbody>
<repo-previous-directory
v-if=
"!isRoot && !loading.tree"
:prev-url=
"prevURL"
v-if=
"!isRoot && treeList.length"
/>
<repo-loading-file
v-if=
"!
flattendFiles.length && loading.tree
"
v-if=
"!
treeList.length && loading
"
v-for=
"n in 5"
:key=
"n"
/>
<repo-file
v-for=
"
file in flattendFiles
"
:key=
"
file.id
"
v-for=
"
(file, index) in treeList
"
:key=
"
index
"
:file=
"file"
/>
</tbody>
...
...
app/assets/javascripts/repo/components/repo_tab.vue
View file @
05728e78
<
script
>
import
Store
from
'
../stores/repo_store
'
;
import
{
mapActions
}
from
'
vuex
'
;
const
RepoTab
=
{
export
default
{
props
:
{
tab
:
{
type
:
Object
,
...
...
@@ -26,29 +26,23 @@ const RepoTab = {
},
methods
:
{
tabClicked
(
file
)
{
Store
.
setActiveFiles
(
file
);
},
closeTab
(
file
)
{
if
(
file
.
changed
||
file
.
tempFile
)
return
;
Store
.
removeFromOpenedFiles
(
file
);
},
...
mapActions
([
'
setFileActive
'
,
'
closeFile
'
,
]),
},
};
export
default
RepoTab
;
</
script
>
<
template
>
<li
:class=
"
{ active : tab.active }"
@click="
tabClicked
(tab)"
@click="
setFileActive
(tab)"
>
<button
type=
"button"
class=
"close-btn"
@
click.stop.prevent=
"close
Tab
(tab)"
@
click.stop.prevent=
"close
File
(tab)"
:aria-label=
"closeLabel"
>
<i
class=
"fa"
...
...
@@ -61,7 +55,7 @@ export default RepoTab;
href=
"#"
class=
"repo-tab"
:title=
"tab.url"
@
click.prevent=
"
tabClicked
(tab)"
>
@
click.prevent=
"
setFileActive
(tab)"
>
{{
tab
.
name
}}
</a>
</li>
...
...
app/assets/javascripts/repo/components/repo_tabs.vue
View file @
05728e78
<
script
>
import
Store
from
'
../stores/repo_store
'
;
import
{
mapState
}
from
'
vuex
'
;
import
RepoTab
from
'
./repo_tab.vue
'
;
import
RepoMixin
from
'
../mixins/repo_mixin
'
;
export
default
{
mixins
:
[
RepoMixin
],
components
:
{
'
repo-tab
'
:
RepoTab
,
},
data
()
{
return
Store
;
computed
:
{
...
mapState
([
'
openFiles
'
,
]),
},
};
</
script
>
...
...
@@ -20,7 +20,7 @@
class=
"list-unstyled"
>
<repo-tab
v-for=
"tab in open
ed
Files"
v-for=
"tab in openFiles"
:key=
"tab.id"
:tab=
"tab"
/>
...
...
app/assets/javascripts/repo/helpers/monaco_loader_helper.js
View file @
05728e78
/* global monaco */
import
RepoEditor
from
'
../components/repo_editor.vue
'
;
import
Store
from
'
../stores/repo_store
'
;
import
Helper
from
'
../helpers/repo_helper
'
;
import
monacoLoader
from
'
../monaco_loader
'
;
function
repoEditorLoader
()
{
Store
.
monacoLoading
=
true
;
return
new
Promise
((
resolve
,
reject
)
=>
{
monacoLoader
([
'
vs/editor/editor.main
'
],
()
=>
{
Helper
.
monaco
=
monaco
;
Store
.
monacoLoading
=
false
;
resolve
(
RepoEditor
);
},
()
=>
{
Store
.
monacoLoading
=
false
;
reject
();
});
});
...
...
app/assets/javascripts/repo/index.js
View file @
05728e78
import
$
from
'
jquery
'
;
import
Vue
from
'
vue
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
{
convertPermissionToBoolean
}
from
'
../lib/utils/common_utils
'
;
import
Service
from
'
./services/repo_service
'
;
import
Store
from
'
./stores/repo_store
'
;
...
...
@@ -7,27 +7,11 @@ import Repo from './components/repo.vue';
import
RepoEditButton
from
'
./components/repo_edit_button.vue
'
;
import
newBranchForm
from
'
./components/new_branch_form.vue
'
;
import
newDropdown
from
'
./components/new_dropdown/index.vue
'
;
import
vStore
from
'
./stores
'
;
import
Translate
from
'
../vue_shared/translate
'
;
function
initDropdowns
()
{
$
(
'
.js-tree-ref-target-holder
'
).
hide
();
}
function
addEventsForNonVueEls
()
{
window
.
onbeforeunload
=
function
confirmUnload
(
e
)
{
const
hasChanged
=
Store
.
openedFiles
.
some
(
file
=>
file
.
changed
);
if
(
!
hasChanged
)
return
undefined
;
const
event
=
e
||
window
.
event
;
if
(
event
)
event
.
returnValue
=
'
Are you sure you want to lose unsaved changes?
'
;
// For Safari
return
'
Are you sure you want to lose unsaved changes?
'
;
};
}
function
setInitialStore
(
data
)
{
Store
.
service
=
Service
;
Store
.
service
.
url
=
data
.
url
;
Store
.
service
.
refsUrl
=
data
.
refsUrl
;
Store
.
path
=
data
.
currentPath
;
Store
.
projectId
=
data
.
projectId
;
...
...
@@ -47,9 +31,37 @@ function setInitialStore(data) {
function
initRepo
(
el
)
{
return
new
Vue
({
el
,
store
:
vStore
,
components
:
{
repo
:
Repo
,
},
methods
:
{
...
mapActions
([
'
setInitialData
'
,
]),
},
created
()
{
const
data
=
el
.
dataset
;
this
.
setInitialData
({
project
:
{
id
:
data
.
projectId
,
name
:
data
.
projectName
,
},
endpoints
:
{
rootEndpoint
:
data
.
url
,
newMergeRequestUrl
:
data
.
newMergeRequestUrl
,
rootUrl
:
data
.
rootUrl
,
},
canCommit
:
convertPermissionToBoolean
(
data
.
canCommit
),
onTopOfBranch
:
convertPermissionToBoolean
(
data
.
onTopOfBranch
),
currentRef
:
data
.
ref
,
// TODO: get through data attribute
currentBranch
:
document
.
querySelector
(
'
.js-project-refs-dropdown
'
).
dataset
.
ref
,
isRoot
:
convertPermissionToBoolean
(
data
.
root
),
isInitialRoot
:
convertPermissionToBoolean
(
data
.
root
),
});
},
render
(
createElement
)
{
return
createElement
(
'
repo
'
);
},
...
...
@@ -59,6 +71,7 @@ function initRepo(el) {
function
initRepoEditButton
(
el
)
{
return
new
Vue
({
el
,
store
:
vStore
,
components
:
{
repoEditButton
:
RepoEditButton
,
},
...
...
@@ -87,32 +100,21 @@ function initNewBranchForm() {
components
:
{
newBranchForm
,
},
store
:
vStore
,
render
(
createElement
)
{
return
createElement
(
'
new-branch-form
'
,
{
props
:
{
currentBranch
:
Store
.
currentBranch
,
},
});
return
createElement
(
'
new-branch-form
'
);
},
});
}
function
initRepoBundle
()
{
const
repo
=
document
.
getElementById
(
'
repo
'
);
const
editButton
=
document
.
querySelector
(
'
.editable-mode
'
);
const
newDropdownHolder
=
document
.
querySelector
(
'
.js-new-dropdown
'
);
setInitialStore
(
repo
.
dataset
);
addEventsForNonVueEls
();
initDropdowns
();
Vue
.
use
(
Translate
);
initRepo
(
repo
);
initRepoEditButton
(
editButton
);
initNewBranchForm
();
initNewDropdown
(
newDropdownHolder
);
}
const
repo
=
document
.
getElementById
(
'
repo
'
);
const
editButton
=
document
.
querySelector
(
'
.editable-mode
'
);
const
newDropdownHolder
=
document
.
querySelector
(
'
.js-new-dropdown
'
);
setInitialStore
(
repo
.
dataset
);
$
(
initRepoBundl
e
);
Vue
.
use
(
Translat
e
);
export
default
initRepoBundle
;
initRepo
(
repo
);
initRepoEditButton
(
editButton
);
initNewBranchForm
();
initNewDropdown
(
newDropdownHolder
);
app/assets/javascripts/repo/mixins/repo_mixin.js
deleted
100644 → 0
View file @
95e56a61
import
Store
from
'
../stores/repo_store
'
;
const
RepoMixin
=
{
computed
:
{
isMini
()
{
return
!!
Store
.
openedFiles
.
length
;
},
changedFiles
()
{
const
changedFileList
=
this
.
openedFiles
.
filter
(
file
=>
file
.
changed
||
file
.
tempFile
);
return
changedFileList
;
},
},
};
export
default
RepoMixin
;
app/assets/javascripts/repo/services/index.js
0 → 100644
View file @
05728e78
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
import
Api
from
'
../../api
'
;
Vue
.
use
(
VueResource
);
export
default
{
getTreeData
(
endpoint
)
{
return
Vue
.
http
.
get
(
endpoint
,
{
params
:
{
format
:
'
json
'
}
});
},
getFileData
(
endpoint
)
{
return
Vue
.
http
.
get
(
endpoint
,
{
params
:
{
format
:
'
json
'
}
});
},
getRawFileData
(
endpoint
)
{
return
Vue
.
http
.
get
(
endpoint
);
},
getBranchData
(
projectId
,
currentBranch
)
{
return
Api
.
branchSingle
(
projectId
,
currentBranch
);
},
createBranch
(
projectId
,
payload
)
{
const
url
=
Api
.
buildUrl
(
Api
.
createBranchPath
).
replace
(
'
:id
'
,
projectId
);
return
Vue
.
http
.
post
(
url
,
payload
);
},
commit
(
projectId
,
payload
)
{
return
Api
.
commitMultiple
(
projectId
,
payload
);
},
};
app/assets/javascripts/repo/stores/actions.js
0 → 100644
View file @
05728e78
import
flash
from
'
../../flash
'
;
import
service
from
'
../services
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
getters
from
'
./getters
'
;
import
{
visitUrl
}
from
'
../../lib/utils/url_utility
'
;
export
const
redirectToUrl
=
url
=>
visitUrl
(
url
);
export
const
setInitialData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_INITIAL_DATA
,
data
);
export
const
closeDiscardPopup
=
({
commit
})
=>
commit
(
types
.
TOGGLE_DISCARD_POPUP
,
false
);
export
const
discardAllChanges
=
({
commit
,
state
})
=>
{
const
changedFiles
=
getters
.
changedFiles
(
state
);
changedFiles
.
forEach
(
file
=>
commit
(
types
.
DISCARD_FILE_CHANGES
,
file
));
};
export
const
closeAllFiles
=
({
state
,
dispatch
})
=>
{
state
.
openFiles
.
forEach
(
file
=>
dispatch
(
'
closeFile
'
,
file
));
};
export
const
toggleEditMode
=
({
commit
,
state
,
dispatch
},
force
=
false
)
=>
{
const
changedFiles
=
getters
.
changedFiles
(
state
);
if
(
changedFiles
.
length
&&
!
force
)
{
commit
(
types
.
TOGGLE_DISCARD_POPUP
,
true
);
}
else
{
commit
(
types
.
TOGGLE_EDIT_MODE
);
commit
(
types
.
TOGGLE_DISCARD_POPUP
,
false
);
dispatch
(
'
toggleBlobView
'
);
dispatch
(
'
discardAllChanges
'
);
}
};
export
const
toggleBlobView
=
({
commit
,
state
})
=>
{
if
(
state
.
editMode
)
{
commit
(
types
.
SET_EDIT_MODE
);
}
else
{
commit
(
types
.
SET_PREVIEW_MODE
);
}
};
export
const
checkCommitStatus
=
({
state
})
=>
service
.
getBranchData
(
state
.
project
.
id
,
state
.
currentBranch
,
)
.
then
((
data
)
=>
{
const
{
id
}
=
data
.
commit
;
if
(
state
.
currentRef
!==
id
)
{
return
true
;
}
return
false
;
})
.
catch
(()
=>
flash
(
'
Error checking branch data. Please try again.
'
));
export
const
commitChanges
=
({
commit
,
state
,
dispatch
},
{
payload
,
newMr
})
=>
service
.
commit
(
state
.
project
.
id
,
payload
)
.
then
((
data
)
=>
{
if
(
!
data
.
short_id
)
{
flash
(
data
.
message
);
return
;
}
flash
(
`Your changes have been committed. Commit
${
data
.
short_id
}
with
${
data
.
stats
.
additions
}
additions,
${
data
.
stats
.
deletions
}
deletions.`
,
'
notice
'
);
if
(
newMr
)
{
redirectToUrl
(
`
${
state
.
endpoints
.
newMergeRequestUrl
}${
payload
.
branch
}
`
);
}
else
{
// TODO: push a new state with the branch name
commit
(
types
.
SET_COMMIT_REF
,
data
.
id
);
dispatch
(
'
discardAllChanges
'
);
dispatch
(
'
closeAllFiles
'
);
dispatch
(
'
toggleEditMode
'
);
}
})
.
catch
(()
=>
flash
(
'
Error committing changes. Please try again.
'
));
export
const
popHistoryState
=
({
state
,
dispatch
})
=>
{
const
treeList
=
getters
.
treeList
(
state
);
const
tree
=
treeList
.
find
(
file
=>
file
.
url
===
state
.
previousUrl
);
if
(
!
tree
)
return
;
if
(
tree
.
type
===
'
tree
'
)
{
dispatch
(
'
toggleTreeOpen
'
,
{
endpoint
:
tree
.
url
,
tree
});
}
};
export
*
from
'
./actions/tree
'
;
export
*
from
'
./actions/file
'
;
export
*
from
'
./actions/branch
'
;
app/assets/javascripts/repo/stores/actions/branch.js
0 → 100644
View file @
05728e78
import
service
from
'
../../services
'
;
import
*
as
types
from
'
../mutation_types
'
;
import
{
pushState
}
from
'
../utils
'
;
// eslint-disable-next-line import/prefer-default-export
export
const
createNewBranch
=
({
rootState
,
commit
},
branch
)
=>
service
.
createBranch
(
rootState
.
project
.
id
,
{
branch
,
ref
:
rootState
.
currentBranch
,
},
).
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
const
branchName
=
data
.
name
;
const
url
=
location
.
href
.
replace
(
rootState
.
currentBranch
,
branchName
);
pushState
(
url
);
commit
(
types
.
SET_CURRENT_BRANCH
,
branchName
);
});
app/assets/javascripts/repo/stores/actions/file.js
0 → 100644
View file @
05728e78
import
flash
from
'
../../../flash
'
;
import
service
from
'
../../services
'
;
import
*
as
types
from
'
../mutation_types
'
;
import
{
activeFile
}
from
'
../getters
'
;
export
const
closeFile
=
({
commit
},
file
)
=>
{
if
(
file
.
changed
||
file
.
tempFile
)
return
;
commit
(
types
.
TOGGLE_FILE_OPEN
,
file
);
commit
(
types
.
SET_FILE_ACTIVE
,
{
file
,
active
:
false
});
};
export
const
setFileActive
=
({
commit
,
state
},
file
)
=>
{
const
currentActiveFile
=
activeFile
(
state
);
if
(
currentActiveFile
)
{
commit
(
types
.
SET_FILE_ACTIVE
,
{
file
:
currentActiveFile
,
active
:
false
});
}
commit
(
types
.
SET_FILE_ACTIVE
,
{
file
,
active
:
true
});
};
export
const
getFileData
=
({
commit
,
dispatch
},
file
)
=>
{
commit
(
types
.
TOGGLE_LOADING
,
file
);
service
.
getFileData
(
file
.
url
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
SET_FILE_DATA
,
{
data
,
file
});
commit
(
types
.
SET_PREVIEW_MODE
);
commit
(
types
.
TOGGLE_FILE_OPEN
,
file
);
dispatch
(
'
setFileActive
'
,
file
);
commit
(
types
.
TOGGLE_LOADING
,
file
);
})
.
catch
(()
=>
{
commit
(
types
.
TOGGLE_LOADING
,
file
);
flash
(
'
Error loading file data. Please try again.
'
);
});
};
export
const
getRawFileData
=
({
commit
,
dispatch
},
file
)
=>
service
.
getRawFileData
(
file
.
rawPath
)
.
then
(
res
=>
res
.
text
())
.
then
((
raw
)
=>
{
commit
(
types
.
SET_FILE_RAW_DATA
,
{
file
,
raw
});
})
.
catch
(()
=>
flash
(
'
Error loading file content. Please try again.
'
));
export
const
changeFileContent
=
({
commit
},
{
file
,
content
})
=>
{
commit
(
types
.
UPDATE_FILE_CONTENT
,
{
file
,
content
});
};
app/assets/javascripts/repo/stores/actions/tree.js
0 → 100644
View file @
05728e78
import
{
normalizeHeaders
}
from
'
../../../lib/utils/common_utils
'
;
import
flash
from
'
../../../flash
'
;
import
service
from
'
../../services
'
;
import
*
as
types
from
'
../mutation_types
'
;
import
{
pushState
,
setPageTitle
}
from
'
../utils
'
;
export
const
getTreeData
=
(
{
commit
,
state
},
{
endpoint
=
state
.
endpoints
.
rootEndpoint
,
tree
=
state
}
=
{},
)
=>
{
commit
(
types
.
TOGGLE_LOADING
,
tree
);
service
.
getTreeData
(
endpoint
)
.
then
((
res
)
=>
{
const
pageTitle
=
decodeURI
(
normalizeHeaders
(
res
.
headers
)[
'
PAGE-TITLE
'
]);
setPageTitle
(
pageTitle
);
return
res
.
json
();
})
.
then
((
data
)
=>
{
if
(
!
state
.
isInitialRoot
)
{
commit
(
types
.
SET_ROOT
,
data
.
path
===
'
/
'
);
}
commit
(
types
.
SET_DIRECTORY_DATA
,
{
data
,
tree
});
commit
(
types
.
SET_PARENT_TREE_URL
,
data
.
parent_tree_url
);
commit
(
types
.
TOGGLE_LOADING
,
tree
);
pushState
(
endpoint
);
})
.
catch
(()
=>
{
flash
(
'
Error loading tree data. Please try again.
'
);
commit
(
types
.
TOGGLE_LOADING
,
tree
);
});
};
export
const
toggleTreeOpen
=
({
commit
,
dispatch
},
{
endpoint
,
tree
})
=>
{
if
(
tree
.
opened
)
{
// send empty data to clear the tree
const
data
=
{
trees
:
[],
blobs
:
[],
submodules
:
[]
};
pushState
(
tree
.
parentTreeUrl
);
commit
(
types
.
SET_PREVIOUS_URL
,
tree
.
parentTreeUrl
);
commit
(
types
.
SET_DIRECTORY_DATA
,
{
data
,
tree
});
}
else
{
commit
(
types
.
SET_PREVIOUS_URL
,
endpoint
);
dispatch
(
'
getTreeData
'
,
{
endpoint
,
tree
});
}
commit
(
types
.
TOGGLE_TREE_OPEN
,
tree
);
};
export
const
clickedTreeRow
=
({
commit
,
dispatch
},
row
)
=>
{
if
(
row
.
type
===
'
tree
'
)
{
dispatch
(
'
toggleTreeOpen
'
,
{
endpoint
:
row
.
url
,
tree
:
row
,
});
}
else
if
(
row
.
type
===
'
submodule
'
)
{
commit
(
types
.
TOGGLE_LOADING
,
row
);
gl
.
utils
.
visitUrl
(
row
.
url
);
}
else
if
(
row
.
type
===
'
blob
'
&&
row
.
opened
)
{
dispatch
(
'
setFileActive
'
,
row
);
}
else
{
dispatch
(
'
getFileData
'
,
row
);
}
};
app/assets/javascripts/repo/stores/getters.js
0 → 100644
View file @
05728e78
import
_
from
'
underscore
'
;
export
const
treeList
=
(
state
)
=>
{
const
mapTree
=
arr
=>
(
!
arr
.
tree
.
length
?
[]
:
_
.
map
(
arr
.
tree
,
a
=>
[
a
,
mapTree
(
a
)]));
return
_
.
chain
(
state
.
tree
)
.
map
(
arr
=>
[
arr
,
mapTree
(
arr
)])
.
flatten
()
.
value
();
};
export
const
changedFiles
=
(
state
)
=>
{
const
files
=
state
.
openFiles
;
return
files
.
filter
(
file
=>
file
.
changed
);
};
export
const
activeFile
=
(
state
)
=>
{
const
openedFiles
=
state
.
openFiles
;
return
openedFiles
.
find
(
file
=>
file
.
active
);
};
export
const
activeFileExtension
=
(
state
)
=>
{
const
file
=
activeFile
(
state
);
return
file
?
`.
${
file
.
path
.
split
(
'
.
'
).
pop
()}
`
:
''
;
};
export
const
isMini
=
state
=>
!!
state
.
openFiles
.
length
;
export
const
canEditFile
=
(
state
)
=>
{
const
currentActiveFile
=
activeFile
(
state
);
const
openedFiles
=
state
.
openFiles
;
return
state
.
canCommit
&&
state
.
onTopOfBranch
&&
openedFiles
.
length
&&
(
currentActiveFile
&&
!
currentActiveFile
.
render_error
&&
!
currentActiveFile
.
binary
);
};
app/assets/javascripts/repo/stores/index.js
0 → 100644
View file @
05728e78
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
state
from
'
./state
'
;
import
*
as
actions
from
'
./actions
'
;
import
*
as
getters
from
'
./getters
'
;
import
mutations
from
'
./mutations
'
;
Vue
.
use
(
Vuex
);
export
default
new
Vuex
.
Store
({
state
,
actions
,
mutations
,
getters
,
});
app/assets/javascripts/repo/stores/mutation_types.js
0 → 100644
View file @
05728e78
export
const
SET_INITIAL_DATA
=
'
SET_INITIAL_DATA
'
;
export
const
TOGGLE_LOADING
=
'
TOGGLE_LOADING
'
;
export
const
SET_COMMIT_REF
=
'
SET_COMMIT_REF
'
;
export
const
SET_PARENT_TREE_URL
=
'
SET_PARENT_TREE_URL
'
;
export
const
SET_ROOT
=
'
SET_ROOT
'
;
export
const
SET_PREVIOUS_URL
=
'
SET_PREVIOUS_URL
'
;
// Tree mutation types
export
const
SET_DIRECTORY_DATA
=
'
SET_DIRECTORY_DATA
'
;
export
const
TOGGLE_TREE_OPEN
=
'
TOGGLE_TREE_OPEN
'
;
// File mutation types
export
const
SET_FILE_DATA
=
'
SET_FILE_DATA
'
;
export
const
TOGGLE_FILE_OPEN
=
'
TOGGLE_FILE_OPEN
'
;
export
const
SET_FILE_ACTIVE
=
'
SET_FILE_ACTIVE
'
;
export
const
SET_FILE_RAW_DATA
=
'
SET_FILE_RAW_DATA
'
;
export
const
UPDATE_FILE_CONTENT
=
'
UPDATE_FILE_CONTENT
'
;
export
const
DISCARD_FILE_CHANGES
=
'
DISCARD_FILE_CHANGES
'
;
// Viewer mutation types
export
const
SET_PREVIEW_MODE
=
'
SET_PREVIEW_MODE
'
;
export
const
SET_EDIT_MODE
=
'
SET_EDIT_MODE
'
;
export
const
TOGGLE_EDIT_MODE
=
'
TOGGLE_EDIT_MODE
'
;
export
const
TOGGLE_DISCARD_POPUP
=
'
TOGGLE_DISCARD_POPUP
'
;
export
const
SET_CURRENT_BRANCH
=
'
SET_CURRENT_BRANCH
'
;
app/assets/javascripts/repo/stores/mutations.js
0 → 100644
View file @
05728e78
import
*
as
types
from
'
./mutation_types
'
;
import
fileMutations
from
'
./mutations/file
'
;
import
treeMutations
from
'
./mutations/tree
'
;
import
branchMutations
from
'
./mutations/branch
'
;
export
default
{
[
types
.
SET_INITIAL_DATA
](
state
,
data
)
{
Object
.
assign
(
state
,
data
);
},
[
types
.
SET_PREVIEW_MODE
](
state
)
{
Object
.
assign
(
state
,
{
currentBlobView
:
'
repo-preview
'
,
});
},
[
types
.
SET_EDIT_MODE
](
state
)
{
Object
.
assign
(
state
,
{
currentBlobView
:
'
repo-editor
'
,
});
},
[
types
.
TOGGLE_LOADING
](
state
,
entry
)
{
Object
.
assign
(
entry
,
{
loading
:
!
entry
.
loading
,
});
},
[
types
.
TOGGLE_EDIT_MODE
](
state
)
{
Object
.
assign
(
state
,
{
editMode
:
!
state
.
editMode
,
});
},
[
types
.
TOGGLE_DISCARD_POPUP
](
state
,
discardPopupOpen
)
{
Object
.
assign
(
state
,
{
discardPopupOpen
,
});
},
[
types
.
SET_COMMIT_REF
](
state
,
ref
)
{
Object
.
assign
(
state
,
{
currentRef
:
ref
,
});
},
[
types
.
SET_ROOT
](
state
,
isRoot
)
{
Object
.
assign
(
state
,
{
isRoot
,
isInitialRoot
:
isRoot
,
});
},
[
types
.
SET_PREVIOUS_URL
](
state
,
previousUrl
)
{
Object
.
assign
(
state
,
{
previousUrl
,
});
},
...
fileMutations
,
...
treeMutations
,
...
branchMutations
,
};
app/assets/javascripts/repo/stores/mutations/branch.js
0 → 100644
View file @
05728e78
import
*
as
types
from
'
../mutation_types
'
;
export
default
{
[
types
.
SET_CURRENT_BRANCH
](
state
,
currentBranch
)
{
Object
.
assign
(
state
,
{
currentBranch
,
});
},
};
app/assets/javascripts/repo/stores/mutations/file.js
0 → 100644
View file @
05728e78
import
*
as
types
from
'
../mutation_types
'
;
import
{
findIndexOfFile
}
from
'
../utils
'
;
export
default
{
[
types
.
SET_FILE_ACTIVE
](
state
,
{
file
,
active
})
{
Object
.
assign
(
file
,
{
active
,
});
},
[
types
.
TOGGLE_FILE_OPEN
](
state
,
file
)
{
Object
.
assign
(
file
,
{
opened
:
!
file
.
opened
,
});
if
(
file
.
opened
)
{
state
.
openFiles
.
push
(
file
);
}
else
{
state
.
openFiles
.
splice
(
findIndexOfFile
(
state
.
openFiles
,
file
),
1
);
}
},
[
types
.
SET_FILE_DATA
](
state
,
{
data
,
file
})
{
Object
.
assign
(
file
,
{
blamePath
:
data
.
blame_path
,
commitsPath
:
data
.
commits_path
,
permalink
:
data
.
permalink
,
rawPath
:
data
.
raw_path
,
binary
:
data
.
binary
,
html
:
data
.
html
,
});
},
[
types
.
SET_FILE_RAW_DATA
](
state
,
{
file
,
raw
})
{
Object
.
assign
(
file
,
{
raw
,
});
},
[
types
.
UPDATE_FILE_CONTENT
](
state
,
{
file
,
content
})
{
const
changed
=
content
!==
file
.
raw
;
Object
.
assign
(
file
,
{
content
,
changed
,
});
},
[
types
.
DISCARD_FILE_CHANGES
](
state
,
file
)
{
Object
.
assign
(
file
,
{
content
:
''
,
changed
:
false
,
});
},
};
app/assets/javascripts/repo/stores/mutations/tree.js
0 → 100644
View file @
05728e78
import
*
as
types
from
'
../mutation_types
'
;
import
*
as
utils
from
'
../utils
'
;
export
default
{
[
types
.
TOGGLE_TREE_OPEN
](
state
,
tree
)
{
Object
.
assign
(
tree
,
{
opened
:
!
tree
.
opened
,
});
},
[
types
.
SET_DIRECTORY_DATA
](
state
,
{
data
,
tree
})
{
const
level
=
tree
.
level
!==
undefined
?
tree
.
level
+
1
:
0
;
const
parentTreeUrl
=
data
.
parent_tree_url
?
`
${
data
.
parent_tree_url
}${
data
.
path
}
`
:
state
.
endpoints
.
rootUrl
;
Object
.
assign
(
tree
,
{
tree
:
[
...
data
.
trees
.
map
(
t
=>
utils
.
decorateData
(
t
,
'
tree
'
,
parentTreeUrl
,
level
)),
...
data
.
submodules
.
map
(
m
=>
utils
.
decorateData
(
m
,
'
submodule
'
,
parentTreeUrl
,
level
)),
...
data
.
blobs
.
map
(
b
=>
utils
.
decorateData
(
b
,
'
blob
'
,
parentTreeUrl
,
level
)),
],
});
},
[
types
.
SET_PARENT_TREE_URL
](
state
,
url
)
{
Object
.
assign
(
state
,
{
parentTreeUrl
:
url
,
});
},
};
app/assets/javascripts/repo/stores/repo_store.js
View file @
05728e78
...
...
@@ -7,15 +7,12 @@ const RepoStore = {
canCommit
:
false
,
onTopOfBranch
:
false
,
editMode
:
false
,
isRoot
:
null
,
isInitialRoot
:
null
,
prevURL
:
''
,
projectId
:
''
,
projectName
:
''
,
projectUrl
:
''
,
branchUrl
:
''
,
blobRaw
:
''
,
currentBlobView
:
'
repo-preview
'
,
openedFiles
:
[],
submitCommitsLoading
:
false
,
dialog
:
{
...
...
@@ -40,10 +37,6 @@ const RepoStore = {
branchChanged
:
false
,
commitMessage
:
''
,
path
:
''
,
loading
:
{
tree
:
false
,
blob
:
false
,
},
setBranchHash
()
{
return
Service
.
getBranch
()
...
...
app/assets/javascripts/repo/stores/state.js
0 → 100644
View file @
05728e78
export
default
{
project
:
{
id
:
0
,
name
:
''
,
},
currentBranch
:
''
,
endpoints
:
{},
isRoot
:
false
,
isInitialRoot
:
false
,
currentRef
:
''
,
canCommit
:
false
,
onTopOfBranch
:
false
,
editMode
:
false
,
loading
:
false
,
currentBlobView
:
''
,
discardPopupOpen
:
false
,
tree
:
[],
openFiles
:
[],
parentTreeUrl
:
''
,
previousUrl
:
''
,
};
app/assets/javascripts/repo/stores/utils.js
0 → 100644
View file @
05728e78
export
const
dataStructure
=
({
id
:
''
,
type
:
''
,
name
:
''
,
url
:
''
,
path
:
''
,
level
:
0
,
tempFile
:
false
,
icon
:
''
,
tree
:
[],
loading
:
false
,
opened
:
false
,
active
:
false
,
changed
:
false
,
lastCommit
:
{},
tree_url
:
''
,
blamePath
:
''
,
commitsPath
:
''
,
permalink
:
''
,
rawPath
:
''
,
binary
:
false
,
html
:
''
,
raw
:
''
,
content
:
''
,
parentTreeUrl
:
''
,
});
export
const
decorateData
=
(
entity
,
type
,
parentTreeUrl
=
''
,
level
=
0
)
=>
{
const
{
id
,
url
,
name
,
icon
,
last_commit
,
tree_url
,
path
,
tempFile
,
active
=
false
,
opened
=
false
,
}
=
entity
;
return
{
...
dataStructure
,
id
,
type
,
name
,
url
,
tree_url
,
path
,
level
,
tempFile
,
icon
:
`fa-
${
icon
}
`
,
opened
,
active
,
parentTreeUrl
,
// eslint-disable-next-line camelcase
lastCommit
:
last_commit
?
{
// url: `${Store.projectUrl}/commit/${last_commit.id}`,
message
:
last_commit
.
message
,
updatedAt
:
last_commit
.
committed_date
,
}
:
{},
};
};
export
const
findIndexOfFile
=
(
state
,
file
)
=>
state
.
findIndex
(
f
=>
f
.
path
===
file
.
path
);
export
const
setPageTitle
=
(
title
)
=>
{
document
.
title
=
title
;
};
export
const
pushState
=
(
url
)
=>
{
history
.
pushState
({
url
},
''
,
url
);
};
app/views/shared/repo/_repo.html.haml
View file @
05728e78
#repo
{
data:
{
root:
@path
.
empty?
.
to_s
,
root_url:
project_tree_path
(
@project
),
url:
content_url
,
ref:
@commit
.
id
,
project_name:
project
.
name
,
refs_url:
refs_project_path
(
project
,
format: :json
),
project_url:
project_path
(
project
),
project_id:
project
.
id
,
blob_url:
namespace_project_blob_path
(
project
.
namespace
,
project
,
'
{{
branch
}}
'
),
new_m
r_template_url:
namespace_project_new_merge_request_path
(
project
.
namespace
,
project
,
merge_request:
{
source_branch:
'
{{
source_branch
}}
'
}),
new_m
erge_request_url:
namespace_project_new_merge_request_path
(
project
.
namespace
,
project
,
merge_request:
{
source_branch:
'
'
}),
can_commit:
(
!!
can_push_branch?
(
project
,
@ref
)).
to_s
,
on_top_of_branch:
(
!!
on_top_of_branch?
(
project
,
@ref
)).
to_s
,
current_path:
@path
}
}
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