diff --git a/app/components/TimeAgo.tsx b/app/components/TimeAgo.tsx index 289f027ca3..24e9eb9471 100644 --- a/app/components/TimeAgo.tsx +++ b/app/components/TimeAgo.tsx @@ -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', diff --git a/app/routes/circulars._archive._index/route.tsx b/app/routes/circulars._archive._index/route.tsx index d10dc49917..8beb179417 100644 --- a/app/routes/circulars._archive._index/route.tsx +++ b/app/routes/circulars._archive._index/route.tsx @@ -36,6 +36,7 @@ import { createChangeRequest, get, getChangeRequests, + moderatorGroup, put, putVersion, search, @@ -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 }, diff --git a/app/routes/circulars.correction.$circularId.tsx b/app/routes/circulars.correction.$circularId.tsx index 243ab77a57..4e94e220c7 100644 --- a/app/routes/circulars.correction.$circularId.tsx +++ b/app/routes/circulars.correction.$circularId.tsx @@ -36,6 +36,7 @@ export async function loader({ defaultFormat: circular.format, circularId: circular.circularId, submitter: circular.submitter, + createdOn: circular.createdOn, searchString: '', } } diff --git a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx index fb0f635a24..79ef4be979 100644 --- a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx +++ b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx @@ -9,14 +9,17 @@ 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' @@ -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, @@ -108,6 +116,7 @@ export function CircularEditForm({ defaultBody, defaultSubject, searchString, + createdOn, intent, }: { formattedContributor: string @@ -117,6 +126,7 @@ export function CircularEditForm({ defaultBody: string defaultSubject: string searchString: string + createdOn?: number intent: 'correction' | 'edit' | 'new' }) { let formSearchString = '?index' @@ -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) { @@ -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 (

{headerText} GCN Circular

- {circularId !== undefined && ( - <> - + + + {circularId !== undefined && ( + <> + + + From + + setUpdatedSubmitter(event.target.value) + } + required + disabled={!userIsModerator} + /> + + + )} + + - From - {submitter} + + {circularId === undefined ? 'From' : 'Editor'} + + {formattedContributor} + + + - - )} - - - {circularId === undefined ? 'From' : 'Editor'} - - {formattedContributor} - + + + + Date + { + setDate(value ?? '') + }} + name="submissionDatePicker" + id="submissionDatePicker" + /> + + + Time + { + setTime(value ?? '') + }} + step={1} + label="" + /> + + + + + Subject + { + setSubject(value) + setSubjectValid(subjectIsValid(value)) + }} + /> + + + - - - - - Subject - + + + { - setSubject(value) - setSubjectValid(subjectIsValid(value)) + setBody(value) }} + markdownStateSetter={setFormat} /> - - - - - - { - setBody(value) - }} - markdownStateSetter={setFormat} - /> - - Body text. If this is your first Circular, please review the{' '} - style guide. - References to Circulars, DOIs, arXiv preprints, and transients are - automatically shown as links; see - - } - buttonText="syntax" - > - - - - - Back - - - {sending && ( -
- Sending... -
- )} -
+ + + + + Back + + + {sending && ( +
+ Sending... +
+ )} +
+
) diff --git a/app/routes/circulars.edit.$circularId/CircularsEditForm.module.scss b/app/routes/circulars.edit.$circularId/CircularsEditForm.module.scss new file mode 100644 index 0000000000..5d16e2d9fa --- /dev/null +++ b/app/routes/circulars.edit.$circularId/CircularsEditForm.module.scss @@ -0,0 +1,5 @@ +.DatePicker { + button { + margin-top: 0; + } +} diff --git a/app/routes/circulars.moderation.$circularId.$requestor.tsx b/app/routes/circulars.moderation.$circularId.$requestor.tsx index f74a422a2e..f614ecc091 100644 --- a/app/routes/circulars.moderation.$circularId.$requestor.tsx +++ b/app/routes/circulars.moderation.$circularId.$requestor.tsx @@ -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' @@ -70,14 +71,22 @@ export async function loader({ export default function () { const { circular, correction } = useLoaderData() - return ( <>

Circular {circular.circularId}

Original Author

- {circular.submitter} +

Requestor

{correction.requestor} +

Created On

+

Subject

diff --git a/app/routes/circulars/circulars.lib.ts b/app/routes/circulars/circulars.lib.ts index 060b2ddf09..ed03ba5f6d 100644 --- a/app/routes/circulars/circulars.lib.ts +++ b/app/routes/circulars/circulars.lib.ts @@ -37,6 +37,8 @@ export interface CircularChangeRequest extends CircularMetadata { requestorSub: string requestorEmail: string format: CircularFormat + submitter: string + createdOn: number } export interface CircularChangeRequestKeys { @@ -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?: number) { + return date !== undefined +} + +export function submitterIsValid(submitter?: string) { + return submitter !== undefined +} + export function emailIsAutoReply(subject: string) { const lowercaseSubject = subject.toLowerCase() return emailAutoReplyChecklist.some((x) => lowercaseSubject.includes(x)) diff --git a/app/routes/circulars/circulars.server.ts b/app/routes/circulars/circulars.server.ts index 08864b594c..3acf25bf46 100644 --- a/app/routes/circulars/circulars.server.ts +++ b/app/routes/circulars/circulars.server.ts @@ -369,15 +369,17 @@ export async function createChangeRequest( item: Omit< Circular, | 'sub' - | 'createdOn' - | 'submitter' | 'submittedHow' | 'bibcode' | 'editedBy' | 'version' | 'editedOn' + | 'submitter' + | 'createdOn' >, - user?: User + user?: User, + submitter?: string, + createdOn?: number ) { validateCircular(item) if (!user) @@ -386,11 +388,16 @@ export async function createChangeRequest( }) const requestor = formatAuthor(user) const db = await tables() + const circular = (await db.circulars.get({ + circularId: item.circularId, + })) as Circular await db.circulars_change_requests.put({ ...item, requestorSub: user.sub, requestorEmail: user.email, requestor, + createdOn: createdOn ?? circular.createdOn, + submitter: submitter ?? circular.submitter, }) await sendEmail({ @@ -518,6 +525,8 @@ export async function approveChangeRequest( editedBy: `${formatAuthor(user)} on behalf of ${changeRequest.requestor}`, editedOn: Date.now(), format: changeRequest.format, + submitter: changeRequest.submitter, + createdOn: changeRequest.createdOn, }) await deleteChangeRequestRaw(circularId, requestorSub)