comment edit spagetti

This commit is contained in:
keyan 2021-08-10 17:59:06 -05:00
parent fec2a6ecc0
commit b4be2c613b
13 changed files with 212 additions and 17 deletions

View File

@ -159,11 +159,35 @@ export default {
} }
if (!parentId) { 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 }) 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 }) => { vote: async (parent, { id, sats = 1 }, { me, models }) => {
// need to make sure we are logged in // need to make sure we are logged in
if (!me) { if (!me) {

View File

@ -13,6 +13,7 @@ export default gql`
createLink(title: String!, url: String): Item! createLink(title: String!, url: String): Item!
createDiscussion(title: String!, text: String): Item! createDiscussion(title: String!, text: String): Item!
createComment(text: String!, parentId: ID!): Item! createComment(text: String!, parentId: ID!): Item!
updateComment(id: ID!, text: String!): Item!
vote(id: ID!, sats: Int): Int! vote(id: ID!, sats: Int): Int!
} }

View File

@ -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>
)
}

View File

@ -9,6 +9,9 @@ import UpVote from './upvote'
import Eye from '../svgs/eye-fill.svg' import Eye from '../svgs/eye-fill.svg'
import EyeClose from '../svgs/eye-close-line.svg' import EyeClose from '../svgs/eye-close-line.svg'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useMe } from './me'
import CommentEdit from './comment-edit'
import Countdown from 'react-countdown'
function Parent ({ item }) { function Parent ({ item }) {
const ParentFrag = () => ( const ParentFrag = () => (
@ -37,9 +40,15 @@ function Parent ({ item }) {
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) { export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) {
const [reply, setReply] = useState(replyOpen) const [reply, setReply] = useState(replyOpen)
const [edit, setEdit] = useState()
const [collapse, setCollapse] = useState(false) const [collapse, setCollapse] = useState(false)
const ref = useRef(null) const ref = useRef(null)
const router = useRouter() 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(() => { useEffect(() => {
if (Number(router.query.commentId) === Number(item.id)) { 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)} />)} : <EyeClose className={styles.collapser} height={10} width={10} onClick={() => setCollapse(true)} />)}
</div> </div>
<div className={styles.text}> {edit
<Text>{item.text}</Text> ? (
</div> <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> </div>
<div className={`${itemStyles.children} ${styles.children}`}> <div className={`${itemStyles.children} ${styles.children}`}>
{!noReply && {!noReply && !edit && (
<div <div className={`${itemStyles.other} ${styles.reply}`}>
className={`${itemStyles.other} ${styles.reply}`} <div
onClick={() => setReply(!reply)} className='d-inline-block'
> onClick={() => setReply(!reply)}
{reply ? 'cancel' : 'reply'} >
</div>} {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'}> <div className={reply ? styles.replyWrapper : 'd-none'}>
<Reply <Reply
parentId={item.id} autoFocus={!replyOpen} parentId={item.id} autoFocus={!replyOpen}
onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId} onSuccess={() => setReply(replyOpen || false)}
/> />
</div> </div>
{children} {children}

View File

@ -45,7 +45,8 @@
} }
.children { .children {
margin-top: .25rem; margin-top: 0;
padding-top: .25rem;
} }
.comments { .comments {

View File

@ -89,8 +89,8 @@ export function MarkdownInput ({ label, groupClassName, ...props }) {
{...props} {...props}
/> />
</div> </div>
<div className='form-group'> <div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
<div className={tab !== 'preview' ? 'd-none' : `${styles.text} form-control`}> <div className={`${styles.text} form-control`}>
<Text>{meta.value}</Text> <Text>{meta.value}</Text>
</div> </div>
</div> </div>

View File

@ -53,7 +53,6 @@
.skeleton .other { .skeleton .other {
height: 17px; height: 17px;
align-items: center; align-items: center;
display: flex;
} }
.skeleton .title { .skeleton .title {

View File

@ -47,7 +47,7 @@ export default function Reply ({ parentId, onSuccess, autoFocus }) {
) )
return ( return (
<div className={styles.reply}> <div className={`${styles.reply} mb-1`}>
<Form <Form
initial={{ initial={{
text: '' text: ''

View File

@ -8,6 +8,7 @@ export const COMMENT_FIELDS = gql`
text text
user { user {
name name
id
} }
sats sats
boost boost

View File

@ -9,6 +9,7 @@ export const ITEM_FIELDS = gql`
url url
user { user {
name name
id
} }
sats sats
boost boost

21
package-lock.json generated
View File

@ -28,6 +28,7 @@
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "17.0.1", "react": "17.0.1",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-countdown": "^2.3.2",
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-markdown": "^6.0.2", "react-markdown": "^6.0.2",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",
@ -8552,6 +8553,18 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" "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": { "node_modules/react-dom": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", "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": { "react-dom": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",

View File

@ -30,6 +30,7 @@
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "17.0.1", "react": "17.0.1",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-countdown": "^2.3.2",
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-markdown": "^6.0.2", "react-markdown": "^6.0.2",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",

View File

@ -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;