From 56a769547e41e73b07d45524bfa91f76eb1f0f11 Mon Sep 17 00:00:00 2001 From: dakota002 Date: Thu, 2 May 2024 13:57:24 -0400 Subject: [PATCH 1/4] 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) From 6e9bfd493bfbe00270f88bac49eae959cb18ba38 Mon Sep 17 00:00:00 2001 From: dakota002 Date: Wed, 12 Jun 2024 09:30:00 -0400 Subject: [PATCH 2/4] Adds some fixmes, add validation border to From line, fix submitter validation function --- .../CircularEditForm.tsx | 19 +++++++++++++++++-- app/routes/circulars/circulars.lib.ts | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx index 38c6de131..3c0a32913 100644 --- a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx +++ b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx @@ -188,7 +188,12 @@ export function CircularEditForm({ {circularId !== undefined && userIsModerator && ( <> - + From - {/* FIXME: see https://github.com/trussworks/react-uswds/issues/2806 */} + {/* 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 */} Time + {/* 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 */} Date: Thu, 2 May 2024 13:57:24 -0400 Subject: [PATCH 3/4] 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 Clean up grid stuff --- .../CircularEditForm.tsx | 247 +++++++++--------- app/routes/circulars/circulars.lib.ts | 2 +- app/routes/circulars/circulars.server.ts | 4 +- 3 files changed, 124 insertions(+), 129 deletions(-) diff --git a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx index 3c0a32913..632c6a16a 100644 --- a/app/routes/circulars.edit.$circularId/CircularEditForm.tsx +++ b/app/routes/circulars.edit.$circularId/CircularEditForm.tsx @@ -183,57 +183,50 @@ export function CircularEditForm({

{headerText} GCN Circular

- - - {circularId !== undefined && userIsModerator && ( - <> - - - From - setSubmitter(event.target.value)} - required - /> - - - )} - - - - - {circularId === undefined ? 'From' : 'Editor'} - - {formattedContributor} - - - + {circularId !== undefined && userIsModerator && ( + <> + + + From + setSubmitter(event.target.value)} + required + /> - - {circularId !== undefined && ( - + + )} + + + {circularId === undefined ? 'From' : 'Editor'} + + {formattedContributor} + + + + + {circularId !== undefined && ( + + Date + + - )} - - - Subject - { - setSubject(value) - setSubjectValid(subjectIsValid(value)) - }} - /> - - - - - - + Subject + { - setBody(value) + setSubject(value) + setSubjectValid(subjectIsValid(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" + + + + + + { + 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/circulars.lib.ts b/app/routes/circulars/circulars.lib.ts index f481b86cb..4cd14d42a 100644 --- a/app/routes/circulars/circulars.lib.ts +++ b/app/routes/circulars/circulars.lib.ts @@ -138,7 +138,7 @@ export function dateIsValid(date?: string, time?: string) { } export function submitterIsValid(submitter?: string) { - return Boolean(submitter) //!== undefined + return Boolean(submitter) } export function emailIsAutoReply(subject: string) { diff --git a/app/routes/circulars/circulars.server.ts b/app/routes/circulars/circulars.server.ts index d1afed172..9f2c2ad41 100644 --- a/app/routes/circulars/circulars.server.ts +++ b/app/routes/circulars/circulars.server.ts @@ -370,7 +370,9 @@ export async function createChangeRequest( Circular, 'sub' | 'submittedHow' | 'bibcode' | 'editedBy' | 'version' | 'editedOn' >, - user?: User + user?: User, + submitter?: string, + createdOn?: number ) { validateCircular(item) if (!user) From 6cebad07f11d92634458d656c0a2da75136b5f24 Mon Sep 17 00:00:00 2001 From: dakota002 Date: Thu, 13 Jun 2024 09:28:47 -0400 Subject: [PATCH 4/4] Fix date parse check for consistent browser behavior --- app/routes/circulars/circulars.lib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/circulars/circulars.lib.ts b/app/routes/circulars/circulars.lib.ts index 4cd14d42a..b332dd8be 100644 --- a/app/routes/circulars/circulars.lib.ts +++ b/app/routes/circulars/circulars.lib.ts @@ -134,7 +134,7 @@ export function formatIsValid(format: string): format is CircularFormat { /** For updated dates, check that the date is valid */ export function dateIsValid(date?: string, time?: string) { - return !Number.isNaN(Date.parse(`${date} ${time} UTC`)) + return !Number.isNaN(Date.parse(`${date}T${time}:00.000Z`)) } export function submitterIsValid(submitter?: string) {