@wordrhyme/auto-crud 1.5.6 → 1.6.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/dist/index.cjs CHANGED
@@ -420,13 +420,15 @@ function hasValidCanonicalDateTimePrefix(value) {
420
420
  const match = canonicalDateTimePrefixPattern.exec(value);
421
421
  return !match || isValidCalendarDate(match[1], match[2], match[3]);
422
422
  }
423
+ function getDateLocaleOptions() {
424
+ return hostDateFormatters[hostDateFormatters.length - 1]?.options;
425
+ }
423
426
  /**
424
- * Register the host's process-wide date presentation policy without coupling
425
- * AutoCrud to locale or timezone selection. The returned cleanup removes only
426
- * this registration, so overlapping registrations may be disposed out of order.
427
+ * Register the host's date formatter and optional calendar locale policy.
428
+ * Cleanup removes only this registration, including out-of-order cleanup.
427
429
  * Passing undefined clears every registration.
428
430
  */
429
- function setDateFormatter(formatter) {
431
+ function setDateFormatter(formatter, options) {
430
432
  if (formatter === void 0) {
431
433
  const hadFormatter = hostDateFormatters.length > 0;
432
434
  hostDateFormatters = [];
@@ -435,15 +437,16 @@ function setDateFormatter(formatter) {
435
437
  }
436
438
  const registration = {
437
439
  formatter,
440
+ ...options ? { options } : {},
438
441
  token: Symbol("date-formatter")
439
442
  };
440
443
  hostDateFormatters.push(registration);
441
444
  notifyDateFormatterChange();
442
445
  return () => {
443
- const currentFormatter = getHostDateFormatter();
446
+ const currentRegistration = hostDateFormatters[hostDateFormatters.length - 1];
444
447
  const previousLength = hostDateFormatters.length;
445
448
  hostDateFormatters = hostDateFormatters.filter(({ token }) => token !== registration.token);
446
- if (hostDateFormatters.length !== previousLength && getHostDateFormatter() !== currentFormatter) notifyDateFormatterChange();
449
+ if (hostDateFormatters.length !== previousLength && hostDateFormatters[hostDateFormatters.length - 1] !== currentRegistration) notifyDateFormatterChange();
447
450
  };
448
451
  }
449
452
  function formatDate(date, opts = {}, preset) {
@@ -525,104 +528,6 @@ function DataTable({ table, actionBar, children, className,...props }) {
525
528
  });
526
529
  }
527
530
 
528
- //#endregion
529
- //#region src/lib/id.ts
530
- const prefixes = {};
531
- function generateId(prefixOrOptions, inputOptions = {}) {
532
- const finalOptions = typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;
533
- const prefix = typeof prefixOrOptions === "object" ? void 0 : prefixOrOptions;
534
- const { length = 12, separator = "_" } = finalOptions;
535
- const id = (0, nanoid.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", length)();
536
- return prefix && prefix in prefixes ? `${prefixes[prefix]}${separator}${id}` : id;
537
- }
538
-
539
- //#endregion
540
- //#region src/lib/readable-filters.ts
541
- const OP_SUFFIX = "__op";
542
- const INDEX_SEPARATOR = "__";
543
- const RANGE_VARIANTS = new Set(["range", "dateRange"]);
544
- function getColumnId(column) {
545
- if ("id" in column && column.id) return String(column.id);
546
- if ("accessorKey" in column && column.accessorKey) return String(column.accessorKey);
547
- return null;
548
- }
549
- function getColumnVariant(column) {
550
- if ("columnDef" in column) return column.columnDef.meta?.variant;
551
- return column.meta?.variant;
552
- }
553
- function splitKey(key) {
554
- const match = key.match(/^(.*?)(?:__(\d+))?$/);
555
- if (!match) return { base: key };
556
- const index$1 = match[2] ? Number(match[2]) : void 0;
557
- return {
558
- base: match[1] ?? key,
559
- index: index$1
560
- };
561
- }
562
- function isFilterKey(key, columnIds) {
563
- const { base } = splitKey(key.endsWith(OP_SUFFIX) ? key.slice(0, -4) : key);
564
- return columnIds.has(base);
565
- }
566
- function parseValueByVariant(value, variant) {
567
- if (variant === "multiSelect") return value ? value.split(",").filter(Boolean) : [];
568
- if (RANGE_VARIANTS.has(variant)) {
569
- const parts = value.split(",");
570
- return [parts[0] ?? "", parts[1] ?? ""];
571
- }
572
- if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
573
- return value;
574
- }
575
- function serializeValue(value, variant) {
576
- if (Array.isArray(value)) {
577
- if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
578
- return value.join(",");
579
- }
580
- return value;
581
- }
582
- function parseReadableFilters(params, columns) {
583
- const columnsById = /* @__PURE__ */ new Map();
584
- for (const column of columns) {
585
- const id = getColumnId(column);
586
- if (id) columnsById.set(id, column);
587
- }
588
- const filters = [];
589
- for (const [key, rawValue] of params.entries()) {
590
- if (key.endsWith(OP_SUFFIX)) continue;
591
- const { base } = splitKey(key);
592
- const column = columnsById.get(base);
593
- if (!column) continue;
594
- const variant = getColumnVariant(column) ?? "text";
595
- const opKey = `${key}${OP_SUFFIX}`;
596
- const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
597
- const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
598
- filters.push({
599
- id: base,
600
- value: parsedValue,
601
- variant,
602
- operator,
603
- filterId: key || generateId({ length: 8 })
604
- });
605
- }
606
- return filters;
607
- }
608
- function applyReadableFilters(params, columns, filters) {
609
- const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
610
- for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
611
- const counters = /* @__PURE__ */ new Map();
612
- for (const filter of filters) {
613
- const columnId = String(filter.id);
614
- const count = (counters.get(columnId) ?? 0) + 1;
615
- counters.set(columnId, count);
616
- const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
617
- const variant = filter.variant ?? "text";
618
- const value = serializeValue(filter.value, variant);
619
- params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
620
- const defaultOperator = getDefaultFilterOperator(variant);
621
- if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
622
- }
623
- return params;
624
- }
625
-
626
531
  //#endregion
627
532
  //#region src/hooks/use-url-state.ts
628
533
  const parseAsInteger = {
@@ -873,15 +778,120 @@ function useQueryStates(parsers, options = {}) {
873
778
  }, [setValues])];
874
779
  }
875
780
 
781
+ //#endregion
782
+ //#region src/lib/id.ts
783
+ const prefixes = {};
784
+ function generateId(prefixOrOptions, inputOptions = {}) {
785
+ const finalOptions = typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;
786
+ const prefix = typeof prefixOrOptions === "object" ? void 0 : prefixOrOptions;
787
+ const { length = 12, separator = "_" } = finalOptions;
788
+ const id = (0, nanoid.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", length)();
789
+ return prefix && prefix in prefixes ? `${prefixes[prefix]}${separator}${id}` : id;
790
+ }
791
+
792
+ //#endregion
793
+ //#region src/lib/readable-filters.ts
794
+ const OP_SUFFIX = "__op";
795
+ const INDEX_SEPARATOR = "__";
796
+ const RANGE_VARIANTS = new Set(["range", "dateRange"]);
797
+ function getColumnId(column) {
798
+ if ("id" in column && column.id) return String(column.id);
799
+ if ("accessorKey" in column && column.accessorKey) return String(column.accessorKey);
800
+ return null;
801
+ }
802
+ function getColumnVariant(column) {
803
+ if ("columnDef" in column) return column.columnDef.meta?.variant;
804
+ return column.meta?.variant;
805
+ }
806
+ function splitKey(key) {
807
+ const match = key.match(/^(.*?)(?:__(\d+))?$/);
808
+ if (!match) return { base: key };
809
+ const index$1 = match[2] ? Number(match[2]) : void 0;
810
+ return {
811
+ base: match[1] ?? key,
812
+ index: index$1
813
+ };
814
+ }
815
+ function isFilterKey(key, columnIds) {
816
+ const { base } = splitKey(key.endsWith(OP_SUFFIX) ? key.slice(0, -4) : key);
817
+ return columnIds.has(base);
818
+ }
819
+ function parseValueByVariant(value, variant) {
820
+ if (variant === "multiSelect") return value ? value.split(",").filter(Boolean) : [];
821
+ if (RANGE_VARIANTS.has(variant)) {
822
+ const parts = value.split(",");
823
+ return [parts[0] ?? "", parts[1] ?? ""];
824
+ }
825
+ if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
826
+ return value;
827
+ }
828
+ function serializeValue(value, variant) {
829
+ if (Array.isArray(value)) {
830
+ if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
831
+ return value.join(",");
832
+ }
833
+ return value;
834
+ }
835
+ function parseReadableFilters(params, columns) {
836
+ const columnsById = /* @__PURE__ */ new Map();
837
+ for (const column of columns) {
838
+ const id = getColumnId(column);
839
+ if (id) columnsById.set(id, column);
840
+ }
841
+ const filters = [];
842
+ for (const [key, rawValue] of params.entries()) {
843
+ if (key.endsWith(OP_SUFFIX)) continue;
844
+ const { base } = splitKey(key);
845
+ const column = columnsById.get(base);
846
+ if (!column) continue;
847
+ const variant = getColumnVariant(column) ?? "text";
848
+ const opKey = `${key}${OP_SUFFIX}`;
849
+ const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
850
+ const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
851
+ filters.push({
852
+ id: base,
853
+ value: parsedValue,
854
+ variant,
855
+ operator,
856
+ filterId: key || generateId({ length: 8 })
857
+ });
858
+ }
859
+ return filters;
860
+ }
861
+ function applyReadableFilters(params, columns, filters) {
862
+ const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
863
+ for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
864
+ const counters = /* @__PURE__ */ new Map();
865
+ for (const filter of filters) {
866
+ const columnId = String(filter.id);
867
+ const count = (counters.get(columnId) ?? 0) + 1;
868
+ counters.set(columnId, count);
869
+ const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
870
+ const variant = filter.variant ?? "text";
871
+ const value = serializeValue(filter.value, variant);
872
+ params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
873
+ const defaultOperator = getDefaultFilterOperator(variant);
874
+ if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
875
+ }
876
+ return params;
877
+ }
878
+
876
879
  //#endregion
877
880
  //#region src/hooks/use-readable-filters.ts
878
881
  const URL_CHANGE_EVENT = "urlchange";
879
882
  function toSearchString(params) {
880
- const queryString = params.toString().replace(/%2C/g, ",");
883
+ const queryString = params.toString().replace(/%2C/gu, ",");
881
884
  return queryString ? `?${queryString}` : "";
882
885
  }
886
+ function filterSignature(filters) {
887
+ return JSON.stringify(filters.map(({ id, operator, value, variant }) => JSON.stringify([
888
+ id,
889
+ operator,
890
+ variant === "multiSelect" && Array.isArray(value) ? [...value].sort() : value
891
+ ])).sort());
892
+ }
883
893
  function useReadableFilters(columns, options = {}) {
884
- const { history = "replace", scroll = false } = options;
894
+ const { history = "replace", scroll = false, resetPageKey = "page" } = options;
885
895
  const columnSnapshot = react.useMemo(() => columns, [columns]);
886
896
  const lastWrittenUrlRef = react.useRef(null);
887
897
  const filtersRef = react.useRef(null);
@@ -909,7 +919,10 @@ function useReadableFilters(columns, options = {}) {
909
919
  }, [columnSnapshot]);
910
920
  const writeToUrl = react.useCallback((next) => {
911
921
  const params = getUrlParams();
922
+ const previousFilters = parseReadableFilters(params, columnSnapshot);
912
923
  applyReadableFilters(params, columnSnapshot, next);
924
+ const nextFilters = parseReadableFilters(params, columnSnapshot);
925
+ if (resetPageKey !== false && resetPageKey.length > 0 && filterSignature(previousFilters) !== filterSignature(nextFilters)) params.delete(resetPageKey);
913
926
  lastWrittenUrlRef.current = toSearchString(params);
914
927
  setSearchParams(params, {
915
928
  history,
@@ -918,6 +931,7 @@ function useReadableFilters(columns, options = {}) {
918
931
  }, [
919
932
  columnSnapshot,
920
933
  history,
934
+ resetPageKey,
921
935
  scroll
922
936
  ]);
923
937
  return [filters, react.useCallback((updater) => {
@@ -1032,6 +1046,71 @@ function useDebouncedCallback(callback, delay) {
1032
1046
  }, [handleCallback, delay]);
1033
1047
  }
1034
1048
 
1049
+ //#endregion
1050
+ //#region src/lib/calendar-date.ts
1051
+ /** Calendar days are wall dates, never instants in the host's time zone. */
1052
+ function serializeCalendarDate(date) {
1053
+ return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
1054
+ }
1055
+ function parseCalendarDate(value) {
1056
+ if (value === void 0 || value === "") return void 0;
1057
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
1058
+ const date$1 = /* @__PURE__ */ new Date(`${value}T00:00:00`);
1059
+ return !Number.isNaN(date$1.getTime()) && serializeCalendarDate(date$1) === value ? date$1 : void 0;
1060
+ }
1061
+ const date = new Date(Number(value));
1062
+ return Number.isNaN(date.getTime()) ? void 0 : date;
1063
+ }
1064
+ function calendarPresentation() {
1065
+ const localeOptions = getDateLocaleOptions();
1066
+ const locale = localeOptions?.locale ?? "en-US";
1067
+ const chinese = locale.toLowerCase().startsWith("zh");
1068
+ const localeInfo = new Intl.Locale(locale);
1069
+ const firstDay = localeInfo.getWeekInfo?.().firstDay ?? localeInfo.weekInfo?.firstDay ?? (chinese ? 1 : 7);
1070
+ const format = (date, options) => new Intl.DateTimeFormat(locale, {
1071
+ ...options,
1072
+ timeZone: "UTC"
1073
+ }).format(/* @__PURE__ */ new Date(`${serializeCalendarDate(date)}T00:00:00Z`));
1074
+ return {
1075
+ locale,
1076
+ weekStartsOn: firstDay % 7,
1077
+ formatDate: (date) => {
1078
+ if (!localeOptions) return formatDate(date);
1079
+ return date ? format(date, {
1080
+ year: "numeric",
1081
+ month: "long",
1082
+ day: "numeric"
1083
+ }) : "";
1084
+ },
1085
+ clearLabel: (title) => chinese ? `清除${title ?? ""}筛选` : `Clear ${title ?? ""} filter`,
1086
+ formatters: {
1087
+ formatCaption: (date) => format(date, {
1088
+ year: "numeric",
1089
+ month: "long"
1090
+ }),
1091
+ formatMonthDropdown: (date) => format(date, { month: "long" }),
1092
+ formatWeekdayName: (date) => format(date, { weekday: "short" })
1093
+ },
1094
+ labels: {
1095
+ labelNext: () => chinese ? "下个月" : "Go to the Next Month",
1096
+ labelPrevious: () => chinese ? "上个月" : "Go to the Previous Month",
1097
+ labelMonthDropdown: () => chinese ? "选择月份" : "Choose the Month",
1098
+ labelYearDropdown: () => chinese ? "选择年份" : "Choose the Year",
1099
+ labelGrid: (date) => format(date, {
1100
+ year: "numeric",
1101
+ month: "long"
1102
+ }),
1103
+ labelWeekday: (date) => format(date, { weekday: "long" }),
1104
+ labelDayButton: (date) => format(date, {
1105
+ year: "numeric",
1106
+ month: "long",
1107
+ day: "numeric",
1108
+ weekday: "long"
1109
+ })
1110
+ }
1111
+ };
1112
+ }
1113
+
1035
1114
  //#endregion
1036
1115
  //#region src/components/data-table/data-table-filter-list.tsx
1037
1116
  const DEBOUNCE_MS$2 = 300;
@@ -1053,7 +1132,10 @@ function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = T
1053
1132
  return true;
1054
1133
  });
1055
1134
  }, [table, columnDefs]);
1056
- const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
1135
+ const [filters, setFilters] = useReadableFilters(columns, {
1136
+ debounceMs,
1137
+ resetPageKey: table.options.meta?.queryKeys?.page ?? "page"
1138
+ });
1057
1139
  const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
1058
1140
  const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
1059
1141
  const onFilterAdd = react.useCallback(() => {
@@ -1200,6 +1282,7 @@ function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = T
1200
1282
  });
1201
1283
  }
1202
1284
  function DataTableFilterItem$1({ filter, index: index$1, filterItemId, joinOperator, setJoinOperator, columns, onFilterUpdate, onFilterRemove }) {
1285
+ useDateFormatterVersion();
1203
1286
  const [showFieldSelector, setShowFieldSelector] = react.useState(false);
1204
1287
  const [showOperatorSelector, setShowOperatorSelector] = react.useState(false);
1205
1288
  const [showValueSelector, setShowValueSelector] = react.useState(false);
@@ -1488,10 +1571,10 @@ function onFilterInputRender$1({ filter, inputId, column, columnMeta, onFilterUp
1488
1571
  case "dateRange": {
1489
1572
  const inputListboxId = `${inputId}-listbox`;
1490
1573
  const dateValue = Array.isArray(filter.value) ? filter.value.filter(Boolean) : [filter.value, filter.value].filter(Boolean);
1491
- const startDate = dateValue[0] ? new Date(Number(dateValue[0])) : void 0;
1492
- const endDate = dateValue[1] ? new Date(Number(dateValue[1])) : void 0;
1574
+ const startDate = dateValue[0] ? parseCalendarDate(dateValue[0]) : void 0;
1575
+ const endDate = dateValue[1] ? parseCalendarDate(dateValue[1]) : void 0;
1493
1576
  const isSameDate = startDate && endDate && startDate.toDateString() === endDate.toDateString();
1494
- const displayValue = filter.operator === "isBetween" && dateValue.length === 2 && !isSameDate ? `${formatDate(startDate, { month: "short" })} - ${formatDate(endDate, { month: "short" })}` : startDate ? formatDate(startDate, { month: "short" }) : "Pick a date";
1577
+ const displayValue = filter.operator === "isBetween" && dateValue.length === 2 && !isSameDate ? `${calendarPresentation().formatDate(startDate)} - ${calendarPresentation().formatDate(endDate)}` : startDate ? calendarPresentation().formatDate(startDate) : "Pick a date";
1495
1578
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Popover, {
1496
1579
  open: showValueSelector,
1497
1580
  onOpenChange: setShowValueSelector,
@@ -1514,28 +1597,36 @@ function onFilterInputRender$1({ filter, inputId, column, columnMeta, onFilterUp
1514
1597
  align: "start",
1515
1598
  className: "w-auto p-0",
1516
1599
  children: filter.operator === "isBetween" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
1600
+ lang: calendarPresentation().locale,
1601
+ weekStartsOn: calendarPresentation().weekStartsOn,
1602
+ formatters: calendarPresentation().formatters,
1603
+ labels: calendarPresentation().labels,
1517
1604
  "aria-label": `Select ${columnMeta?.label} date range`,
1518
1605
  autoFocus: true,
1519
1606
  captionLayout: "dropdown",
1520
1607
  mode: "range",
1521
1608
  selected: dateValue.length === 2 ? {
1522
- from: new Date(Number(dateValue[0])),
1523
- to: new Date(Number(dateValue[1]))
1609
+ from: parseCalendarDate(dateValue[0]),
1610
+ to: parseCalendarDate(dateValue[1])
1524
1611
  } : {
1525
1612
  from: /* @__PURE__ */ new Date(),
1526
1613
  to: /* @__PURE__ */ new Date()
1527
1614
  },
1528
1615
  onSelect: (date) => {
1529
- onFilterUpdate(filter.filterId, { value: date ? [(date.from?.getTime() ?? "").toString(), (date.to?.getTime() ?? "").toString()] : [] });
1616
+ onFilterUpdate(filter.filterId, { value: date ? [date.from ? serializeCalendarDate(date.from) : "", date.to ? serializeCalendarDate(date.to) : ""] : [] });
1530
1617
  }
1531
1618
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
1619
+ lang: calendarPresentation().locale,
1620
+ weekStartsOn: calendarPresentation().weekStartsOn,
1621
+ formatters: calendarPresentation().formatters,
1622
+ labels: calendarPresentation().labels,
1532
1623
  "aria-label": `Select ${columnMeta?.label} date`,
1533
1624
  autoFocus: true,
1534
1625
  captionLayout: "dropdown",
1535
1626
  mode: "single",
1536
- selected: dateValue[0] ? new Date(Number(dateValue[0])) : void 0,
1627
+ selected: dateValue[0] ? parseCalendarDate(dateValue[0]) : void 0,
1537
1628
  onSelect: (date) => {
1538
- onFilterUpdate(filter.filterId, { value: (date?.getTime() ?? "").toString() });
1629
+ onFilterUpdate(filter.filterId, { value: date ? serializeCalendarDate(date) : "" });
1539
1630
  setShowValueSelector(false);
1540
1631
  }
1541
1632
  })
@@ -1591,7 +1682,10 @@ function DataTableFilterMenu({ table, debounceMs = DEBOUNCE_MS$1, throttleMs = T
1591
1682
  setSelectedColumn(null);
1592
1683
  }
1593
1684
  }, [inputValue, selectedColumn]);
1594
- const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
1685
+ const [filters, setFilters] = useReadableFilters(columns, {
1686
+ debounceMs,
1687
+ resetPageKey: table.options.meta?.queryKeys?.page ?? "page"
1688
+ });
1595
1689
  const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
1596
1690
  const onFilterAdd = react.useCallback((column, value) => {
1597
1691
  if (!value.trim() && column.columnDef.meta?.variant !== "boolean") return;
@@ -1744,6 +1838,7 @@ function DataTableFilterMenu({ table, debounceMs = DEBOUNCE_MS$1, throttleMs = T
1744
1838
  }
1745
1839
  function DataTableFilterItem({ filter, filterItemId, columns, onFilterUpdate, onFilterRemove }) {
1746
1840
  {
1841
+ useDateFormatterVersion();
1747
1842
  const [showFieldSelector, setShowFieldSelector] = react.useState(false);
1748
1843
  const [showOperatorSelector, setShowOperatorSelector] = react.useState(false);
1749
1844
  const [showValueSelector, setShowValueSelector] = react.useState(false);
@@ -1854,6 +1949,7 @@ function DataTableFilterItem({ filter, filterItemId, columns, onFilterUpdate, on
1854
1949
  }
1855
1950
  }
1856
1951
  function FilterValueSelector({ column, value, onSelect }) {
1952
+ useDateFormatterVersion();
1857
1953
  switch (column.columnDef.meta?.variant ?? "text") {
1858
1954
  case "boolean": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.CommandGroup, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.CommandItem, {
1859
1955
  value: "true",
@@ -1882,11 +1978,15 @@ function FilterValueSelector({ column, value, onSelect }) {
1882
1978
  }, option.value)) });
1883
1979
  case "date":
1884
1980
  case "dateRange": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
1981
+ lang: calendarPresentation().locale,
1982
+ weekStartsOn: calendarPresentation().weekStartsOn,
1983
+ formatters: calendarPresentation().formatters,
1984
+ labels: calendarPresentation().labels,
1885
1985
  autoFocus: true,
1886
1986
  captionLayout: "dropdown",
1887
1987
  mode: "single",
1888
- selected: value ? new Date(value) : void 0,
1889
- onSelect: (date) => onSelect(date?.getTime().toString() ?? "")
1988
+ selected: parseCalendarDate(value),
1989
+ onSelect: (date) => onSelect(date ? serializeCalendarDate(date) : "")
1890
1990
  });
1891
1991
  default: {
1892
1992
  const isEmpty = !value.trim();
@@ -2040,10 +2140,10 @@ function onFilterInputRender({ filter, column, inputId, onFilterUpdate, showValu
2040
2140
  case "dateRange": {
2041
2141
  const inputListboxId = `${inputId}-listbox`;
2042
2142
  const dateValue = Array.isArray(filter.value) ? filter.value.filter(Boolean) : [filter.value, filter.value].filter(Boolean);
2043
- const startDate = dateValue[0] ? new Date(Number(dateValue[0])) : void 0;
2044
- const endDate = dateValue[1] ? new Date(Number(dateValue[1])) : void 0;
2143
+ const startDate = dateValue[0] ? parseCalendarDate(dateValue[0]) : void 0;
2144
+ const endDate = dateValue[1] ? parseCalendarDate(dateValue[1]) : void 0;
2045
2145
  const isSameDate = startDate && endDate && startDate.toDateString() === endDate.toDateString();
2046
- const displayValue = filter.operator === "isBetween" && dateValue.length === 2 && !isSameDate ? `${formatDate(startDate, { month: "short" })} - ${formatDate(endDate, { month: "short" })}` : startDate ? formatDate(startDate, { month: "short" }) : "Pick date...";
2146
+ const displayValue = filter.operator === "isBetween" && dateValue.length === 2 && !isSameDate ? `${calendarPresentation().formatDate(startDate)} - ${calendarPresentation().formatDate(endDate)}` : startDate ? calendarPresentation().formatDate(startDate) : "Pick date...";
2047
2147
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Popover, {
2048
2148
  open: showValueSelector,
2049
2149
  onOpenChange: setShowValueSelector,
@@ -2065,26 +2165,34 @@ function onFilterInputRender({ filter, column, inputId, onFilterUpdate, showValu
2065
2165
  align: "start",
2066
2166
  className: "w-auto p-0",
2067
2167
  children: filter.operator === "isBetween" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
2168
+ lang: calendarPresentation().locale,
2169
+ weekStartsOn: calendarPresentation().weekStartsOn,
2170
+ formatters: calendarPresentation().formatters,
2171
+ labels: calendarPresentation().labels,
2068
2172
  autoFocus: true,
2069
2173
  captionLayout: "dropdown",
2070
2174
  mode: "range",
2071
2175
  selected: dateValue.length === 2 ? {
2072
- from: new Date(Number(dateValue[0])),
2073
- to: new Date(Number(dateValue[1]))
2176
+ from: parseCalendarDate(dateValue[0]),
2177
+ to: parseCalendarDate(dateValue[1])
2074
2178
  } : {
2075
2179
  from: /* @__PURE__ */ new Date(),
2076
2180
  to: /* @__PURE__ */ new Date()
2077
2181
  },
2078
2182
  onSelect: (date) => {
2079
- onFilterUpdate(filter.filterId, { value: date ? [(date.from?.getTime() ?? "").toString(), (date.to?.getTime() ?? "").toString()] : [] });
2183
+ onFilterUpdate(filter.filterId, { value: date ? [date.from ? serializeCalendarDate(date.from) : "", date.to ? serializeCalendarDate(date.to) : ""] : [] });
2080
2184
  }
2081
2185
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
2186
+ lang: calendarPresentation().locale,
2187
+ weekStartsOn: calendarPresentation().weekStartsOn,
2188
+ formatters: calendarPresentation().formatters,
2189
+ labels: calendarPresentation().labels,
2082
2190
  autoFocus: true,
2083
2191
  captionLayout: "dropdown",
2084
2192
  mode: "single",
2085
- selected: dateValue[0] ? new Date(Number(dateValue[0])) : void 0,
2193
+ selected: dateValue[0] ? parseCalendarDate(dateValue[0]) : void 0,
2086
2194
  onSelect: (date) => {
2087
- onFilterUpdate(filter.filterId, { value: (date?.getTime() ?? "").toString() });
2195
+ onFilterUpdate(filter.filterId, { value: date ? serializeCalendarDate(date) : "" });
2088
2196
  }
2089
2197
  })
2090
2198
  })]
@@ -2397,6 +2505,7 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
2397
2505
  const queryStateOptions = table.options.meta?.queryStateOptions;
2398
2506
  const [queryFilters, setQueryFilters] = useReadableFilters(columns, {
2399
2507
  ...queryStateOptions,
2508
+ resetPageKey: table.options.meta?.queryKeys?.page ?? "page",
2400
2509
  shallow
2401
2510
  });
2402
2511
  const filters = externalFilters ?? queryFilters;
@@ -2832,11 +2941,17 @@ function SimpleSliderFilter({ title, range, unit, value, onChange }) {
2832
2941
  })] });
2833
2942
  }
2834
2943
  function SimpleDateFilter({ title, multiple, value, onChange }) {
2944
+ const [open, setOpen] = react.useState(false);
2945
+ const commit = (next) => {
2946
+ setOpen(false);
2947
+ onChange(next);
2948
+ };
2949
+ const formatterVersion = useDateFormatterVersion();
2950
+ const presentation = react.useMemo(() => calendarPresentation(), [formatterVersion]);
2951
+ const { formatDate: formatDate$1 } = presentation;
2835
2952
  const timestamps = Array.isArray(value) ? value : value ? [value] : [];
2836
- const fromTimestamp = timestamps[0] ? Number(timestamps[0]) : void 0;
2837
- const toTimestamp = timestamps[1] ? Number(timestamps[1]) : void 0;
2838
- const fromDate = fromTimestamp ? new Date(fromTimestamp) : void 0;
2839
- const toDate = toTimestamp ? new Date(toTimestamp) : void 0;
2953
+ const fromDate = parseCalendarDate(timestamps[0]);
2954
+ const toDate = parseCalendarDate(timestamps[1]);
2840
2955
  const [localRange, setLocalRange] = react.useState({
2841
2956
  from: void 0,
2842
2957
  to: void 0
@@ -2855,100 +2970,114 @@ function SimpleDateFilter({ title, multiple, value, onChange }) {
2855
2970
  const hasValue = multiple ? fromDate || toDate : fromDate;
2856
2971
  const onReset = (e) => {
2857
2972
  e.stopPropagation();
2858
- onChange(void 0);
2973
+ commit(void 0);
2859
2974
  };
2860
- const label = multiple ? fromDate && toDate ? `${formatDate(fromDate)} - ${formatDate(toDate)}` : fromDate ? formatDate(fromDate) : void 0 : fromDate ? formatDate(fromDate) : void 0;
2861
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.PopoverTrigger, {
2862
- asChild: true,
2863
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Button, {
2864
- variant: "outline",
2865
- size: "sm",
2866
- className: "border-dashed font-normal",
2867
- children: [
2868
- hasValue ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2869
- role: "button",
2870
- tabIndex: 0,
2871
- className: "rounded-sm opacity-70 hover:opacity-100",
2872
- onClick: onReset,
2873
- onKeyDown: (e) => {
2874
- if (e.key === "Enter" || e.key === " ") {
2875
- e.preventDefault();
2876
- onReset(e);
2975
+ const label = multiple ? fromDate && toDate ? `${formatDate$1(fromDate)} - ${formatDate$1(toDate)}` : fromDate ? formatDate$1(fromDate) : void 0 : fromDate ? formatDate$1(fromDate) : void 0;
2976
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Popover, {
2977
+ open,
2978
+ onOpenChange: setOpen,
2979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.PopoverTrigger, {
2980
+ asChild: true,
2981
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Button, {
2982
+ variant: "outline",
2983
+ size: "sm",
2984
+ className: "border-dashed font-normal",
2985
+ children: [
2986
+ hasValue ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2987
+ role: "button",
2988
+ tabIndex: 0,
2989
+ className: "rounded-sm opacity-70 hover:opacity-100",
2990
+ onClick: onReset,
2991
+ onKeyDown: (e) => {
2992
+ if (e.key === "Enter" || e.key === " ") {
2993
+ e.preventDefault();
2994
+ onReset(e);
2995
+ }
2996
+ },
2997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.XCircle, {})
2998
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarIcon, {}),
2999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: title }),
3000
+ label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Separator, {
3001
+ orientation: "vertical",
3002
+ className: "mx-0.5 h-4"
3003
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })] })
3004
+ ]
3005
+ })
3006
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.PopoverContent, {
3007
+ className: "w-auto p-0",
3008
+ align: "start",
3009
+ children: multiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
3010
+ lang: presentation.locale,
3011
+ weekStartsOn: presentation.weekStartsOn,
3012
+ formatters: presentation.formatters,
3013
+ labels: presentation.labels,
3014
+ defaultMonth: fromDate,
3015
+ autoFocus: true,
3016
+ captionLayout: "dropdown",
3017
+ mode: "range",
3018
+ selected: localRange,
3019
+ onSelect: (range) => {
3020
+ if (!range?.from && !range?.to) {
3021
+ commit(void 0);
3022
+ setLocalRange({
3023
+ from: void 0,
3024
+ to: void 0
3025
+ });
3026
+ return;
3027
+ }
3028
+ const hadFrom = Boolean(localRange.from);
3029
+ const hadTo = Boolean(localRange.to);
3030
+ const from = range.from?.getTime();
3031
+ const to = range.to?.getTime();
3032
+ if (from != null && to != null && from === to) {
3033
+ if (!hadFrom || hadTo) {
3034
+ setLocalRange({
3035
+ from: range.from,
3036
+ to: void 0
3037
+ });
3038
+ return;
2877
3039
  }
2878
- },
2879
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.XCircle, {})
2880
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarIcon, {}),
2881
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: title }),
2882
- label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Separator, {
2883
- orientation: "vertical",
2884
- className: "mx-0.5 h-4"
2885
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })] })
2886
- ]
2887
- })
2888
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.PopoverContent, {
2889
- className: "w-auto p-0",
2890
- align: "start",
2891
- children: multiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
2892
- autoFocus: true,
2893
- captionLayout: "dropdown",
2894
- mode: "range",
2895
- selected: localRange,
2896
- onSelect: (range) => {
2897
- if (!range?.from && !range?.to) {
2898
- onChange(void 0);
2899
- setLocalRange({
2900
- from: void 0,
2901
- to: void 0
2902
- });
2903
- return;
2904
- }
2905
- const hadFrom = Boolean(localRange.from);
2906
- const hadTo = Boolean(localRange.to);
2907
- const from = range.from?.getTime();
2908
- const to = range.to?.getTime();
2909
- if (from != null && to != null && from === to) {
2910
- if (!hadFrom || hadTo) {
3040
+ if (hadFrom && !hadTo && localRange.from) {
3041
+ const prevFrom = localRange.from;
3042
+ const newDate = range.from;
3043
+ const [finalFrom, finalTo] = prevFrom <= newDate ? [prevFrom, newDate] : [newDate, prevFrom];
3044
+ setLocalRange({
3045
+ from: finalFrom,
3046
+ to: finalTo
3047
+ });
3048
+ commit([serializeCalendarDate(finalFrom), serializeCalendarDate(finalTo)]);
3049
+ return;
3050
+ }
3051
+ }
3052
+ if (from != null && to != null && from !== to) {
2911
3053
  setLocalRange({
2912
3054
  from: range.from,
2913
- to: void 0
3055
+ to: range.to
2914
3056
  });
3057
+ commit([serializeCalendarDate(range.from), serializeCalendarDate(range.to)]);
2915
3058
  return;
2916
3059
  }
2917
- if (hadFrom && !hadTo && localRange.from) {
2918
- const prevFrom = localRange.from;
2919
- const newDate = range.from;
2920
- const [finalFrom, finalTo] = prevFrom <= newDate ? [prevFrom, newDate] : [newDate, prevFrom];
3060
+ if (range.from && !range.to) {
2921
3061
  setLocalRange({
2922
- from: finalFrom,
2923
- to: finalTo
3062
+ from: range.from,
3063
+ to: void 0
2924
3064
  });
2925
- onChange([finalFrom.getTime().toString(), finalTo.getTime().toString()]);
2926
3065
  return;
2927
3066
  }
2928
3067
  }
2929
- if (from != null && to != null && from !== to) {
2930
- setLocalRange({
2931
- from: range.from,
2932
- to: range.to
2933
- });
2934
- onChange([range.from.getTime().toString(), range.to.getTime().toString()]);
2935
- return;
2936
- }
2937
- if (range.from && !range.to) {
2938
- setLocalRange({
2939
- from: range.from,
2940
- to: void 0
2941
- });
2942
- return;
2943
- }
2944
- }
2945
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
2946
- captionLayout: "dropdown",
2947
- mode: "single",
2948
- selected: fromDate,
2949
- onSelect: (date) => onChange(date ? date.getTime().toString() : void 0)
2950
- })
2951
- })] });
3068
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
3069
+ lang: presentation.locale,
3070
+ weekStartsOn: presentation.weekStartsOn,
3071
+ formatters: presentation.formatters,
3072
+ labels: presentation.labels,
3073
+ defaultMonth: fromDate,
3074
+ captionLayout: "dropdown",
3075
+ mode: "single",
3076
+ selected: fromDate,
3077
+ onSelect: (date) => commit(date ? serializeCalendarDate(date) : void 0)
3078
+ })
3079
+ })]
3080
+ });
2952
3081
  }
2953
3082
 
2954
3083
  //#endregion
@@ -3956,7 +4085,8 @@ function useDataTable(props) {
3956
4085
  if (enableAdvancedFilter) return [];
3957
4086
  return Object.entries(filterValues).reduce((filters, [key, value]) => {
3958
4087
  if (value !== null) {
3959
- const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
4088
+ const variant = filterableColumns.find((column) => column.id === key)?.meta?.variant;
4089
+ const processedValue = Array.isArray(value) ? value : typeof value === "string" && (variant === "date" || variant === "dateRange") ? value.split(",").filter(Boolean) : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
3960
4090
  filters.push({
3961
4091
  id: key,
3962
4092
  value: processedValue
@@ -3964,7 +4094,11 @@ function useDataTable(props) {
3964
4094
  }
3965
4095
  return filters;
3966
4096
  }, []);
3967
- }, [filterValues, enableAdvancedFilter]);
4097
+ }, [
4098
+ filterValues,
4099
+ enableAdvancedFilter,
4100
+ filterableColumns
4101
+ ]);
3968
4102
  const [columnFilters, setColumnFilters] = react.useState(initialColumnFilters);
3969
4103
  const onColumnFiltersChange = react.useCallback((updaterOrValue) => {
3970
4104
  if (enableAdvancedFilter) return;
@@ -6118,13 +6252,6 @@ function mergeFieldOptions(current, incoming) {
6118
6252
  if ((!current || current.length === 0) && (!incoming || incoming.length === 0)) return;
6119
6253
  return mergeFilterOptions(current, incoming ?? []);
6120
6254
  }
6121
- function mergeOptionsByField(left, right) {
6122
- const keys = new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})]);
6123
- return Object.fromEntries(Array.from(keys).flatMap((key) => {
6124
- const options = mergeFieldOptions(left?.[key], right?.[key]);
6125
- return options ? [[key, options]] : [];
6126
- }));
6127
- }
6128
6255
  function shouldResolveOptions(field, config, hiddenColumns) {
6129
6256
  if (!config) return false;
6130
6257
  if (hiddenColumns.has(field)) return false;
@@ -6661,7 +6788,7 @@ function buildBatchUpdateFields(schema, batchFields, fields) {
6661
6788
  /**
6662
6789
  * 渲染字段值
6663
6790
  */
6664
- function renderFieldValue(value, type, booleanLocale, options, display = "auto") {
6791
+ function renderFieldValue(value, type, booleanLocale, options, display = "auto", maxItems = 5) {
6665
6792
  if (value === null || value === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6666
6793
  className: "text-muted-foreground",
6667
6794
  children: "-"
@@ -6676,12 +6803,12 @@ function renderFieldValue(value, type, booleanLocale, options, display = "auto")
6676
6803
  if (display === "badge" && (!options || options.length === 0)) {
6677
6804
  if (Array.isArray(value)) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6678
6805
  className: "flex gap-1 flex-wrap",
6679
- children: [value.slice(0, 5).map((item, index$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6806
+ children: [value.slice(0, maxItems).map((item, index$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6680
6807
  variant: "secondary",
6681
6808
  children: String(item)
6682
- }, index$1)), value.length > 5 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6809
+ }, index$1)), value.length > maxItems && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6683
6810
  variant: "outline",
6684
- children: ["+", value.length - 5]
6811
+ children: ["+", value.length - maxItems]
6685
6812
  })]
6686
6813
  });
6687
6814
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
@@ -6693,12 +6820,12 @@ function renderFieldValue(value, type, booleanLocale, options, display = "auto")
6693
6820
  if ((display === "badge" || display === "auto") && options && options.length > 0) {
6694
6821
  if (Array.isArray(value)) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6695
6822
  className: "flex gap-1 flex-wrap",
6696
- children: [value.slice(0, 5).map((v, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6823
+ children: [value.slice(0, maxItems).map((v, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6697
6824
  variant: "secondary",
6698
6825
  children: getOptionLabel(v, options)
6699
- }, i)), value.length > 5 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6826
+ }, i)), value.length > maxItems && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6700
6827
  variant: "outline",
6701
- children: ["+", value.length - 5]
6828
+ children: ["+", value.length - maxItems]
6702
6829
  })]
6703
6830
  });
6704
6831
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
@@ -6717,48 +6844,81 @@ function renderFieldValue(value, type, booleanLocale, options, display = "auto")
6717
6844
  });
6718
6845
  case "array": return Array.isArray(value) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6719
6846
  className: "flex gap-1 flex-wrap",
6720
- children: [value.slice(0, 5).map((v, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6847
+ children: [value.slice(0, maxItems).map((v, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Badge, {
6721
6848
  variant: "secondary",
6722
6849
  children: String(v)
6723
- }, i)), value.length > 5 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6850
+ }, i)), value.length > maxItems && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Badge, {
6724
6851
  variant: "outline",
6725
- children: ["+", value.length - 5]
6852
+ children: ["+", value.length - maxItems]
6726
6853
  })]
6727
6854
  }) : null;
6728
6855
  default: return String(value);
6729
6856
  }
6730
6857
  }
6731
- /**
6732
- * ViewModal 组件 - 详情查看弹窗
6733
- */
6734
- function ViewModal({ open, onOpenChange, variant, data, schema, fields: fieldConfig, dynamicOptions, denyFields, locale }) {
6735
- if (!data) return null;
6736
- const shape = schema.shape;
6737
- const denySet = new Set(denyFields ?? []);
6858
+ function ViewModal(props) {
6859
+ if (!props.open || !props.data) return null;
6860
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewModalContent, { ...props });
6861
+ }
6862
+ function ViewModalContent({ open, onOpenChange, variant, data, schema, fields: fieldConfig, dynamicOptions, denyFields, locale, tableOverrides, view }) {
6863
+ const excluded = react.useMemo(() => {
6864
+ const denied = new Set(denyFields ?? []);
6865
+ return Object.keys(schema.shape).filter((key) => {
6866
+ const config = fieldConfig?.[key];
6867
+ return denied.has(key) || config?.hidden || view?.overrides?.[key]?.hidden || isDefaultHiddenAuditField(key) && !isFieldTableExplicitlyVisible(config) && view?.overrides?.[key]?.hidden !== false;
6868
+ });
6869
+ }, [
6870
+ schema,
6871
+ fieldConfig,
6872
+ denyFields,
6873
+ view?.overrides
6874
+ ]);
6875
+ const rows = react.useMemo(() => data ? [data] : [], [data]);
6876
+ const resolvedOptions = useDynamicResolveOptions(fieldConfig, rows, excluded);
6738
6877
  const content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
6739
6878
  className: "grid gap-4 py-4",
6740
- children: Object.entries(shape).filter(([key]) => {
6741
- if (denySet.has(key)) return false;
6742
- const config = fieldConfig?.[key];
6743
- if (config?.hidden) return false;
6744
- if (isDefaultHiddenAuditField(key) && !isFieldTableExplicitlyVisible(config)) return false;
6745
- return true;
6746
- }).map(([key, fieldSchema]) => {
6747
- const parsed = parseZodField(fieldSchema);
6748
- const label = fieldConfig?.[key]?.label ?? humanize(key);
6749
- const value = data[key];
6750
- const options = normalizeFieldOptions(fieldConfig?.[key]?.enum) ?? normalizeFieldOptions(dynamicOptions?.[key]);
6751
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6752
- className: "grid grid-cols-3 items-start gap-4",
6753
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
6754
- className: "text-sm font-medium text-muted-foreground",
6755
- children: label
6756
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
6757
- className: "col-span-2 text-sm",
6758
- children: renderFieldValue(value, parsed.type, locale.boolean, options)
6759
- })]
6760
- }, key);
6761
- })
6879
+ children: (0, __tanstack_react_table.useReactTable)({
6880
+ data: rows,
6881
+ columns: react.useMemo(() => {
6882
+ const overrides = {};
6883
+ for (const [key, fieldSchema] of Object.entries(schema.shape).filter(([fieldKey]) => !excluded.includes(fieldKey))) {
6884
+ const config = fieldConfig?.[key] ?? {};
6885
+ const tableConfig = getTableConfig(config);
6886
+ const listOverride = tableOverrides?.[key];
6887
+ const options = getTableOptions(config) ?? resolvedOptions.optionsByField[key] ?? (tableConfig?.dataSource !== void 0 ? void 0 : dynamicOptions?.[key]);
6888
+ overrides[key] = {
6889
+ label: tableConfig?.label ?? config.label ?? listOverride?.label ?? humanize(key),
6890
+ index: tableConfig?.index ?? listOverride?.index,
6891
+ cell: ({ getValue }) => renderFieldValue(getValue(), parseZodField(fieldSchema).type, locale.boolean, options, tableConfig?.display ?? "auto", Infinity),
6892
+ ...view?.presentation === "table" ? listOverride : void 0,
6893
+ hidden: false,
6894
+ ...view?.overrides?.[key]
6895
+ };
6896
+ }
6897
+ return createTableSchema(schema, {
6898
+ overrides,
6899
+ exclude: excluded
6900
+ });
6901
+ }, [
6902
+ schema,
6903
+ excluded,
6904
+ fieldConfig,
6905
+ tableOverrides,
6906
+ view,
6907
+ resolvedOptions.optionsByField,
6908
+ dynamicOptions,
6909
+ locale.boolean
6910
+ ]),
6911
+ getCoreRowModel: (0, __tanstack_react_table.getCoreRowModel)()
6912
+ }).getRowModel().rows[0]?.getAllCells().map((cell) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6913
+ className: "grid grid-cols-3 items-start gap-4",
6914
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
6915
+ className: "text-sm font-medium text-muted-foreground",
6916
+ children: cell.column.columnDef.meta?.label ?? humanize(cell.column.id)
6917
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
6918
+ className: "col-span-2 min-w-0 whitespace-pre-wrap break-words text-sm",
6919
+ children: (0, __tanstack_react_table.flexRender)(cell.column.columnDef.cell, cell.getContext())
6920
+ })]
6921
+ }, cell.id))
6762
6922
  });
6763
6923
  if (variant === "sheet") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Sheet, {
6764
6924
  open,
@@ -6879,7 +7039,7 @@ function resolveActions(actionsOrFn, defaults, rowActionsLocale, context) {
6879
7039
  *
6880
7040
  * 高级 CRUD 表格组件,封装了完整的增删改查流程
6881
7041
  */
6882
- function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate }) {
7042
+ function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, view: viewConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate }) {
6883
7043
  const locale = resolveLocale(localeProp);
6884
7044
  const resolvedSchema = resource.schema ?? schema;
6885
7045
  const resolvedFields = react.useMemo(() => applyResourceTablePresentation(mergeFields(resource.fields, fields), resource.fields) ?? {}, [fields, resource.fields]);
@@ -6905,18 +7065,6 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6905
7065
  tableConfig?.overrides
6906
7066
  ]);
6907
7067
  const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
6908
- const sharedResolveOptions = useDynamicResolveOptions(react.useMemo(() => Object.fromEntries(Object.entries(resolvedFields).flatMap(([field, config]) => {
6909
- const table = getTableConfig(config);
6910
- if (table?.options === void 0 && table?.dataSource === void 0) return [];
6911
- const { table: _table,...sharedConfig } = config;
6912
- return [[field, sharedConfig]];
6913
- })), [resolvedFields]), resource.tableData.data, hiddenColumns);
6914
- const viewDynamicOptionsByField = react.useMemo(() => mergeOptionsByField(mergeOptionsByField(dynamicFilterOptions.labelOptionsByField, sharedResolveOptions.optionsByField), Object.fromEntries(Object.entries(dynamicResolveOptions.optionsByField).filter(([field]) => !getTableConfig(resolvedFields[field] ?? {})?.dataSource))), [
6915
- dynamicFilterOptions.labelOptionsByField,
6916
- dynamicResolveOptions.optionsByField,
6917
- sharedResolveOptions.optionsByField,
6918
- resolvedFields
6919
- ]);
6920
7068
  const resourceIdKey = resource.idKey ?? "id";
6921
7069
  const actionRegistryVersion = useCrudActionsVersion();
6922
7070
  const unifiedActions = react.useMemo(() => {
@@ -7248,9 +7396,11 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
7248
7396
  data: resource.modal.selected,
7249
7397
  schema: resolvedSchema,
7250
7398
  fields: resolvedFields,
7251
- dynamicOptions: viewDynamicOptionsByField,
7399
+ dynamicOptions: dynamicFilterOptions.labelOptionsByField,
7252
7400
  denyFields,
7253
- locale
7401
+ locale,
7402
+ tableOverrides,
7403
+ view: viewConfig
7254
7404
  }),
7255
7405
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.AlertDialog, {
7256
7406
  open: resource.modal.deleteOpen,
@@ -7447,10 +7597,7 @@ function getIsDateRange(value) {
7447
7597
  return value && typeof value === "object" && !Array.isArray(value);
7448
7598
  }
7449
7599
  function parseAsDate(timestamp) {
7450
- if (!timestamp) return void 0;
7451
- const numericTimestamp = typeof timestamp === "string" ? Number(timestamp) : timestamp;
7452
- const date = new Date(numericTimestamp);
7453
- return !Number.isNaN(date.getTime()) ? date : void 0;
7600
+ return parseCalendarDate(timestamp);
7454
7601
  }
7455
7602
  function parseColumnFilterValue(value) {
7456
7603
  if (value === null || value === void 0) return [];
@@ -7461,6 +7608,9 @@ function parseColumnFilterValue(value) {
7461
7608
  return [];
7462
7609
  }
7463
7610
  function DataTableDateFilter({ column, title, multiple }) {
7611
+ const formatterVersion = useDateFormatterVersion();
7612
+ const presentation = react.useMemo(() => calendarPresentation(), [formatterVersion]);
7613
+ const { formatDate: formatDate$1 } = presentation;
7464
7614
  const columnFilterValue = column.getFilterValue();
7465
7615
  const selectedDates = react.useMemo(() => {
7466
7616
  if (!columnFilterValue) return multiple ? {
@@ -7534,12 +7684,12 @@ function DataTableDateFilter({ column, title, multiple }) {
7534
7684
  from: nextFrom,
7535
7685
  to: nextTo
7536
7686
  });
7537
- if (nextTo) column.setFilterValue([nextFrom.getTime(), nextTo.getTime()]);
7687
+ if (nextTo) column.setFilterValue([serializeCalendarDate(nextFrom), serializeCalendarDate(nextTo)]);
7538
7688
  return;
7539
7689
  }
7540
7690
  setLocalRange(date);
7541
- if (from && to) column.setFilterValue([from, to]);
7542
- } else if (!multiple && "getTime" in date) column.setFilterValue(date.getTime());
7691
+ if (from && to) column.setFilterValue([serializeCalendarDate(new Date(from)), serializeCalendarDate(new Date(to))]);
7692
+ } else if (!multiple && "getTime" in date) column.setFilterValue(serializeCalendarDate(date));
7543
7693
  }, [
7544
7694
  column,
7545
7695
  multiple,
@@ -7559,9 +7709,9 @@ function DataTableDateFilter({ column, title, multiple }) {
7559
7709
  }, [multiple, selectedDates]);
7560
7710
  const formatDateRange = react.useCallback((range) => {
7561
7711
  if (!range.from && !range.to) return "";
7562
- if (range.from && range.to) return `${formatDate(range.from)} - ${formatDate(range.to)}`;
7563
- return formatDate(range.from ?? range.to);
7564
- }, []);
7712
+ if (range.from && range.to) return `${formatDate$1(range.from)} - ${formatDate$1(range.to)}`;
7713
+ return formatDate$1(range.from ?? range.to);
7714
+ }, [formatDate$1]);
7565
7715
  const label = react.useMemo(() => {
7566
7716
  if (multiple) {
7567
7717
  if (!getIsDateRange(selectedDates)) return null;
@@ -7577,7 +7727,7 @@ function DataTableDateFilter({ column, title, multiple }) {
7577
7727
  }
7578
7728
  if (getIsDateRange(selectedDates)) return null;
7579
7729
  const hasSelectedDate = selectedDates.length > 0;
7580
- const dateText = hasSelectedDate ? formatDate(selectedDates[0]) : "Select date";
7730
+ const dateText = hasSelectedDate ? formatDate$1(selectedDates[0]) : "Select date";
7581
7731
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7582
7732
  className: "flex items-center gap-2",
7583
7733
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: title }), hasSelectedDate && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Separator, {
@@ -7589,7 +7739,8 @@ function DataTableDateFilter({ column, title, multiple }) {
7589
7739
  selectedDates,
7590
7740
  multiple,
7591
7741
  formatDateRange,
7592
- title
7742
+ title,
7743
+ formatDate$1
7593
7744
  ]);
7594
7745
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.PopoverTrigger, {
7595
7746
  asChild: true,
@@ -7599,7 +7750,7 @@ function DataTableDateFilter({ column, title, multiple }) {
7599
7750
  className: "border-dashed font-normal",
7600
7751
  children: [hasValue ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7601
7752
  role: "button",
7602
- "aria-label": `Clear ${title} filter`,
7753
+ "aria-label": presentation.clearLabel(title),
7603
7754
  tabIndex: 0,
7604
7755
  onClick: onReset,
7605
7756
  className: "rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
@@ -7610,14 +7761,24 @@ function DataTableDateFilter({ column, title, multiple }) {
7610
7761
  className: "w-auto p-0",
7611
7762
  align: "start",
7612
7763
  children: multiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
7764
+ lang: presentation.locale,
7765
+ weekStartsOn: presentation.weekStartsOn,
7766
+ formatters: presentation.formatters,
7767
+ labels: presentation.labels,
7613
7768
  autoFocus: true,
7614
7769
  captionLayout: "dropdown",
7615
7770
  mode: "range",
7771
+ defaultMonth: localRange.from,
7616
7772
  selected: localRange,
7617
7773
  onSelect
7618
7774
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Calendar, {
7775
+ lang: presentation.locale,
7776
+ weekStartsOn: presentation.weekStartsOn,
7777
+ formatters: presentation.formatters,
7778
+ labels: presentation.labels,
7619
7779
  captionLayout: "dropdown",
7620
7780
  mode: "single",
7781
+ defaultMonth: !getIsDateRange(selectedDates) ? selectedDates[0] : void 0,
7621
7782
  selected: !getIsDateRange(selectedDates) ? selectedDates[0] : void 0,
7622
7783
  onSelect
7623
7784
  })