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:
@@ -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>
|
||||
|
||||
+2
-2
@@ -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
@@ -1 +0,0 @@
|
||||
export * from "./localized-date-picker"
|
||||
-37
@@ -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"
|
||||
|
||||
+3
-8
@@ -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"
|
||||
+8
-14
@@ -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>
|
||||
|
||||
+6
-10
@@ -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>
|
||||
|
||||
+6
-10
@@ -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>
|
||||
|
||||
-1
@@ -46,7 +46,6 @@ type PriceListCreateFormProps = {
|
||||
currencies: HttpTypes.AdminStoreCurrency[]
|
||||
}
|
||||
|
||||
// TODO: Fix DatePickers once new version is merged.
|
||||
export const PriceListCreateForm = ({
|
||||
regions,
|
||||
currencies,
|
||||
|
||||
+6
-7
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user