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
6b1974f5
Commit
6b1974f5
authored
Jul 20, 2020
by
Payton Burdette
Committed by
Andrew Fontaine
Jul 20, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor pipeline form
Refactor the new pipeline form from HAML to Vue.
parent
0d9183ba
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
512 additions
and
36 deletions
+512
-36
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+11
-0
app/assets/javascripts/pages/projects/pipelines/new/index.js
app/assets/javascripts/pages/projects/pipelines/new/index.js
+12
-5
app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
...javascripts/pipeline_new/components/pipeline_new_form.vue
+247
-0
app/assets/javascripts/pipeline_new/constants.js
app/assets/javascripts/pipeline_new/constants.js
+2
-0
app/assets/javascripts/pipeline_new/index.js
app/assets/javascripts/pipeline_new/index.js
+36
-0
app/controllers/projects/pipelines_controller.rb
app/controllers/projects/pipelines_controller.rb
+1
-0
app/views/projects/pipelines/new.html.haml
app/views/projects/pipelines/new.html.haml
+35
-31
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/features/populate_new_pipeline_vars_with_params_spec.rb
spec/features/populate_new_pipeline_vars_with_params_spec.rb
+1
-0
spec/features/projects/pipelines/pipelines_spec.rb
spec/features/projects/pipelines/pipelines_spec.rb
+2
-0
spec/frontend/api_spec.js
spec/frontend/api_spec.js
+30
-0
spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
...rontend/pipeline_new/components/pipeline_new_form_spec.js
+108
-0
spec/frontend/pipeline_new/mock_data.js
spec/frontend/pipeline_new/mock_data.js
+21
-0
No files found.
app/assets/javascripts/api.js
View file @
6b1974f5
...
...
@@ -55,6 +55,7 @@ const Api = {
adminStatisticsPath
:
'
/api/:version/application/statistics
'
,
pipelineSinglePath
:
'
/api/:version/projects/:id/pipelines/:pipeline_id
'
,
pipelinesPath
:
'
/api/:version/projects/:id/pipelines/
'
,
createPipelinePath
:
'
/api/:version/projects/:id/pipeline
'
,
environmentsPath
:
'
/api/:version/projects/:id/environments
'
,
rawFilePath
:
'
/api/:version/projects/:id/repository/files/:path/raw
'
,
issuePath
:
'
/api/:version/projects/:id/issues/:issue_iid
'
,
...
...
@@ -576,6 +577,16 @@ const Api = {
});
},
createPipeline
(
id
,
data
)
{
const
url
=
Api
.
buildUrl
(
this
.
createPipelinePath
).
replace
(
'
:id
'
,
encodeURIComponent
(
id
));
return
axios
.
post
(
url
,
data
,
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
},
});
},
environments
(
id
)
{
const
url
=
Api
.
buildUrl
(
this
.
environmentsPath
).
replace
(
'
:id
'
,
encodeURIComponent
(
id
));
return
axios
.
get
(
url
);
...
...
app/assets/javascripts/pages/projects/pipelines/new/index.js
View file @
6b1974f5
import
$
from
'
jquery
'
;
import
NewBranchForm
from
'
~/new_branch_form
'
;
import
setupNativeFormVariableList
from
'
~/ci_variable_list/native_form_variable_list
'
;
import
initNewPipeline
from
'
~/pipeline_new/index
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
const
el
=
document
.
getElementById
(
'
js-new-pipeline
'
);
if
(
el
)
{
initNewPipeline
();
}
else
{
new
NewBranchForm
(
$
(
'
.js-new-pipeline-form
'
));
// eslint-disable-line no-new
setupNativeFormVariableList
({
container
:
$
(
'
.js-ci-variable-list-section
'
),
formField
:
'
variables_attributes
'
,
});
}
});
app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
0 → 100644
View file @
6b1974f5
<
script
>
import
Vue
from
'
vue
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
Api
from
'
~/api
'
;
import
{
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
{
VARIABLE_TYPE
,
FILE_TYPE
}
from
'
../constants
'
;
import
{
uniqueId
}
from
'
lodash
'
;
import
{
GlAlert
,
GlButton
,
GlForm
,
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
GlLink
,
GlNewDropdown
,
GlNewDropdownItem
,
GlSearchBoxByType
,
GlSprintf
,
}
from
'
@gitlab/ui
'
;
export
default
{
typeOptions
:
[
{
value
:
VARIABLE_TYPE
,
text
:
__
(
'
Variable
'
)
},
{
value
:
FILE_TYPE
,
text
:
__
(
'
File
'
)
},
],
variablesDescription
:
s__
(
'
Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.
'
,
),
formElementClasses
:
'
gl-mr-3 gl-mb-3 table-section section-15
'
,
errorTitle
:
__
(
'
The form contains the following error:
'
),
components
:
{
GlAlert
,
GlButton
,
GlForm
,
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
GlLink
,
GlNewDropdown
,
GlNewDropdownItem
,
GlSearchBoxByType
,
GlSprintf
,
},
props
:
{
pipelinesPath
:
{
type
:
String
,
required
:
true
,
},
projectId
:
{
type
:
String
,
required
:
true
,
},
refs
:
{
type
:
Array
,
required
:
true
,
},
settingsLink
:
{
type
:
String
,
required
:
true
,
},
fileParams
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
refParam
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
variableParams
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
data
()
{
return
{
searchTerm
:
''
,
refValue
:
this
.
refParam
,
variables
:
{},
error
:
false
,
};
},
computed
:
{
filteredRefs
()
{
const
lowerCasedSearchTerm
=
this
.
searchTerm
.
toLowerCase
();
return
this
.
refs
.
filter
(
ref
=>
ref
.
toLowerCase
().
includes
(
lowerCasedSearchTerm
));
},
variablesLength
()
{
return
Object
.
keys
(
this
.
variables
).
length
;
},
},
created
()
{
if
(
this
.
variableParams
)
{
this
.
setVariableParams
(
VARIABLE_TYPE
,
this
.
variableParams
);
}
if
(
this
.
fileParams
)
{
this
.
setVariableParams
(
FILE_TYPE
,
this
.
fileParams
);
}
this
.
addEmptyVariable
();
},
methods
:
{
addEmptyVariable
()
{
this
.
variables
[
uniqueId
(
'
var
'
)]
=
{
variable_type
:
VARIABLE_TYPE
,
key
:
''
,
value
:
''
,
};
},
setVariableParams
(
type
,
paramsObj
)
{
Object
.
entries
(
paramsObj
).
forEach
(([
key
,
value
])
=>
{
this
.
variables
[
uniqueId
(
'
var
'
)]
=
{
key
,
value
,
variable_type
:
type
,
};
});
},
setRefSelected
(
ref
)
{
this
.
refValue
=
ref
;
},
isSelected
(
ref
)
{
return
ref
===
this
.
refValue
;
},
insertNewVariable
()
{
Vue
.
set
(
this
.
variables
,
uniqueId
(
'
var
'
),
{
variable_type
:
VARIABLE_TYPE
,
key
:
''
,
value
:
''
,
});
},
removeVariable
(
key
)
{
Vue
.
delete
(
this
.
variables
,
key
);
},
canRemove
(
index
)
{
return
index
<
this
.
variablesLength
-
1
;
},
createPipeline
()
{
const
filteredVariables
=
Object
.
values
(
this
.
variables
).
filter
(
({
key
,
value
})
=>
key
!==
''
&&
value
!==
''
,
);
return
Api
.
createPipeline
(
this
.
projectId
,
{
ref
:
this
.
refValue
,
variables
:
filteredVariables
,
})
.
then
(({
data
})
=>
redirectTo
(
data
.
web_url
))
.
catch
(
err
=>
{
this
.
error
=
err
.
response
.
data
.
message
.
base
;
});
},
},
};
</
script
>
<
template
>
<gl-form
@
submit.prevent=
"createPipeline"
>
<gl-alert
v-if=
"error"
:title=
"$options.errorTitle"
:dismissible=
"false"
variant=
"danger"
class=
"gl-mb-4"
>
{{
error
}}
</gl-alert
>
<gl-form-group
:label=
"s__('Pipeline|Run for')"
>
<gl-new-dropdown
:text=
"refValue"
block
>
<gl-search-box-by-type
v-model.trim=
"searchTerm"
:placeholder=
"__('Search branches and tags')"
class=
"gl-p-2"
/>
<gl-new-dropdown-item
v-for=
"(ref, index) in filteredRefs"
:key=
"index"
class=
"gl-font-monospace"
is-check-item
:is-checked=
"isSelected(ref)"
@
click=
"setRefSelected(ref)"
>
{{
ref
}}
</gl-new-dropdown-item>
</gl-new-dropdown>
<template
#description
>
<div>
{{
s__
(
'
Pipeline|Existing branch name or tag
'
)
}}
</div></
template
>
</gl-form-group>
<gl-form-group
:label=
"s__('Pipeline|Variables')"
>
<div
v-for=
"(value, key, index) in variables"
:key=
"key"
class=
"gl-display-flex gl-align-items-center gl-mb-4 gl-pb-2 gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-flex-direction-column gl-md-flex-direction-row"
data-testid=
"ci-variable-row"
>
<gl-form-select
v-model=
"variables[key].variable_type"
:class=
"$options.formElementClasses"
:options=
"$options.typeOptions"
/>
<gl-form-input
v-model=
"variables[key].key"
:placeholder=
"s__('CiVariables|Input variable key')"
:class=
"$options.formElementClasses"
data-testid=
"pipeline-form-ci-variable-key"
@
change.once=
"insertNewVariable()"
/>
<gl-form-input
v-model=
"variables[key].value"
:placeholder=
"s__('CiVariables|Input variable value')"
class=
"gl-mr-5 gl-mb-3 table-section section-15"
/>
<gl-button
v-if=
"canRemove(index)"
icon=
"issue-close"
class=
"gl-mb-3"
data-testid=
"remove-ci-variable-row"
@
click=
"removeVariable(key)"
/>
</div>
<
template
#description
><gl-sprintf
:message=
"$options.variablesDescription"
>
<template
#link
="
{ content }">
<gl-link
:href=
"settingsLink"
>
{{
content
}}
</gl-link>
</
template
>
</gl-sprintf></template
>
</gl-form-group>
<div
class=
"gl-border-t-solid gl-border-gray-100 gl-border-t-1 gl-p-5 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between"
>
<gl-button
type=
"submit"
category=
"primary"
variant=
"success"
>
{{
s__('Pipeline|Run Pipeline')
}}
</gl-button>
<gl-button
:href=
"pipelinesPath"
>
{{ __('Cancel') }}
</gl-button>
</div>
</gl-form>
</template>
app/assets/javascripts/pipeline_new/constants.js
0 → 100644
View file @
6b1974f5
export
const
VARIABLE_TYPE
=
'
env_var
'
;
export
const
FILE_TYPE
=
'
file
'
;
app/assets/javascripts/pipeline_new/index.js
0 → 100644
View file @
6b1974f5
import
Vue
from
'
vue
'
;
import
PipelineNewForm
from
'
./components/pipeline_new_form.vue
'
;
export
default
()
=>
{
const
el
=
document
.
getElementById
(
'
js-new-pipeline
'
);
const
{
projectId
,
pipelinesPath
,
refParam
,
varParam
,
fileParam
,
refNames
,
settingsLink
,
}
=
el
?.
dataset
;
const
variableParams
=
JSON
.
parse
(
varParam
);
const
fileParams
=
JSON
.
parse
(
fileParam
);
const
refs
=
JSON
.
parse
(
refNames
);
return
new
Vue
({
el
,
render
(
createElement
)
{
return
createElement
(
PipelineNewForm
,
{
props
:
{
projectId
,
pipelinesPath
,
refParam
,
variableParams
,
fileParams
,
refs
,
settingsLink
,
},
});
},
});
};
app/controllers/projects/pipelines_controller.rb
View file @
6b1974f5
...
...
@@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag
(
:filter_pipelines_search
,
project
,
default_enabled:
true
)
push_frontend_feature_flag
(
:dag_pipeline_tab
,
project
,
default_enabled:
true
)
push_frontend_feature_flag
(
:pipelines_security_report_summary
,
project
)
push_frontend_feature_flag
(
:new_pipeline_form
,
default_enabled:
true
)
end
before_action
:ensure_pipeline
,
only:
[
:show
]
...
...
app/views/projects/pipelines/new.html.haml
View file @
6b1974f5
...
...
@@ -6,7 +6,11 @@
=
s_
(
'Pipeline|Run Pipeline'
)
%hr
=
form_for
@pipeline
,
as: :pipeline
,
url:
project_pipelines_path
(
@project
),
html:
{
id:
"new-pipeline-form"
,
class:
"js-new-pipeline-form js-requires-input"
}
do
|
f
|
-
if
Feature
.
enabled?
(
:new_pipeline_form
,
default_enabled:
true
)
#js-new-pipeline
{
data:
{
project_id:
@project
.
id
,
pipelines_path:
project_pipelines_path
(
@project
),
ref_param:
params
[
:ref
]
||
@project
.
default_branch
,
var_param:
params
[
:var
].
to_json
,
file_param:
params
[
:file_var
].
to_json
,
ref_names:
@project
.
repository
.
ref_names
.
to_json
.
html_safe
,
settings_link:
project_settings_ci_cd_path
(
@project
)
}
}
-
else
=
form_for
@pipeline
,
as: :pipeline
,
url:
project_pipelines_path
(
@project
),
html:
{
id:
"new-pipeline-form"
,
class:
"js-new-pipeline-form js-requires-input"
}
do
|
f
|
=
form_errors
(
@pipeline
)
.form-group.row
.col-sm-12
...
...
@@ -38,5 +42,5 @@
=
f
.
submit
s_
(
'Pipeline|Run Pipeline'
),
class:
'btn btn-success js-variables-save-button'
,
tabindex:
3
=
link_to
_
(
'Cancel'
),
project_pipelines_path
(
@project
),
class:
'btn btn-default float-right'
-# haml-lint:disable InlineJavaScript
%script
#availableRefs
{
type:
"application/json"
}=
@project
.
repository
.
ref_names
.
to_json
.
html_safe
-# haml-lint:disable InlineJavaScript
%script
#availableRefs
{
type:
"application/json"
}=
@project
.
repository
.
ref_names
.
to_json
.
html_safe
locale/gitlab.pot
View file @
6b1974f5
...
...
@@ -17281,6 +17281,9 @@ msgstr ""
msgid "Pipeline|Skipped"
msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default."
msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
...
...
@@ -23510,6 +23513,9 @@ msgstr[1] ""
msgid "The fork relationship has been removed."
msgstr ""
msgid "The form contains the following error:"
msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr ""
...
...
spec/features/populate_new_pipeline_vars_with_params_spec.rb
View file @
6b1974f5
...
...
@@ -8,6 +8,7 @@ RSpec.describe "Populate new pipeline CI variables with url params", :js do
let
(
:page_path
)
{
new_project_pipeline_path
(
project
)
}
before
do
stub_feature_flags
(
new_pipeline_form:
false
)
sign_in
(
user
)
project
.
add_maintainer
(
user
)
...
...
spec/features/projects/pipelines/pipelines_spec.rb
View file @
6b1974f5
...
...
@@ -652,6 +652,7 @@ RSpec.describe 'Pipelines', :js do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
before
do
stub_feature_flags
(
new_pipeline_form:
false
)
visit
new_project_pipeline_path
(
project
)
end
...
...
@@ -718,6 +719,7 @@ RSpec.describe 'Pipelines', :js do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
before
do
stub_feature_flags
(
new_pipeline_form:
false
)
visit
new_project_pipeline_path
(
project
)
end
...
...
spec/frontend/api_spec.js
View file @
6b1974f5
...
...
@@ -891,4 +891,34 @@ describe('Api', () => {
});
});
});
describe
(
'
createPipeline
'
,
()
=>
{
it
(
'
creates new pipeline
'
,
()
=>
{
const
redirectUrl
=
'
ci-project/-/pipelines/95
'
;
const
projectId
=
8
;
const
postData
=
{
ref
:
'
tag-1
'
,
variables
:
[
{
key
:
'
test_file
'
,
value
:
'
test_file_val
'
,
variable_type
:
'
file
'
},
{
key
:
'
test_var
'
,
value
:
'
test_var_val
'
,
variable_type
:
'
env_var
'
},
],
};
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects/
${
projectId
}
/pipeline`
;
jest
.
spyOn
(
axios
,
'
post
'
);
mock
.
onPost
(
expectedUrl
).
replyOnce
(
200
,
{
web_url
:
redirectUrl
,
});
return
Api
.
createPipeline
(
projectId
,
postData
).
then
(({
data
})
=>
{
expect
(
data
.
web_url
).
toBe
(
redirectUrl
);
expect
(
axios
.
post
).
toHaveBeenCalledWith
(
expectedUrl
,
postData
,
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
},
});
});
});
});
});
spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
0 → 100644
View file @
6b1974f5
import
Api
from
'
~/api
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
PipelineNewForm
from
'
~/pipeline_new/components/pipeline_new_form.vue
'
;
import
{
GlNewDropdown
,
GlNewDropdownItem
,
GlForm
}
from
'
@gitlab/ui
'
;
import
{
mockRefs
,
mockParams
,
mockPostParams
,
mockProjectId
}
from
'
../mock_data
'
;
describe
(
'
Pipeline New Form
'
,
()
=>
{
let
wrapper
;
const
dummySubmitEvent
=
{
preventDefault
()
{},
};
const
findForm
=
()
=>
wrapper
.
find
(
GlForm
);
const
findDropdown
=
()
=>
wrapper
.
find
(
GlNewDropdown
);
const
findDropdownItems
=
()
=>
wrapper
.
findAll
(
GlNewDropdownItem
);
const
findVariableRows
=
()
=>
wrapper
.
findAll
(
'
[data-testid="ci-variable-row"]
'
);
const
findRemoveIcons
=
()
=>
wrapper
.
findAll
(
'
[data-testid="remove-ci-variable-row"]
'
);
const
findKeyInputs
=
()
=>
wrapper
.
findAll
(
'
[data-testid="pipeline-form-ci-variable-key"]
'
);
const
createComponent
=
(
term
=
''
,
props
=
{},
method
=
shallowMount
)
=>
{
wrapper
=
method
(
PipelineNewForm
,
{
propsData
:
{
projectId
:
mockProjectId
,
pipelinesPath
:
''
,
refs
:
mockRefs
,
defaultBranch
:
'
master
'
,
settingsLink
:
''
,
...
props
,
},
data
()
{
return
{
searchTerm
:
term
,
};
},
});
};
beforeEach
(()
=>
{
jest
.
spyOn
(
Api
,
'
createPipeline
'
).
mockResolvedValue
({
data
:
{
web_url
:
'
/
'
}
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
Dropdown with branches and tags
'
,
()
=>
{
it
(
'
displays dropdown with all branches and tags
'
,
()
=>
{
createComponent
();
expect
(
findDropdownItems
().
length
).
toBe
(
mockRefs
.
length
);
});
it
(
'
when user enters search term the list is filtered
'
,
()
=>
{
createComponent
(
'
master
'
);
expect
(
findDropdownItems
().
length
).
toBe
(
1
);
expect
(
findDropdownItems
()
.
at
(
0
)
.
text
(),
).
toBe
(
'
master
'
);
});
});
describe
(
'
Form
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
''
,
mockParams
,
mount
);
});
it
(
'
displays the correct values for the provided query params
'
,
()
=>
{
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
'
tag-1
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findVariableRows
().
length
).
toBe
(
3
);
});
});
it
(
'
does not display remove icon for last row
'
,
()
=>
{
expect
(
findRemoveIcons
().
length
).
toBe
(
2
);
});
it
(
'
removes ci variable row on remove icon button click
'
,
()
=>
{
findRemoveIcons
()
.
at
(
1
)
.
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findVariableRows
().
length
).
toBe
(
2
);
});
});
it
(
'
creates a pipeline on submit
'
,
()
=>
{
findForm
().
vm
.
$emit
(
'
submit
'
,
dummySubmitEvent
);
expect
(
Api
.
createPipeline
).
toHaveBeenCalledWith
(
mockProjectId
,
mockPostParams
);
});
it
(
'
creates blank variable on input change event
'
,
()
=>
{
findKeyInputs
()
.
at
(
2
)
.
trigger
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findVariableRows
().
length
).
toBe
(
4
);
});
});
});
});
spec/frontend/pipeline_new/mock_data.js
0 → 100644
View file @
6b1974f5
export
const
mockRefs
=
[
'
master
'
,
'
branch-1
'
,
'
tag-1
'
];
export
const
mockParams
=
{
refParam
:
'
tag-1
'
,
variableParams
:
{
test_var
:
'
test_var_val
'
,
},
fileParams
:
{
test_file
:
'
test_file_val
'
,
},
};
export
const
mockProjectId
=
'
21
'
;
export
const
mockPostParams
=
{
ref
:
'
tag-1
'
,
variables
:
[
{
key
:
'
test_var
'
,
value
:
'
test_var_val
'
,
variable_type
:
'
env_var
'
},
{
key
:
'
test_file
'
,
value
:
'
test_file_val
'
,
variable_type
:
'
file
'
},
],
};
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