fix(ui,dashboard): Revamp DatePicker component (#7891)

**What**
- Revamps the DatePicker component. 
- Addresses all issues with broken DatePickers across admin.

**Note**
- Part of this PR is adding a I18nProvider which is used to set the locale that is used for our DatePicker and Calendar components. Per default they use the browser locale. In the current implementation, we are grabbing the locale to use from the language that is picked in the "Profile" section. This means that currently the only possible locale is "en-US", meaning times uses AM/PM. This is likely not what we want, but we need to make a decision on how we want to handle this globally, will create a ticket for it and we can then clean it up later on.
- This PR does not include "presets" or a DateRange picker that were part of the old implementation. Will open tickets to re-add this later on, but since we aren't using it in admin any where it makes sense to address later.
- This PR also bumps and pin every `@radix-ui` dependency in `@medusajs/ui` and `@medusajs/dashboard`. Our different versions were pulling in multiple versions of internal radix dependencies which were breaking Popover and Dialog behaviour across admin. One thing to note is that Radix have started to print warnings for missing Descriptions and Titles in dialogs. We should add these as we go, for better accessibility. Its not an urgent task but something we can add as we clean up admin over the following weeks. 

CLOSES CORE-2382
This commit is contained in:
Kasper Fabricius Kristensen
2024-07-02 08:59:32 +00:00
committed by GitHub
parent 074e4a888e
commit a84e5a6ced
47 changed files with 2874 additions and 2424 deletions
+4 -1
View File
@@ -3,6 +3,7 @@ import { QueryClientProvider } from "@tanstack/react-query"
import { I18n } from "./components/utilities/i18n"
import { queryClient } from "./lib/query-client"
import { I18nProvider } from "./providers/i18n-provider"
import { RouterProvider } from "./providers/router-provider"
import { ThemeProvider } from "./providers/theme-provider"
@@ -14,7 +15,9 @@ function App() {
<ThemeProvider>
<I18n />
<TooltipProvider>
<RouterProvider />
<I18nProvider>
<RouterProvider />
</I18nProvider>
</TooltipProvider>
<Toaster />
</ThemeProvider>
@@ -27,7 +27,7 @@ export const DateRangeDisplay = ({
<Text weight="plus" size="small">
{t("fields.startDate")}
</Text>
<Text size="small">
<Text size="small" className="tabular-nums">
{startDate
? getFullDate({
date: startDate,
@@ -44,7 +44,7 @@ export const DateRangeDisplay = ({
<Text size="small" weight="plus">
{t("fields.endDate")}
</Text>
<Text size="small">
<Text size="small" className="tabular-nums">
{endDate
? getFullDate({
date: endDate,
@@ -78,6 +78,7 @@ const useFormField = () => {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formLabelId: `${id}-form-item-label`,
formDescriptionId: `${id}-form-item-description`,
formErrorMessageId: `${id}-form-item-message`,
...fieldState,
@@ -109,12 +110,13 @@ const Label = forwardRef<
icon?: ReactNode
}
>(({ className, optional = false, tooltip, icon, ...props }, ref) => {
const { formItemId } = useFormField()
const { formLabelId, formItemId } = useFormField()
const { t } = useTranslation()
return (
<div className="flex items-center gap-x-1">
<LabelComponent
id={formLabelId}
ref={ref}
className={clx(className)}
htmlFor={formItemId}
@@ -142,8 +144,13 @@ const Control = forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formErrorMessageId } =
useFormField()
const {
error,
formItemId,
formDescriptionId,
formErrorMessageId,
formLabelId,
} = useFormField()
return (
<Slot
@@ -155,6 +162,7 @@ const Control = forwardRef<
: `${formDescriptionId} ${formErrorMessageId}`
}
aria-invalid={!!error}
aria-labelledby={formLabelId}
{...props}
/>
)
@@ -1 +0,0 @@
export * from "./localized-date-picker"
@@ -1,37 +0,0 @@
import { DatePicker } from "@medusajs/ui"
import { ComponentPropsWithoutRef } from "react"
import { useTranslation } from "react-i18next"
import { languages } from "../../../i18n/languages"
type LocalizedDatePickerProps = Omit<
ComponentPropsWithoutRef<typeof DatePicker>,
"translations" | "locale"
>
export const LocalizedDatePicker = ({
mode = "single",
...props
}: LocalizedDatePickerProps) => {
const { i18n, t } = useTranslation()
const locale = languages.find(
(lang) => lang.code === i18n.language
)?.date_locale
const translations = {
cancel: t("actions.cancel"),
apply: t("general.apply"),
end: t("general.end"),
start: t("general.start"),
range: t("general.range"),
}
return (
<DatePicker
mode={mode}
translations={translations}
locale={locale}
{...(props as any)}
/>
)
}
@@ -1 +1 @@
export * from "./stacked-foucs-modal"
export * from "./stacked-focus-modal"
@@ -65,10 +65,7 @@ export const DateFilter = ({
const customStartValue = getDateFromComparison(currentDateComparison, "$gte")
const customEndValue = getDateFromComparison(currentDateComparison, "$lte")
const handleCustomDateChange = (
value: Date | undefined,
pos: "start" | "end"
) => {
const handleCustomDateChange = (value: Date | null, pos: "start" | "end") => {
const key = pos === "start" ? "$gte" : "$lte"
const dateValue = value ? value.toISOString() : undefined
@@ -201,8 +198,7 @@ export const DateFilter = ({
</div>
<div className="px-2 py-1">
<DatePicker
// placeholder="MM/DD/YYYY" TODO: Fix DatePicker component not working with placeholder
toDate={customEndValue}
maxValue={customEndValue}
value={customStartValue}
onChange={(d) => handleCustomDateChange(d, "start")}
/>
@@ -216,8 +212,7 @@ export const DateFilter = ({
</div>
<div className="px-2 py-1">
<DatePicker
// placeholder="MM/DD/YYYY"
fromDate={customStartValue}
minValue={customStartValue}
value={customEndValue || undefined}
onChange={(d) => {
handleCustomDateChange(d, "end")
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"
import { languages } from "../i18n/languages"
// TODO: We rely on the current language to determine the date locale. This is not ideal, as we use en-US for the english translation.
// We either need to also have an en-GB translation or we need to separate the date locale from the translation language.
export const useDate = () => {
const { i18n } = useTranslation()
@@ -0,0 +1,16 @@
import { I18nProvider as Provider } from "@medusajs/ui"
import { PropsWithChildren } from "react"
import { useTranslation } from "react-i18next"
import { languages } from "../../i18n/languages"
type I18nProviderProps = PropsWithChildren
export const I18nProvider = ({ children }: I18nProviderProps) => {
const { i18n } = useTranslation()
const locale =
languages.find((lan) => lan.code === i18n.language)?.code ||
languages[0].code
return <Provider locale={locale}>{children}</Provider>
}
@@ -0,0 +1 @@
export * from "./i18n-provider"
@@ -5,10 +5,7 @@ import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/modals"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
type EditCampaignFormProps = {
@@ -133,18 +130,16 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
<Form.Field
control={form.control}
name="starts_at"
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => {
onChange(v ?? null)
}}
granularity="minute"
hourCycle={12}
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
@@ -158,16 +153,15 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
<Form.Field
control={form.control}
name="ends_at"
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => onChange(v ?? null)}
granularity="minute"
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
@@ -138,18 +138,15 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
<Form.Field
control={form.control}
name={`${fieldScope}starts_at`}
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => {
onChange(v ?? null)
}}
granularity="minute"
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
@@ -163,16 +160,15 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
<Form.Field
control={form.control}
name={`${fieldScope}ends_at`}
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
<Form.Control>
<DatePicker
showTimePicker
value={value ?? undefined}
onChange={(v) => onChange(v ?? null)}
granularity="minute"
shouldCloseOnSelect={false}
{...field}
/>
</Form.Control>
@@ -41,7 +41,6 @@ const PriceListConfigurationSchema = z.object({
const STACKED_MODAL_ID = "cg"
// TODO: Fix DatePickers once new version is merged.
export const PriceListConfigurationForm = ({
priceList,
customerGroups,
@@ -122,7 +121,7 @@ export const PriceListConfigurationForm = ({
<Form.Field
control={form.control}
name="starts_at"
render={({ field: { value, onChange, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<div className="grid grid-cols-1 gap-3">
@@ -135,12 +134,10 @@ export const PriceListConfigurationForm = ({
</Form.Hint>
</div>
<Form.Control>
{/* TODO: Add timepicker see CORE-2382 */}
<DatePicker
mode="single"
granularity="minute"
shouldCloseOnSelect={false}
{...field}
onChange={(value) => onChange(value ?? null)}
value={value ?? undefined}
/>
</Form.Control>
</div>
@@ -153,7 +150,7 @@ export const PriceListConfigurationForm = ({
<Form.Field
control={form.control}
name="ends_at"
render={({ field: { value, onChange, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<div className="grid grid-cols-1 gap-3">
@@ -167,10 +164,9 @@ export const PriceListConfigurationForm = ({
</div>
<Form.Control>
<DatePicker
mode="single"
granularity="minute"
shouldCloseOnSelect={false}
{...field}
onChange={(value) => onChange(value ?? null)}
value={value ?? undefined}
/>
</Form.Control>
</div>
@@ -46,7 +46,6 @@ type PriceListCreateFormProps = {
currencies: HttpTypes.AdminStoreCurrency[]
}
// TODO: Fix DatePickers once new version is merged.
export const PriceListCreateForm = ({
regions,
currencies,
@@ -173,7 +173,7 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
<Form.Field
control={form.control}
name="starts_at"
render={({ field: { value, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -186,11 +186,10 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
</Form.Hint>
</div>
<Form.Control>
{/* TODO: Add timepicker see CORE-2382 */}
<DatePicker
mode="single"
granularity="minute"
shouldCloseOnSelect={false}
{...field}
value={value ?? undefined}
/>
</Form.Control>
</div>
@@ -203,7 +202,7 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
<Form.Field
control={form.control}
name="ends_at"
render={({ field: { value, ...field } }) => {
render={({ field }) => {
return (
<Form.Item>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -215,9 +214,9 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
</div>
<Form.Control>
<DatePicker
mode="single"
granularity="minute"
shouldCloseOnSelect={false}
{...field}
value={value ?? undefined}
/>
</Form.Control>
</div>
@@ -34,7 +34,7 @@ export const PriceListDetails = () => {
)
})}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-3">
<div className="flex flex-1 flex-col gap-y-3">
<PriceListGeneralSection priceList={price_list} />
<PriceListProductSection priceList={price_list} />
{after.widgets.map((w, i) => {
@@ -48,7 +48,7 @@ export const PriceListDetails = () => {
<JsonViewSection data={price_list} />
</div>
</div>
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[423px]">
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[440px]">
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>