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
dbf848ce
Commit
dbf848ce
authored
Jun 29, 2021
by
Coung Ngo
Committed by
Kushal Pandya
Jun 29, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Convert filter searching to GraphQL in issues refactor
parent
91e3aeba
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
122 additions
and
56 deletions
+122
-56
app/assets/javascripts/issues_list/components/issues_list_app.vue
...ts/javascripts/issues_list/components/issues_list_app.vue
+39
-20
app/assets/javascripts/issues_list/index.js
app/assets/javascripts/issues_list/index.js
+2
-8
app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
...ripts/issues_list/queries/search_iterations.query.graphql
+10
-0
app/assets/javascripts/issues_list/queries/search_labels.query.graphql
...vascripts/issues_list/queries/search_labels.query.graphql
+12
-0
app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
...ripts/issues_list/queries/search_milestones.query.graphql
+10
-0
app/assets/javascripts/issues_list/queries/search_users.query.graphql
...avascripts/issues_list/queries/search_users.query.graphql
+14
-0
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
...ared/components/filtered_search_bar/tokens/base_token.vue
+6
-5
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
...components/filtered_search_bar/tokens/iteration_token.vue
+9
-3
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
...components/filtered_search_bar/tokens/milestone_token.vue
+8
-3
app/helpers/issues_helper.rb
app/helpers/issues_helper.rb
+0
-3
ee/app/helpers/ee/issues_helper.rb
ee/app/helpers/ee/issues_helper.rb
+1
-4
ee/spec/helpers/ee/issues_helper_spec.rb
ee/spec/helpers/ee/issues_helper_spec.rb
+3
-3
spec/frontend/issues_list/components/issues_list_app_spec.js
spec/frontend/issues_list/components/issues_list_app_spec.js
+8
-4
spec/helpers/issues_helper_spec.rb
spec/helpers/issues_helper_spec.rb
+0
-3
No files found.
app/assets/javascripts/issues_list/components/issues_list_app.vue
View file @
dbf848ce
...
...
@@ -11,6 +11,7 @@ import {
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
getIssuesQuery
from
'
ee_else_ce/issues_list/queries/get_issues.query.graphql
'
;
import
createFlash
from
'
~/flash
'
;
import
{
convertToGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
CsvImportExportButtons
from
'
~/issuable/components/csv_import_export_buttons.vue
'
;
import
IssuableByEmail
from
'
~/issuable/components/issuable_by_email.vue
'
;
import
IssuableList
from
'
~/issuable_list/components/issuable_list_root.vue
'
;
...
...
@@ -70,6 +71,10 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import
MilestoneToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
'
;
import
WeightToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
'
;
import
eventHub
from
'
../eventhub
'
;
import
searchIterationsQuery
from
'
../queries/search_iterations.query.graphql
'
;
import
searchLabelsQuery
from
'
../queries/search_labels.query.graphql
'
;
import
searchMilestonesQuery
from
'
../queries/search_milestones.query.graphql
'
;
import
searchUsersQuery
from
'
../queries/search_users.query.graphql
'
;
import
IssueCardTimeInfo
from
'
./issue_card_time_info.vue
'
;
export
default
{
...
...
@@ -94,9 +99,6 @@ export default {
autocompleteAwardEmojisPath
:
{
default
:
''
,
},
autocompleteUsersPath
:
{
default
:
''
,
},
calendarPath
:
{
default
:
''
,
},
...
...
@@ -118,6 +120,9 @@ export default {
hasIssueWeightsFeature
:
{
default
:
false
,
},
hasIterationsFeature
:
{
default
:
false
,
},
hasMultipleIssueAssigneesFeature
:
{
default
:
false
,
},
...
...
@@ -139,15 +144,6 @@ export default {
newIssuePath
:
{
default
:
''
,
},
projectIterationsPath
:
{
default
:
''
,
},
projectLabelsPath
:
{
default
:
''
,
},
projectMilestonesPath
:
{
default
:
''
,
},
projectPath
:
{
default
:
''
,
},
...
...
@@ -233,7 +229,7 @@ export default {
if
(
gon
.
current_user_id
)
{
preloadedAuthors
.
push
({
id
:
gon
.
current_user_id
,
id
:
convertToGraphQLId
(
'
User
'
,
gon
.
current_user_id
),
// eslint-disable-line @gitlab/require-i18n-strings
name
:
gon
.
current_user_fullname
,
username
:
gon
.
current_username
,
avatar_url
:
gon
.
current_user_avatar_url
,
...
...
@@ -308,7 +304,7 @@ export default {
});
}
if
(
this
.
projectIterationsPath
)
{
if
(
this
.
hasIterationsFeature
)
{
tokens
.
push
({
type
:
TOKEN_TYPE_ITERATION
,
title
:
TOKEN_TITLE_ITERATION
,
...
...
@@ -407,19 +403,42 @@ export default {
:
epics
.
filter
((
epic
)
=>
epic
.
id
===
number
);
},
fetchLabels
(
search
)
{
return
this
.
fetchWithCache
(
this
.
projectLabelsPath
,
'
labels
'
,
'
title
'
,
search
);
return
this
.
$apollo
.
query
({
query
:
searchLabelsQuery
,
variables
:
{
projectPath
:
this
.
projectPath
,
search
},
})
.
then
(({
data
})
=>
data
.
project
.
labels
.
nodes
);
},
fetchMilestones
(
search
)
{
return
this
.
fetchWithCache
(
this
.
projectMilestonesPath
,
'
milestones
'
,
'
title
'
,
search
,
true
);
return
this
.
$apollo
.
query
({
query
:
searchMilestonesQuery
,
variables
:
{
projectPath
:
this
.
projectPath
,
search
},
})
.
then
(({
data
})
=>
data
.
project
.
milestones
.
nodes
);
},
fetchIterations
(
search
)
{
const
id
=
Number
(
search
);
return
!
search
||
Number
.
isNaN
(
id
)
?
axios
.
get
(
this
.
projectIterationsPath
,
{
params
:
{
search
}
})
:
axios
.
get
(
this
.
projectIterationsPath
,
{
params
:
{
id
}
});
const
variables
=
!
search
||
Number
.
isNaN
(
id
)
?
{
projectPath
:
this
.
projectPath
,
search
}
:
{
projectPath
:
this
.
projectPath
,
id
};
return
this
.
$apollo
.
query
({
query
:
searchIterationsQuery
,
variables
,
})
.
then
(({
data
})
=>
data
.
project
.
iterations
.
nodes
);
},
fetchUsers
(
search
)
{
return
axios
.
get
(
this
.
autocompleteUsersPath
,
{
params
:
{
search
}
});
return
this
.
$apollo
.
query
({
query
:
searchUsersQuery
,
variables
:
{
projectPath
:
this
.
projectPath
,
search
},
})
.
then
(({
data
})
=>
data
.
project
.
projectMembers
.
nodes
.
map
((
member
)
=>
member
.
user
));
},
getExportCsvPathWithQuery
()
{
return
`
${
this
.
exportCsvPath
}${
window
.
location
.
search
}
`
;
...
...
app/assets/javascripts/issues_list/index.js
View file @
dbf848ce
...
...
@@ -82,7 +82,6 @@ export function mountIssuesListApp() {
const
{
autocompleteAwardEmojisPath
,
autocompleteUsersPath
,
calendarPath
,
canBulkUpdate
,
canEdit
,
...
...
@@ -95,6 +94,7 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature
,
hasIssuableHealthStatusFeature
,
hasIssueWeightsFeature
,
hasIterationsFeature
,
hasMultipleIssueAssigneesFeature
,
hasProjectIssues
,
importCsvIssuesPath
,
...
...
@@ -106,9 +106,6 @@ export function mountIssuesListApp() {
maxAttachmentSize
,
newIssuePath
,
projectImportJiraPath
,
projectIterationsPath
,
projectLabelsPath
,
projectMilestonesPath
,
projectPath
,
quickActionsHelpPath
,
resetPath
,
...
...
@@ -122,7 +119,6 @@ export function mountIssuesListApp() {
apolloProvider
,
provide
:
{
autocompleteAwardEmojisPath
,
autocompleteUsersPath
,
calendarPath
,
canBulkUpdate
:
parseBoolean
(
canBulkUpdate
),
emptyStateSvgPath
,
...
...
@@ -130,15 +126,13 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature
:
parseBoolean
(
hasBlockedIssuesFeature
),
hasIssuableHealthStatusFeature
:
parseBoolean
(
hasIssuableHealthStatusFeature
),
hasIssueWeightsFeature
:
parseBoolean
(
hasIssueWeightsFeature
),
hasIterationsFeature
:
parseBoolean
(
hasIterationsFeature
),
hasMultipleIssueAssigneesFeature
:
parseBoolean
(
hasMultipleIssueAssigneesFeature
),
hasProjectIssues
:
parseBoolean
(
hasProjectIssues
),
isSignedIn
:
parseBoolean
(
isSignedIn
),
issuesPath
,
jiraIntegrationPath
,
newIssuePath
,
projectIterationsPath
,
projectLabelsPath
,
projectMilestonesPath
,
projectPath
,
rssPath
,
showNewIssueLink
:
parseBoolean
(
showNewIssueLink
),
...
...
app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
0 → 100644
View file @
dbf848ce
query
searchIterations
(
$projectPath
:
ID
!,
$search
:
String
,
$id
:
ID
)
{
project
(
fullPath
:
$projectPath
)
{
iterations
(
title
:
$search
,
id
:
$id
)
{
nodes
{
id
title
}
}
}
}
app/assets/javascripts/issues_list/queries/search_labels.query.graphql
0 → 100644
View file @
dbf848ce
query
searchLabels
(
$projectPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$projectPath
)
{
labels
(
searchTerm
:
$search
,
includeAncestorGroups
:
true
)
{
nodes
{
id
color
textColor
title
}
}
}
}
app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
0 → 100644
View file @
dbf848ce
query
searchMilestones
(
$projectPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$projectPath
)
{
milestones
(
searchTitle
:
$search
,
includeAncestors
:
true
)
{
nodes
{
id
title
}
}
}
}
app/assets/javascripts/issues_list/queries/search_users.query.graphql
0 → 100644
View file @
dbf848ce
query
searchUsers
(
$projectPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$projectPath
)
{
projectMembers
(
search
:
$search
)
{
nodes
{
user
{
id
avatarUrl
name
username
}
}
}
}
}
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
View file @
dbf848ce
...
...
@@ -6,6 +6,7 @@ import {
GlDropdownSectionHeader
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
import
{
DEBOUNCE_DELAY
}
from
'
../constants
'
;
import
{
getRecentlyUsedSuggestions
,
setTokenValueToRecentlyUsed
}
from
'
../filtered_search_utils
'
;
...
...
@@ -128,12 +129,12 @@ export default {
},
},
methods
:
{
handleInput
({
data
})
{
handleInput
:
debounce
(
function
debouncedSearch
({
data
})
{
this
.
searchKey
=
data
;
setTimeout
(()
=>
{
if
(
!
this
.
suggestionsLoading
)
this
.
$emit
(
'
fetch-suggestions
'
,
data
);
}
,
DEBOUNCE_DELAY
);
},
if
(
!
this
.
suggestionsLoading
)
{
this
.
$emit
(
'
fetch-suggestions
'
,
data
);
}
},
DEBOUNCE_DELAY
),
handleTokenValueSelected
(
activeTokenValue
)
{
// Make sure that;
// 1. Recently used values feature is enabled
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
View file @
dbf848ce
...
...
@@ -7,6 +7,7 @@ import {
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
DEBOUNCE_DELAY
,
DEFAULT_ITERATIONS
}
from
'
../constants
'
;
...
...
@@ -30,7 +31,7 @@ export default {
data
()
{
return
{
iterations
:
this
.
config
.
initialIterations
||
[],
loading
:
tru
e
,
loading
:
fals
e
,
};
},
computed
:
{
...
...
@@ -38,7 +39,9 @@ export default {
return
this
.
value
.
data
;
},
activeIteration
()
{
return
this
.
iterations
.
find
((
iteration
)
=>
iteration
.
id
===
Number
(
this
.
currentValue
));
return
this
.
iterations
.
find
(
(
iteration
)
=>
getIdFromGraphQLId
(
iteration
.
id
)
===
Number
(
this
.
currentValue
),
);
},
defaultIterations
()
{
return
this
.
config
.
defaultIterations
||
DEFAULT_ITERATIONS
;
...
...
@@ -55,6 +58,9 @@ export default {
},
},
methods
:
{
getValue
(
iteration
)
{
return
String
(
getIdFromGraphQLId
(
iteration
.
id
));
},
fetchIterationBySearchTerm
(
searchTerm
)
{
const
fetchPromise
=
this
.
config
.
fetchPath
?
this
.
config
.
fetchIterations
(
this
.
config
.
fetchPath
,
searchTerm
)
...
...
@@ -102,7 +108,7 @@ export default {
<gl-filtered-search-suggestion
v-for=
"iteration in iterations"
:key=
"iteration.id"
:value=
"
String(iteration.id
)"
:value=
"
getValue(iteration
)"
>
{{
iteration
.
title
}}
</gl-filtered-search-suggestion>
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
View file @
dbf848ce
...
...
@@ -35,7 +35,7 @@ export default {
return
{
milestones
:
this
.
config
.
initialMilestones
||
[],
defaultMilestones
:
this
.
config
.
defaultMilestones
||
DEFAULT_MILESTONES
,
loading
:
tru
e
,
loading
:
fals
e
,
};
},
computed
:
{
...
...
@@ -60,11 +60,16 @@ export default {
},
methods
:
{
fetchMilestoneBySearchTerm
(
searchTerm
=
''
)
{
if
(
this
.
loading
)
{
return
;
}
this
.
loading
=
true
;
this
.
config
.
fetchMilestones
(
searchTerm
)
.
then
(({
data
})
=>
{
this
.
milestones
=
data
.
sort
(
sortMilestonesByDueDate
);
.
then
((
response
)
=>
{
const
data
=
Array
.
isArray
(
response
)
?
response
:
response
.
data
;
this
.
milestones
=
data
.
slice
().
sort
(
sortMilestonesByDueDate
);
})
.
catch
(()
=>
createFlash
({
message
:
__
(
'
There was a problem fetching milestones.
'
)
}))
.
finally
(()
=>
{
...
...
app/helpers/issues_helper.rb
View file @
dbf848ce
...
...
@@ -181,7 +181,6 @@ module IssuesHelper
def
issues_list_data
(
project
,
current_user
,
finder
)
{
autocomplete_users_path:
autocomplete_users_path
(
active:
true
,
current_user:
true
,
project_id:
project
.
id
,
format: :json
),
autocomplete_award_emojis_path:
autocomplete_award_emojis_path
,
calendar_path:
url_for
(
safe_params
.
merge
(
calendar_url_options
)),
can_bulk_update:
can?
(
current_user
,
:admin_issue
,
project
).
to_s
,
...
...
@@ -201,8 +200,6 @@ module IssuesHelper
max_attachment_size:
number_to_human_size
(
Gitlab
::
CurrentSettings
.
max_attachment_size
.
megabytes
),
new_issue_path:
new_project_issue_path
(
project
,
issue:
{
milestone_id:
finder
.
milestones
.
first
.
try
(
:id
)
}),
project_import_jira_path:
project_import_jira_path
(
project
),
project_labels_path:
project_labels_path
(
project
,
include_ancestor_groups:
true
,
format: :json
),
project_milestones_path:
project_milestones_path
(
project
,
format: :json
),
project_path:
project
.
full_path
,
quick_actions_help_path:
help_page_path
(
'user/project/quick_actions'
),
reset_path:
new_issuable_address_project_path
(
project
,
issuable_type:
'issue'
),
...
...
ee/app/helpers/ee/issues_helper.rb
View file @
dbf848ce
...
...
@@ -48,6 +48,7 @@ module EE
has_blocked_issues_feature:
project
.
feature_available?
(
:blocked_issues
).
to_s
,
has_issuable_health_status_feature:
project
.
feature_available?
(
:issuable_health_status
).
to_s
,
has_issue_weights_feature:
project
.
feature_available?
(
:issue_weights
).
to_s
,
has_iterations_feature:
project
.
feature_available?
(
:iterations
).
to_s
,
has_multiple_issue_assignees_feature:
project
.
feature_available?
(
:multiple_issue_assignees
).
to_s
)
...
...
@@ -55,10 +56,6 @@ module EE
data
[
:group_epics_path
]
=
group_epics_path
(
project
.
group
,
format: :json
)
end
if
project
.
feature_available?
(
:iterations
)
data
[
:project_iterations_path
]
=
api_v4_projects_iterations_path
(
id:
project
.
id
)
end
data
end
end
...
...
ee/spec/helpers/ee/issues_helper_spec.rb
View file @
dbf848ce
...
...
@@ -145,9 +145,9 @@ RSpec.describe EE::IssuesHelper do
has_blocked_issues_feature:
'true'
,
has_issuable_health_status_feature:
'true'
,
has_issue_weights_feature:
'true'
,
has_iterations_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
group_epics_path:
group_epics_path
(
project
.
group
,
format: :json
),
project_iterations_path:
api_v4_projects_iterations_path
(
id:
project
.
id
)
group_epics_path:
group_epics_path
(
project
.
group
,
format: :json
)
}
expect
(
helper
.
issues_list_data
(
project
,
current_user
,
finder
)).
to
include
(
expected
)
...
...
@@ -172,6 +172,7 @@ RSpec.describe EE::IssuesHelper do
has_blocked_issues_feature:
'false'
,
has_issuable_health_status_feature:
'false'
,
has_issue_weights_feature:
'false'
,
has_iterations_feature:
'false'
,
has_multiple_issue_assignees_feature:
'false'
}
...
...
@@ -179,7 +180,6 @@ RSpec.describe EE::IssuesHelper do
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
not_to
include
(
:group_epics_path
)
expect
(
result
).
not_to
include
(
:project_iterations_path
)
end
end
end
...
...
spec/frontend/issues_list/components/issues_list_app_spec.js
View file @
dbf848ce
...
...
@@ -15,6 +15,7 @@ import {
urlParams
,
}
from
'
jest/issues_list/mock_data
'
;
import
createFlash
from
'
~/flash
'
;
import
{
convertToGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
CsvImportExportButtons
from
'
~/issuable/components/csv_import_export_buttons.vue
'
;
import
IssuableByEmail
from
'
~/issuable/components/issuable_by_email.vue
'
;
import
IssuableList
from
'
~/issuable_list/components/issuable_list_root.vue
'
;
...
...
@@ -54,19 +55,18 @@ describe('IssuesListApp component', () => {
localVue
.
use
(
VueApollo
);
const
defaultProvide
=
{
autocompleteUsersPath
:
'
autocomplete/users/path
'
,
calendarPath
:
'
calendar/path
'
,
canBulkUpdate
:
false
,
emptyStateSvgPath
:
'
empty-state.svg
'
,
exportCsvPath
:
'
export/csv/path
'
,
hasBlockedIssuesFeature
:
true
,
hasIssueWeightsFeature
:
true
,
hasIterationsFeature
:
true
,
hasProjectIssues
:
true
,
isSignedIn
:
false
,
issuesPath
:
'
path/to/issues
'
,
jiraIntegrationPath
:
'
jira/integration/path
'
,
newIssuePath
:
'
new/issue/path
'
,
projectLabelsPath
:
'
project/labels/path
'
,
projectPath
:
'
path/to/project
'
,
rssPath
:
'
rss/path
'
,
showNewIssueLink
:
true
,
...
...
@@ -545,9 +545,13 @@ describe('IssuesListApp component', () => {
});
it
(
'
renders all tokens
'
,
()
=>
{
const
preloadedAuthors
=
[
{
...
mockCurrentUser
,
id
:
convertToGraphQLId
(
'
User
'
,
mockCurrentUser
.
id
)
},
];
expect
(
findIssuableList
().
props
(
'
searchTokens
'
)).
toMatchObject
([
{
type
:
TOKEN_TYPE_AUTHOR
,
preloadedAuthors
:
[
mockCurrentUser
]
},
{
type
:
TOKEN_TYPE_ASSIGNEE
,
preloadedAuthors
:
[
mockCurrentUser
]
},
{
type
:
TOKEN_TYPE_AUTHOR
,
preloadedAuthors
},
{
type
:
TOKEN_TYPE_ASSIGNEE
,
preloadedAuthors
},
{
type
:
TOKEN_TYPE_MILESTONE
},
{
type
:
TOKEN_TYPE_LABEL
},
{
type
:
TOKEN_TYPE_MY_REACTION
},
...
...
spec/helpers/issues_helper_spec.rb
View file @
dbf848ce
...
...
@@ -294,7 +294,6 @@ RSpec.describe IssuesHelper do
expected
=
{
autocomplete_award_emojis_path:
autocomplete_award_emojis_path
,
autocomplete_users_path:
autocomplete_users_path
(
active:
true
,
current_user:
true
,
project_id:
project
.
id
,
format: :json
),
calendar_path:
'#'
,
can_bulk_update:
'true'
,
can_edit:
'true'
,
...
...
@@ -313,8 +312,6 @@ RSpec.describe IssuesHelper do
max_attachment_size:
number_to_human_size
(
Gitlab
::
CurrentSettings
.
max_attachment_size
.
megabytes
),
new_issue_path:
new_project_issue_path
(
project
,
issue:
{
milestone_id:
finder
.
milestones
.
first
.
id
}),
project_import_jira_path:
project_import_jira_path
(
project
),
project_labels_path:
project_labels_path
(
project
,
include_ancestor_groups:
true
,
format: :json
),
project_milestones_path:
project_milestones_path
(
project
,
format: :json
),
project_path:
project
.
full_path
,
quick_actions_help_path:
help_page_path
(
'user/project/quick_actions'
),
reset_path:
new_issuable_address_project_path
(
project
,
issuable_type:
'issue'
),
...
...
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