@object-ui/plugin-detail 3.1.2 → 3.1.3

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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +43 -41
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{AddressField-QBIlXCFl.js → AddressField-BtiTrEpf.js} +1 -1
  4. package/dist/{AvatarField-BEZuQTAH.js → AvatarField-CwlnWNSf.js} +7 -7
  5. package/dist/{BooleanField-doa93aFX.js → BooleanField-DpMXU2ya.js} +1 -1
  6. package/dist/{CodeField-jVV-hIXg.js → CodeField-gwmcFihg.js} +2 -2
  7. package/dist/{ColorField-B53qKQGW.js → ColorField-CWmF_zoW.js} +1 -1
  8. package/dist/{CurrencyField-og0NJ2ax.js → CurrencyField-BF3tYAgm.js} +1 -1
  9. package/dist/{DateField-BFx64AtG.js → DateField-a6Ka9ph2.js} +1 -1
  10. package/dist/{DateTimeField-Cxs2Rx2f.js → DateTimeField-C4wWOEiw.js} +1 -1
  11. package/dist/{EmailField-BfcpzRe7.js → EmailField-DJqiQ4sp.js} +1 -1
  12. package/dist/{FileField-KarqvhYm.js → FileField-ChjjCydz.js} +1 -1
  13. package/dist/{GeolocationField-B5SKZaqn.js → GeolocationField-BnkeUBek.js} +1 -1
  14. package/dist/{GridField-DOotrUTo.js → GridField-DoHqc2ON.js} +6 -6
  15. package/dist/{ImageField-Ddotp4u-.js → ImageField-Ld7SHA8N.js} +1 -1
  16. package/dist/{LocationField-tOkQaPIM.js → LocationField-Bgu-vMAE.js} +1 -1
  17. package/dist/{MasterDetailField-CpHw3nTE.js → MasterDetailField-Bp5WBTzU.js} +3 -3
  18. package/dist/{NumberField-CzBb2a28.js → NumberField-uBqVZ-gt.js} +1 -1
  19. package/dist/{ObjectField-BoL-JqE4.js → ObjectField-BH1Md9gH.js} +6 -6
  20. package/dist/{PasswordField-DrTzkYgj.js → PasswordField-D8GZjY7d.js} +1 -1
  21. package/dist/{PercentField-B9ZUQ3zE.js → PercentField-DyK8vg8M.js} +1 -1
  22. package/dist/{PhoneField-Bf9lhpdu.js → PhoneField-B3qJyLP0.js} +1 -1
  23. package/dist/{QRCodeField-PzMpdBKd.js → QRCodeField-CGiRTCZq.js} +1 -1
  24. package/dist/{RatingField-CeBMFe8o.js → RatingField-CWVaJNyf.js} +4 -4
  25. package/dist/{RichTextField-Ch7CHSQ0.js → RichTextField-CusveP9T.js} +1 -1
  26. package/dist/{SelectField-f5Nbi02x.js → SelectField-UdDfsEZo.js} +1 -1
  27. package/dist/{SignatureField-CpxTX2tR.js → SignatureField-DFvPKbuI.js} +1 -1
  28. package/dist/{SliderField-BoZtzgcr.js → SliderField-C-HvGV9e.js} +1 -1
  29. package/dist/{TextAreaField-rT1DLnV2.js → TextAreaField-C5KygUT3.js} +1 -1
  30. package/dist/{TextField-CflRxusu.js → TextField-oUjuqQ1x.js} +1 -1
  31. package/dist/{TimeField-DeVeCpRu.js → TimeField-SsQ6rfk5.js} +1 -1
  32. package/dist/{UrlField-UWKfhP9T.js → UrlField-kd48Ip95.js} +1 -1
  33. package/dist/{UserField-Cp2zQDjz.js → UserField-BOjE_CAz.js} +11 -11
  34. package/dist/{index-V_WBvcaA.js → index-D2t9pLAg.js} +56341 -56642
  35. package/dist/index.js +10 -10
  36. package/dist/index.umd.cjs +61 -63
  37. package/dist/plugin-detail.css +1 -1
  38. package/dist/src/DetailSection.d.ts +10 -0
  39. package/dist/src/DetailSection.d.ts.map +1 -1
  40. package/dist/src/HeaderHighlight.d.ts +2 -0
  41. package/dist/src/HeaderHighlight.d.ts.map +1 -1
  42. package/dist/src/RecordChatterPanel.d.ts +2 -0
  43. package/dist/src/RecordChatterPanel.d.ts.map +1 -1
  44. package/dist/src/autoLayout.d.ts +10 -3
  45. package/dist/src/autoLayout.d.ts.map +1 -1
  46. package/dist/src/index.d.ts +1 -1
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/package.json +7 -7
  49. package/src/DetailSection.tsx +40 -1
  50. package/src/DetailView.tsx +1 -1
  51. package/src/HeaderHighlight.tsx +22 -1
  52. package/src/RecordChatterPanel.tsx +6 -1
  53. package/src/RelatedList.tsx +1 -1
  54. package/src/__tests__/DetailSection.test.tsx +61 -0
  55. package/src/__tests__/HeaderHighlight.test.tsx +145 -0
  56. package/src/__tests__/RecordChatterPanel.test.tsx +38 -0
  57. package/src/__tests__/RelatedList.test.tsx +3 -3
  58. package/src/__tests__/autoLayout.test.ts +44 -0
  59. package/src/autoLayout.ts +25 -8
  60. package/src/index.tsx +1 -1
  61. package/dist/LookupField-DF36GvIP.js +0 -96
@@ -16,6 +16,14 @@ import * as React from 'react';
16
16
  * For columns>=3: md:col-span-{min(span,2)} lg:col-span-{min(span,3)}
17
17
  */
18
18
  export declare function getResponsiveSpanClass(span: number | undefined, columns: number): string;
19
+ export interface VirtualScrollOptions {
20
+ /** Enable virtual scrolling for large field sets */
21
+ enabled?: boolean;
22
+ /** Height of each field row in px (default: 60) */
23
+ itemHeight?: number;
24
+ /** Number of fields to render in the initial batch before revealing all (default: 20) */
25
+ batchSize?: number;
26
+ }
19
27
  export interface DetailSectionProps {
20
28
  section: DetailViewSectionType;
21
29
  data?: any;
@@ -28,6 +36,8 @@ export interface DetailSectionProps {
28
36
  isEditing?: boolean;
29
37
  /** Callback when a field value changes during inline editing */
30
38
  onFieldChange?: (field: string, value: any) => void;
39
+ /** Virtual scrolling configuration for sections with many fields */
40
+ virtualScroll?: VirtualScrollOptions;
31
41
  }
32
42
  export declare const DetailSection: React.FC<DetailSectionProps>;
33
43
  //# sourceMappingURL=DetailSection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DetailSection.d.ts","sourceRoot":"","sources":["../../src/DetailSection.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,iBAAiB,IAAI,qBAAqB,EAAkC,MAAM,kBAAkB,CAAC;AAKnH;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAYxF;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACrD;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAuOtD,CAAC"}
1
+ {"version":3,"file":"DetailSection.d.ts","sourceRoot":"","sources":["../../src/DetailSection.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,iBAAiB,IAAI,qBAAqB,EAAkC,MAAM,kBAAkB,CAAC;AAKnH;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAYxF;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IACpD,oEAAoE;IACpE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACtC;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmQtD,CAAC"}
@@ -13,6 +13,8 @@ export interface HeaderHighlightProps {
13
13
  className?: string;
14
14
  /** Object name for i18n field label resolution */
15
15
  objectName?: string;
16
+ /** Object schema for field metadata enrichment */
17
+ objectSchema?: any;
16
18
  }
17
19
  export declare const HeaderHighlight: React.FC<HeaderHighlightProps>;
18
20
  //# sourceMappingURL=HeaderHighlight.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HeaderHighlight.d.ts","sourceRoot":"","sources":["../../src/HeaderHighlight.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA6C1D,CAAC"}
1
+ {"version":3,"file":"HeaderHighlight.d.ts","sourceRoot":"","sources":["../../src/HeaderHighlight.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAIvD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,YAAY,CAAC,EAAE,GAAG,CAAC;CACpB;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA+D1D,CAAC"}
@@ -33,6 +33,8 @@ export interface RecordChatterPanelProps {
33
33
  filterMode?: FeedFilterMode;
34
34
  /** Called when filter changes */
35
35
  onFilterChange?: (mode: FeedFilterMode) => void;
36
+ /** When true, auto-collapse panel when there are no feed items */
37
+ collapseWhenEmpty?: boolean;
36
38
  className?: string;
37
39
  }
38
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"RecordChatterPanel.d.ts","sourceRoot":"","sources":["../../src/RecordChatterPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,2BAA2B,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,qDAAqD;IACrD,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,uCAAuC;IACvC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,yBAAyB;IACzB,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,4CAA4C;IAC5C,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,kBAAkB;IAClB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,iCAAiC;IACjC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAqJhE,CAAC"}
1
+ {"version":3,"file":"RecordChatterPanel.d.ts","sourceRoot":"","sources":["../../src/RecordChatterPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,2BAA2B,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,qDAAqD;IACrD,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,uCAAuC;IACvC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,yBAAyB;IACzB,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,4CAA4C;IAC5C,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,kBAAkB;IAClB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,iCAAiC;IACjC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAwJhE,CAAC"}
@@ -5,13 +5,19 @@ import { DetailViewField } from '../../types/src';
5
5
  export declare function isWideFieldType(type: string): boolean;
6
6
  /**
7
7
  * Infer optimal number of columns for a detail section based on field count.
8
+ * When containerWidth is provided, limits columns for narrower viewports.
8
9
  *
9
- * Rules:
10
+ * Rules (field-count based):
10
11
  * - 0-3 fields → 1 column
11
12
  * - 4-10 fields → 2 columns
12
13
  * - 11+ fields → 3 columns
14
+ *
15
+ * Responsive capping (when containerWidth is supplied):
16
+ * - containerWidth < 640px → max 1 column
17
+ * - containerWidth < 900px → max 2 columns
18
+ * - containerWidth >= 900px → no cap
13
19
  */
14
- export declare function inferDetailColumns(fieldCount: number): number;
20
+ export declare function inferDetailColumns(fieldCount: number, containerWidth?: number): number;
15
21
  /**
16
22
  * Apply auto span to wide fields so they span the full row.
17
23
  * Only sets span if the field does not already have one explicitly set.
@@ -25,9 +31,10 @@ export declare function applyAutoSpan(fields: DetailViewField[], columns: number
25
31
  *
26
32
  * @param fields - The section fields
27
33
  * @param schemaColumns - User-provided columns (from DetailViewSection or DetailViewSchema)
34
+ * @param containerWidth - Optional container width in px for responsive column capping
28
35
  * @returns Object with processed fields and inferred columns
29
36
  */
30
- export declare function applyDetailAutoLayout(fields: DetailViewField[], schemaColumns: number | undefined): {
37
+ export declare function applyDetailAutoLayout(fields: DetailViewField[], schemaColumns: number | undefined, containerWidth?: number): {
31
38
  fields: DetailViewField[];
32
39
  columns: number;
33
40
  };
@@ -1 +1 @@
1
- {"version":3,"file":"autoLayout.d.ts","sourceRoot":"","sources":["../../src/autoLayout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAgBxD;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EAAE,EACzB,OAAO,EAAE,MAAM,GACd,eAAe,EAAE,CAcnB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EAAE,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC;IAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAchD"}
1
+ {"version":3,"file":"autoLayout.d.ts","sourceRoot":"","sources":["../../src/autoLayout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAgBxD;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAatF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EAAE,EACzB,OAAO,EAAE,MAAM,GACd,eAAe,EAAE,CAcnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EAAE,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,cAAc,CAAC,EAAE,MAAM,GACtB;IAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAchD"}
@@ -25,7 +25,7 @@ export { SubscriptionToggle } from './SubscriptionToggle';
25
25
  export { ReactionPicker } from './ReactionPicker';
26
26
  export { ThreadedReplies } from './ThreadedReplies';
27
27
  export type { DetailViewProps } from './DetailView';
28
- export type { DetailSectionProps } from './DetailSection';
28
+ export type { DetailSectionProps, VirtualScrollOptions } from './DetailSection';
29
29
  export type { DetailTabsProps } from './DetailTabs';
30
30
  export type { RelatedListProps } from './RelatedList';
31
31
  export type { SectionGroupProps } from './SectionGroup';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-detail",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "DetailView plugin for Object UI - comprehensive detail page with sections, tabs, and related lists",
@@ -24,12 +24,12 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "lucide-react": "^0.576.0",
28
- "@object-ui/components": "3.1.2",
29
- "@object-ui/core": "3.1.2",
30
- "@object-ui/fields": "3.1.2",
31
- "@object-ui/types": "3.1.2",
32
- "@object-ui/react": "3.1.2"
27
+ "lucide-react": "^0.577.0",
28
+ "@object-ui/components": "3.1.3",
29
+ "@object-ui/core": "3.1.3",
30
+ "@object-ui/fields": "3.1.3",
31
+ "@object-ui/react": "3.1.3",
32
+ "@object-ui/types": "3.1.3"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",
@@ -53,6 +53,15 @@ export function getResponsiveSpanClass(span: number | undefined, columns: number
53
53
  return '';
54
54
  }
55
55
 
56
+ export interface VirtualScrollOptions {
57
+ /** Enable virtual scrolling for large field sets */
58
+ enabled?: boolean;
59
+ /** Height of each field row in px (default: 60) */
60
+ itemHeight?: number;
61
+ /** Number of fields to render in the initial batch before revealing all (default: 20) */
62
+ batchSize?: number;
63
+ }
64
+
56
65
  export interface DetailSectionProps {
57
66
  section: DetailViewSectionType;
58
67
  data?: any;
@@ -65,6 +74,8 @@ export interface DetailSectionProps {
65
74
  isEditing?: boolean;
66
75
  /** Callback when a field value changes during inline editing */
67
76
  onFieldChange?: (field: string, value: any) => void;
77
+ /** Virtual scrolling configuration for sections with many fields */
78
+ virtualScroll?: VirtualScrollOptions;
68
79
  }
69
80
 
70
81
  export const DetailSection: React.FC<DetailSectionProps> = ({
@@ -75,9 +86,11 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
75
86
  objectName,
76
87
  isEditing = false,
77
88
  onFieldChange,
89
+ virtualScroll,
78
90
  }) => {
79
91
  const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
80
92
  const [copiedField, setCopiedField] = React.useState<string | null>(null);
93
+ const [visibleCount, setVisibleCount] = React.useState<number | undefined>(undefined);
81
94
  const { t } = useDetailTranslation();
82
95
  const { fieldLabel } = useSafeFieldLabel();
83
96
 
@@ -213,6 +226,32 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
213
226
  );
214
227
  };
215
228
 
229
+ // Virtual scroll: progressive batch rendering for large field sets
230
+ const vsEnabled = virtualScroll?.enabled === true;
231
+ const vsBatchSize = virtualScroll?.batchSize ?? 20;
232
+ /** Delay (ms) before revealing remaining fields after the initial batch */
233
+ const VS_REVEAL_DELAY = 100;
234
+
235
+ React.useEffect(() => {
236
+ if (!vsEnabled) {
237
+ setVisibleCount(undefined);
238
+ return;
239
+ }
240
+ // Start with a batch, then progressively reveal more
241
+ if (layoutFields.length <= vsBatchSize) {
242
+ setVisibleCount(undefined);
243
+ return;
244
+ }
245
+ setVisibleCount(vsBatchSize);
246
+ const timer = setTimeout(() => setVisibleCount(undefined), VS_REVEAL_DELAY);
247
+ return () => clearTimeout(timer);
248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
249
+ }, [vsEnabled, layoutFields.length, vsBatchSize]);
250
+
251
+ const renderedFields = visibleCount !== undefined
252
+ ? layoutFields.slice(0, visibleCount)
253
+ : layoutFields;
254
+
216
255
  const content = (
217
256
  <div
218
257
  className={cn(
@@ -223,7 +262,7 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
223
262
  "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
224
263
  )}
225
264
  >
226
- {layoutFields.map(renderField)}
265
+ {renderedFields.map(renderField)}
227
266
  </div>
228
267
  );
229
268
 
@@ -600,7 +600,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
600
600
 
601
601
  {/* Header Highlight Area */}
602
602
  {schema.highlightFields && schema.highlightFields.length > 0 && (
603
- <HeaderHighlight fields={schema.highlightFields} data={data} objectName={schema.objectName} />
603
+ <HeaderHighlight fields={schema.highlightFields} data={data} objectName={schema.objectName} objectSchema={objectSchema} />
604
604
  )}
605
605
 
606
606
  {/* Auto Tabs mode: wrap sections, related, activity into tabs */}
@@ -9,6 +9,7 @@
9
9
  import * as React from 'react';
10
10
  import { cn, Card, CardContent } from '@object-ui/components';
11
11
  import type { HighlightField } from '@object-ui/types';
12
+ import { getCellRenderer } from '@object-ui/fields';
12
13
  import { useSafeFieldLabel } from '@object-ui/react';
13
14
 
14
15
  export interface HeaderHighlightProps {
@@ -17,6 +18,8 @@ export interface HeaderHighlightProps {
17
18
  className?: string;
18
19
  /** Object name for i18n field label resolution */
19
20
  objectName?: string;
21
+ /** Object schema for field metadata enrichment */
22
+ objectSchema?: any;
20
23
  }
21
24
 
22
25
  export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
@@ -24,6 +27,7 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
24
27
  data,
25
28
  className,
26
29
  objectName,
30
+ objectSchema,
27
31
  }) => {
28
32
  const { fieldLabel } = useSafeFieldLabel();
29
33
  if (!fields.length || !data) return null;
@@ -48,6 +52,23 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
48
52
  )}>
49
53
  {visibleFields.map((field) => {
50
54
  const value = data[field.name];
55
+ // Enrich field metadata from objectSchema
56
+ const objectDefField = objectSchema?.fields?.[field.name];
57
+ const resolvedType = field.type || objectDefField?.type;
58
+ const enrichedField = {
59
+ name: field.name,
60
+ label: field.label,
61
+ type: resolvedType || 'text',
62
+ ...(objectDefField?.options && { options: objectDefField.options }),
63
+ ...(objectDefField?.currency && { currency: objectDefField.currency }),
64
+ ...(objectDefField?.precision !== undefined && { precision: objectDefField.precision }),
65
+ ...(objectDefField?.format && { format: objectDefField.format }),
66
+ };
67
+
68
+ // Use type-aware cell renderer — all renderers coerce values via
69
+ // coerceToSafeValue() so even object/array data is safe (no error #310).
70
+ const CellRenderer = getCellRenderer(resolvedType || 'text');
71
+
51
72
  return (
52
73
  <div key={field.name} className="flex flex-col gap-0.5">
53
74
  <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
@@ -55,7 +76,7 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
55
76
  {fieldLabel(objectName || '', field.name, field.label)}
56
77
  </span>
57
78
  <span className="text-sm font-semibold truncate">
58
- {String(value)}
79
+ <CellRenderer value={value} field={enrichedField as any} />
59
80
  </span>
60
81
  </div>
61
82
  );
@@ -38,6 +38,8 @@ export interface RecordChatterPanelProps {
38
38
  filterMode?: FeedFilterMode;
39
39
  /** Called when filter changes */
40
40
  onFilterChange?: (mode: FeedFilterMode) => void;
41
+ /** When true, auto-collapse panel when there are no feed items */
42
+ collapseWhenEmpty?: boolean;
41
43
  className?: string;
42
44
  }
43
45
 
@@ -63,12 +65,13 @@ export const RecordChatterPanel: React.FC<RecordChatterPanelProps> = ({
63
65
  onToggleSubscription,
64
66
  filterMode,
65
67
  onFilterChange,
68
+ collapseWhenEmpty = false,
66
69
  className,
67
70
  }) => {
68
71
  const position = config?.position ?? 'right';
69
72
  const width = config?.width ?? '360px';
70
73
  const collapsible = config?.collapsible ?? true;
71
- const defaultCollapsed = config?.defaultCollapsed ?? false;
74
+ const defaultCollapsed = (collapseWhenEmpty && items.length === 0) || (config?.defaultCollapsed ?? false);
72
75
 
73
76
  const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
74
77
 
@@ -142,6 +145,7 @@ export const RecordChatterPanel: React.FC<RecordChatterPanelProps> = ({
142
145
  onToggleSubscription={onToggleSubscription}
143
146
  filterMode={filterMode}
144
147
  onFilterChange={onFilterChange}
148
+ collapseWhenEmpty={collapseWhenEmpty}
145
149
  className="border-0 shadow-none"
146
150
  />
147
151
  </div>
@@ -194,6 +198,7 @@ export const RecordChatterPanel: React.FC<RecordChatterPanelProps> = ({
194
198
  onToggleSubscription={onToggleSubscription}
195
199
  filterMode={filterMode}
196
200
  onFilterChange={onFilterChange}
201
+ collapseWhenEmpty={collapseWhenEmpty}
197
202
  />
198
203
  </div>
199
204
  )}
@@ -197,7 +197,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
197
197
  if (!objectSchema?.fields) return [];
198
198
  const resolvedObjectName = objectName || api || '';
199
199
  return Object.entries(objectSchema.fields)
200
- .filter(([key]) => !key.startsWith('_'))
200
+ .filter(([key]) => !key.startsWith('_') && key !== 'id')
201
201
  .map(([key, def]: [string, any]) => {
202
202
  const col: any = {
203
203
  accessorKey: key,
@@ -391,6 +391,67 @@ describe('DetailSection', () => {
391
391
  });
392
392
  });
393
393
  });
394
+
395
+ it('should initially render a batch when virtualScroll is enabled with many fields', () => {
396
+ const section = {
397
+ title: 'Virtual',
398
+ fields: Array.from({ length: 50 }, (_, i) => ({
399
+ name: `field_${i}`,
400
+ label: `Field ${i}`,
401
+ type: 'text',
402
+ })),
403
+ };
404
+ const { container } = render(
405
+ <DetailSection
406
+ section={section}
407
+ data={{}}
408
+ virtualScroll={{ enabled: true, batchSize: 10 }}
409
+ />
410
+ );
411
+ const grid = container.querySelector('.grid');
412
+ expect(grid).toBeTruthy();
413
+ // Initially should render only the batch (10 fields), not all 50
414
+ const fieldElements = grid!.children;
415
+ expect(fieldElements.length).toBeLessThanOrEqual(10);
416
+ });
417
+
418
+ it('should render all fields when virtualScroll is disabled', () => {
419
+ const section = {
420
+ title: 'No Virtual',
421
+ fields: Array.from({ length: 50 }, (_, i) => ({
422
+ name: `field_${i}`,
423
+ label: `Field ${i}`,
424
+ type: 'text',
425
+ })),
426
+ };
427
+ const { container } = render(
428
+ <DetailSection section={section} data={{}} />
429
+ );
430
+ const grid = container.querySelector('.grid');
431
+ expect(grid).toBeTruthy();
432
+ expect(grid!.children.length).toBe(50);
433
+ });
434
+
435
+ it('should render all fields when virtualScroll is enabled but field count is below batch size', () => {
436
+ const section = {
437
+ title: 'Small',
438
+ fields: Array.from({ length: 5 }, (_, i) => ({
439
+ name: `field_${i}`,
440
+ label: `Field ${i}`,
441
+ type: 'text',
442
+ })),
443
+ };
444
+ const { container } = render(
445
+ <DetailSection
446
+ section={section}
447
+ data={{}}
448
+ virtualScroll={{ enabled: true, batchSize: 20 }}
449
+ />
450
+ );
451
+ const grid = container.querySelector('.grid');
452
+ expect(grid).toBeTruthy();
453
+ expect(grid!.children.length).toBe(5);
454
+ });
394
455
  });
395
456
 
396
457
  describe('getResponsiveSpanClass', () => {
@@ -65,4 +65,149 @@ describe('HeaderHighlight', () => {
65
65
  render(<HeaderHighlight fields={fieldsWithIcon} data={{ revenue: '$5M' }} />);
66
66
  expect(screen.getByText('💰')).toBeInTheDocument();
67
67
  });
68
+
69
+ it('should render currency fields with formatted value via CellRenderer', () => {
70
+ const currencyFields: HighlightField[] = [
71
+ { name: 'amount', label: 'Amount', type: 'currency' },
72
+ ];
73
+ render(<HeaderHighlight fields={currencyFields} data={{ amount: 250000 }} />);
74
+ // CurrencyCellRenderer should format — should NOT show raw "250000"
75
+ expect(screen.queryByText('250000')).not.toBeInTheDocument();
76
+ expect(screen.getByText(/250,000/)).toBeInTheDocument();
77
+ });
78
+
79
+ it('should render select fields as badge via CellRenderer', () => {
80
+ const selectFields: HighlightField[] = [
81
+ { name: 'stage', label: 'Stage', type: 'select' },
82
+ ];
83
+ render(
84
+ <HeaderHighlight
85
+ fields={selectFields}
86
+ data={{ stage: 'prospecting' }}
87
+ objectSchema={{
88
+ fields: {
89
+ stage: {
90
+ type: 'select',
91
+ options: [
92
+ { value: 'prospecting', label: 'Prospecting', color: 'blue' },
93
+ ],
94
+ },
95
+ },
96
+ }}
97
+ />
98
+ );
99
+ expect(screen.getByText('Prospecting')).toBeInTheDocument();
100
+ });
101
+
102
+ it('should enrich field type from objectSchema when field.type is not set', () => {
103
+ const fieldsNoType: HighlightField[] = [
104
+ { name: 'amount', label: 'Amount' },
105
+ ];
106
+ render(
107
+ <HeaderHighlight
108
+ fields={fieldsNoType}
109
+ data={{ amount: 5000 }}
110
+ objectSchema={{
111
+ fields: {
112
+ amount: { type: 'currency', currency: 'USD' },
113
+ },
114
+ }}
115
+ />
116
+ );
117
+ // CurrencyCellRenderer should format, not raw String()
118
+ expect(screen.queryByText('5000')).not.toBeInTheDocument();
119
+ expect(screen.getByText(/5,000/)).toBeInTheDocument();
120
+ });
121
+
122
+ it('should fall back to text when no type info is available', () => {
123
+ const fieldsNoType: HighlightField[] = [
124
+ { name: 'custom', label: 'Custom' },
125
+ ];
126
+ render(<HeaderHighlight fields={fieldsNoType} data={{ custom: 'raw-value' }} />);
127
+ expect(screen.getByText('raw-value')).toBeInTheDocument();
128
+ });
129
+
130
+ it('should safely render object values without crashing (React error #310 guard)', () => {
131
+ // Simulates MongoDB Decimal128 or expanded reference objects
132
+ const fieldsWithNumber: HighlightField[] = [
133
+ { name: 'amount', label: 'Amount' },
134
+ { name: 'account', label: 'Account' },
135
+ ];
136
+ const objectData = {
137
+ amount: { $numberDecimal: '250000' },
138
+ account: { id: 'abc', name: 'Acme Corp' },
139
+ };
140
+ const objectSchema = {
141
+ fields: {
142
+ amount: { type: 'number' },
143
+ account: { type: 'text' },
144
+ },
145
+ };
146
+ // Should NOT crash — cell renderers coerce values via coerceToSafeValue
147
+ const { container } = render(
148
+ <HeaderHighlight
149
+ fields={fieldsWithNumber}
150
+ data={objectData}
151
+ objectSchema={objectSchema}
152
+ />
153
+ );
154
+ expect(container.innerHTML).not.toBe('');
155
+ // NumberCellRenderer coerces $numberDecimal to number
156
+ expect(screen.getByText('250,000')).toBeInTheDocument();
157
+ // TextCellRenderer extracts name from object
158
+ expect(screen.getByText('Acme Corp')).toBeInTheDocument();
159
+ });
160
+
161
+ it('should safely render lookup object values via LookupCellRenderer', () => {
162
+ const lookupFields: HighlightField[] = [
163
+ { name: 'owner', label: 'Owner' },
164
+ ];
165
+ const lookupData = {
166
+ owner: { id: 'u1', name: 'Jane Doe' },
167
+ };
168
+ const objectSchema = {
169
+ fields: {
170
+ owner: { type: 'lookup' },
171
+ },
172
+ };
173
+ // LookupCellRenderer handles object values natively
174
+ render(
175
+ <HeaderHighlight
176
+ fields={lookupFields}
177
+ data={lookupData}
178
+ objectSchema={objectSchema}
179
+ />
180
+ );
181
+ expect(screen.getByText('Jane Doe')).toBeInTheDocument();
182
+ });
183
+
184
+ it('should safely render array values via TextCellRenderer', () => {
185
+ const arrayFields: HighlightField[] = [
186
+ { name: 'tags', label: 'Tags' },
187
+ ];
188
+ const arrayData = {
189
+ tags: ['urgent', 'follow-up'],
190
+ };
191
+ const { container } = render(
192
+ <HeaderHighlight fields={arrayFields} data={arrayData} />
193
+ );
194
+ expect(container.innerHTML).not.toBe('');
195
+ // TextCellRenderer coerces arrays to comma-separated string
196
+ expect(screen.getByText('urgent, follow-up')).toBeInTheDocument();
197
+ });
198
+
199
+ it('should safely render array of objects via TextCellRenderer', () => {
200
+ const arrayFields: HighlightField[] = [
201
+ { name: 'contacts', label: 'Contacts' },
202
+ ];
203
+ const arrayData = {
204
+ contacts: [{ name: 'Alice' }, { name: 'Bob' }],
205
+ };
206
+ const { container } = render(
207
+ <HeaderHighlight fields={arrayFields} data={arrayData} />
208
+ );
209
+ expect(container.innerHTML).not.toBe('');
210
+ // TextCellRenderer coerces array of objects to "Alice, Bob"
211
+ expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
212
+ });
68
213
  });
@@ -224,4 +224,42 @@ describe('RecordChatterPanel', () => {
224
224
  expect(screen.getByLabelText('Show discussion')).toBeInTheDocument();
225
225
  });
226
226
  });
227
+
228
+ describe('collapseWhenEmpty', () => {
229
+ it('should auto-collapse when empty and collapseWhenEmpty is true (inline mode)', () => {
230
+ render(
231
+ <RecordChatterPanel
232
+ config={{ position: 'bottom', collapsible: true }}
233
+ collapseWhenEmpty
234
+ items={[]}
235
+ />,
236
+ );
237
+ // Should be collapsed because items is empty
238
+ expect(screen.getByLabelText('Show discussion')).toBeInTheDocument();
239
+ });
240
+
241
+ it('should not auto-collapse when items exist and collapseWhenEmpty is true', () => {
242
+ render(
243
+ <RecordChatterPanel
244
+ config={{ position: 'bottom', collapsible: true }}
245
+ collapseWhenEmpty
246
+ items={mockItems}
247
+ />,
248
+ );
249
+ // Should be expanded because there are items
250
+ expect(screen.getByText('Activity')).toBeInTheDocument();
251
+ });
252
+
253
+ it('should auto-collapse sidebar when empty and collapseWhenEmpty is true', () => {
254
+ render(
255
+ <RecordChatterPanel
256
+ config={{ position: 'right', collapsible: true }}
257
+ collapseWhenEmpty
258
+ items={[]}
259
+ />,
260
+ );
261
+ // Should be collapsed
262
+ expect(screen.getByLabelText('Open discussion panel')).toBeInTheDocument();
263
+ });
264
+ });
227
265
  });
@@ -71,7 +71,7 @@ describe('RelatedList', () => {
71
71
  fields: {
72
72
  product: { type: 'string', label: 'Product' },
73
73
  quantity: { type: 'number', label: 'Quantity' },
74
- _id: { type: 'string', label: 'ID' },
74
+ id: { type: 'string', label: 'ID' },
75
75
  },
76
76
  }),
77
77
  find: vi.fn(),
@@ -92,12 +92,12 @@ describe('RelatedList', () => {
92
92
  expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('order_item');
93
93
  });
94
94
 
95
- // Verify columns are generated from schema (excluding _id)
95
+ // Verify columns are generated from schema (excluding id)
96
96
  await waitFor(() => {
97
97
  expect(screen.getByText('Product')).toBeInTheDocument();
98
98
  expect(screen.getByText('Quantity')).toBeInTheDocument();
99
99
  });
100
- // _id should be filtered out
100
+ // id should be filtered out
101
101
  expect(screen.queryByText('ID')).not.toBeInTheDocument();
102
102
  });
103
103