@open-mercato/ui 0.8.1-develop.7296.1.2111d779db → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/backend/CrudForm.js +19 -6
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +62 -10
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/RowActions.js +54 -5
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/charts/KpiCard.js +2 -1
- package/dist/backend/charts/KpiCard.js.map +2 -2
- package/dist/backend/detail/AttachmentsSection.js +17 -5
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +38 -20
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js +11 -1
- package/dist/backend/dev/DevRuntimeDiagnosticsBanner.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +26 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/eventBridge.js +33 -0
- package/dist/backend/injection/eventBridge.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +20 -3
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +1 -1
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +20 -2
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/messages/EmailThreadsPanel.js +2 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/messages/MessageComposer.js.map +2 -2
- package/dist/backend/messages/index.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +24 -0
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +27 -0
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/messages/useMessageComposeOperations.js +1 -0
- package/dist/backend/messages/useMessageComposeOperations.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +8 -1
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/portal/PortalShell.js +2 -2
- package/dist/portal/PortalShell.js.map +2 -2
- package/dist/primitives/calendar.js +69 -14
- package/dist/primitives/calendar.js.map +2 -2
- package/package.json +4 -5
- package/src/backend/CrudForm.tsx +30 -8
- package/src/backend/DataTable.tsx +95 -20
- package/src/backend/RowActions.tsx +114 -6
- package/src/backend/__tests__/AttachmentsSection.upload.test.tsx +183 -0
- package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +139 -2
- package/src/backend/__tests__/CrudForm.hiddenGroups.test.tsx +1 -1
- package/src/backend/__tests__/DataTable.extensions.test.tsx +30 -0
- package/src/backend/__tests__/DataTable.perspectiveReconcile.test.tsx +320 -2
- package/src/backend/__tests__/DataTable.stickyResponsive.test.tsx +19 -0
- package/src/backend/__tests__/DevRuntimeDiagnosticsBanner.test.tsx +117 -0
- package/src/backend/__tests__/NotesSection.test.tsx +138 -0
- package/src/backend/__tests__/RowActions.test.tsx +202 -0
- package/src/backend/charts/KpiCard.tsx +1 -0
- package/src/backend/charts/__tests__/KpiCard.test.tsx +20 -0
- package/src/backend/detail/AttachmentsSection.tsx +17 -5
- package/src/backend/detail/NotesSection.tsx +122 -66
- package/src/backend/dev/DevRuntimeDiagnosticsBanner.tsx +24 -1
- package/src/backend/icons/lucideRegistry.generated.tsx +26 -0
- package/src/backend/injection/__tests__/eventBridge.test.tsx +47 -1
- package/src/backend/injection/eventBridge.ts +35 -0
- package/src/backend/inputs/LookupSelect.tsx +32 -3
- package/src/backend/inputs/PhoneNumberField.tsx +1 -1
- package/src/backend/inputs/TagsInput.tsx +24 -3
- package/src/backend/inputs/__tests__/LookupSelect.test.tsx +166 -1
- package/src/backend/inputs/__tests__/PhoneNumberField.typeahead.test.tsx +43 -0
- package/src/backend/inputs/__tests__/TagsInput.test.tsx +81 -0
- package/src/backend/messages/EmailThreadsPanel.tsx +21 -1
- package/src/backend/messages/MessageComposer.tsx +1 -0
- package/src/backend/messages/__tests__/EmailThreadsPanel.senderLabel.test.tsx +99 -0
- package/src/backend/messages/__tests__/MessageComposer.test.tsx +44 -0
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +69 -0
- package/src/backend/messages/__tests__/useMessageComposeSender.test.tsx +208 -0
- package/src/backend/messages/index.ts +1 -0
- package/src/backend/messages/message-compose-form-groups.tsx +41 -0
- package/src/backend/messages/message-composer.types.ts +26 -0
- package/src/backend/messages/useMessageCompose.ts +46 -0
- package/src/backend/messages/useMessageComposeOperations.ts +8 -0
- package/src/backend/notifications/NotificationItem.tsx +7 -0
- package/src/backend/notifications/__tests__/NotificationItem.customRenderer.test.tsx +139 -0
- package/src/portal/PortalShell.tsx +7 -4
- package/src/portal/__tests__/PortalShell.test.tsx +29 -1
- package/src/primitives/__tests__/calendar.test.tsx +174 -1
- package/src/primitives/calendar.tsx +168 -96
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/calendar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CompactButton } from './compact-button'\n\nexport type CalendarProps = DayPickerProps & { daySize?: 36 | 40 }\n\nexport type CalendarMonthSelectorProps = React.HTMLAttributes<HTMLDivElement> & {\n month: Date\n locale?: Locale\n onPreviousMonth?: () => void\n onNextMonth?: () => void\n disabledPrevious?: boolean\n disabledNext?: boolean\n}\n\nexport function CalendarMonthSelector({\n month, locale, onPreviousMonth, onNextMonth, disabledPrevious, disabledNext, className, ...props\n}: CalendarMonthSelectorProps) {\n const t = useT()\n return (\n <div data-slot=\"calendar-month-selector\" className={cn('flex h-9 items-center gap-1.5 rounded-md bg-muted p-1.5', className)} {...props}>\n {onPreviousMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onPreviousMonth} disabled={disabledPrevious} aria-label={t('ui.calendar.previousMonth', 'Previous month')}><ChevronLeft className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n <span className=\"min-w-0 flex-1 text-center text-sm font-medium text-muted-foreground\" aria-live=\"polite\">{format(month, 'MMMM yyyy', locale ? { locale } : undefined)}</span>\n {onNextMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onNextMonth} disabled={disabledNext} aria-label={t('ui.calendar.nextMonth', 'Next month')}><ChevronRight className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n </div>\n )\n}\n\n// `LLLL`/`LLL`, not `MMMM`/`MMM`: these labels name a month on its own, and languages that\n// inflect month names \u2014 Polish among the five this repo ships \u2014 spell that differently from a\n// month inside a full date. `MMMM` is the inflected (genitive) form, so a Polish header read\n// \"stycznia\" (\"of January\") where it should read \"stycze\u0144\". The two tokens are identical in\n// English, which is why the bug was invisible until a non-English tenant opened a picker.\nconst STANDALONE_MONTH_YEAR = 'LLLL yyyy'\nconst STANDALONE_MONTH_SHORT = 'LLL'\n\nconst navButtonClassName = cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n)\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const ariaLabel = `Go to ${direction === 'prev' ? 'previous' : 'next'} month: ${targetLabel}`\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={navButtonClassName}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(\n locale: Locale | undefined,\n totalMonths: number,\n onOpenMonthGrid: (() => void) | null,\n) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n // The month grid (fast navigation) is only offered on single-month\n // calendars; on the leftmost caption it owns the click target.\n const labelInteractive = onOpenMonthGrid !== null && index === 0\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n {labelInteractive ? (\n <button\n type=\"button\"\n onClick={onOpenMonthGrid ?? undefined}\n aria-label={`${label} \u2013 open month and year navigation`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {label}\n </button>\n ) : (\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n )}\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nfunction MonthGrid({\n initialYear,\n selectedMonth,\n locale,\n onSelectMonth,\n onClose,\n}: {\n initialYear: number\n selectedMonth: Date\n locale?: Locale\n onSelectMonth: (month: Date) => void\n onClose: () => void\n}) {\n const [year, setYear] = React.useState(initialYear)\n const today = new Date()\n const monthLabels = React.useMemo(\n () =>\n Array.from({ length: 12 }, (_, monthIndex) =>\n format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : undefined),\n ),\n [year, locale],\n )\n const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)\n return (\n <div\n className=\"absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3\"\n role=\"dialog\"\n aria-label=\"Select month and year\"\n >\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n <button\n type=\"button\"\n aria-label={`Go to previous year: ${year - 1}`}\n onClick={() => setYear((current) => current - 1)}\n className={navButtonClassName}\n >\n <ChevronLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={`${yearLabel} \u2013 back to day selection`}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {yearLabel}\n </button>\n <button\n type=\"button\"\n aria-label={`Go to next year: ${year + 1}`}\n onClick={() => setYear((current) => current + 1)}\n className={navButtonClassName}\n >\n <ChevronRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n <div className=\"grid grid-cols-3 gap-2\">\n {monthLabels.map((monthLabel, monthIndex) => {\n const isSelected =\n selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex\n const isCurrentMonth =\n today.getFullYear() === year && today.getMonth() === monthIndex\n return (\n <button\n key={monthIndex}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}\n className={cn(\n 'h-9 rounded-md text-sm font-normal transition-colors',\n 'inline-flex items-center justify-center',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n isCurrentMonth && !isSelected && 'font-semibold text-primary',\n isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',\n )}\n >\n {monthLabel}\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n daySize = 36,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n month,\n defaultMonth,\n onMonthChange,\n ...props\n}: CalendarProps) {\n // The month/year grid (fast navigation) is only meaningful for single-month\n // calendars; multi-month layouts (range pickers) keep the static caption.\n const monthGridEnabled = numberOfMonths === 1\n const [displayMonth, setDisplayMonth] = React.useState<Date>(\n () => month ?? defaultMonth ?? new Date(),\n )\n const [showMonthGrid, setShowMonthGrid] = React.useState(false)\n\n // Honor a controlled `month` prop when consumers drive navigation externally.\n React.useEffect(() => {\n if (month) setDisplayMonth(month)\n }, [month])\n\n const handleMonthChange = React.useCallback(\n (next: Date) => {\n setDisplayMonth(next)\n onMonthChange?.(next)\n },\n [onMonthChange],\n )\n\n const handleSelectMonth = React.useCallback(\n (next: Date) => {\n handleMonthChange(next)\n setShowMonthGrid(false)\n },\n [handleMonthChange],\n )\n\n const monthCaption = React.useMemo(\n () =>\n buildMonthCaption(\n locale as Locale | undefined,\n numberOfMonths,\n monthGridEnabled ? () => setShowMonthGrid(true) : null,\n ),\n [locale, numberOfMonths, monthGridEnabled],\n )\n\n return (\n <div className={monthGridEnabled ? 'relative' : 'contents'}>\n <div\n className={monthGridEnabled && showMonthGrid ? 'pointer-events-none' : 'contents'}\n aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}\n >\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n locale={locale}\n numberOfMonths={numberOfMonths}\n month={displayMonth}\n onMonthChange={handleMonthChange}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: cn('text-muted-foreground rounded-md font-normal text-xs', daySize === 40 ? 'w-10' : 'w-9'),\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: cn('text-center text-sm p-0 relative focus-within:relative focus-within:z-20', daySize === 40 ? 'size-10' : 'size-9'),\n day_button: cn(\n 'p-0 font-normal aria-selected:opacity-100',\n daySize === 40 ? 'size-10' : 'size-9',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n </div>\n {monthGridEnabled && showMonthGrid ? (\n <MonthGrid\n initialYear={displayMonth.getFullYear()}\n selectedMonth={displayMonth}\n locale={locale as Locale | undefined}\n onSelectMonth={handleSelectMonth}\n onClose={() => setShowMonthGrid(false)}\n />\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { DayPicker, useDayPicker } from 'react-day-picker'\nimport type { DayPickerProps, CalendarMonth } from 'react-day-picker'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { format } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useOptionalT, useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { createTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { CompactButton } from './compact-button'\n\nexport type CalendarProps = DayPickerProps & { daySize?: 36 | 40 }\n\nexport type CalendarMonthSelectorProps = React.HTMLAttributes<HTMLDivElement> & {\n month: Date\n locale?: Locale\n onPreviousMonth?: () => void\n onNextMonth?: () => void\n disabledPrevious?: boolean\n disabledNext?: boolean\n}\n\nexport function CalendarMonthSelector({\n month, locale, onPreviousMonth, onNextMonth, disabledPrevious, disabledNext, className, ...props\n}: CalendarMonthSelectorProps) {\n const t = useT()\n return (\n <div data-slot=\"calendar-month-selector\" className={cn('flex h-9 items-center gap-1.5 rounded-md bg-muted p-1.5', className)} {...props}>\n {onPreviousMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onPreviousMonth} disabled={disabledPrevious} aria-label={t('ui.calendar.previousMonth', 'Previous month')}><ChevronLeft className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n <span className=\"min-w-0 flex-1 text-center text-sm font-medium text-muted-foreground\" aria-live=\"polite\">{format(month, 'MMMM yyyy', locale ? { locale } : undefined)}</span>\n {onNextMonth\n ? <CompactButton appearance=\"white\" size={24} className=\"shadow-xs\" onClick={onNextMonth} disabled={disabledNext} aria-label={t('ui.calendar.nextMonth', 'Next month')}><ChevronRight className=\"size-5\" aria-hidden=\"true\" /></CompactButton>\n : <span className=\"size-6 shrink-0\" aria-hidden=\"true\" />}\n </div>\n )\n}\n\n// `LLLL`/`LLL`, not `MMMM`/`MMM`: these labels name a month on its own, and languages that\n// inflect month names \u2014 Polish among the five this repo ships \u2014 spell that differently from a\n// month inside a full date. `MMMM` is the inflected (genitive) form, so a Polish header read\n// \"stycznia\" (\"of January\") where it should read \"stycze\u0144\". The two tokens are identical in\n// English, which is why the bug was invisible until a non-English tenant opened a picker.\nconst STANDALONE_MONTH_YEAR = 'LLLL yyyy'\nconst STANDALONE_MONTH_SHORT = 'LLL'\n\nconst fallbackTranslate = createTranslator({})\n\nfunction useCalendarT(): TranslateFn {\n return useOptionalT() ?? fallbackTranslate\n}\n\ntype CalendarLabels = DayPickerProps['labels']\n\nconst CalendarLabelsContext = React.createContext<CalendarLabels>(undefined)\n\nconst navButtonClassName = cn(\n 'h-9 w-9 inline-flex items-center justify-center rounded-md shrink-0',\n 'border border-border bg-background text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground hover:border-input',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:opacity-30 disabled:pointer-events-none disabled:hover:bg-background',\n)\n\nfunction MonthNavButton({\n direction,\n locale,\n}: {\n direction: 'prev' | 'next'\n locale?: Locale\n}) {\n const t = useCalendarT()\n const labels = React.useContext(CalendarLabelsContext)\n const dayPicker = useDayPicker() as unknown as {\n previousMonth?: Date\n nextMonth?: Date\n goToMonth?: (month: Date) => void\n }\n const target = direction === 'prev' ? dayPicker.previousMonth : dayPicker.nextMonth\n const Icon = direction === 'prev' ? ChevronLeft : ChevronRight\n const targetLabel = format(target ?? new Date(), STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const customLabel = direction === 'prev' ? labels?.labelPrevious : labels?.labelNext\n const ariaLabel = customLabel\n ? customLabel(target)\n : direction === 'prev'\n ? t('ui.calendar.goToPreviousMonth', 'Go to previous month: {month}', { month: targetLabel })\n : t('ui.calendar.goToNextMonth', 'Go to next month: {month}', { month: targetLabel })\n return (\n <button\n type=\"button\"\n disabled={!target}\n aria-label={ariaLabel}\n onClick={() => {\n if (target && dayPicker.goToMonth) dayPicker.goToMonth(target)\n }}\n className={navButtonClassName}\n >\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\nfunction buildMonthCaption(\n locale: Locale | undefined,\n totalMonths: number,\n onOpenMonthGrid: (() => void) | null,\n monthGridTriggerRef: React.Ref<HTMLButtonElement>,\n) {\n return function MonthCaption({\n calendarMonth,\n displayIndex,\n }: {\n calendarMonth: CalendarMonth\n displayIndex?: number\n }) {\n const t = useCalendarT()\n const label = format(calendarMonth.date, STANDALONE_MONTH_YEAR, locale ? { locale } : undefined)\n const index = typeof displayIndex === 'number' ? displayIndex : 0\n // For multi-month layouts (e.g. range pickers) only the leftmost month\n // exposes the previous-month chevron and only the rightmost exposes the\n // next-month chevron. Navigation is always global across all visible\n // months, so showing both arrows on every month is confusing.\n const showPrev = index === 0\n const showNext = index === totalMonths - 1\n // The month grid (fast navigation) is only offered on single-month\n // calendars; on the leftmost caption it owns the click target.\n const labelInteractive = onOpenMonthGrid !== null && index === 0\n return (\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n {showPrev ? (\n <MonthNavButton direction=\"prev\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n {labelInteractive ? (\n <button\n ref={monthGridTriggerRef}\n type=\"button\"\n onClick={onOpenMonthGrid ?? undefined}\n aria-label={t('ui.calendar.openMonthYearNavigation', '{month} \u2013 open month and year navigation', { month: label })}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {label}\n </button>\n ) : (\n <div\n className=\"flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium\"\n aria-live=\"polite\"\n >\n {label}\n </div>\n )}\n {showNext ? (\n <MonthNavButton direction=\"next\" locale={locale} />\n ) : (\n <div className=\"h-9 w-9 shrink-0\" aria-hidden=\"true\" />\n )}\n </div>\n )\n }\n}\n\nfunction MonthGrid({\n initialYear,\n selectedMonth,\n locale,\n onSelectMonth,\n onClose,\n onRestoreFocus,\n}: {\n initialYear: number\n selectedMonth: Date\n locale?: Locale\n onSelectMonth: (month: Date) => void\n onClose: () => void\n onRestoreFocus: () => void\n}) {\n const t = useCalendarT()\n const [year, setYear] = React.useState(initialYear)\n const gridRef = React.useRef<HTMLDivElement>(null)\n const selectedMonthRef = React.useRef<HTMLButtonElement>(null)\n React.useEffect(() => {\n selectedMonthRef.current?.focus()\n return onRestoreFocus\n }, [onRestoreFocus])\n\n React.useEffect(() => {\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || !(event.target instanceof Node) || !gridRef.current?.contains(event.target)) return\n event.preventDefault()\n event.stopPropagation()\n onClose()\n }\n window.addEventListener('keydown', handleEscape, true)\n return () => window.removeEventListener('keydown', handleEscape, true)\n }, [onClose])\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Tab') return\n\n const buttons = event.currentTarget.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')\n const firstButton = buttons[0]\n const lastButton = buttons[buttons.length - 1]\n if (event.shiftKey && document.activeElement === firstButton) {\n event.preventDefault()\n lastButton?.focus()\n } else if (!event.shiftKey && document.activeElement === lastButton) {\n event.preventDefault()\n firstButton?.focus()\n }\n }\n const today = new Date()\n const monthLabels = React.useMemo(\n () =>\n Array.from({ length: 12 }, (_, monthIndex) =>\n format(new Date(year, monthIndex, 1), STANDALONE_MONTH_SHORT, locale ? { locale } : undefined),\n ),\n [year, locale],\n )\n const yearLabel = format(new Date(year, 0, 1), 'yyyy', locale ? { locale } : undefined)\n return (\n <div\n ref={gridRef}\n className=\"absolute inset-0 z-10 flex flex-col rounded-md bg-popover p-3\"\n role=\"dialog\"\n aria-label={t('ui.calendar.selectMonthAndYear', 'Select month and year')}\n onKeyDown={handleKeyDown}\n >\n <div className=\"flex items-center justify-between gap-2 mb-3\">\n <button\n type=\"button\"\n aria-label={t('ui.calendar.goToPreviousYear', 'Go to previous year: {year}', { year: year - 1 })}\n onClick={() => setYear((current) => current - 1)}\n className={navButtonClassName}\n >\n <ChevronLeft className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('ui.calendar.backToDaySelection', '{year} \u2013 back to day selection', { year: yearLabel })}\n className={cn(\n 'flex-1 flex items-center justify-center h-9 rounded-md bg-muted px-3 text-sm font-medium',\n 'transition-colors hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n >\n {yearLabel}\n </button>\n <button\n type=\"button\"\n aria-label={t('ui.calendar.goToNextYear', 'Go to next year: {year}', { year: year + 1 })}\n onClick={() => setYear((current) => current + 1)}\n className={navButtonClassName}\n >\n <ChevronRight className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n <div className=\"grid grid-cols-3 gap-2\">\n {monthLabels.map((monthLabel, monthIndex) => {\n const isSelected =\n selectedMonth.getFullYear() === year && selectedMonth.getMonth() === monthIndex\n const isCurrentMonth =\n today.getFullYear() === year && today.getMonth() === monthIndex\n return (\n <button\n key={monthIndex}\n ref={monthIndex === selectedMonth.getMonth() ? selectedMonthRef : undefined}\n type=\"button\"\n aria-pressed={isSelected}\n onClick={() => onSelectMonth(new Date(year, monthIndex, 1))}\n className={cn(\n 'h-9 rounded-md text-sm font-normal transition-colors',\n 'inline-flex items-center justify-center',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n isCurrentMonth && !isSelected && 'font-semibold text-primary',\n isSelected && '!bg-primary !text-primary-foreground hover:!bg-primary',\n )}\n >\n {monthLabel}\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function Calendar({\n className,\n classNames,\n showOutsideDays = true,\n daySize = 36,\n fixedWeeks = true,\n locale,\n components,\n numberOfMonths = 1,\n pagedNavigation = true,\n month,\n defaultMonth,\n onMonthChange,\n labels,\n ...props\n}: CalendarProps) {\n // The month/year grid (fast navigation) is only meaningful for single-month\n // calendars; multi-month layouts (range pickers) keep the static caption.\n const monthGridEnabled = numberOfMonths === 1\n const [displayMonth, setDisplayMonth] = React.useState<Date>(\n () => month ?? defaultMonth ?? new Date(),\n )\n const [showMonthGrid, setShowMonthGrid] = React.useState(false)\n const monthGridTriggerRef = React.useRef<HTMLButtonElement>(null)\n const restoreDayViewFocus = React.useCallback(() => {\n monthGridTriggerRef.current?.focus()\n }, [])\n\n // Honor a controlled `month` prop when consumers drive navigation externally.\n React.useEffect(() => {\n if (month) setDisplayMonth(month)\n }, [month])\n\n const handleMonthChange = React.useCallback(\n (next: Date) => {\n setDisplayMonth(next)\n onMonthChange?.(next)\n },\n [onMonthChange],\n )\n\n const handleSelectMonth = React.useCallback(\n (next: Date) => {\n handleMonthChange(next)\n setShowMonthGrid(false)\n },\n [handleMonthChange],\n )\n\n const monthCaption = React.useMemo(\n () =>\n buildMonthCaption(\n locale as Locale | undefined,\n numberOfMonths,\n monthGridEnabled ? () => setShowMonthGrid(true) : null,\n monthGridTriggerRef,\n ),\n [locale, numberOfMonths, monthGridEnabled],\n )\n\n return (\n <CalendarLabelsContext.Provider value={labels}>\n <div className={monthGridEnabled ? 'relative' : 'contents'}>\n <div\n className={monthGridEnabled && showMonthGrid ? 'invisible pointer-events-none' : 'contents'}\n aria-hidden={monthGridEnabled && showMonthGrid ? true : undefined}\n inert={monthGridEnabled && showMonthGrid ? true : undefined}\n >\n <DayPicker\n showOutsideDays={showOutsideDays}\n fixedWeeks={fixedWeeks}\n pagedNavigation={pagedNavigation}\n hideNavigation\n locale={locale}\n labels={labels}\n numberOfMonths={numberOfMonths}\n month={displayMonth}\n onMonthChange={handleMonthChange}\n className={cn('p-3', className)}\n classNames={{\n months: 'flex flex-col sm:flex-row gap-4',\n month: 'space-y-2',\n month_caption: '',\n caption_label: 'sr-only',\n nav: 'sr-only',\n month_grid: 'w-full border-collapse',\n weekdays: 'flex',\n weekday: cn('text-muted-foreground rounded-md font-normal text-xs', daySize === 40 ? 'w-10' : 'w-9'),\n weeks: 'w-full border-collapse',\n week: 'flex w-full mt-1',\n day: cn('text-center text-sm p-0 relative focus-within:relative focus-within:z-20', daySize === 40 ? 'size-10' : 'size-9'),\n day_button: cn(\n 'p-0 font-normal aria-selected:opacity-100',\n daySize === 40 ? 'size-10' : 'size-9',\n 'inline-flex items-center justify-center rounded-md text-sm',\n 'transition-colors focus:outline-none focus-visible:outline-none disabled:pointer-events-none',\n // Focus indicator is a soft accent fill instead of a ring overlay \u2014 keyboard\n // users get a visible cue, but mouse-click focus does not leave a stuck ring\n // on top of the selected cell.\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:bg-accent focus-visible:text-accent-foreground',\n ),\n // React-day-picker v9 applies `classNames.selected` / `range_*` to the day\n // CELL (`<td>`) wrapper, not to the inner `<button>`. To keep the parent\n // fill visible through interaction, we (a) paint the cell with the desired\n // bg/text, and (b) force the inner button to render transparent \u2014 so the\n // button's own hover/focus-visible bg overrides cannot cover the cell fill.\n selected: cn(\n '!bg-primary !text-primary-foreground rounded-md',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_start: cn(\n '!bg-primary !text-primary-foreground rounded-l-md !rounded-r-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_end: cn(\n '!bg-primary !text-primary-foreground rounded-r-md !rounded-l-none',\n '[&_button]:!bg-transparent [&_button]:!text-primary-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-primary-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-primary-foreground',\n ),\n range_middle: cn(\n '!bg-accent !text-accent-foreground !rounded-none',\n '[&_button]:!bg-transparent [&_button]:!text-accent-foreground',\n '[&_button:hover]:!bg-transparent [&_button:hover]:!text-accent-foreground',\n '[&_button:focus-visible]:!bg-transparent [&_button:focus-visible]:!text-accent-foreground',\n ),\n today: 'font-semibold text-primary rounded-md',\n outside:\n 'day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n disabled: 'text-muted-foreground opacity-50',\n hidden: 'invisible',\n ...classNames,\n }}\n components={{\n MonthCaption: monthCaption,\n ...components,\n }}\n {...props}\n />\n </div>\n {monthGridEnabled && showMonthGrid ? (\n <MonthGrid\n initialYear={displayMonth.getFullYear()}\n selectedMonth={displayMonth}\n locale={locale as Locale | undefined}\n onSelectMonth={handleSelectMonth}\n onClose={() => setShowMonthGrid(false)}\n onRestoreFocus={restoreDayViewFocus}\n />\n ) : null}\n </div>\n </CalendarLabelsContext.Provider>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.
|
|
158
|
+
"@open-mercato/shared": "0.9.0",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.
|
|
165
|
+
"@open-mercato/shared": "0.9.0",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
|
@@ -186,6 +186,5 @@
|
|
|
186
186
|
"type": "git",
|
|
187
187
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
188
188
|
"directory": "packages/ui"
|
|
189
|
-
}
|
|
190
|
-
"stableVersion": "0.8.0"
|
|
189
|
+
}
|
|
191
190
|
}
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -1991,6 +1991,27 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1991
1991
|
() => new Set(injectedFieldDefinitions.map((definition) => definition.id)),
|
|
1992
1992
|
[injectedFieldDefinitions],
|
|
1993
1993
|
)
|
|
1994
|
+
// An injected field may reuse a host-declared field id to replace the host's built-in
|
|
1995
|
+
// input for that field (the later, injected entry wins in `fieldById`) — e.g.
|
|
1996
|
+
// `customer_groups`' `GroupPickerField` replacing catalog's plain-text `customerGroupId`.
|
|
1997
|
+
// Such a field is still a host schema field, so its value must reach `coreValues` and the
|
|
1998
|
+
// host `onSubmit` payload. Only an injected field with NO host-declared counterpart is an
|
|
1999
|
+
// "extra" field that is stripped before schema validation/submission (widgets still see
|
|
2000
|
+
// it via `onBeforeSave`/`onSave`). See UPGRADE_NOTES.md (0.8.1). Host fields declared
|
|
2001
|
+
// inline in `groups[].fields` count as host-declared too.
|
|
2002
|
+
const hostFieldIdSet = React.useMemo(() => {
|
|
2003
|
+
const ids = new Set(fields.map((field) => field.id))
|
|
2004
|
+
for (const group of groups ?? []) {
|
|
2005
|
+
for (const entry of group.fields ?? []) {
|
|
2006
|
+
if (typeof entry !== 'string') ids.add(entry.id)
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return ids
|
|
2010
|
+
}, [fields, groups])
|
|
2011
|
+
const injectedOnlyFieldIdSet = React.useMemo(
|
|
2012
|
+
() => new Set(Array.from(injectedFieldIdSet).filter((id) => !hostFieldIdSet.has(id))),
|
|
2013
|
+
[injectedFieldIdSet, hostFieldIdSet],
|
|
2014
|
+
)
|
|
1994
2015
|
|
|
1995
2016
|
const injectedCrudFields = React.useMemo<CrudField[]>(() => {
|
|
1996
2017
|
return injectedFieldDefinitions.map((definition) => {
|
|
@@ -2043,8 +2064,9 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2043
2064
|
}, [allFields])
|
|
2044
2065
|
|
|
2045
2066
|
// Declared base fields whose id is a dot-path (e.g. `metadata.category`).
|
|
2046
|
-
// Injected and custom (cf) fields already get dot-path/cf-path hydration and
|
|
2047
|
-
// are excluded here so their dedicated handling is preserved
|
|
2067
|
+
// Injected-only and custom (cf) fields already get dot-path/cf-path hydration and
|
|
2068
|
+
// are excluded here so their dedicated handling is preserved; an injected field
|
|
2069
|
+
// that reuses a host field id stays a host field and is collapsed. CrudForm hydrates
|
|
2048
2070
|
// these flat keys from nested initial values on load and projects them back into
|
|
2049
2071
|
// nested objects before schema.safeParse on submit. See issue #2503.
|
|
2050
2072
|
const dotPathBaseFieldIds = React.useMemo(() => {
|
|
@@ -2053,13 +2075,13 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2053
2075
|
for (const field of allFields) {
|
|
2054
2076
|
const fieldId = field.id
|
|
2055
2077
|
if (!fieldId.includes('.')) continue
|
|
2056
|
-
if (
|
|
2078
|
+
if (injectedOnlyFieldIdSet.has(fieldId)) continue
|
|
2057
2079
|
if (fieldId.startsWith('cf_') || fieldId.startsWith('cf:')) continue
|
|
2058
2080
|
if (cfFieldIds.has(fieldId)) continue
|
|
2059
2081
|
ids.add(fieldId)
|
|
2060
2082
|
}
|
|
2061
2083
|
return ids
|
|
2062
|
-
}, [allFields,
|
|
2084
|
+
}, [allFields, injectedOnlyFieldIdSet, cfFields])
|
|
2063
2085
|
|
|
2064
2086
|
const validateFieldOnBlur = React.useCallback(async (
|
|
2065
2087
|
fieldId: string,
|
|
@@ -2132,7 +2154,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2132
2154
|
delete widgetValues[hiddenId]
|
|
2133
2155
|
}
|
|
2134
2156
|
const coreValues = { ...widgetValues }
|
|
2135
|
-
for (const injectedId of
|
|
2157
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
2136
2158
|
delete coreValues[injectedId]
|
|
2137
2159
|
}
|
|
2138
2160
|
const result = schema.safeParse(collapseDotPathFields(coreValues, dotPathBaseFieldIds))
|
|
@@ -2181,7 +2203,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2181
2203
|
hiddenBaseFieldIds,
|
|
2182
2204
|
hiddenGroupFieldIds,
|
|
2183
2205
|
hiddenInjectedFieldIds,
|
|
2184
|
-
|
|
2206
|
+
injectedOnlyFieldIdSet,
|
|
2185
2207
|
mapDefsForValidation,
|
|
2186
2208
|
schema,
|
|
2187
2209
|
t,
|
|
@@ -3050,7 +3072,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3050
3072
|
delete widgetValues[hiddenId]
|
|
3051
3073
|
}
|
|
3052
3074
|
const coreValues = { ...widgetValues }
|
|
3053
|
-
for (const injectedId of
|
|
3075
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
3054
3076
|
delete coreValues[injectedId]
|
|
3055
3077
|
}
|
|
3056
3078
|
if (customEntity) {
|
|
@@ -3089,7 +3111,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3089
3111
|
if (result.data) {
|
|
3090
3112
|
submitValues = result.data as TValues
|
|
3091
3113
|
const projectedCoreValues = { ...(result.data as Record<string, unknown>) }
|
|
3092
|
-
for (const injectedId of
|
|
3114
|
+
for (const injectedId of injectedOnlyFieldIdSet) {
|
|
3093
3115
|
delete projectedCoreValues[injectedId]
|
|
3094
3116
|
}
|
|
3095
3117
|
if (customEntity) {
|
|
@@ -63,6 +63,7 @@ import type {
|
|
|
63
63
|
InjectionRowActionDefinition,
|
|
64
64
|
} from '@open-mercato/shared/modules/widgets/injection'
|
|
65
65
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
66
|
+
import { useRegisteredComponent } from './injection/useRegisteredComponent'
|
|
66
67
|
import { dataTableExtensionSpotId, extensionSpotChildId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
67
68
|
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
68
69
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
@@ -1252,7 +1253,7 @@ function ViewSwitcherDropdown({
|
|
|
1252
1253
|
)
|
|
1253
1254
|
}
|
|
1254
1255
|
|
|
1255
|
-
|
|
1256
|
+
function DataTableImpl<T extends RowData>({
|
|
1256
1257
|
columns,
|
|
1257
1258
|
data,
|
|
1258
1259
|
toolbar,
|
|
@@ -1396,6 +1397,11 @@ export function DataTable<T extends RowData>({
|
|
|
1396
1397
|
// cannot serve here: applying a snapshot rewrites the stored copy with a
|
|
1397
1398
|
// fresh `Date.now()`, which would make every server row look older than it is.
|
|
1398
1399
|
const hydratedSnapshotRef = React.useRef<PerspectiveSnapshot | null>(null)
|
|
1400
|
+
const hydratedAdvancedFilterRestoredRef = React.useRef(false)
|
|
1401
|
+
const pendingPerspectiveFiltersRef = React.useRef<{ filters: PerspectiveSettings['filters'] } | null>(null)
|
|
1402
|
+
React.useLayoutEffect(() => {
|
|
1403
|
+
pendingPerspectiveFiltersRef.current = null
|
|
1404
|
+
}, [advancedFilter?.value, perspectiveTableId])
|
|
1399
1405
|
const initialSettingsSource = perspectiveConfig?.initialState?.initialSettings ?? null
|
|
1400
1406
|
// Memoized on the host's own object: `sanitizePerspectiveSettings` returns a
|
|
1401
1407
|
// fresh result on every call, so without this every effect keyed on the
|
|
@@ -2032,6 +2038,12 @@ export function DataTable<T extends RowData>({
|
|
|
2032
2038
|
},
|
|
2033
2039
|
) => {
|
|
2034
2040
|
const normalized = sanitizePerspectiveSettings(settings) ?? {}
|
|
2041
|
+
if (!options?.preserveAdvancedFilter && advancedFilter?.onApplyTree) {
|
|
2042
|
+
hydratedAdvancedFilterRestoredRef.current = false
|
|
2043
|
+
pendingPerspectiveFiltersRef.current = {
|
|
2044
|
+
filters: isPersistedFilterTree(normalized.filters) ? normalized.filters : undefined,
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2035
2047
|
// The applied settings replace the baseline outright, so the defaults hidden under
|
|
2036
2048
|
// the *previous* view must not leak into it. The pass below re-runs and re-folds
|
|
2037
2049
|
// whatever this view leaves undecided.
|
|
@@ -2042,7 +2054,12 @@ export function DataTable<T extends RowData>({
|
|
|
2042
2054
|
// filter change the user never made.
|
|
2043
2055
|
setViewBaseline(
|
|
2044
2056
|
options?.preserveAdvancedFilter
|
|
2045
|
-
? {
|
|
2057
|
+
? {
|
|
2058
|
+
...normalized,
|
|
2059
|
+
filters: pendingPerspectiveFiltersRef.current
|
|
2060
|
+
? pendingPerspectiveFiltersRef.current.filters
|
|
2061
|
+
: getCurrentSettings().filters,
|
|
2062
|
+
}
|
|
2046
2063
|
: normalized,
|
|
2047
2064
|
)
|
|
2048
2065
|
if (normalized.columnOrder && normalized.columnOrder.length) {
|
|
@@ -2110,7 +2127,14 @@ export function DataTable<T extends RowData>({
|
|
|
2110
2127
|
if (perspectiveTableId) {
|
|
2111
2128
|
writePerspectiveCookie(perspectiveTableId, nextId)
|
|
2112
2129
|
if (nextId) {
|
|
2113
|
-
const
|
|
2130
|
+
const previousSnapshot = readPerspectiveSnapshot(perspectiveTableId) ?? initialSnapshotRef.current
|
|
2131
|
+
const preserveClearedFilter = options?.preserveAdvancedFilter
|
|
2132
|
+
&& !getCurrentSettings().filters
|
|
2133
|
+
&& !previousSnapshot?.settings.filters
|
|
2134
|
+
const snapshotSettings = preserveClearedFilter
|
|
2135
|
+
? { ...normalized, filters: undefined }
|
|
2136
|
+
: normalized
|
|
2137
|
+
const snapshot: PerspectiveSnapshot = { perspectiveId: nextId, settings: snapshotSettings, updatedAt: Date.now() }
|
|
2114
2138
|
writePerspectiveSnapshot(perspectiveTableId, snapshot)
|
|
2115
2139
|
initialSnapshotRef.current = snapshot
|
|
2116
2140
|
} else if (normalized.columnSizing && Object.keys(normalized.columnSizing).length) {
|
|
@@ -2175,29 +2199,45 @@ export function DataTable<T extends RowData>({
|
|
|
2175
2199
|
if (!perspectiveTableId) return
|
|
2176
2200
|
if (snapshotHydratedTableRef.current === perspectiveTableId) return
|
|
2177
2201
|
snapshotHydratedTableRef.current = perspectiveTableId
|
|
2202
|
+
hydratedAdvancedFilterRestoredRef.current = false
|
|
2178
2203
|
const snapshot = readPerspectiveSnapshot(perspectiveTableId)
|
|
2179
2204
|
if (!snapshot) return
|
|
2180
2205
|
initialSnapshotRef.current = snapshot
|
|
2181
2206
|
hydratedSnapshotRef.current = snapshot
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
// The snapshot still drives non-filter settings: column order, visibility,
|
|
2189
|
-
// sorting, search. Explicit perspective selection and "No view" go through
|
|
2190
|
-
// a different applyPerspectiveSettings call without this option, so they
|
|
2191
|
-
// still update the host filter as expected.
|
|
2192
|
-
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree
|
|
2207
|
+
const restoredTree = isPersistedFilterTree(snapshot.settings.filters)
|
|
2208
|
+
? deserializeTreeFromPersist(snapshot.settings.filters)
|
|
2209
|
+
: null
|
|
2210
|
+
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree && (
|
|
2211
|
+
advancedFilter.value.root.children.length > 0 || !restoredTree?.root.children.length
|
|
2212
|
+
)
|
|
2193
2213
|
applyPerspectiveSettings(
|
|
2194
2214
|
snapshot.settings,
|
|
2195
2215
|
snapshot.perspectiveId ?? null,
|
|
2196
2216
|
{ preserveAdvancedFilter },
|
|
2197
2217
|
)
|
|
2218
|
+
hydratedAdvancedFilterRestoredRef.current = !!advancedFilter?.onApplyTree && !preserveAdvancedFilter
|
|
2198
2219
|
initialPerspectiveAppliedRef.current = true
|
|
2199
2220
|
}, [perspectiveTableId, applyPerspectiveSettings, advancedFilter])
|
|
2200
2221
|
|
|
2222
|
+
const previousAdvancedFilterRef = React.useRef<{
|
|
2223
|
+
tableId: string | null
|
|
2224
|
+
hasRules: boolean
|
|
2225
|
+
} | null>(null)
|
|
2226
|
+
const advancedFilterHasRules = !!advancedFilter?.value.root.children.length
|
|
2227
|
+
React.useEffect(() => {
|
|
2228
|
+
const previous = previousAdvancedFilterRef.current
|
|
2229
|
+
previousAdvancedFilterRef.current = { tableId: perspectiveTableId, hasRules: advancedFilterHasRules }
|
|
2230
|
+
if (!perspectiveTableId || !advancedFilter?.onApplyTree) return
|
|
2231
|
+
if (previous?.tableId !== perspectiveTableId || !previous.hasRules || advancedFilterHasRules) return
|
|
2232
|
+
const snapshot = readPerspectiveSnapshot(perspectiveTableId) ?? initialSnapshotRef.current
|
|
2233
|
+
if (!snapshot) return
|
|
2234
|
+
const settings = { ...snapshot.settings }
|
|
2235
|
+
delete settings.filters
|
|
2236
|
+
const clearedSnapshot: PerspectiveSnapshot = { ...snapshot, settings, updatedAt: Date.now() }
|
|
2237
|
+
writePerspectiveSnapshot(perspectiveTableId, clearedSnapshot)
|
|
2238
|
+
initialSnapshotRef.current = clearedSnapshot
|
|
2239
|
+
}, [perspectiveTableId, advancedFilterHasRules, advancedFilter?.onApplyTree])
|
|
2240
|
+
|
|
2201
2241
|
type SavePerspectivePayload = {
|
|
2202
2242
|
name: string
|
|
2203
2243
|
isDefault: boolean
|
|
@@ -2716,11 +2756,23 @@ export function DataTable<T extends RowData>({
|
|
|
2716
2756
|
React.useLayoutEffect(() => {
|
|
2717
2757
|
if (!canUsePerspectives) return
|
|
2718
2758
|
if (!perspectiveTableId) return
|
|
2759
|
+
if (
|
|
2760
|
+
(initialSnapshotRef.current || initialPerspectiveAppliedRef.current)
|
|
2761
|
+
&& serverReconciledTableRef.current === perspectiveTableId
|
|
2762
|
+
) return
|
|
2719
2763
|
|
|
2720
2764
|
const source = perspectiveData ?? perspectiveConfig?.initialState?.response
|
|
2721
2765
|
if (!source) return
|
|
2722
2766
|
|
|
2723
2767
|
let orphanedSnapshotDropped = false
|
|
2768
|
+
const currentFilters = pendingPerspectiveFiltersRef.current
|
|
2769
|
+
? pendingPerspectiveFiltersRef.current.filters
|
|
2770
|
+
: getCurrentSettings().filters
|
|
2771
|
+
const restoredFilterStillActive = hydratedAdvancedFilterRestoredRef.current && diffPerspectiveSettings(
|
|
2772
|
+
{ filters: currentFilters },
|
|
2773
|
+
{ filters: hydratedSnapshotRef.current?.settings.filters },
|
|
2774
|
+
).length === 0
|
|
2775
|
+
const preserveAdvancedFilter = !!advancedFilter?.onApplyTree && !restoredFilterStillActive
|
|
2724
2776
|
|
|
2725
2777
|
const tryResolve = (id: string | null | undefined): PerspectiveDto | RolePerspectiveDto | undefined => {
|
|
2726
2778
|
if (!id) return undefined
|
|
@@ -2736,7 +2788,6 @@ export function DataTable<T extends RowData>({
|
|
|
2736
2788
|
// localStorage entry (#5113). One-shot matters as much as reconciling at
|
|
2737
2789
|
// all — a later refetch must not clobber edits made after mount.
|
|
2738
2790
|
if (initialSnapshotRef.current || initialPerspectiveAppliedRef.current) {
|
|
2739
|
-
if (serverReconciledTableRef.current === perspectiveTableId) return
|
|
2740
2791
|
serverReconciledTableRef.current = perspectiveTableId
|
|
2741
2792
|
// A snapshot only speaks for the active perspective: once the user has
|
|
2742
2793
|
// picked a different view, reconciling the mount-time one would undo that
|
|
@@ -2768,7 +2819,7 @@ export function DataTable<T extends RowData>({
|
|
|
2768
2819
|
// selection: on a host that owns filter persistence through the URL,
|
|
2769
2820
|
// it must not overwrite the filter currently on screen.
|
|
2770
2821
|
applyPerspectiveSettings(local.settings, local.id, {
|
|
2771
|
-
preserveAdvancedFilter
|
|
2822
|
+
preserveAdvancedFilter,
|
|
2772
2823
|
})
|
|
2773
2824
|
initialPerspectiveAppliedRef.current = true
|
|
2774
2825
|
}
|
|
@@ -2808,7 +2859,7 @@ export function DataTable<T extends RowData>({
|
|
|
2808
2859
|
applyPerspectiveSettings(
|
|
2809
2860
|
target.settings,
|
|
2810
2861
|
target.id,
|
|
2811
|
-
orphanedSnapshotDropped ? { preserveAdvancedFilter
|
|
2862
|
+
orphanedSnapshotDropped ? { preserveAdvancedFilter } : undefined,
|
|
2812
2863
|
)
|
|
2813
2864
|
} else if (orphanedSnapshotDropped) {
|
|
2814
2865
|
// Nothing is left to fall back to — the deleted view was the only one. The
|
|
@@ -2820,11 +2871,11 @@ export function DataTable<T extends RowData>({
|
|
|
2820
2871
|
// another session — so it must not clear the filter a host that owns
|
|
2821
2872
|
// filter persistence through the URL currently has on screen.
|
|
2822
2873
|
applyPerspectiveSettings({}, null, {
|
|
2823
|
-
preserveAdvancedFilter
|
|
2874
|
+
preserveAdvancedFilter,
|
|
2824
2875
|
})
|
|
2825
2876
|
}
|
|
2826
2877
|
initialPerspectiveAppliedRef.current = true
|
|
2827
|
-
}, [canUsePerspectives, perspectiveData, perspectiveTableId, perspectiveConfig, applyPerspectiveSettings, activePerspectiveId, advancedFilter?.onApplyTree])
|
|
2878
|
+
}, [canUsePerspectives, perspectiveData, perspectiveTableId, perspectiveConfig, applyPerspectiveSettings, activePerspectiveId, advancedFilter?.onApplyTree, getCurrentSettings])
|
|
2828
2879
|
|
|
2829
2880
|
const scrollTableIntoView = React.useCallback(() => {
|
|
2830
2881
|
const rect = containerRef.current?.getBoundingClientRect()
|
|
@@ -3497,7 +3548,9 @@ export function DataTable<T extends RowData>({
|
|
|
3497
3548
|
// the wrapped action buttons rendered over it on narrow layouts.
|
|
3498
3549
|
const headerContentClassName = 'flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between'
|
|
3499
3550
|
const toolbarWrapperClassName = embedded ? 'mt-2' : 'mt-3 pt-3 border-t'
|
|
3500
|
-
|
|
3551
|
+
// `isolate` keeps the z-index of pinned cells local to the table so they
|
|
3552
|
+
// scroll beneath the sticky app top bar instead of over it.
|
|
3553
|
+
const tableScrollWrapperClassName = embedded ? 'isolate' : 'isolate overflow-auto'
|
|
3501
3554
|
|
|
3502
3555
|
const virtualScrollRef = React.useRef<HTMLDivElement>(null)
|
|
3503
3556
|
// Measure the horizontal scroll viewport so the empty state can center within
|
|
@@ -4004,3 +4057,25 @@ export function DataTable<T extends RowData>({
|
|
|
4004
4057
|
</TooltipProvider>
|
|
4005
4058
|
)
|
|
4006
4059
|
}
|
|
4060
|
+
|
|
4061
|
+
const DataTableFallback = DataTableImpl as unknown as React.ComponentType<DataTableProps<RowData>>
|
|
4062
|
+
|
|
4063
|
+
/**
|
|
4064
|
+
* Resolves the public DataTable component handle before mounting the table so
|
|
4065
|
+
* registered props transforms, wrappers, and replacements execute at the real
|
|
4066
|
+
* host boundary rather than being exposed only as a diagnostic DOM attribute.
|
|
4067
|
+
*/
|
|
4068
|
+
export function DataTable<T extends RowData>(props: DataTableProps<T>) {
|
|
4069
|
+
const extensionTableId = props.perspective?.tableId
|
|
4070
|
+
?? props.extensionTableId
|
|
4071
|
+
?? (props.injectionSpotId?.startsWith('data-table:')
|
|
4072
|
+
? props.injectionSpotId.slice('data-table:'.length)
|
|
4073
|
+
: null)
|
|
4074
|
+
const handle = props.replacementHandle
|
|
4075
|
+
?? ComponentReplacementHandles.dataTable(extensionTableId ?? 'unknown')
|
|
4076
|
+
const Resolved = useRegisteredComponent<DataTableProps<T>>(
|
|
4077
|
+
handle,
|
|
4078
|
+
DataTableFallback as unknown as React.ComponentType<DataTableProps<T>>,
|
|
4079
|
+
)
|
|
4080
|
+
return <Resolved {...props} />
|
|
4081
|
+
}
|