Skip to content

Commit

Permalink
Mod submitter date update (#2264)
Browse files Browse the repository at this point in the history
Resolves #2237
  • Loading branch information
dakota002 authored Jun 13, 2024
1 parent c94e2d6 commit 0726e62
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 25 deletions.
2 changes: 1 addition & 1 deletion app/components/TimeAgo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import RelativeTime from 'dayjs/plugin/relativeTime'

dayjs.locale(locale)
dayjs.extend(RelativeTime)
const dateTimeFormat = new Intl.DateTimeFormat(locale.name, {
export const dateTimeFormat = new Intl.DateTimeFormat(locale.name, {
dateStyle: 'full',
timeStyle: 'long',
timeZone: 'utc',
Expand Down
18 changes: 16 additions & 2 deletions app/routes/circulars._archive._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
createChangeRequest,
get,
getChangeRequests,
moderatorGroup,
put,
putVersion,
search,
Expand Down Expand Up @@ -97,9 +98,22 @@ export async function action({ request }: ActionFunctionArgs) {
if (circularId === undefined)
throw new Response('circularId is required', { status: 400 })
if (!user?.name || !user.email) throw new Response(null, { status: 403 })

let submitter, createdOnDate, createdOnTime, createdOn
if (user.groups.includes(moderatorGroup)) {
submitter = getFormDataString(data, 'submitter')
createdOnDate = getFormDataString(data, 'createdOnDate')
createdOnTime = getFormDataString(data, 'createdOnTime')
createdOn = Date.parse(`${createdOnDate} ${createdOnTime} UTC`)
}
if (!submitter || !createdOnDate || !createdOnTime || !createdOn)
throw new Response(null, { status: 400 })
await createChangeRequest(
{ circularId: parseFloat(circularId), ...props },
{
circularId: parseFloat(circularId),
...props,
submitter,
createdOn,
},
user
)
await postZendeskRequest({
Expand Down
8 changes: 7 additions & 1 deletion app/routes/circulars.correction.$circularId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ export async function loader({
const user = await getUser(request)
if (!user?.groups.includes(group)) throw new Response(null, { status: 403 })
const circular = await get(parseFloat(circularId))
const defaultDateTime = new Date(circular.createdOn ?? 0)
.toISOString()
.split('T')

return {
formattedContributor: user ? formatAuthor(user) : '',
defaultBody: circular.body,
defaultSubject: circular.subject,
defaultFormat: circular.format,
circularId: circular.circularId,
submitter: circular.submitter,
defaultSubmitter: circular.submitter,
defaultCreatedOnDate: defaultDateTime[0],
defaultCreatedOnTime: defaultDateTime[1].substring(0, 5),
searchString: '',
}
}
Expand Down
117 changes: 109 additions & 8 deletions app/routes/circulars.edit.$circularId/CircularEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import { Form, Link, useNavigation } from '@remix-run/react'
import {
Button,
ButtonGroup,
DatePicker,
Grid,
Icon,
InputGroup,
InputPrefix,
Table,
TextInput,
TimePicker,
} from '@trussworks/react-uswds'
import classnames from 'classnames'
import { type ReactNode, useContext, useState } from 'react'
Expand All @@ -24,12 +27,17 @@ import { MarkdownBody } from '../circulars.$circularId.($version)/Body'
import {
type CircularFormat,
bodyIsValid,
dateIsValid,
subjectIsValid,
submitterIsValid,
} from '../circulars/circulars.lib'
import { RichEditor } from './RichEditor'
import { CircularsKeywords } from '~/components/CircularsKeywords'
import CollapsableInfo from '~/components/CollapsableInfo'
import Spinner from '~/components/Spinner'
import { useModStatus } from '~/root'

import styles from './CircularsEditForm.module.css'

function SyntaxExample({
label,
Expand Down Expand Up @@ -103,20 +111,24 @@ export function SyntaxReference() {
export function CircularEditForm({
formattedContributor,
circularId,
submitter,
defaultSubmitter,
defaultFormat,
defaultBody,
defaultSubject,
searchString,
defaultCreatedOnDate,
defaultCreatedOnTime,
intent,
}: {
formattedContributor: string
circularId?: number
submitter?: string
defaultSubmitter?: string
defaultFormat?: CircularFormat
defaultBody: string
defaultSubject: string
searchString: string
defaultCreatedOnDate?: string
defaultCreatedOnTime?: string
intent: 'correction' | 'edit' | 'new'
}) {
let formSearchString = '?index'
Expand All @@ -130,9 +142,15 @@ export function CircularEditForm({
const [body, setBody] = useState(defaultBody)
const [subject, setSubject] = useState(defaultSubject)
const [format, setFormat] = useState(defaultFormat)
const [date, setDate] = useState(defaultCreatedOnDate)
const [time, setTime] = useState(defaultCreatedOnTime ?? '12:00')
const dateValid = circularId ? dateIsValid(date, time) : true

const [submitter, setSubmitter] = useState(defaultSubmitter)
const submitterValid = circularId ? submitterIsValid(submitter) : true
const bodyValid = bodyIsValid(body)
const sending = Boolean(useNavigation().formData)
const valid = subjectValid && bodyValid
const valid = subjectValid && bodyValid && dateValid && submitterValid
let headerText, saveButtonText

switch (intent) {
Expand All @@ -150,11 +168,16 @@ export function CircularEditForm({
break
}
const bodyPlaceholder = useBodyPlaceholder()

const changesHaveBeenMade =
body.trim() !== defaultBody.trim() ||
subject.trim() !== defaultSubject.trim() ||
format !== defaultFormat
format !== defaultFormat ||
submitter?.trim() !== defaultSubmitter ||
date !== defaultCreatedOnDate ||
time !== defaultCreatedOnTime

const userIsModerator = useModStatus()

return (
<AstroDataContext.Provider value={{ rel: 'noopener', target: '_blank' }}>
<h1>{headerText} GCN Circular</h1>
Expand All @@ -169,12 +192,25 @@ export function CircularEditForm({
)}
<Form method="POST" action={`/circulars${formSearchString}`}>
<input type="hidden" name="intent" value={intent} />
{circularId !== undefined && (
{circularId !== undefined && userIsModerator && (
<>
<input type="hidden" name="circularId" value={circularId} />
<InputGroup className="border-0 maxw-full">
<InputGroup
className={classnames('maxw-full', {
'usa-input--error': !submitterValid,
'usa-input--success': submitterValid,
})}
>
<InputPrefix className="wide-input-prefix">From</InputPrefix>
<span className="padding-1">{submitter}</span>
<TextInput
className="maxw-full"
name="submitter"
id="submitter"
type="text"
defaultValue={defaultSubmitter}
onChange={(event) => setSubmitter(event.target.value)}
required
/>
</InputGroup>
</>
)}
Expand All @@ -192,6 +228,71 @@ export function CircularEditForm({
</Button>
</Link>
</InputGroup>
{circularId !== undefined && (
<Grid row gap="md">
<Grid tablet={{ col: 'auto' }}>
<InputGroup
className={classnames({
'usa-input--error': !date || !Date.parse(date),
'usa-input--success': date && Date.parse(date),
})}
>
<InputPrefix className="wide-input-prefix">Date</InputPrefix>
<DatePicker
defaultValue={defaultCreatedOnDate}
className={classnames(
styles.DatePicker,
'border-0 flex-fill'
)}
onChange={(value) => {
setDate(value ?? '')
}}
name="createdOnDate"
id="createdOnDate"
dateFormat="YYYY-MM-DD"
/>
</InputGroup>
</Grid>
<Grid tablet={{ col: 'auto' }}>
<InputGroup
className={classnames({
'usa-input--error': !time,
'usa-input--success': time,
})}
>
{/* FIXME: The TimePicker component does not by itself
contribute useful form data because only the element has
a name, and the field does not. So the form data is only
populated correctly if the user selects an option from the
dropdown, but not if they type a valid value into the combo box.
See https://github.com/trussworks/react-uswds/issues/2806 */}
<input
type="hidden"
id="createdOnTime"
name="createdOnTime"
value={time}
/>
<InputPrefix className="wide-input-prefix">Time</InputPrefix>
{/* FIXME: Currently only 12 hour formats are supported. We should
switch to 24 hours as it is more common/useful for the community.
See https://github.com/trussworks/react-uswds/issues/2947 */}
<TimePicker
id="createdOnTimeSetter"
name="createdOnTimeSetter"
defaultValue={defaultCreatedOnTime}
className={classnames(styles.TimePicker, 'margin-top-neg-3')}
onChange={(value) => {
setTime(value ?? '')
}}
step={1}
label=""
/>
</InputGroup>
</Grid>
</Grid>
)}
<InputGroup
className={classnames('maxw-full', {
'usa-input--error': subjectValid === false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.DatePicker {
button {
margin-top: 0;
}
input {
background-color: transparent;
}
}

.TimePicker {
input {
background-color: transparent;
}
}
2 changes: 1 addition & 1 deletion app/routes/circulars.edit.$circularId/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function loader({
defaultSubject: circular.subject,
defaultFormat: circular.format,
circularId: circular.circularId,
submitter: circular.submitter,
defaultSubmitter: circular.submitter,
searchString: '',
}
}
Expand Down
22 changes: 19 additions & 3 deletions app/routes/circulars.moderation.$circularId.$requestor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getChangeRequest,
moderatorGroup,
} from './circulars/circulars.server'
import { dateTimeFormat } from '~/components/TimeAgo'
import { getFormDataString } from '~/lib/utils'
import type { BreadcrumbHandle } from '~/root/Title'

Expand Down Expand Up @@ -70,14 +71,22 @@ export async function loader({

export default function () {
const { circular, correction } = useLoaderData<typeof loader>()

return (
<>
<h2>Circular {circular.circularId}</h2>
<h3>Original Author</h3>
{circular.submitter}
<DiffedContent
oldString={circular.submitter}
newString={correction.submitter}
/>
<h3>Requestor</h3>
{correction.requestor}
<h3>Created On</h3>
<DiffedContent
oldString={dateTimeFormat.format(circular.createdOn)}
newString={dateTimeFormat.format(correction.createdOn)}
method="lines"
/>
<h3>Subject</h3>
<DiffedContent
oldString={circular.subject}
Expand All @@ -104,14 +113,21 @@ export default function () {
)
}

const methodMap = {
words: Diff.diffWords,
lines: Diff.diffLines,
}

function DiffedContent({
oldString,
newString,
method,
}: {
oldString: string
newString: string
method?: 'words' | 'lines'
}) {
const diff = Diff.diffWords(oldString, newString)
const diff = methodMap[method ?? 'words'](oldString, newString)

return (
<div>
Expand Down
11 changes: 11 additions & 0 deletions app/routes/circulars/circulars.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface CircularChangeRequest extends CircularMetadata {
requestorSub: string
requestorEmail: string
format: CircularFormat
submitter: string
createdOn: number
}

export interface CircularChangeRequestKeys {
Expand Down Expand Up @@ -130,6 +132,15 @@ export function formatIsValid(format: string): format is CircularFormat {
return (circularFormats as any as string[]).includes(format)
}

/** For updated dates, check that the date is valid */
export function dateIsValid(date?: string, time?: string) {
return !Number.isNaN(Date.parse(`${date}T${time}:00.000Z`))
}

export function submitterIsValid(submitter?: string) {
return Boolean(submitter)
}

export function emailIsAutoReply(subject: string) {
const lowercaseSubject = subject.toLowerCase()
return emailAutoReplyChecklist.some((x) => lowercaseSubject.includes(x))
Expand Down
Loading

0 comments on commit 0726e62

Please sign in to comment.