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) {
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) {

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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: ''

View File

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

View File

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

21
package-lock.json generated
View File

@ -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",

View File

@ -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",

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;