Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
803d86ff
Commit
803d86ff
authored
Jul 22, 2018
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: save page - updated + page history
parent
076e923d
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
289 additions
and
53 deletions
+289
-53
editor.vue
client/components/editor.vue
+73
-26
editor-modal-properties.vue
client/components/editor/editor-modal-properties.vue
+14
-3
setup.vue
client/components/setup.vue
+23
-16
update.gql
client/graph/editor/update.gql
+12
-0
editor.js
client/store/editor.js
+1
-0
2.0.0.js
server/db/migrations/2.0.0.js
+26
-0
pageHistory.js
server/db/models/pageHistory.js
+100
-0
pages.js
server/db/models/pages.js
+29
-1
storage.js
server/db/models/storage.js
+2
-2
page.js
server/graph/resolvers/page.js
+7
-4
page.graphql
server/graph/schemas/page.graphql
+2
-1
No files found.
client/components/editor.vue
View file @
803d86ff
...
...
@@ -6,7 +6,9 @@
v-icon(color='green', left) check
span.white--text(v-if='mode === "create"')
{{
$t
(
'common:actions.create'
)
}}
span.white--text(v-else)
{{
$t
(
'common:actions.save'
)
}}
v-btn.is-icon(outline, color='red').mx-0: v-icon(color='red') close
v-btn(outline, color='red').mx-0
v-icon(color='red', left) close
span.white--text
{{
$t
(
'common:actions.discard'
)
}}
v-btn(outline, color='blue', @click.native.stop='openModal(`properties`)', dark)
v-icon(left) sort_by_alpha
span.white--text
{{
$t
(
'editor:page'
)
}}
...
...
@@ -42,6 +44,7 @@ import { get, sync } from 'vuex-pathify'
import
{
AtomSpinner
}
from
'epic-spinners'
import
createPageMutation
from
'gql/editor/create.gql'
import
updatePageMutation
from
'gql/editor/update.gql'
import
editorStore
from
'@/store/editor'
...
...
@@ -90,35 +93,79 @@ export default {
},
async
save
()
{
this
.
showProgressDialog
(
'saving'
)
if
(
this
.
$store
.
get
(
'editor/mode'
)
===
'create'
)
{
const
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
createPageMutation
,
variables
:
{
content
:
this
.
$store
.
get
(
'editor/content'
),
description
:
this
.
$store
.
get
(
'editor/description'
),
editor
:
'markdown'
,
locale
:
this
.
$store
.
get
(
'editor/locale'
),
isPrivate
:
false
,
isPublished
:
this
.
$store
.
get
(
'editor/isPublished'
),
path
:
this
.
$store
.
get
(
'editor/path'
),
publishEndDate
:
this
.
$store
.
get
(
'editor/publishEndDate'
),
publishStartDate
:
this
.
$store
.
get
(
'editor/publishStartDate'
),
tags
:
this
.
$store
.
get
(
'editor/tags'
),
title
:
this
.
$store
.
get
(
'editor/title'
)
}
})
if
(
_
.
get
(
resp
,
'data.pages.create.responseResult.succeeded'
))
{
this
.
$store
.
commit
(
'showNotification'
,
{
message
:
this
.
$t
(
'editor:save.success'
),
style
:
'success'
,
icon
:
'check'
try
{
if
(
this
.
$store
.
get
(
'editor/mode'
)
===
'create'
)
{
// --------------------------------------------
// -> CREATE PAGE
// --------------------------------------------
let
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
createPageMutation
,
variables
:
{
content
:
this
.
$store
.
get
(
'editor/content'
),
description
:
this
.
$store
.
get
(
'editor/description'
),
editor
:
'markdown'
,
locale
:
this
.
$store
.
get
(
'editor/locale'
),
isPrivate
:
false
,
isPublished
:
this
.
$store
.
get
(
'editor/isPublished'
),
path
:
this
.
$store
.
get
(
'editor/path'
),
publishEndDate
:
this
.
$store
.
get
(
'editor/publishEndDate'
),
publishStartDate
:
this
.
$store
.
get
(
'editor/publishStartDate'
),
tags
:
this
.
$store
.
get
(
'editor/tags'
),
title
:
this
.
$store
.
get
(
'editor/title'
)
}
})
this
.
$store
.
set
(
'editor/mode'
,
'update'
)
resp
=
_
.
get
(
resp
,
'data.pages.create'
,
{})
if
(
_
.
get
(
resp
,
'responseResult.succeeded'
))
{
this
.
$store
.
commit
(
'showNotification'
,
{
message
:
this
.
$t
(
'editor:save.success'
),
style
:
'success'
,
icon
:
'check'
})
this
.
$store
.
set
(
'editor/id'
,
_
.
get
(
resp
,
'page.id'
))
this
.
$store
.
set
(
'editor/mode'
,
'update'
)
}
else
{
throw
new
Error
(
_
.
get
(
resp
,
'responseResult.message'
))
}
}
else
{
// --------------------------------------------
// -> UPDATE EXISTING PAGE
// --------------------------------------------
let
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
updatePageMutation
,
variables
:
{
id
:
this
.
$store
.
get
(
'editor/id'
),
content
:
this
.
$store
.
get
(
'editor/content'
),
description
:
this
.
$store
.
get
(
'editor/description'
),
editor
:
'markdown'
,
locale
:
this
.
$store
.
get
(
'editor/locale'
),
isPrivate
:
false
,
isPublished
:
this
.
$store
.
get
(
'editor/isPublished'
),
path
:
this
.
$store
.
get
(
'editor/path'
),
publishEndDate
:
this
.
$store
.
get
(
'editor/publishEndDate'
),
publishStartDate
:
this
.
$store
.
get
(
'editor/publishStartDate'
),
tags
:
this
.
$store
.
get
(
'editor/tags'
),
title
:
this
.
$store
.
get
(
'editor/title'
)
}
})
resp
=
_
.
get
(
resp
,
'data.pages.update'
,
{})
if
(
_
.
get
(
resp
,
'responseResult.succeeded'
))
{
this
.
$store
.
commit
(
'showNotification'
,
{
message
:
this
.
$t
(
'editor:save.success'
),
style
:
'success'
,
icon
:
'check'
})
}
else
{
throw
new
Error
(
_
.
get
(
resp
,
'responseResult.message'
))
}
}
}
else
{
}
catch
(
err
)
{
this
.
$store
.
commit
(
'showNotification'
,
{
message
:
err
.
message
,
style
:
'error'
,
icon
:
'warning'
})
}
this
.
hideProgressDialog
()
}
...
...
client/components/editor/editor-modal-properties.vue
View file @
803d86ff
...
...
@@ -7,13 +7,24 @@
v-icon(color='white') sort_by_alpha
.subheading.white--text.ml-2 Page Properties
v-spacer
v-btn(
v-btn
.mx-0
(
outline
dark
@click.native='close'
)
v-icon(left) close
span Close
v-icon(left) check
span
{{
$t
(
'common:actions.ok'
)
}}
v-menu
v-btn.is-icon(
slot='activator'
outline
dark
)
v-icon more_horiz
v-list
v-list-tile
v-list-tile-avatar: v-icon delete
v-list-tile-title Delete Page
v-card(tile)
v-card-text
v-subheader.pl-0 Page Info
...
...
client/components/setup.vue
View file @
803d86ff
...
...
@@ -50,7 +50,7 @@
.body-1.pt-3
svg.icons.is-18.is-outlined.has-right-pad.is-text: use(xlink:href='#nc-cd-reader')
span You are about to install Wiki.js #[strong
{{
wikiVersion
}}
].
v-divider
v-divider
.mt-3
v-form
v-checkbox(
color='primary',
...
...
@@ -67,7 +67,7 @@
hint='Check this box if you are upgrading from Wiki.js 1.x and wish to migrate your existing data.'
)
v-divider
.text-xs-center
.
pt-3.
text-xs-center
v-btn(color='primary', @click='proceedToSyscheck', :disabled='loading') Start
//- ==============================================
...
...
@@ -94,7 +94,7 @@
v-list-tile-title
{{
rs
.
title
}}
v-list-tile-sub-title
{{
rs
.
subtitle
}}
v-divider
.text-xs-center
.
pt-3.
text-xs-center
v-btn(@click='proceedToWelcome', :disabled='loading') Back
v-btn(color='primary', @click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again
v-btn(color='red', dark, @click='proceedToGeneral', v-if='!loading && !syscheck.ok') Continue Anyway
...
...
@@ -113,6 +113,8 @@
v-layout(row, wrap)
v-flex(xs12, sm6).pr-3
v-text-field(
outline
background-color='grey lighten-2'
v-model='conf.title',
label='Site Title',
:counter='255',
...
...
@@ -126,6 +128,8 @@
)
v-flex.pr-3(xs12, sm6)
v-text-field(
outline
background-color='grey lighten-2'
v-model='conf.port',
label='Server Port',
persistent-hint,
...
...
@@ -139,6 +143,8 @@
v-layout(row, wrap).mt-3
v-flex(xs12, sm6).pr-3
v-text-field(
outline
background-color='grey lighten-2'
v-model='conf.pathContent',
label='Content Data Path',
persistent-hint,
...
...
@@ -151,6 +157,8 @@
)
v-flex(xs12, sm6)
v-text-field(
outline
background-color='grey lighten-2'
v-model='conf.pathData',
label='Temporary Data Path',
persistent-hint,
...
...
@@ -170,15 +178,8 @@
persistent-hint,
hint='Should the site be accessible (read only) without login.'
)
v-checkbox.mt-2(
color='primary',
v-model='conf.selfRegister',
label='Allow Self-Registration',
persistent-hint,
hint='Can users create their own account to gain access?'
)
v-divider
.text-xs-center
.
pt-3.
text-xs-center
v-btn(@click='proceedToSyscheck', :disabled='loading') Back
v-btn(color='primary', @click='proceedToAdmin', :disabled='loading') Continue
...
...
@@ -196,7 +197,8 @@
v-layout(row, wrap)
v-flex(xs12)
v-text-field(
autofocus
outline
background-color='grey lighten-2'
v-model='conf.adminEmail',
label='Administrator Email',
hint='The email address of the administrator account',
...
...
@@ -208,6 +210,8 @@
)
v-flex.pr-3(xs6)
v-text-field(
outline
background-color='grey lighten-2'
ref='adminPassword',
counter='255'
v-model='conf.adminPassword',
...
...
@@ -224,6 +228,8 @@
)
v-flex(xs6)
v-text-field(
outline
background-color='grey lighten-2'
ref='adminPasswordConfirm',
counter='255'
v-model='conf.adminPasswordConfirm',
...
...
@@ -238,7 +244,7 @@
data-vv-scope='admin',
:error-messages='errors.collect(`adminPasswordConfirm`)'
)
.text-xs-center
.
pt-3.
text-xs-center
v-btn(@click='proceedToGeneral', :disabled='loading') Back
v-btn(color='primary', @click='proceedToUpgrade', :disabled='loading') Continue
...
...
@@ -256,6 +262,8 @@
v-layout(row)
v-flex(xs12)
v-text-field(
outline
background-color='grey lighten-2'
v-model='conf.upgMongo',
placeholder='mongodb://',
label='Connection String to Wiki.js 1.x MongoDB database',
...
...
@@ -267,7 +275,7 @@
data-vv-scope='upgrade',
:error-messages='errors.collect(`upgMongo`)'
)
.text-xs-center
.
pt-3.
text-xs-center
v-btn(@click='proceedToAdmin', :disabled='loading') Back
v-btn(color='primary', @click='proceedToFinal', :disabled='loading') Continue
...
...
@@ -290,7 +298,7 @@
v-alert(type='success', outline, :value='!loading && final.ok') Wiki.js was configured successfully and is now ready for use.
v-alert(type='error', outline, :value='!loading && !final.ok')
{{
final
.
error
}}
v-divider
.text-xs-center
.
pt-3.
text-xs-center
v-btn(@click='!conf.upgrade ? proceedToAdmin() : proceedToUpgrade()', :disabled='loading') Back
v-btn(color='primary', @click='proceedToFinal', v-if='!loading && !final.ok') Try Again
v-btn(color='success', @click='finish', v-if='loading || final.ok', :disabled='loading') Continue
...
...
@@ -342,7 +350,6 @@ export default {
pathContent
:
'./content'
,
port
:
siteConfig
.
port
||
80
,
public
:
(
siteConfig
.
public
===
true
),
selfRegister
:
(
siteConfig
.
selfRegister
===
true
),
telemetry
:
true
,
title
:
siteConfig
.
title
||
'Wiki'
,
upgrade
:
false
,
...
...
client/graph/editor/update.gql
0 → 100644
View file @
803d86ff
mutation
(
$id
:
Int
!,
$content
:
String
,
$description
:
String
,
$editor
:
String
,
$isPrivate
:
Boolean
,
$isPublished
:
Boolean
,
$locale
:
String
,
$path
:
String
,
$publishEndDate
:
Date
,
$publishStartDate
:
Date
,
$tags
:
[
String
],
$title
:
String
)
{
pages
{
update
(
id
:
$id
,
content
:
$content
,
description
:
$description
,
editor
:
$editor
,
isPrivate
:
$isPrivate
,
isPublished
:
$isPublished
,
locale
:
$locale
,
path
:
$path
,
publishEndDate
:
$publishEndDate
,
publishStartDate
:
$publishStartDate
,
tags
:
$tags
,
title
:
$title
)
{
responseResult
{
succeeded
errorCode
slug
message
}
}
}
}
client/store/editor.js
View file @
803d86ff
import
{
make
}
from
'vuex-pathify'
const
state
=
{
id
:
0
,
content
:
''
,
description
:
''
,
isPublished
:
true
,
...
...
server/db/migrations/2.0.0.js
View file @
803d86ff
...
...
@@ -68,6 +68,19 @@ exports.up = knex => {
table
.
string
(
'createdAt'
).
notNullable
()
table
.
string
(
'updatedAt'
).
notNullable
()
})
// PAGE HISTORY ------------------------
.
createTable
(
'pageHistory'
,
table
=>
{
table
.
increments
(
'id'
).
primary
()
table
.
string
(
'path'
).
notNullable
()
table
.
string
(
'title'
).
notNullable
()
table
.
string
(
'description'
)
table
.
boolean
(
'isPrivate'
).
notNullable
().
defaultTo
(
false
)
table
.
boolean
(
'isPublished'
).
notNullable
().
defaultTo
(
false
)
table
.
string
(
'publishStartDate'
)
table
.
string
(
'publishEndDate'
)
table
.
text
(
'content'
)
table
.
string
(
'createdAt'
).
notNullable
()
})
// PAGES -------------------------------
.
createTable
(
'pages'
,
table
=>
{
table
.
increments
(
'id'
).
primary
()
...
...
@@ -126,6 +139,12 @@ exports.up = knex => {
// =====================================
// RELATION TABLES
// =====================================
// PAGE HISTORY TAGS ---------------------------
.
createTable
(
'pageHistoryTags'
,
table
=>
{
table
.
increments
(
'id'
).
primary
()
table
.
integer
(
'pageId'
).
unsigned
().
references
(
'id'
).
inTable
(
'pageHistory'
).
onDelete
(
'CASCADE'
)
table
.
integer
(
'tagId'
).
unsigned
().
references
(
'id'
).
inTable
(
'tags'
).
onDelete
(
'CASCADE'
)
})
// PAGE TAGS ---------------------------
.
createTable
(
'pageTags'
,
table
=>
{
table
.
increments
(
'id'
).
primary
()
...
...
@@ -149,10 +168,17 @@ exports.up = knex => {
table
.
integer
(
'pageId'
).
unsigned
().
references
(
'id'
).
inTable
(
'pages'
)
table
.
integer
(
'authorId'
).
unsigned
().
references
(
'id'
).
inTable
(
'users'
)
})
.
table
(
'pageHistory'
,
table
=>
{
table
.
integer
(
'pageId'
).
unsigned
().
references
(
'id'
).
inTable
(
'pages'
)
table
.
string
(
'editorKey'
).
references
(
'key'
).
inTable
(
'editors'
)
table
.
string
(
'localeCode'
,
2
).
references
(
'code'
).
inTable
(
'locales'
)
table
.
integer
(
'authorId'
).
unsigned
().
references
(
'id'
).
inTable
(
'users'
)
})
.
table
(
'pages'
,
table
=>
{
table
.
string
(
'editorKey'
).
references
(
'key'
).
inTable
(
'editors'
)
table
.
string
(
'localeCode'
,
2
).
references
(
'code'
).
inTable
(
'locales'
)
table
.
integer
(
'authorId'
).
unsigned
().
references
(
'id'
).
inTable
(
'users'
)
table
.
integer
(
'creatorId'
).
unsigned
().
references
(
'id'
).
inTable
(
'users'
)
})
.
table
(
'users'
,
table
=>
{
table
.
string
(
'providerKey'
).
references
(
'key'
).
inTable
(
'authentication'
).
notNullable
().
defaultTo
(
'local'
)
...
...
server/db/models/pageHistory.js
0 → 100644
View file @
803d86ff
const
Model
=
require
(
'objection'
).
Model
/* global WIKI */
/**
* Page History model
*/
module
.
exports
=
class
PageHistory
extends
Model
{
static
get
tableName
()
{
return
'pageHistory'
}
static
get
jsonSchema
()
{
return
{
type
:
'object'
,
required
:
[
'path'
,
'title'
],
properties
:
{
id
:
{
type
:
'integer'
},
path
:
{
type
:
'string'
},
title
:
{
type
:
'string'
},
description
:
{
type
:
'string'
},
isPublished
:
{
type
:
'boolean'
},
publishStartDate
:
{
type
:
'string'
},
publishEndDate
:
{
type
:
'string'
},
content
:
{
type
:
'string'
},
createdAt
:
{
type
:
'string'
}
}
}
}
static
get
relationMappings
()
{
return
{
tags
:
{
relation
:
Model
.
ManyToManyRelation
,
modelClass
:
require
(
'./tags'
),
join
:
{
from
:
'pageHistory.id'
,
through
:
{
from
:
'pageHistoryTags.pageId'
,
to
:
'pageHistoryTags.tagId'
},
to
:
'tags.id'
}
},
page
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./pages'
),
join
:
{
from
:
'pageHistory.pageId'
,
to
:
'pages.id'
}
},
author
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./users'
),
join
:
{
from
:
'pageHistory.authorId'
,
to
:
'users.id'
}
},
editor
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./editors'
),
join
:
{
from
:
'pageHistory.editorKey'
,
to
:
'editors.key'
}
},
locale
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./locales'
),
join
:
{
from
:
'pageHistory.localeCode'
,
to
:
'locales.code'
}
}
}
}
$beforeInsert
()
{
this
.
createdAt
=
new
Date
().
toISOString
()
}
static
async
addVersion
(
opts
)
{
await
WIKI
.
db
.
pageHistory
.
query
().
insert
({
pageId
:
opts
.
id
,
authorId
:
opts
.
authorId
,
content
:
opts
.
content
,
description
:
opts
.
description
,
editorKey
:
opts
.
editorKey
,
isPrivate
:
opts
.
isPrivate
,
isPublished
:
opts
.
isPublished
,
localeCode
:
opts
.
localeCode
,
path
:
opts
.
path
,
publishEndDate
:
opts
.
publishEndDate
,
publishStartDate
:
opts
.
publishStartDate
,
title
:
opts
.
title
})
}
}
server/db/models/pages.js
View file @
803d86ff
...
...
@@ -51,6 +51,14 @@ module.exports = class Page extends Model {
to
:
'users.id'
}
},
creator
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./users'
),
join
:
{
from
:
'pages.creatorId'
,
to
:
'users.id'
}
},
editor
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
require
(
'./editors'
),
...
...
@@ -82,6 +90,7 @@ module.exports = class Page extends Model {
const
page
=
await
WIKI
.
db
.
pages
.
query
().
insertAndFetch
({
authorId
:
opts
.
authorId
,
content
:
opts
.
content
,
creatorId
:
opts
.
authorId
,
description
:
opts
.
description
,
editorKey
:
opts
.
editor
,
isPrivate
:
opts
.
isPrivate
,
...
...
@@ -92,7 +101,26 @@ module.exports = class Page extends Model {
publishStartDate
:
opts
.
publishStartDate
,
title
:
opts
.
title
})
await
WIKI
.
db
.
storage
.
createPage
(
page
)
await
WIKI
.
db
.
storage
.
pageEvent
({
event
:
'created'
,
page
})
return
page
}
static
async
updatePage
(
opts
)
{
const
ogPage
=
await
WIKI
.
db
.
pages
.
query
().
findById
(
opts
.
id
)
if
(
!
ogPage
)
{
throw
new
Error
(
'Invalid Page Id'
)
}
await
WIKI
.
db
.
pageHistory
.
addVersion
(
ogPage
)
const
page
=
await
WIKI
.
db
.
pages
.
query
().
patchAndFetch
({
title
:
opts
.
title
}).
where
(
'id'
,
opts
.
id
)
await
WIKI
.
db
.
storage
.
pageEvent
({
event
:
'updated'
,
page
})
return
page
}
}
server/db/models/storage.js
View file @
803d86ff
...
...
@@ -87,12 +87,12 @@ module.exports = class Storage extends Model {
}
}
static
async
createPage
(
page
)
{
static
async
pageEvent
(
event
,
page
)
{
const
targets
=
await
WIKI
.
db
.
storage
.
query
().
where
(
'isEnabled'
,
true
)
if
(
targets
&&
targets
.
length
>
0
)
{
_
.
forEach
(
targets
,
target
=>
{
WIKI
.
queue
.
job
.
syncStorage
.
add
({
event
:
'created'
,
event
,
target
,
page
},
{
...
...
server/graph/resolvers/page.js
View file @
803d86ff
...
...
@@ -24,7 +24,6 @@ module.exports = {
async
create
(
obj
,
args
,
context
)
{
const
page
=
await
WIKI
.
db
.
pages
.
createPage
({
...
args
,
isPrivate
:
false
,
authorId
:
context
.
req
.
user
.
id
})
return
{
...
...
@@ -38,10 +37,14 @@ module.exports = {
responseResult
:
graphHelper
.
generateSuccess
(
'Page has been deleted.'
)
}
},
async
update
(
obj
,
args
)
{
await
WIKI
.
db
.
groups
.
query
().
patch
({
name
:
args
.
name
}).
where
(
'id'
,
args
.
id
)
async
update
(
obj
,
args
,
context
)
{
const
page
=
await
WIKI
.
db
.
pages
.
updatePage
({
...
args
,
authorId
:
context
.
req
.
user
.
id
})
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'Page has been updated.'
)
responseResult
:
graphHelper
.
generateSuccess
(
'Page has been updated.'
),
page
}
}
},
...
...
server/graph/schemas/page.graphql
View file @
803d86ff
...
...
@@ -52,6 +52,7 @@ type PageMutation {
content
:
String
description
:
String
editor
:
String
isPrivate
:
Boolean
isPublished
:
Boolean
locale
:
String
path
:
String
...
...
@@ -59,7 +60,7 @@ type PageMutation {
publishStartDate
:
Date
tags
:
[
String
]
title
:
String
):
Default
Response
):
Page
Response
delete
(
id
:
Int
!
...
...
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