diff --git a/src/components/Header/UserDropdown.tsx b/src/components/Header/UserDropdown.tsx index 2ead04aca1..d63301c51b 100644 --- a/src/components/Header/UserDropdown.tsx +++ b/src/components/Header/UserDropdown.tsx @@ -33,7 +33,7 @@ export const UserDropdown = () => { }, ]; if (permissions?.canEditAdminSettings) { - menuItems.splice(1, 0, { + menuItems.splice(2, 0, { "data-cy": "admin-link", text: "Admin", href: adminSettingsURL, diff --git a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx index 20fbe51099..3f6be8e1f2 100644 --- a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx +++ b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx @@ -211,6 +211,7 @@ export const LeafyGreenSelect: React.FC< export const LeafyGreenRadio: React.FC = ({ disabled, + id, label, onChange, options, @@ -224,11 +225,17 @@ export const LeafyGreenRadio: React.FC = ({ } = options; return ( + {label && ( + + + + )} onChange(e.target.value)} - data-cy={dataCy} + value={value} > {enumOptions.map((o) => { const optionDisabled = enumDisabled?.includes(o.value) ?? false; @@ -275,12 +282,12 @@ export const LeafyGreenRadioBox: React.FC< return ( {showLabel !== false && ( - + {description && {description}} - + )} {!!errors && ( @@ -320,7 +327,7 @@ const StyledBanner = styled(Banner)` margin-bottom: ${size.s}; `; -const RadioBoxLabelContainer = styled.div` +const LabelContainer = styled.div` margin-bottom: ${size.xs}; `; diff --git a/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot b/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot index f7b5501ecb..dc3c20a9bf 100644 --- a/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot +++ b/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot @@ -589,6 +589,7 @@ exports[`Snapshot Tests SpruceForm.stories Example2 1`] = `
({ - value: format, - str: `${format} - ${getDateCopy("08/31/2022", { - dateFormat: format, - dateOnly: true, - })}`, -})); +export enum TimeFormat { + TwelveHour = "h:mm:ss aa", + TwentyFourHour = "H:mm:ss", +} export const notificationFields = { patchFinish: "Patch finish", diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index bc6a8b8af2..da25867e93 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -8724,6 +8724,7 @@ export type UserSettingsQuery = { region?: string | null; slackMemberId?: string | null; slackUsername?: string | null; + timeFormat?: string | null; timezone?: string | null; githubUser?: { __typename?: "GithubUser"; diff --git a/src/gql/queries/user-settings.graphql b/src/gql/queries/user-settings.graphql index 6dbaa71043..bacf7059e6 100644 --- a/src/gql/queries/user-settings.graphql +++ b/src/gql/queries/user-settings.graphql @@ -15,6 +15,7 @@ query UserSettings { region slackMemberId slackUsername + timeFormat timezone useSpruceOptions { hasUsedMainlineCommitsBefore diff --git a/src/hooks/useDateFormat.ts b/src/hooks/useDateFormat.ts index 1955c86366..4e63aa11fc 100644 --- a/src/hooks/useDateFormat.ts +++ b/src/hooks/useDateFormat.ts @@ -5,12 +5,13 @@ import { useUserTimeZone } from "./useUserTimeZone"; export const useDateFormat = () => { const timezone = useUserTimeZone(); const { userSettings } = useUserSettings(); - const { dateFormat } = userSettings || {}; + const { dateFormat, timeFormat } = userSettings || {}; return (date: string | number | Date, options: DateCopyOptions = {}) => getDateCopy(date, { tz: timezone, dateFormat, + timeFormat, ...options, }); }; diff --git a/src/pages/preferences/preferencesTabs/ProfileTab.tsx b/src/pages/preferences/preferencesTabs/ProfileTab.tsx index 26313f1164..f9eb529aa3 100644 --- a/src/pages/preferences/preferencesTabs/ProfileTab.tsx +++ b/src/pages/preferences/preferencesTabs/ProfileTab.tsx @@ -6,7 +6,11 @@ import { Skeleton } from "antd"; import { usePreferencesAnalytics } from "analytics"; import { SettingsCard } from "components/SettingsCard"; import { SpruceForm } from "components/SpruceForm"; -import { timeZones, dateFormats } from "constants/fieldMaps"; +import { + listOfDateFormatStrings, + timeZones, + TimeFormat, +} from "constants/fieldMaps"; import { useToastContext } from "context/toast"; import { UpdateUserSettingsMutation, @@ -16,15 +20,30 @@ import { import { UPDATE_USER_SETTINGS } from "gql/mutations"; import { AWS_REGIONS } from "gql/queries"; import { useUserSettings } from "hooks"; -import { omitTypename } from "utils/string"; +import { getDateCopy, omitTypename } from "utils/string"; + +const dateFormats = listOfDateFormatStrings.map((format) => ({ + value: format, + str: `${format} - ${getDateCopy("08/31/2022", { + dateFormat: format, + dateOnly: true, + })}`, +})); export const ProfileTab: React.FC = () => { const { sendEvent } = usePreferencesAnalytics(); const dispatchToast = useToastContext(); const { loading, userSettings } = useUserSettings(); - const { dateFormat, githubUser, region, timezone } = userSettings ?? {}; + const { + dateFormat, + githubUser, + region, + timeFormat: dbTimeFormat, + timezone, + } = userSettings ?? {}; const lastKnownAs = githubUser?.lastKnownAs || ""; + const timeFormat = dbTimeFormat || TimeFormat.TwelveHour; const { data: awsRegionData, loading: awsRegionLoading } = useQuery(AWS_REGIONS); @@ -49,11 +68,13 @@ export const ProfileTab: React.FC = () => { region: string; githubUser: { lastKnownAs?: string }; dateFormat: string; + timeFormat: string; }>({ timezone, region, githubUser: { lastKnownAs }, dateFormat, + timeFormat, }); useEffect(() => { @@ -62,8 +83,9 @@ export const ProfileTab: React.FC = () => { timezone, region, dateFormat, + timeFormat, }); - }, [dateFormat, githubUser, region, timezone]); + }, [dateFormat, githubUser, region, timeFormat, timezone]); const handleSubmit = () => { updateUserSettings({ @@ -111,6 +133,9 @@ export const ProfileTab: React.FC = () => { dateFormat: { "ui:placeholder": "Select a date format", }, + timeFormat: { + "ui:widget": "radio", + }, }} schema={{ properties: { @@ -150,6 +175,25 @@ export const ProfileTab: React.FC = () => { })), ], }, + timeFormat: { + type: "string", + title: "Time Format", + oneOf: [ + { + type: "string" as "string", + title: "12-hour clock", + description: "Display time with AM/PM, e.g. 12:34 PM", + enum: [TimeFormat.TwelveHour], + }, + + { + type: "string" as "string", + title: "24-hour clock", + description: "Use 24-hour notation, e.g. 13:34", + enum: [TimeFormat.TwentyFourHour], + }, + ], + }, }, }} /> diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx index 339383628e..339c68ebd6 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx @@ -225,6 +225,7 @@ export const getFormSchema = ( properties: { mergeQueue: { type: "string" as "string", + title: "", oneOf: [ { type: "string" as "string", diff --git a/src/pages/projectSettings/tabs/ViewsAndFiltersTab/getFormSchema.ts b/src/pages/projectSettings/tabs/ViewsAndFiltersTab/getFormSchema.ts index 9e7ff85cb1..54d9512cca 100644 --- a/src/pages/projectSettings/tabs/ViewsAndFiltersTab/getFormSchema.ts +++ b/src/pages/projectSettings/tabs/ViewsAndFiltersTab/getFormSchema.ts @@ -73,6 +73,7 @@ export const getFormSchema = ( properties: { projectHealthView: { type: "string" as "string", + title: "", oneOf: [ { type: "string" as "string", diff --git a/src/utils/string/index.ts b/src/utils/string/index.ts index c2c7146177..8781676286 100644 --- a/src/utils/string/index.ts +++ b/src/utils/string/index.ts @@ -1,5 +1,6 @@ import { format, utcToZonedTime } from "date-fns-tz"; import get from "lodash/get"; +import { TimeFormat } from "constants/fieldMaps"; export { githubPRLinkify } from "./githubPRLinkify"; @@ -100,6 +101,7 @@ export type DateCopyOptions = { omitSeconds?: boolean; omitTimezone?: boolean; dateFormat?: string; + timeFormat?: string; }; /** @@ -111,7 +113,7 @@ export type DateCopyOptions = { * @param options.omitSeconds - if true, will not return the seconds * @param options.omitTimezone - if true, will not return the timezone * @param options.dateFormat - a date format string, such as "MMM d, yyyy" - * @returns - a string representing the date in the format of "MMM d, yyyy h:mm:ss a z" + * @returns - a string representing the date in either the user's specified format or the default, "MMM d, yyyy h:mm:ss aa z" */ export const getDateCopy = ( time: string | number | Date, @@ -121,15 +123,19 @@ export const getDateCopy = ( return ""; } const { dateOnly, omitSeconds, omitTimezone, tz } = options || {}; - let { dateFormat } = options || {}; + let { dateFormat, timeFormat } = options || {}; if (!dateFormat) { dateFormat = "MMM d, yyyy"; } + if (!timeFormat) { + timeFormat = TimeFormat.TwelveHour; + } + if (omitSeconds) { + timeFormat = timeFormat.replace(":ss", ""); + } const finalDateFormat = dateOnly ? dateFormat - : `${dateFormat}, h:mm${omitSeconds ? "" : ":ss"} aa${ - omitTimezone ? "" : " z" - }`; + : `${dateFormat}, ${timeFormat}${omitTimezone ? "" : " z"}`; if (tz) { return format(utcToZonedTime(time, tz), finalDateFormat, { timeZone: tz, diff --git a/src/utils/string/string.test.ts b/src/utils/string/string.test.ts index d3ab32b7fd..bc90a0898e 100644 --- a/src/utils/string/string.test.ts +++ b/src/utils/string/string.test.ts @@ -1,3 +1,4 @@ +import { TimeFormat } from "constants/fieldMaps"; import { msToDuration, sortFunctionDate, @@ -292,7 +293,25 @@ describe("getDateCopy", () => { getDateCopy("08/31/1996", { dateFormat: "MM/dd/yyyy", dateOnly: true }), ).toBe("08/31/1996"); }); + + it("returns dates with a custom time format when supplied with the option", () => { + expect( + getDateCopy(new Date("2020-11-16T22:17:29z"), { + omitTimezone: true, + timeFormat: TimeFormat.TwentyFourHour, + }), + ).toBe("Nov 16, 2020, 22:17:29"); + + expect( + getDateCopy(new Date("2020-11-16T22:17:29z"), { + omitSeconds: true, + omitTimezone: true, + timeFormat: TimeFormat.TwelveHour, + }), + ).toBe("Nov 16, 2020, 10:17 PM"); + }); }); + describe("applyStrictRegex", () => { it("converts string to strict regex", () => { expect(applyStrictRegex("dog")).toBe("^dog$");