Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mod submitter date update #2264

Merged
merged 5 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
lpsinger marked this conversation as resolved.
Show resolved Hide resolved
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,
dakota002 marked this conversation as resolved.
Show resolved Hide resolved
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,22 +168,40 @@ 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>
<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 @@ -183,6 +219,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}
/>
lpsinger marked this conversation as resolved.
Show resolved Hide resolved
<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} ${time} UTC`))
dakota002 marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading