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