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
8a749047
Commit
8a749047
authored
May 31, 2020
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: comments delete + refresh on post + formatting
parent
83f7c286
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
369 additions
and
46 deletions
+369
-46
comments.vue
client/components/comments.vue
+173
-32
app.scss
client/themes/default/scss/app.scss
+1
-1
comment.js
server/graph/resolvers/comment.js
+42
-5
comment.graphql
server/graph/schemas/comment.graphql
+8
-2
error.js
server/helpers/error.js
+20
-0
comments.js
server/models/comments.js
+102
-0
comment.js
server/modules/comments/default/comment.js
+23
-6
No files found.
client/components/comments.vue
View file @
8a749047
...
...
@@ -51,7 +51,7 @@
v-icon(left) mdi-comment
span.text-none Post Comment
v-divider.mt-3(v-if='permissions.write')
.pa-5.d-flex.align-center.justify-center(v-if='isLoading')
.pa-5.d-flex.align-center.justify-center(v-if='isLoading
&& !hasLoadedOnce
')
v-progress-circular(
indeterminate
size='20'
...
...
@@ -63,22 +63,38 @@
dense
v-else-if='comments && comments.length > 0'
)
v-timeline-item(
v-timeline-item
.comments-post
(
color='pink darken-4'
large
v-for='cm of comments'
:key='`comment-` + cm.id'
:id='`comment-post-id-` + cm.id'
)
template(v-slot:icon)
v-avatar
v-img(src='http://i.pravatar.cc/64')
v-avatar(color='blue-grey')
//- v-img(src='http://i.pravatar.cc/64')
span.white--text.title
{{
cm
.
initials
}}
v-card.elevation-1
v-card-text
.caption: strong
{{
cm
.
authorName
}}
.overline.grey--text 3 minutes ago
.mt-3
{{
cm
.
render
}}
.comments-post-actions(v-if='permissions.manage && !isBusy')
v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil
v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete
.comments-post-name.caption: strong
{{
cm
.
authorName
}}
.comments-post-date.overline.grey--text
{{
cm
.
createdAt
|
moment
(
'from'
)
}}
#[em(v-if='cm.createdAt !== cm.updatedAt') - modified
{{
cm
.
updatedAt
|
moment
(
'from'
)
}}
]
.comments-post-content.mt-3(v-html='cm.render')
.pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') Be the first to comment.
.text-center.body-2.blue-grey--text(v-else) No comments yet.
v-dialog(v-model='deleteCommentDialogShown', max-width='500')
v-card
.dialog-header.is-red Confirm Delete
v-card-text.pt-5
span Are you sure you want to permanently delete this comment?
.caption: strong This action cannot be undone!
v-card-chin
v-spacer
v-btn(text, @click='deleteCommentDialogShown = false')
{{
$t
(
'common:actions.cancel'
)
}}
v-btn(color='red', dark, @click='deleteComment')
{{
$t
(
'common:actions.delete'
)
}}
</
template
>
<
script
>
...
...
@@ -92,10 +108,18 @@ export default {
return
{
newcomment
:
''
,
isLoading
:
true
,
canFetch
:
false
,
hasLoadedOnce
:
false
,
comments
:
[],
guestName
:
''
,
guestEmail
:
''
guestEmail
:
''
,
commentToDelete
:
{},
deleteCommentDialogShown
:
false
,
isBusy
:
false
,
scrollOpts
:
{
duration
:
1500
,
offset
:
0
,
easing
:
'easeInOutCubic'
}
}
},
computed
:
{
...
...
@@ -107,10 +131,46 @@ export default {
methods
:
{
onIntersect
(
entries
,
observer
,
isIntersecting
)
{
if
(
isIntersecting
)
{
this
.
fetch
()
}
},
async
fetch
()
{
this
.
isLoading
=
true
this
.
canFetch
=
true
const
results
=
await
this
.
$apollo
.
query
({
query
:
gql
`
query ($locale: String!, $path: String!) {
comments {
list(locale: $locale, path: $path) {
id
render
authorName
createdAt
updatedAt
}
}
}
`
,
variables
:
{
locale
:
this
.
$store
.
get
(
'page/locale'
),
path
:
this
.
$store
.
get
(
'page/path'
)
},
fetchPolicy
:
'network-only'
})
this
.
comments
=
_
.
get
(
results
,
'data.comments.list'
,
[]).
map
(
c
=>
{
const
nameParts
=
c
.
authorName
.
toUpperCase
().
split
(
' '
)
let
initials
=
_
.
head
(
nameParts
).
charAt
(
0
)
if
(
nameParts
.
length
>
1
)
{
initials
+=
_
.
last
(
nameParts
).
charAt
(
0
)
}
c
.
initials
=
initials
return
c
})
this
.
isLoading
=
false
this
.
hasLoadedOnce
=
true
},
/**
* Post New Comment
*/
async
postComment
()
{
let
rules
=
{
comment
:
{
...
...
@@ -177,6 +237,7 @@ export default {
slug
message
}
id
}
}
}
...
...
@@ -198,6 +259,10 @@ export default {
})
this
.
newcomment
=
''
await
this
.
fetch
()
this
.
$nextTick
(()
=>
{
this
.
$vuetify
.
goTo
(
`#comment-post-id-
${
_
.
get
(
resp
,
'data.comments.create.id'
,
0
)}
`
,
this
.
scrollOpts
)
})
}
else
{
this
.
$store
.
commit
(
'showNotification'
,
{
style
:
'red'
,
...
...
@@ -205,41 +270,117 @@ export default {
icon
:
'alert'
})
}
}
},
apollo
:
{
comments
:
{
query
:
gql
`
query ($pageId: Int!) {
async
editComment
(
cm
)
{
},
deleteCommentConfirm
(
cm
)
{
this
.
commentToDelete
=
cm
this
.
deleteCommentDialogShown
=
true
},
/**
* Delete Comment
*/
async
deleteComment
()
{
this
.
$store
.
commit
(
`loadingStart`
,
'comments-delete'
)
this
.
isBusy
=
true
this
.
deleteCommentDialogShown
=
false
const
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
gql
`
mutation (
$id: Int!
) {
comments {
list(pageId: $pageId) {
id
render
authorName
createdAt
updatedAt
delete (
id: $id
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`
,
variables
()
{
return
{
pageId
:
this
.
pageId
variables
:
{
id
:
this
.
commentToDelete
.
id
}
},
skip
()
{
return
!
this
.
canFetch
},
fetchPolicy
:
'cache-and-network'
,
update
:
(
data
)
=>
data
.
comments
.
list
,
watchLoading
(
isLoading
)
{
this
.
isLoading
=
isLoading
})
if
(
_
.
get
(
resp
,
'data.comments.delete.responseResult.succeeded'
,
false
))
{
this
.
$store
.
commit
(
'showNotification'
,
{
style
:
'success'
,
message
:
'Comment was deleted successfully.'
,
icon
:
'check'
})
this
.
comments
=
_
.
reject
(
this
.
comments
,
[
'id'
,
this
.
commentToDelete
.
id
])
}
else
{
this
.
$store
.
commit
(
'showNotification'
,
{
style
:
'red'
,
message
:
_
.
get
(
resp
,
'data.comments.delete.responseResult.message'
,
'An unexpected error occured.'
),
icon
:
'alert'
})
}
this
.
isBusy
=
false
this
.
$store
.
commit
(
`loadingStop`
,
'comments-delete'
)
}
}
}
</
script
>
<
style
lang=
"scss"
>
.comments-post
{
position
:
relative
;
&
:hover
{
.comments-post-actions
{
opacity
:
1
;
}
}
&
-actions
{
position
:
absolute
;
top
:
16px
;
right
:
16px
;
opacity
:
0
;
transition
:
opacity
.4s
ease
;
}
&
-content
{
>
p
:first-child
{
padding-top
:
0
;
}
p
{
padding-top
:
1rem
;
margin-bottom
:
0
;
}
img
{
max-width
:
100%
;
}
code
{
background-color
:
rgba
(
mc
(
'pink'
,
'500'
)
,
.1
);
box-shadow
:
none
;
}
pre
>
code
{
margin-top
:
1rem
;
padding
:
12px
;
background-color
:
#111
;
box-shadow
:
none
;
border-radius
:
5px
;
width
:
100%
;
color
:
#FFF
;
font-weight
:
400
;
font-size
:
.85rem
;
font-family
:
Roboto
Mono
,
monospace
;
}
}
}
</
style
>
client/themes/default/scss/app.scss
View file @
8a749047
...
...
@@ -823,7 +823,7 @@
border-radius
:
7px
7px
0
0
;
@at-root
.theme--dark
&
{
background-color
:
lighten
(
mc
(
'grey'
,
'900'
)
,
5%
);
background-color
:
lighten
(
mc
(
'
blue-
grey'
,
'900'
)
,
5%
);
}
}
...
...
server/graph/resolvers/comment.js
View file @
8a749047
...
...
@@ -40,8 +40,26 @@ module.exports = {
* Fetch list of comments for a page
*/
async
list
(
obj
,
args
,
context
)
{
const
page
=
await
WIKI
.
models
.
pages
.
getPage
(
args
)
if
(
page
)
{
if
(
WIKI
.
auth
.
checkAccess
(
context
.
req
.
user
,
[
'read:comments'
],
{
path
:
page
.
path
,
locale
:
page
.
localeCode
}))
{
const
comments
=
await
WIKI
.
models
.
comments
.
query
().
where
(
'pageId'
,
page
.
id
)
return
comments
.
map
(
c
=>
({
...
c
,
authorName
:
c
.
name
,
authorEmail
:
c
.
email
,
authorIP
:
c
.
ip
}))
}
else
{
throw
new
WIKI
.
Error
.
PageViewForbidden
()
}
}
else
{
return
[]
}
}
},
CommentMutation
:
{
/**
...
...
@@ -49,12 +67,31 @@ module.exports = {
*/
async
create
(
obj
,
args
,
context
)
{
try
{
// WIKI.data.commentProvider.create({
// ...args,
// user: context.req.user
// })
const
cmId
=
await
WIKI
.
models
.
comments
.
postNewComment
({
...
args
,
user
:
context
.
req
.
user
,
ip
:
context
.
req
.
ip
})
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'New comment posted successfully'
),
id
:
cmId
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
},
/**
* Delete an Existing Comment
*/
async
delete
(
obj
,
args
,
context
)
{
try
{
await
WIKI
.
models
.
comments
.
deleteComment
({
id
:
args
.
id
,
user
:
context
.
req
.
user
,
ip
:
context
.
req
.
ip
})
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'
New comment pos
ted successfully'
)
responseResult
:
graphHelper
.
generateSuccess
(
'
Comment dele
ted successfully'
)
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
...
...
server/graph/schemas/comment.graphql
View file @
8a749047
...
...
@@ -18,7 +18,8 @@ type CommentQuery {
providers
:
[
CommentProvider
]
@
auth
(
requires
:
[
"
manage
:
system
"
])
list
(
pageId
:
Int
!
locale
:
String
!
path
:
String
!
):
[
CommentPost
]!
@
auth
(
requires
:
[
"
read
:
comments
"
,
"
manage
:
system
"
])
single
(
...
...
@@ -41,7 +42,7 @@ type CommentMutation {
content
:
String
!
guestName
:
String
guestEmail
:
String
):
Default
Response
@
auth
(
requires
:
[
"
write
:
comments
"
,
"
manage
:
system
"
])
):
CommentCreate
Response
@
auth
(
requires
:
[
"
write
:
comments
"
,
"
manage
:
system
"
])
update
(
id
:
Int
!
...
...
@@ -85,3 +86,8 @@ type CommentPost {
createdAt
:
Date
!
updatedAt
:
Date
!
}
type
CommentCreateResponse
{
responseResult
:
ResponseStatus
id
:
Int
}
server/helpers/error.js
View file @
8a749047
...
...
@@ -97,6 +97,26 @@ module.exports = {
message
:
'Too many attempts! Try again later.'
,
code
:
1008
}),
CommentGenericError
:
CustomError
(
'CommentGenericError'
,
{
message
:
'An unexpected error occured.'
,
code
:
8001
}),
CommentPostForbidden
:
CustomError
(
'CommentPostForbidden'
,
{
message
:
'You are not authorized to post a comment on this page.'
,
code
:
8002
}),
CommentContentMissing
:
CustomError
(
'CommentContentMissing'
,
{
message
:
'Comment content is missing or too short.'
,
code
:
8003
}),
CommentManageForbidden
:
CustomError
(
'CommentManageForbidden'
,
{
message
:
'You are not authorized to manage comments on this page.'
,
code
:
8004
}),
CommentNotFound
:
CustomError
(
'CommentNotFound'
,
{
message
:
'This comment does not exist.'
,
code
:
8005
}),
InputInvalid
:
CustomError
(
'InputInvalid'
,
{
message
:
'Input data is invalid.'
,
code
:
1012
...
...
server/models/comments.js
View file @
8a749047
const
Model
=
require
(
'objection'
).
Model
const
validate
=
require
(
'validate.js'
)
const
_
=
require
(
'lodash'
)
/* global WIKI */
/**
* Comments model
...
...
@@ -52,4 +56,102 @@ module.exports = class Comment extends Model {
this
.
createdAt
=
new
Date
().
toISOString
()
this
.
updatedAt
=
new
Date
().
toISOString
()
}
/**
* Post New Comment
*/
static
async
postNewComment
({
pageId
,
replyTo
,
content
,
guestName
,
guestEmail
,
user
,
ip
})
{
// -> Input validation
if
(
user
.
id
===
2
)
{
const
validation
=
validate
({
email
:
_
.
toLower
(
guestEmail
),
name
:
guestName
},
{
email
:
{
email
:
true
,
length
:
{
maximum
:
255
}
},
name
:
{
presence
:
{
allowEmpty
:
false
},
length
:
{
minimum
:
2
,
maximum
:
255
}
}
},
{
format
:
'flat'
})
if
(
validation
&&
validation
.
length
>
0
)
{
throw
new
WIKI
.
Error
.
InputInvalid
(
validation
[
0
])
}
}
content
=
_
.
trim
(
content
)
if
(
content
.
length
<
2
)
{
throw
new
WIKI
.
Error
.
CommentContentMissing
()
}
// -> Load Page
const
page
=
await
WIKI
.
models
.
pages
.
getPageFromDb
(
pageId
)
if
(
page
)
{
if
(
!
WIKI
.
auth
.
checkAccess
(
user
,
[
'write:comments'
],
{
path
:
page
.
path
,
locale
:
page
.
localeCode
}))
{
throw
new
WIKI
.
Error
.
CommentPostForbidden
()
}
}
else
{
throw
new
WIKI
.
Error
.
PageNotFound
()
}
// -> Process by comment provider
return
WIKI
.
data
.
commentProvider
.
create
({
page
,
replyTo
,
content
,
user
:
{
...
user
,
...(
user
.
id
===
2
)
?
{
name
:
guestName
,
email
:
guestEmail
}
:
{},
ip
}
})
}
/**
* Delete an Existing Comment
*/
static
async
deleteComment
({
id
,
user
,
ip
})
{
// -> Load Page
const
pageId
=
await
WIKI
.
data
.
commentProvider
.
getPageIdFromCommentId
(
id
)
if
(
!
pageId
)
{
throw
new
WIKI
.
Error
.
CommentNotFound
()
}
const
page
=
await
WIKI
.
models
.
pages
.
getPageFromDb
(
pageId
)
if
(
page
)
{
if
(
!
WIKI
.
auth
.
checkAccess
(
user
,
[
'manage:comments'
],
{
path
:
page
.
path
,
locale
:
page
.
localeCode
}))
{
throw
new
WIKI
.
Error
.
CommentManageForbidden
()
}
}
else
{
throw
new
WIKI
.
Error
.
PageNotFound
()
}
// -> Process by comment provider
await
WIKI
.
data
.
commentProvider
.
remove
({
id
,
page
,
user
:
{
...
user
,
ip
}
})
}
}
server/modules/comments/default/comment.js
View file @
8a749047
...
...
@@ -74,7 +74,7 @@ module.exports = {
ip
:
user
.
ip
}
// Check for Spam with Akismet
//
->
Check for Spam with Akismet
if
(
akismetClient
)
{
let
userRole
=
'user'
if
(
user
.
groups
.
indexOf
(
1
)
>=
0
)
{
...
...
@@ -106,16 +106,33 @@ module.exports = {
}
}
// Save Comment
await
WIKI
.
models
.
comments
.
query
().
insert
(
newComment
)
// -> Save Comment to DB
const
cm
=
await
WIKI
.
models
.
comments
.
query
().
insert
(
newComment
)
// -> Return Comment ID
return
cm
.
id
},
async
update
({
id
,
content
,
user
,
ip
})
{
},
/**
* Delete an existing comment by ID
*/
async
remove
({
id
,
user
,
ip
})
{
return
WIKI
.
models
.
comments
.
query
().
findById
(
id
).
delete
()
},
async
count
({
pageId
})
{
/**
* Get the page ID from a comment ID
*/
async
getPageIdFromCommentId
(
id
)
{
const
result
=
await
WIKI
.
models
.
comments
.
query
().
select
(
'pageId'
).
findById
(
id
)
return
(
result
)
?
result
.
pageId
:
false
},
/**
* Get the total comments count for a page ID
*/
async
count
(
pageId
)
{
const
result
=
await
WIKI
.
models
.
comments
.
query
().
count
(
'* as total'
).
where
(
'pageId'
,
pageId
).
first
()
return
_
.
toSafeInteger
(
result
.
total
)
}
}
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