From 56a769547e41e73b07d45524bfa91f76eb1f0f11 Mon Sep 17 00:00:00 2001 From: dakota002 Date: Thu, 2 May 2024 13:57:24 -0400 Subject: [PATCH] Updating available fields for updates 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 Date edit refactoring rename fields for consistency, add hidden field to fix time not getting set correctly Remove date replacement, still need to test against latest update Remove FIXME, reorder of submitter check for consistency, reorder createdOn and submitter to minimize changes Update app/routes/circulars._archive._index/route.tsx Co-authored-by: Leo Singer Style updates for consistency, spacing, other PR feedback implementations refine the cirteria for error or success class on date string --- app/components/TimeAgo.tsx | 2 +- .../circulars._archive._index/route.tsx | 18 +- .../circulars.correction.$circularId.tsx | 8 +- .../CircularEditForm.tsx | 291 ++++++++++++------ .../CircularsEditForm.module.scss | 14 + .../circulars.edit.$circularId/route.tsx | 2 +- ...lars.moderation.$circularId.$requestor.tsx | 22 +- app/routes/circulars/circulars.lib.ts | 11 + app/routes/circulars/circulars.server.ts | 16 +- 9 files changed, 269 insertions(+), 115 deletions(-) create mode 100644 app/routes/circulars.edit.$circularId/CircularsEditForm.module.scss diff --git a/app/components/TimeAgo.tsx b/app/components/TimeAgo.tsx index 289f027ca..24e9eb947 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 a3c04c0c8..4870fe677 100644 --- a/app/routes/circulars._archive._index/route.tsx +++ b/app/routes/circulars._archive._index/route.tsx @@ -35,6 +35,7 @@ import { createChangeRequest, get, getChangeRequests, + moderatorGroup, put, putVersion, search, @@ -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({ diff --git a/app/routes/circulars.correction.$circularId.tsx b/app/routes/circulars.correction.$circularId.tsx index 243ab77a5..3893477b2 100644 --- a/app/routes/circulars.correction.$circularId.tsx +++ b/app/routes/circulars.correction.$circularId.tsx @@ -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: '', } } diff --git a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx index fb0f635a2..38c6de131 100644 --- a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx +++ b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx @@ -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' @@ -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, @@ -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' @@ -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) { @@ -150,118 +168,193 @@ 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 (

{headerText} GCN Circular

- {circularId !== undefined && ( - <> - + + + {circularId !== undefined && userIsModerator && ( + <> + + + From + setSubmitter(event.target.value)} + required + /> + + + )} + + - From - {submitter} + + {circularId === undefined ? 'From' : 'Editor'} + + {formattedContributor} + + + - - )} - - - {circularId === undefined ? 'From' : 'Editor'} - - {formattedContributor} - + {circularId !== undefined && ( + + + Date + { + setDate(value ?? '') + }} + name="createdOnDate" + id="createdOnDate" + dateFormat="YYYY-MM-DD" + /> + + + {/* FIXME: see https://github.com/trussworks/react-uswds/issues/2806 */} + + 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" - > - - - - + 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 000000000..8f899e799 --- /dev/null +++ b/app/routes/circulars.edit.$circularId/CircularsEditForm.module.scss @@ -0,0 +1,14 @@ +.DatePicker { + button { + margin-top: 0; + } + input { + background-color: transparent; + } +} + +.TimePicker { + input { + background-color: transparent; + } +} diff --git a/app/routes/circulars.edit.$circularId/route.tsx b/app/routes/circulars.edit.$circularId/route.tsx index c9788cb8b..58a659e36 100644 --- a/app/routes/circulars.edit.$circularId/route.tsx +++ b/app/routes/circulars.edit.$circularId/route.tsx @@ -37,7 +37,7 @@ export async function loader({ defaultSubject: circular.subject, defaultFormat: circular.format, circularId: circular.circularId, - submitter: circular.submitter, + defaultSubmitter: circular.submitter, searchString: '', } } diff --git a/app/routes/circulars.moderation.$circularId.$requestor.tsx b/app/routes/circulars.moderation.$circularId.$requestor.tsx index f74a422a2..f614ecc09 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 060b2ddf0..77931205d 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?: string, time?: string) { + return !Number.isNaN(Date.parse(`${date} ${time} UTC`)) +} + +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 08864b594..d1afed172 100644 --- a/app/routes/circulars/circulars.server.ts +++ b/app/routes/circulars/circulars.server.ts @@ -368,14 +368,7 @@ export async function getVersions(circularId: number): Promise { export async function createChangeRequest( item: Omit< Circular, - | 'sub' - | 'createdOn' - | 'submitter' - | 'submittedHow' - | 'bibcode' - | 'editedBy' - | 'version' - | 'editedOn' + 'sub' | 'submittedHow' | 'bibcode' | 'editedBy' | 'version' | 'editedOn' >, user?: User ) { @@ -386,11 +379,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: item.createdOn ?? circular.createdOn, + submitter: item.submitter ?? circular.submitter, }) await sendEmail({ @@ -518,6 +516,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)