Skip to content

Commit

Permalink
Updating available fields for updates
Browse files Browse the repository at this point in the history
Updates the moderation form to allow edits of createdOn and submitter

Adds some validations

Include HH:MM in time selection

UTC, Grid, and fix button height

refactor to reuse TimeAgo, tidy up date time checks
  • Loading branch information
dakota002 committed May 13, 2024
1 parent 54496af commit ad71c16
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 107 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
16 changes: 13 additions & 3 deletions app/routes/circulars._archive._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
createChangeRequest,
get,
getChangeRequests,
moderatorGroup,
put,
putVersion,
search,
Expand Down Expand Up @@ -97,10 +98,19 @@ 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, createdOn
if (user.groups.includes(moderatorGroup)) {
submitter = getFormDataString(data, 'submitter')
createdOn = getFormDataString(data, 'createdOn')
}
await createChangeRequest(
{ circularId: parseFloat(circularId), ...props },
user
{
circularId: parseFloat(circularId),
...props,
},
user,
submitter,
createdOn ? parseFloat(createdOn) : undefined
)
await postZendeskRequest({
requester: { name: user.name, email: user.email },
Expand Down
1 change: 1 addition & 0 deletions app/routes/circulars.correction.$circularId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function loader({
defaultFormat: circular.format,
circularId: circular.circularId,
submitter: circular.submitter,
createdOn: circular.createdOn,
searchString: '',
}
}
Expand Down
279 changes: 182 additions & 97 deletions app/routes/circulars.edit.$circularId/CircularEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,35 @@ 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'
import { type ReactNode, useContext, useEffect, useState } from 'react'
import { dedent } from 'ts-dedent'

import { AstroDataContext } from '../circulars.$circularId.($version)/AstroDataContext'
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 @@ -108,6 +116,7 @@ export function CircularEditForm({
defaultBody,
defaultSubject,
searchString,
createdOn,
intent,
}: {
formattedContributor: string
Expand All @@ -117,6 +126,7 @@ export function CircularEditForm({
defaultBody: string
defaultSubject: string
searchString: string
createdOn?: number
intent: 'correction' | 'edit' | 'new'
}) {
let formSearchString = '?index'
Expand All @@ -130,9 +140,21 @@ export function CircularEditForm({
const [body, setBody] = useState(defaultBody)
const [subject, setSubject] = useState(defaultSubject)
const [format, setFormat] = useState(defaultFormat)

const defaultDateTime = new Date(createdOn ?? 0).toISOString().split('T')
// Trimmed to remove seconds since Date and time selectors are limited to "HH:MM" accuracy
const defaultDateString = `${defaultDateTime[0]} ${defaultDateTime[1].substring(0, 5)} UTC`
const [date, setDate] = useState(`${defaultDateTime[0]}`)
const [time, setTime] = useState(`${defaultDateTime[1].substring(0, 5)}`)
const [dateString, setDateString] = useState(defaultDateString)
const dateNumber = Date.parse(dateString)
const dateValid = dateIsValid(dateNumber)

const [updatedSubmitter, setUpdatedSubmitter] = useState(submitter)
const submitterValid = submitterIsValid(updatedSubmitter)
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 @@ -154,114 +176,177 @@ export function CircularEditForm({
const changesHaveBeenMade =
body.trim() !== defaultBody.trim() ||
subject.trim() !== defaultSubject.trim() ||
format !== defaultFormat
format !== defaultFormat ||
submitter !== updatedSubmitter?.trim() ||
Date.parse(defaultDateString) !== Date.parse(dateString)

const userIsModerator = useModStatus()

useEffect(() => {
setDateString(`${date} ${time} UTC`)
}, [date, time])

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 && (
<>
<input type="hidden" name="circularId" value={circularId} />
<Grid>
<Grid row>
{circularId !== undefined && (
<>
<input type="hidden" name="circularId" value={circularId} />
<InputGroup className="maxw-full">
<InputPrefix className="wide-input-prefix">From</InputPrefix>
<TextInput
autoFocus
className="maxw-full"
name="submitter"
id="submitter"
type="text"
defaultValue={submitter}
onChange={(event) =>
setUpdatedSubmitter(event.target.value)
}
required
disabled={!userIsModerator}
/>
</InputGroup>
</>
)}
</Grid>
<Grid row>
<InputGroup className="border-0 maxw-full">
<InputPrefix className="wide-input-prefix">From</InputPrefix>
<span className="padding-1">{submitter}</span>
<InputPrefix className="wide-input-prefix">
{circularId === undefined ? 'From' : 'Editor'}
</InputPrefix>
<span className="padding-1">{formattedContributor} </span>
<Link
to="/user"
title="Adjust how your name and affiliation appear in new GCN Circulars"
>
<Button unstyled type="button">
<Icon.Edit role="presentation" /> Edit
</Button>
</Link>
</InputGroup>
</>
)}
<InputGroup className="border-0 maxw-full">
<InputPrefix className="wide-input-prefix">
{circularId === undefined ? 'From' : 'Editor'}
</InputPrefix>
<span className="padding-1">{formattedContributor} </span>
<Link
to="/user"
title="Adjust how your name and affiliation appear in new GCN Circulars"
</Grid>
<Grid row>
<input
type="hidden"
name="createdOn"
id="createdOn"
value={dateNumber}
/>
<InputGroup className="border-0">
<InputPrefix className="wide-input-prefix">Date</InputPrefix>
<DatePicker
defaultValue={date}
className={classnames(styles.DatePicker, 'border-1px ')}
onChange={(value) => {
setDate(value ?? '')
}}
name="submissionDatePicker"
id="submissionDatePicker"
/>
</InputGroup>
<InputGroup className="tablet:grid-col-auto">
<InputPrefix className="wide-input-prefix">Time</InputPrefix>
<TimePicker
id="submissionTimePicker"
name="submissionTimePicker"
defaultValue={time}
className="margin-top-neg-3"
onChange={(value) => {
setTime(value ?? '')
}}
step={1}
label=""
/>
</InputGroup>
</Grid>
<Grid row>
<InputGroup
className={classnames('maxw-full', {
'usa-input--error': subjectValid === false,
'usa-input--success': subjectValid,
})}
>
<InputPrefix className="wide-input-prefix">Subject</InputPrefix>
<TextInput
autoFocus
aria-describedby="subjectDescription"
className="maxw-full"
name="subject"
id="subject"
type="text"
placeholder={useSubjectPlaceholder()}
defaultValue={defaultSubject}
required
onChange={({ target: { value } }) => {
setSubject(value)
setSubjectValid(subjectIsValid(value))
}}
/>
</InputGroup>
</Grid>
<CollapsableInfo
id="subjectDescription"
preambleText="The subject line must contain (and should start with) the name of the transient, which must start with one of the"
buttonText="known keywords"
>
<Button unstyled type="button">
<Icon.Edit role="presentation" /> Edit
</Button>
</Link>
</InputGroup>
<InputGroup
className={classnames('maxw-full', {
'usa-input--error': subjectValid === false,
'usa-input--success': subjectValid,
})}
>
<InputPrefix className="wide-input-prefix">Subject</InputPrefix>
<TextInput
autoFocus
aria-describedby="subjectDescription"
className="maxw-full"
name="subject"
id="subject"
type="text"
placeholder={useSubjectPlaceholder()}
defaultValue={defaultSubject}
<CircularsKeywords />
</CollapsableInfo>
<label hidden htmlFor="body">
Body
</label>
<RichEditor
aria-describedby="bodyDescription"
placeholder={bodyPlaceholder}
defaultValue={defaultBody}
defaultMarkdown={defaultFormat === 'text/markdown'}
required
className={bodyValid ? 'usa-input--success' : undefined}
onChange={({ target: { value } }) => {
setSubject(value)
setSubjectValid(subjectIsValid(value))
setBody(value)
}}
markdownStateSetter={setFormat}
/>
</InputGroup>
<CollapsableInfo
id="subjectDescription"
preambleText="The subject line must contain (and should start with) the name of the transient, which must start with one of the"
buttonText="known keywords"
>
<CircularsKeywords />
</CollapsableInfo>
<label hidden htmlFor="body">
Body
</label>
<RichEditor
aria-describedby="bodyDescription"
placeholder={bodyPlaceholder}
defaultValue={defaultBody}
defaultMarkdown={defaultFormat === 'text/markdown'}
required
className={bodyValid ? 'usa-input--success' : undefined}
onChange={({ target: { value } }) => {
setBody(value)
}}
markdownStateSetter={setFormat}
/>
<CollapsableInfo
id="bodyDescription"
preambleText={
<>
Body text. If this is your first Circular, please review the{' '}
<Link to="/docs/circulars/styleguide">style guide</Link>.
References to Circulars, DOIs, arXiv preprints, and transients are
automatically shown as links; see
</>
}
buttonText="syntax"
>
<SyntaxReference />
</CollapsableInfo>
<ButtonGroup>
<Link
to={`/circulars${searchString}`}
className="usa-button usa-button--outline"
>
Back
</Link>
<Button
disabled={sending || !valid || !changesHaveBeenMade}
type="submit"
value="save"
<CollapsableInfo
id="bodyDescription"
preambleText={
<>
Body text. If this is your first Circular, please review the{' '}
<Link to="/docs/circulars/styleguide">style guide</Link>.
References to Circulars, DOIs, arXiv preprints, and transients
are automatically shown as links; see
</>
}
buttonText="syntax"
>
{saveButtonText}
</Button>
{sending && (
<div className="padding-top-1 padding-bottom-1">
<Spinner /> Sending...
</div>
)}
</ButtonGroup>
<SyntaxReference />
</CollapsableInfo>
<ButtonGroup>
<Link
to={`/circulars${searchString}`}
className="usa-button usa-button--outline"
>
Back
</Link>
<Button
disabled={sending || !valid || !changesHaveBeenMade}
type="submit"
value="save"
>
{saveButtonText}
</Button>
{sending && (
<div className="padding-top-1 padding-bottom-1">
<Spinner /> Sending...
</div>
)}
</ButtonGroup>
</Grid>
</Form>
</AstroDataContext.Provider>
)
Expand Down
Loading

0 comments on commit ad71c16

Please sign in to comment.