comment edit spagetti
This commit is contained in:
parent
fec2a6ecc0
commit
b4be2c613b
|
@ -159,11 +159,35 @@ export default {
|
|||
}
|
||||
|
||||
if (!parentId) {
|
||||
throw new UserInputError('comment must have parent', { argumentName: 'text' })
|
||||
throw new UserInputError('comment must have parent', { argumentName: 'parentId' })
|
||||
}
|
||||
|
||||
return await createItem(parent, { text, parentId }, { me, models })
|
||||
},
|
||||
updateComment: async (parent, { id, text }, { me, models }) => {
|
||||
if (!text) {
|
||||
throw new UserInputError('comment must have text', { argumentName: 'text' })
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new UserInputError('comment must have id', { argumentName: 'id' })
|
||||
}
|
||||
|
||||
// update iff this comment belongs to me
|
||||
const comment = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
if (Number(comment.userId) !== Number(me.id)) {
|
||||
throw new AuthenticationError('comment must belong to you')
|
||||
}
|
||||
|
||||
if (Date.now() > new Date(comment.createdAt).getTime() + 10 * 60000) {
|
||||
throw new UserInputError('comment can no longer be editted')
|
||||
}
|
||||
|
||||
return await models.item.update({
|
||||
where: { id: Number(id) },
|
||||
data: { text }
|
||||
})
|
||||
},
|
||||
vote: async (parent, { id, sats = 1 }, { me, models }) => {
|
||||
// need to make sure we are logged in
|
||||
if (!me) {
|
||||
|
|
|
@ -13,6 +13,7 @@ export default gql`
|
|||
createLink(title: String!, url: String): Item!
|
||||
createDiscussion(title: String!, text: String): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
vote(id: ID!, sats: Int): Int!
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import Countdown from 'react-countdown'
|
||||
|
||||
export const CommentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
})
|
||||
|
||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||
const [updateComment] = useMutation(
|
||||
gql`
|
||||
mutation updateComment($id: ID! $text: String!) {
|
||||
updateComment(id: $id, text: $text) {
|
||||
text
|
||||
}
|
||||
}`, {
|
||||
update (cache, { data: { updateComment } }) {
|
||||
cache.modify({
|
||||
id: `Item:${comment.id}`,
|
||||
fields: {
|
||||
text () {
|
||||
return updateComment.text
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`${styles.reply} mt-2`}>
|
||||
<Form
|
||||
initial={{
|
||||
text: comment.text
|
||||
}}
|
||||
schema={CommentSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
const { error } = await updateComment({ variables: { ...values, id: comment.id } })
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MarkdownInput
|
||||
name='text'
|
||||
as={TextareaAutosize}
|
||||
minRows={4}
|
||||
autoFocus
|
||||
required
|
||||
groupClassName='mb-0'
|
||||
hint={
|
||||
<span className='text-muted font-weight-bold'>
|
||||
<Countdown
|
||||
date={editThreshold}
|
||||
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<div className='d-flex align-items-center justify-content-between'>
|
||||
<SubmitButton variant='secondary' className='mt-1'>save</SubmitButton>
|
||||
<div
|
||||
className='font-weight-bold text-muted mr-3'
|
||||
style={{ fontSize: '80%', cursor: 'pointer' }}
|
||||
onClick={onCancel}
|
||||
>
|
||||
cancel
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -9,6 +9,9 @@ import UpVote from './upvote'
|
|||
import Eye from '../svgs/eye-fill.svg'
|
||||
import EyeClose from '../svgs/eye-close-line.svg'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMe } from './me'
|
||||
import CommentEdit from './comment-edit'
|
||||
import Countdown from 'react-countdown'
|
||||
|
||||
function Parent ({ item }) {
|
||||
const ParentFrag = () => (
|
||||
|
@ -37,9 +40,15 @@ function Parent ({ item }) {
|
|||
|
||||
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) {
|
||||
const [reply, setReply] = useState(replyOpen)
|
||||
const [edit, setEdit] = useState()
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const router = useRouter()
|
||||
const me = useMe()
|
||||
const mine = me.id === item.user.id
|
||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||
const [canEdit, setCanEdit] =
|
||||
useState(mine && (Date.now() < editThreshold))
|
||||
|
||||
useEffect(() => {
|
||||
if (Number(router.query.commentId) === Number(item.id)) {
|
||||
|
@ -81,23 +90,63 @@ export default function Comment ({ item, children, replyOpen, includeParent, cac
|
|||
: <EyeClose className={styles.collapser} height={10} width={10} onClick={() => setCollapse(true)} />)}
|
||||
|
||||
</div>
|
||||
<div className={styles.text}>
|
||||
<Text>{item.text}</Text>
|
||||
</div>
|
||||
{edit
|
||||
? (
|
||||
<div className={styles.replyWrapper}>
|
||||
<CommentEdit
|
||||
comment={item}
|
||||
onSuccess={() => {
|
||||
setEdit(!edit)
|
||||
setCanEdit(mine && (Date.now() < editThreshold))
|
||||
}}
|
||||
onCancel={() => {
|
||||
setEdit(!edit)
|
||||
setCanEdit(mine && (Date.now() < editThreshold))
|
||||
}}
|
||||
editThreshold={editThreshold}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={styles.text}>
|
||||
<Text>{item.text}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${itemStyles.children} ${styles.children}`}>
|
||||
{!noReply &&
|
||||
<div
|
||||
className={`${itemStyles.other} ${styles.reply}`}
|
||||
onClick={() => setReply(!reply)}
|
||||
>
|
||||
{reply ? 'cancel' : 'reply'}
|
||||
</div>}
|
||||
{!noReply && !edit && (
|
||||
<div className={`${itemStyles.other} ${styles.reply}`}>
|
||||
<div
|
||||
className='d-inline-block'
|
||||
onClick={() => setReply(!reply)}
|
||||
>
|
||||
{reply ? 'cancel' : 'reply'}
|
||||
</div>
|
||||
{canEdit && !reply && !edit &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
<div
|
||||
className='d-inline-block'
|
||||
onClick={() => setEdit(!edit)}
|
||||
>
|
||||
edit
|
||||
<Countdown
|
||||
date={editThreshold}
|
||||
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
|
||||
onComplete={() => {
|
||||
setCanEdit(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={reply ? styles.replyWrapper : 'd-none'}>
|
||||
<Reply
|
||||
parentId={item.id} autoFocus={!replyOpen}
|
||||
onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId}
|
||||
onSuccess={() => setReply(replyOpen || false)}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
}
|
||||
|
||||
.children {
|
||||
margin-top: .25rem;
|
||||
margin-top: 0;
|
||||
padding-top: .25rem;
|
||||
}
|
||||
|
||||
.comments {
|
||||
|
|
|
@ -89,8 +89,8 @@ export function MarkdownInput ({ label, groupClassName, ...props }) {
|
|||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<div className={tab !== 'preview' ? 'd-none' : `${styles.text} form-control`}>
|
||||
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
||||
<div className={`${styles.text} form-control`}>
|
||||
<Text>{meta.value}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,6 @@
|
|||
.skeleton .other {
|
||||
height: 17px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.skeleton .title {
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function Reply ({ parentId, onSuccess, autoFocus }) {
|
|||
)
|
||||
|
||||
return (
|
||||
<div className={styles.reply}>
|
||||
<div className={`${styles.reply} mb-1`}>
|
||||
<Form
|
||||
initial={{
|
||||
text: ''
|
||||
|
|
|
@ -8,6 +8,7 @@ export const COMMENT_FIELDS = gql`
|
|||
text
|
||||
user {
|
||||
name
|
||||
id
|
||||
}
|
||||
sats
|
||||
boost
|
||||
|
|
|
@ -9,6 +9,7 @@ export const ITEM_FIELDS = gql`
|
|||
url
|
||||
user {
|
||||
name
|
||||
id
|
||||
}
|
||||
sats
|
||||
boost
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.0.1",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-countdown": "^2.3.2",
|
||||
"react-dom": "17.0.1",
|
||||
"react-markdown": "^6.0.2",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
|
@ -8552,6 +8553,18 @@
|
|||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
||||
},
|
||||
"node_modules/react-countdown": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 15",
|
||||
"react-dom": ">= 15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
||||
|
@ -17996,6 +18009,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-countdown": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.0.1",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-countdown": "^2.3.2",
|
||||
"react-dom": "17.0.1",
|
||||
"react-markdown": "^6.0.2",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
-- Only update path if we have conditions that require us to reset it
|
||||
CREATE OR REPLACE FUNCTION update_item_path() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
npath ltree;
|
||||
BEGIN
|
||||
IF NEW."parentId" IS NULL THEN
|
||||
SELECT NEW.id::text::ltree INTO npath;
|
||||
NEW."path" = npath;
|
||||
ELSEIF TG_OP = 'INSERT' OR OLD."parentId" IS NULL OR OLD."parentId" != NEW."parentId" THEN
|
||||
SELECT "path" || NEW.id::text FROM "Item" WHERE id = NEW."parentId" INTO npath;
|
||||
IF npath IS NULL THEN
|
||||
RAISE EXCEPTION 'Invalid parent_id %', NEW."parentId";
|
||||
END IF;
|
||||
NEW."path" = npath;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
Loading…
Reference in New Issue