@object-ui/plugin-grid 0.3.1 → 2.0.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 +30 -0
- package/CHANGELOG.md +15 -0
- package/README.md +97 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1117 -303
- package/dist/index.umd.cjs +5 -2
- package/dist/packages/plugin-grid/src/ListColumnExtensions.test.d.ts +0 -0
- package/dist/packages/plugin-grid/src/ListColumnSchema.test.d.ts +1 -0
- package/dist/{plugin-grid → packages/plugin-grid}/src/ObjectGrid.d.ts +7 -1
- package/dist/packages/plugin-grid/src/ObjectGrid.msw.test.d.ts +0 -0
- package/dist/packages/plugin-grid/src/VirtualGrid.d.ts +35 -0
- package/dist/packages/plugin-grid/src/VirtualGrid.test.d.ts +8 -0
- package/dist/packages/plugin-grid/src/__tests__/VirtualGrid.test.d.ts +0 -0
- package/dist/packages/plugin-grid/src/index.d.ts +10 -0
- package/dist/packages/plugin-grid/src/index.test.d.ts +1 -0
- package/package.json +11 -8
- package/src/ListColumnExtensions.test.tsx +374 -0
- package/src/ListColumnSchema.test.ts +88 -0
- package/src/ObjectGrid.msw.test.tsx +130 -0
- package/src/ObjectGrid.tsx +341 -117
- package/src/VirtualGrid.test.tsx +23 -0
- package/src/VirtualGrid.tsx +183 -0
- package/src/__tests__/VirtualGrid.test.tsx +438 -0
- package/src/index.test.tsx +29 -0
- package/src/index.tsx +33 -5
- package/vite.config.ts +18 -0
- package/vitest.config.ts +13 -0
- package/vitest.setup.ts +1 -0
- package/dist/plugin-grid/src/index.d.ts +0 -3
package/dist/index.umd.cjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(A,R){typeof exports=="object"&&typeof module<"u"?R(exports,require("react"),require("@object-ui/core"),require("@object-ui/react"),require("@object-ui/fields"),require("@object-ui/components"),require("lucide-react"),require("react-dom")):typeof define=="function"&&define.amd?define(["exports","react","@object-ui/core","@object-ui/react","@object-ui/fields","@object-ui/components","lucide-react","react-dom"],R):(A=typeof globalThis<"u"?globalThis:A||self,R(A.ObjectUIPluginGrid={},A.React,A.core,A.react,A.fields,A.components,A.lucideReact,A.ReactDOM))})(this,(function(A,R,he,B,me,q,de,Ce){"use strict";function _e(n){const a=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const e in n)if(e!=="default"){const s=Object.getOwnPropertyDescriptor(n,e);Object.defineProperty(a,e,s.get?s:{enumerable:!0,get:()=>n[e]})}}return a.default=n,Object.freeze(a)}const le=_e(R);var ae={exports:{}},Q={};var pe;function Ae(){if(pe)return Q;pe=1;var n=Symbol.for("react.transitional.element"),a=Symbol.for("react.fragment");function e(s,t,i){var o=null;if(i!==void 0&&(o=""+i),t.key!==void 0&&(o=""+t.key),"key"in t){i={};for(var l in t)l!=="key"&&(i[l]=t[l])}else i=t;return t=i.ref,{$$typeof:n,type:s,key:o,ref:t!==void 0?t:null,props:i}}return Q.Fragment=a,Q.jsx=e,Q.jsxs=e,Q}var ee={};var ge;function Re(){return ge||(ge=1,process.env.NODE_ENV!=="production"&&(function(){function n(r){if(r==null)return null;if(typeof r=="function")return r.$$typeof===U?null:r.displayName||r.name||null;if(typeof r=="string")return r;switch(r){case w:return"Fragment";case O:return"Profiler";case z:return"StrictMode";case Y:return"Suspense";case $:return"SuspenseList";case J:return"Activity"}if(typeof r=="object")switch(typeof r.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),r.$$typeof){case v:return"Portal";case M:return r.displayName||"Context";case F:return(r._context.displayName||"Context")+".Consumer";case C:var h=r.render;return r=r.displayName,r||(r=h.displayName||h.name||"",r=r!==""?"ForwardRef("+r+")":"ForwardRef"),r;case L:return h=r.displayName||null,h!==null?h:n(r.type)||"Memo";case I:h=r._payload,r=r._init;try{return n(r(h))}catch{}}return null}function a(r){return""+r}function e(r){try{a(r);var h=!1}catch{h=!0}if(h){h=console;var y=h.error,S=typeof Symbol=="function"&&Symbol.toStringTag&&r[Symbol.toStringTag]||r.constructor.name||"Object";return y.call(h,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",S),a(r)}}function s(r){if(r===w)return"<>";if(typeof r=="object"&&r!==null&&r.$$typeof===I)return"<...>";try{var h=n(r);return h?"<"+h+">":"<...>"}catch{return"<...>"}}function t(){var r=K.A;return r===null?null:r.getOwner()}function i(){return Error("react-stack-top-frame")}function o(r){if(te.call(r,"key")){var h=Object.getOwnPropertyDescriptor(r,"key").get;if(h&&h.isReactWarning)return!1}return r.key!==void 0}function l(r,h){function y(){ce||(ce=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",h))}y.isReactWarning=!0,Object.defineProperty(r,"key",{get:y,configurable:!0})}function p(){var r=n(this.type);return ne[r]||(ne[r]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),r=this.props.ref,r!==void 0?r:null}function g(r,h,y,S,G,Z){var f=y.ref;return r={$$typeof:_,type:r,key:h,props:y,_owner:S},(f!==void 0?f:null)!==null?Object.defineProperty(r,"ref",{enumerable:!1,get:p}):Object.defineProperty(r,"ref",{enumerable:!1,value:null}),r._store={},Object.defineProperty(r._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(r,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(r,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:G}),Object.defineProperty(r,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:Z}),Object.freeze&&(Object.freeze(r.props),Object.freeze(r)),r}function m(r,h,y,S,G,Z){var f=h.children;if(f!==void 0)if(S)if(D(f)){for(S=0;S<f.length;S++)E(f[S]);Object.freeze&&Object.freeze(f)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else E(f);if(te.call(h,"key")){f=n(r);var x=Object.keys(h).filter(function(j){return j!=="key"});S=0<x.length?"{key: someKey, "+x.join(": ..., ")+": ...}":"{key: someKey}",ue[f+S]||(x=0<x.length?"{"+x.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
|
|
2
2
|
let props = %s;
|
|
3
3
|
<%s {...props} />
|
|
4
4
|
React keys must be passed directly to JSX without using spread:
|
|
5
5
|
let props = %s;
|
|
6
|
-
<%s key={someKey} {...props} />`,
|
|
6
|
+
<%s key={someKey} {...props} />`,S,f,x,f),ue[f+S]=!0)}if(f=null,y!==void 0&&(e(y),f=""+y),o(h)&&(e(h.key),f=""+h.key),"key"in h){y={};for(var N in h)N!=="key"&&(y[N]=h[N])}else y=h;return f&&l(y,typeof r=="function"?r.displayName||r.name||"Unknown":r),g(r,f,y,t(),G,Z)}function E(r){u(r)?r._store&&(r._store.validated=1):typeof r=="object"&&r!==null&&r.$$typeof===I&&(r._payload.status==="fulfilled"?u(r._payload.value)&&r._payload.value._store&&(r._payload.value._store.validated=1):r._store&&(r._store.validated=1))}function u(r){return typeof r=="object"&&r!==null&&r.$$typeof===_}var b=R,_=Symbol.for("react.transitional.element"),v=Symbol.for("react.portal"),w=Symbol.for("react.fragment"),z=Symbol.for("react.strict_mode"),O=Symbol.for("react.profiler"),F=Symbol.for("react.consumer"),M=Symbol.for("react.context"),C=Symbol.for("react.forward_ref"),Y=Symbol.for("react.suspense"),$=Symbol.for("react.suspense_list"),L=Symbol.for("react.memo"),I=Symbol.for("react.lazy"),J=Symbol.for("react.activity"),U=Symbol.for("react.client.reference"),K=b.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,te=Object.prototype.hasOwnProperty,D=Array.isArray,X=console.createTask?console.createTask:function(){return null};b={react_stack_bottom_frame:function(r){return r()}};var ce,ne={},V=b.react_stack_bottom_frame.bind(b,i)(),se=X(s(i)),ue={};ee.Fragment=w,ee.jsx=function(r,h,y){var S=1e4>K.recentlyCreatedOwnerStacks++;return m(r,h,y,!1,S?Error("react-stack-top-frame"):V,S?X(s(r)):se)},ee.jsxs=function(r,h,y){var S=1e4>K.recentlyCreatedOwnerStacks++;return m(r,h,y,!0,S?Error("react-stack-top-frame"):V,S?X(s(r)):se)}})()),ee}var be;function Te(){return be||(be=1,process.env.NODE_ENV==="production"?ae.exports=Ae():ae.exports=Re()),ae.exports}var c=Te();function Ne(n){return n.data?Array.isArray(n.data)?{provider:"value",items:n.data}:n.data:n.staticData?{provider:"value",items:n.staticData}:n.objectName?{provider:"object",object:n.objectName}:null}function ve(n){if(!(!n||n.length===0))return typeof n[0]=="object"&&n[0]!==null,n}const ye=({schema:n,dataSource:a,onEdit:e,onDelete:s,onRowSelect:t,onRowClick:i,onCellChange:o,onRowSave:l,onBatchSave:p,...g})=>{const[m,E]=R.useState([]),[u,b]=R.useState(!0),[_,v]=R.useState(null),[w,z]=R.useState(null),O=g.data,F=B.useDataScope(n.bind),M=Ne(n),C=R.useMemo(()=>O&&Array.isArray(O)?{provider:"value",items:O}:F&&Array.isArray(F)?{provider:"value",items:F}:M,[JSON.stringify(M),F,O]),Y=C?.provider==="value",$=C?.provider==="object"&&C&&"object"in C?C.object:n.objectName,L=n.fields,I=n.columns,J=n.filter,U=n.sort,K=n.pagination,te=n.pageSize;R.useEffect(()=>{Y&&C?.provider==="value"&&(E(f=>{const x=C.items;return JSON.stringify(f)!==JSON.stringify(x)?x:f}),b(!1))},[Y,C]),R.useEffect(()=>{if(Y)return;let f=!1;return(async()=>{b(!0),v(null);try{let N=null;if((ve(I)||L)&&$)N={name:$,fields:{}};else if($&&a){const d=await a.getObjectSchema($);if(f)return;N=d}else throw $?new Error("DataSource required"):new Error("Object name required for data fetching");if(f||z(N),a&&$){const k={$select:(()=>{if(L)return L;if(I&&Array.isArray(I))return I.map(T=>typeof T=="string"?T:T.field)})(),$top:K?.pageSize||te||50};J&&Array.isArray(J)?k.$filter=J:n.defaultFilters&&(k.$filter=n.defaultFilters),U?typeof U=="string"?k.$orderby=U:Array.isArray(U)&&(k.$orderby=U.map(T=>`${T.field} ${T.order}`).join(", ")):n.defaultSort&&(k.$orderby=`${n.defaultSort.field} ${n.defaultSort.order}`);const W=await a.find($,k);if(f)return;E(W.data||[])}}catch(N){f||v(N)}finally{f||b(!1)}})(),()=>{f=!0}},[$,L,I,J,U,K,te,a,Y,C]);const D=B.useNavigationOverlay({navigation:n.navigation,objectName:n.objectName,onNavigate:n.onNavigate,onRowClick:i}),{execute:X}=B.useAction(),ce=R.useCallback(()=>{const f=ve(I);if(f){if(f.length>0&&typeof f[0]=="object"&&f[0]!==null){const j=f[0];if("accessorKey"in j)return f;if("field"in j)return f.filter(d=>d?.field&&typeof d.field=="string"&&!d.hidden).map(d=>{const k=d.label||d.field.charAt(0).toUpperCase()+d.field.slice(1).replace(/_/g," ");let W;const T=d.type?me.getCellRenderer(d.type):null;return d.link&&d.action?W=(P,re)=>{const ie=T?c.jsx(T,{value:P,field:{name:d.field,type:d.type||"text"}}):String(P??"");return c.jsx("button",{type:"button",className:"text-primary underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit",onClick:oe=>{oe.stopPropagation(),D.handleClick(re)},children:ie})}:d.link?W=(P,re)=>{const ie=T?c.jsx(T,{value:P,field:{name:d.field,type:d.type||"text"}}):String(P??"");return c.jsx("button",{type:"button",className:"text-primary underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit",onClick:oe=>{oe.stopPropagation(),D.handleClick(re)},children:ie})}:d.action?W=(P,re)=>{const ie=T?c.jsx(T,{value:P,field:{name:d.field,type:d.type||"text"}}):String(P??"");return c.jsx("button",{type:"button",className:"text-primary underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit",onClick:oe=>{oe.stopPropagation(),X({type:d.action,params:{record:re,field:d.field,value:P}})},children:ie})}:T&&(W=P=>c.jsx(T,{value:P,field:{name:d.field,type:d.type||"text"}})),{header:k,accessorKey:d.field,...d.width&&{width:d.width},...d.align&&{align:d.align},sortable:d.sortable!==!1,...d.resizable!==void 0&&{resizable:d.resizable},...d.wrap!==void 0&&{wrap:d.wrap},...W&&{cell:W}}})}return f.filter(j=>typeof j=="string"&&j.trim().length>0).map(j=>({header:w?.fields?.[j]?.label||j.charAt(0).toUpperCase()+j.slice(1).replace(/_/g," "),accessorKey:j}))}if(Y){const j=C?.provider==="value"?C.items:[];if(j.length>0)return(L||Object.keys(j[0])).map(k=>({header:k.charAt(0).toUpperCase()+k.slice(1).replace(/_/g," "),accessorKey:k}))}if(!w)return[];const x=[];return(L||Object.keys(w.fields||{})).forEach(j=>{const d=w.fields?.[j];if(!d||d.permissions&&d.permissions.read===!1)return;const k=me.getCellRenderer(d.type);x.push({header:d.label||j,accessorKey:j,cell:W=>c.jsx(k,{value:W,field:d}),sortable:d.sortable!==!1})}),x},[w,L,I,C,Y,D.handleClick,X]);if(_)return c.jsxs("div",{className:"p-4 border border-red-300 bg-red-50 rounded-md",children:[c.jsx("h3",{className:"text-red-800 font-semibold",children:"Error loading grid"}),c.jsx("p",{className:"text-red-600 text-sm mt-1",children:_.message})]});if(u&&m.length===0)return c.jsxs("div",{className:"p-8 text-center",children:[c.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"}),c.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading grid..."})]});const ne=ce(),V="operations"in n?n.operations:void 0,se=V&&(V.update||V.delete),ue=se?[...ne,{header:"Actions",accessorKey:"_actions",cell:(f,x)=>c.jsxs(q.DropdownMenu,{children:[c.jsx(q.DropdownMenuTrigger,{asChild:!0,children:c.jsxs(q.Button,{variant:"ghost",size:"icon",className:"h-8 w-8",children:[c.jsx(de.MoreVertical,{className:"h-4 w-4"}),c.jsx("span",{className:"sr-only",children:"Open menu"})]})}),c.jsxs(q.DropdownMenuContent,{align:"end",children:[V?.update&&e&&c.jsxs(q.DropdownMenuItem,{onClick:()=>e(x),children:[c.jsx(de.Edit,{className:"mr-2 h-4 w-4"}),"Edit"]}),V?.delete&&s&&c.jsxs(q.DropdownMenuItem,{onClick:()=>s(x),children:[c.jsx(de.Trash2,{className:"mr-2 h-4 w-4"}),"Delete"]})]})]}),sortable:!1}]:ne;let r=!1;n.selection?.type?r=n.selection.type==="none"?!1:n.selection.type:n.selectable!==void 0&&(r=n.selectable);const h=n.pagination!==void 0?!0:n.showPagination!==void 0?n.showPagination:!0,y=n.pagination?.pageSize||n.pageSize||10,S=n.searchableFields!==void 0?n.searchableFields.length>0:n.showSearch!==void 0?n.showSearch:!0,G={type:"data-table",caption:n.label||n.title,columns:ue,data:m,pagination:h,pageSize:y,searchable:S,selectable:r,sortable:!0,exportable:V?.export,rowActions:se,resizableColumns:n.resizable??n.resizableColumns??!0,reorderableColumns:n.reorderableColumns??!1,editable:n.editable??!1,className:n.className,onSelectionChange:t,onRowClick:D.handleClick,onCellChange:o,onRowSave:l,onBatchSave:p},Z=n.label?`${n.label} Detail`:n.objectName?`${n.objectName.charAt(0).toUpperCase()+n.objectName.slice(1)} Detail`:"Record Detail";return D.isOverlay&&D.mode==="split"?c.jsx(q.NavigationOverlay,{...D,title:Z,mainContent:c.jsx(B.SchemaRenderer,{schema:G}),children:f=>c.jsx("div",{className:"space-y-3",children:Object.entries(f).map(([x,N])=>c.jsxs("div",{className:"flex flex-col",children:[c.jsx("span",{className:"text-xs font-medium text-muted-foreground uppercase tracking-wide",children:x.replace(/_/g," ")}),c.jsx("span",{className:"text-sm",children:String(N??"—")})]},x))})}):c.jsxs(c.Fragment,{children:[c.jsx(B.SchemaRenderer,{schema:G}),D.isOverlay&&c.jsx(q.NavigationOverlay,{...D,title:Z,children:f=>c.jsx("div",{className:"space-y-3",children:Object.entries(f).map(([x,N])=>c.jsxs("div",{className:"flex flex-col",children:[c.jsx("span",{className:"text-xs font-medium text-muted-foreground uppercase tracking-wide",children:x.replace(/_/g," ")}),c.jsx("span",{className:"text-sm",children:String(N??"—")})]},x))})})]})};function H(n,a,e){let s=e.initialDeps??[],t,i=!0;function o(){var l,p,g;let m;e.key&&((l=e.debug)!=null&&l.call(e))&&(m=Date.now());const E=n();if(!(E.length!==s.length||E.some((_,v)=>s[v]!==_)))return t;s=E;let b;if(e.key&&((p=e.debug)!=null&&p.call(e))&&(b=Date.now()),t=a(...E),e.key&&((g=e.debug)!=null&&g.call(e))){const _=Math.round((Date.now()-m)*100)/100,v=Math.round((Date.now()-b)*100)/100,w=v/16,z=(O,F)=>{for(O=String(O);O.length<F;)O=" "+O;return O};console.info(`%c⏱ ${z(v,5)} /${z(_,5)} ms`,`
|
|
7
|
+
font-size: .6rem;
|
|
8
|
+
font-weight: bold;
|
|
9
|
+
color: hsl(${Math.max(0,Math.min(120-120*w,120))}deg 100% 31%);`,e?.key)}return e?.onChange&&!(i&&e.skipInitialOnChange)&&e.onChange(t),i=!1,t}return o.updateDeps=l=>{s=l},o}function xe(n,a){if(n===void 0)throw new Error("Unexpected undefined");return n}const ke=(n,a)=>Math.abs(n-a)<1.01,ze=(n,a,e)=>{let s;return function(...t){n.clearTimeout(s),s=n.setTimeout(()=>a.apply(this,t),e)}},Ee=n=>{const{offsetWidth:a,offsetHeight:e}=n;return{width:a,height:e}},Me=n=>n,Ie=n=>{const a=Math.max(n.startIndex-n.overscan,0),e=Math.min(n.endIndex+n.overscan,n.count-1),s=[];for(let t=a;t<=e;t++)s.push(t);return s},De=(n,a)=>{const e=n.scrollElement;if(!e)return;const s=n.targetWindow;if(!s)return;const t=o=>{const{width:l,height:p}=o;a({width:Math.round(l),height:Math.round(p)})};if(t(Ee(e)),!s.ResizeObserver)return()=>{};const i=new s.ResizeObserver(o=>{const l=()=>{const p=o[0];if(p?.borderBoxSize){const g=p.borderBoxSize[0];if(g){t({width:g.inlineSize,height:g.blockSize});return}}t(Ee(e))};n.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(l):l()});return i.observe(e,{box:"border-box"}),()=>{i.unobserve(e)}},Se={passive:!0},je=typeof window>"u"?!0:"onscrollend"in window,Pe=(n,a)=>{const e=n.scrollElement;if(!e)return;const s=n.targetWindow;if(!s)return;let t=0;const i=n.options.useScrollendEvent&&je?()=>{}:ze(s,()=>{a(t,!1)},n.options.isScrollingResetDelay),o=m=>()=>{const{horizontal:E,isRtl:u}=n.options;t=E?e.scrollLeft*(u&&-1||1):e.scrollTop,i(),a(t,m)},l=o(!0),p=o(!1);e.addEventListener("scroll",l,Se);const g=n.options.useScrollendEvent&&je;return g&&e.addEventListener("scrollend",p,Se),()=>{e.removeEventListener("scroll",l),g&&e.removeEventListener("scrollend",p)}},Fe=(n,a,e)=>{if(a?.borderBoxSize){const s=a.borderBoxSize[0];if(s)return Math.round(s[e.options.horizontal?"inlineSize":"blockSize"])}return n[e.options.horizontal?"offsetWidth":"offsetHeight"]},$e=(n,{adjustments:a=0,behavior:e},s)=>{var t,i;const o=n+a;(i=(t=s.scrollElement)==null?void 0:t.scrollTo)==null||i.call(t,{[s.options.horizontal?"left":"top"]:o,behavior:e})};class We{constructor(a){this.unsubs=[],this.scrollElement=null,this.targetWindow=null,this.isScrolling=!1,this.currentScrollToIndex=null,this.measurementsCache=[],this.itemSizeCache=new Map,this.laneAssignments=new Map,this.pendingMeasuredCacheIndexes=[],this.prevLanes=void 0,this.lanesChangedFlag=!1,this.lanesSettling=!1,this.scrollRect=null,this.scrollOffset=null,this.scrollDirection=null,this.scrollAdjustments=0,this.elementsCache=new Map,this.observer=(()=>{let e=null;const s=()=>e||(!this.targetWindow||!this.targetWindow.ResizeObserver?null:e=new this.targetWindow.ResizeObserver(t=>{t.forEach(i=>{const o=()=>{this._measureElement(i.target,i)};this.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(o):o()})}));return{disconnect:()=>{var t;(t=s())==null||t.disconnect(),e=null},observe:t=>{var i;return(i=s())==null?void 0:i.observe(t,{box:"border-box"})},unobserve:t=>{var i;return(i=s())==null?void 0:i.unobserve(t)}}})(),this.range=null,this.setOptions=e=>{Object.entries(e).forEach(([s,t])=>{typeof t>"u"&&delete e[s]}),this.options={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:Me,rangeExtractor:Ie,onChange:()=>{},measureElement:Fe,initialRect:{width:0,height:0},scrollMargin:0,gap:0,indexAttribute:"data-index",initialMeasurementsCache:[],lanes:1,isScrollingResetDelay:150,enabled:!0,isRtl:!1,useScrollendEvent:!1,useAnimationFrameWithResizeObserver:!1,...e}},this.notify=e=>{var s,t;(t=(s=this.options).onChange)==null||t.call(s,this,e)},this.maybeNotify=H(()=>(this.calculateRange(),[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]),e=>{this.notify(e)},{key:process.env.NODE_ENV!=="production"&&"maybeNotify",debug:()=>this.options.debug,initialDeps:[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]}),this.cleanup=()=>{this.unsubs.filter(Boolean).forEach(e=>e()),this.unsubs=[],this.observer.disconnect(),this.scrollElement=null,this.targetWindow=null},this._didMount=()=>()=>{this.cleanup()},this._willUpdate=()=>{var e;const s=this.options.enabled?this.options.getScrollElement():null;if(this.scrollElement!==s){if(this.cleanup(),!s){this.maybeNotify();return}this.scrollElement=s,this.scrollElement&&"ownerDocument"in this.scrollElement?this.targetWindow=this.scrollElement.ownerDocument.defaultView:this.targetWindow=((e=this.scrollElement)==null?void 0:e.window)??null,this.elementsCache.forEach(t=>{this.observer.observe(t)}),this.unsubs.push(this.options.observeElementRect(this,t=>{this.scrollRect=t,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(t,i)=>{this.scrollAdjustments=0,this.scrollDirection=i?this.getScrollOffset()<t?"forward":"backward":null,this.scrollOffset=t,this.isScrolling=i,this.maybeNotify()})),this._scrollToOffset(this.getScrollOffset(),{adjustments:void 0,behavior:void 0})}},this.getSize=()=>this.options.enabled?(this.scrollRect=this.scrollRect??this.options.initialRect,this.scrollRect[this.options.horizontal?"width":"height"]):(this.scrollRect=null,0),this.getScrollOffset=()=>this.options.enabled?(this.scrollOffset=this.scrollOffset??(typeof this.options.initialOffset=="function"?this.options.initialOffset():this.options.initialOffset),this.scrollOffset):(this.scrollOffset=null,0),this.getFurthestMeasurement=(e,s)=>{const t=new Map,i=new Map;for(let o=s-1;o>=0;o--){const l=e[o];if(t.has(l.lane))continue;const p=i.get(l.lane);if(p==null||l.end>p.end?i.set(l.lane,l):l.end<p.end&&t.set(l.lane,!0),t.size===this.options.lanes)break}return i.size===this.options.lanes?Array.from(i.values()).sort((o,l)=>o.end===l.end?o.index-l.index:o.end-l.end)[0]:void 0},this.getMeasurementOptions=H(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes],(e,s,t,i,o,l)=>(this.prevLanes!==void 0&&this.prevLanes!==l&&(this.lanesChangedFlag=!0),this.prevLanes=l,this.pendingMeasuredCacheIndexes=[],{count:e,paddingStart:s,scrollMargin:t,getItemKey:i,enabled:o,lanes:l}),{key:!1}),this.getMeasurements=H(()=>[this.getMeasurementOptions(),this.itemSizeCache],({count:e,paddingStart:s,scrollMargin:t,getItemKey:i,enabled:o,lanes:l},p)=>{if(!o)return this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>e)for(const u of this.laneAssignments.keys())u>=e&&this.laneAssignments.delete(u);this.lanesChangedFlag&&(this.lanesChangedFlag=!1,this.lanesSettling=!0,this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),this.pendingMeasuredCacheIndexes=[]),this.measurementsCache.length===0&&!this.lanesSettling&&(this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(u=>{this.itemSizeCache.set(u.key,u.size)}));const g=this.lanesSettling?0:this.pendingMeasuredCacheIndexes.length>0?Math.min(...this.pendingMeasuredCacheIndexes):0;this.pendingMeasuredCacheIndexes=[],this.lanesSettling&&this.measurementsCache.length===e&&(this.lanesSettling=!1);const m=this.measurementsCache.slice(0,g),E=new Array(l).fill(void 0);for(let u=0;u<g;u++){const b=m[u];b&&(E[b.lane]=u)}for(let u=g;u<e;u++){const b=i(u),_=this.laneAssignments.get(u);let v,w;if(_!==void 0&&this.options.lanes>1){v=_;const M=E[v],C=M!==void 0?m[M]:void 0;w=C?C.end+this.options.gap:s+t}else{const M=this.options.lanes===1?m[u-1]:this.getFurthestMeasurement(m,u);w=M?M.end+this.options.gap:s+t,v=M?M.lane:u%this.options.lanes,this.options.lanes>1&&this.laneAssignments.set(u,v)}const z=p.get(b),O=typeof z=="number"?z:this.options.estimateSize(u),F=w+O;m[u]={index:u,start:w,size:O,end:F,key:b,lane:v},E[v]=u}return this.measurementsCache=m,m},{key:process.env.NODE_ENV!=="production"&&"getMeasurements",debug:()=>this.options.debug}),this.calculateRange=H(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes],(e,s,t,i)=>this.range=e.length>0&&s>0?Le({measurements:e,outerSize:s,scrollOffset:t,lanes:i}):null,{key:process.env.NODE_ENV!=="production"&&"calculateRange",debug:()=>this.options.debug}),this.getVirtualIndexes=H(()=>{let e=null,s=null;const t=this.calculateRange();return t&&(e=t.startIndex,s=t.endIndex),this.maybeNotify.updateDeps([this.isScrolling,e,s]),[this.options.rangeExtractor,this.options.overscan,this.options.count,e,s]},(e,s,t,i,o)=>i===null||o===null?[]:e({startIndex:i,endIndex:o,overscan:s,count:t}),{key:process.env.NODE_ENV!=="production"&&"getVirtualIndexes",debug:()=>this.options.debug}),this.indexFromElement=e=>{const s=this.options.indexAttribute,t=e.getAttribute(s);return t?parseInt(t,10):(console.warn(`Missing attribute name '${s}={index}' on measured element.`),-1)},this._measureElement=(e,s)=>{const t=this.indexFromElement(e),i=this.measurementsCache[t];if(!i)return;const o=i.key,l=this.elementsCache.get(o);l!==e&&(l&&this.observer.unobserve(l),this.observer.observe(e),this.elementsCache.set(o,e)),e.isConnected&&this.resizeItem(t,this.options.measureElement(e,s,this))},this.resizeItem=(e,s)=>{const t=this.measurementsCache[e];if(!t)return;const i=this.itemSizeCache.get(t.key)??t.size,o=s-i;o!==0&&((this.shouldAdjustScrollPositionOnItemSizeChange!==void 0?this.shouldAdjustScrollPositionOnItemSizeChange(t,o,this):t.start<this.getScrollOffset()+this.scrollAdjustments)&&(process.env.NODE_ENV!=="production"&&this.options.debug&&console.info("correction",o),this._scrollToOffset(this.getScrollOffset(),{adjustments:this.scrollAdjustments+=o,behavior:void 0})),this.pendingMeasuredCacheIndexes.push(t.index),this.itemSizeCache=new Map(this.itemSizeCache.set(t.key,s)),this.notify(!1))},this.measureElement=e=>{if(!e){this.elementsCache.forEach((s,t)=>{s.isConnected||(this.observer.unobserve(s),this.elementsCache.delete(t))});return}this._measureElement(e,void 0)},this.getVirtualItems=H(()=>[this.getVirtualIndexes(),this.getMeasurements()],(e,s)=>{const t=[];for(let i=0,o=e.length;i<o;i++){const l=e[i],p=s[l];t.push(p)}return t},{key:process.env.NODE_ENV!=="production"&&"getVirtualItems",debug:()=>this.options.debug}),this.getVirtualItemForOffset=e=>{const s=this.getMeasurements();if(s.length!==0)return xe(s[we(0,s.length-1,t=>xe(s[t]).start,e)])},this.getMaxScrollOffset=()=>{if(!this.scrollElement)return 0;if("scrollHeight"in this.scrollElement)return this.options.horizontal?this.scrollElement.scrollWidth-this.scrollElement.clientWidth:this.scrollElement.scrollHeight-this.scrollElement.clientHeight;{const e=this.scrollElement.document.documentElement;return this.options.horizontal?e.scrollWidth-this.scrollElement.innerWidth:e.scrollHeight-this.scrollElement.innerHeight}},this.getOffsetForAlignment=(e,s,t=0)=>{if(!this.scrollElement)return 0;const i=this.getSize(),o=this.getScrollOffset();s==="auto"&&(s=e>=o+i?"end":"start"),s==="center"?e+=(t-i)/2:s==="end"&&(e-=i);const l=this.getMaxScrollOffset();return Math.max(Math.min(l,e),0)},this.getOffsetForIndex=(e,s="auto")=>{e=Math.max(0,Math.min(e,this.options.count-1));const t=this.measurementsCache[e];if(!t)return;const i=this.getSize(),o=this.getScrollOffset();if(s==="auto")if(t.end>=o+i-this.options.scrollPaddingEnd)s="end";else if(t.start<=o+this.options.scrollPaddingStart)s="start";else return[o,s];if(s==="end"&&e===this.options.count-1)return[this.getMaxScrollOffset(),s];const l=s==="end"?t.end+this.options.scrollPaddingEnd:t.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(l,s,t.size),s]},this.isDynamicMode=()=>this.elementsCache.size>0,this.scrollToOffset=(e,{align:s="start",behavior:t}={})=>{t==="smooth"&&this.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size."),this._scrollToOffset(this.getOffsetForAlignment(e,s),{adjustments:void 0,behavior:t})},this.scrollToIndex=(e,{align:s="auto",behavior:t}={})=>{t==="smooth"&&this.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size."),e=Math.max(0,Math.min(e,this.options.count-1)),this.currentScrollToIndex=e;let i=0;const o=10,l=g=>{if(!this.targetWindow)return;const m=this.getOffsetForIndex(e,g);if(!m){console.warn("Failed to get offset for index:",e);return}const[E,u]=m;this._scrollToOffset(E,{adjustments:void 0,behavior:t}),this.targetWindow.requestAnimationFrame(()=>{const b=()=>{if(this.currentScrollToIndex!==e)return;const _=this.getScrollOffset(),v=this.getOffsetForIndex(e,u);if(!v){console.warn("Failed to get offset for index:",e);return}ke(v[0],_)||p(u)};this.isDynamicMode()?this.targetWindow.requestAnimationFrame(b):b()})},p=g=>{this.targetWindow&&this.currentScrollToIndex===e&&(i++,i<o?(process.env.NODE_ENV!=="production"&&this.options.debug&&console.info("Schedule retry",i,o),this.targetWindow.requestAnimationFrame(()=>l(g))):console.warn(`Failed to scroll to index ${e} after ${o} attempts.`))};l(s)},this.scrollBy=(e,{behavior:s}={})=>{s==="smooth"&&this.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size."),this._scrollToOffset(this.getScrollOffset()+e,{adjustments:void 0,behavior:s})},this.getTotalSize=()=>{var e;const s=this.getMeasurements();let t;if(s.length===0)t=this.options.paddingStart;else if(this.options.lanes===1)t=((e=s[s.length-1])==null?void 0:e.end)??0;else{const i=Array(this.options.lanes).fill(null);let o=s.length-1;for(;o>=0&&i.some(l=>l===null);){const l=s[o];i[l.lane]===null&&(i[l.lane]=l.end),o--}t=Math.max(...i.filter(l=>l!==null))}return Math.max(t-this.options.scrollMargin+this.options.paddingEnd,0)},this._scrollToOffset=(e,{adjustments:s,behavior:t})=>{this.options.scrollToFn(e,{behavior:t,adjustments:s},this)},this.measure=()=>{this.itemSizeCache=new Map,this.laneAssignments=new Map,this.notify(!1)},this.setOptions(a)}}const we=(n,a,e,s)=>{for(;n<=a;){const t=(n+a)/2|0,i=e(t);if(i<s)n=t+1;else if(i>s)a=t-1;else return t}return n>0?n-1:0};function Le({measurements:n,outerSize:a,scrollOffset:e,lanes:s}){const t=n.length-1,i=p=>n[p].start;if(n.length<=s)return{startIndex:0,endIndex:t};let o=we(0,t,i,e),l=o;if(s===1)for(;l<t&&n[l].end<e+a;)l++;else if(s>1){const p=Array(s).fill(0);for(;l<t&&p.some(m=>m<e+a);){const m=n[l];p[m.lane]=m.end,l++}const g=Array(s).fill(e+a);for(;o>=0&&g.some(m=>m>=e);){const m=n[o];g[m.lane]=m.start,o--}o=Math.max(0,o-o%s),l=Math.min(t,l+(s-1-l%s))}return{startIndex:o,endIndex:l}}const Oe=typeof document<"u"?le.useLayoutEffect:le.useEffect;function Ve({useFlushSync:n=!0,...a}){const e=le.useReducer(()=>({}),{})[1],s={...a,onChange:(i,o)=>{var l;n&&o?Ce.flushSync(e):e(),(l=a.onChange)==null||l.call(a,i,o)}},[t]=le.useState(()=>new We(s));return t.setOptions(s),Oe(()=>t._didMount(),[]),Oe(()=>t._willUpdate()),t}function qe(n){return Ve({observeElementRect:De,observeElementOffset:Pe,scrollToFn:$e,...n})}const Ye=({data:n,columns:a,rowHeight:e=40,height:s=600,className:t="",headerClassName:i="",rowClassName:o,onRowClick:l,overscan:p=5})=>{const g=R.useRef(null),m=qe({count:n.length,getScrollElement:()=>g.current,estimateSize:()=>e,overscan:p}),E=m.getVirtualItems();return c.jsxs("div",{className:t,children:[c.jsx("div",{className:`grid border-b sticky top-0 bg-background z-10 ${i}`,style:{gridTemplateColumns:a.map(u=>u.width||"1fr").join(" ")},children:a.map((u,b)=>c.jsx("div",{className:`px-4 py-2 font-semibold text-sm ${u.align==="center"?"text-center":u.align==="right"?"text-right":"text-left"}`,children:u.header},b))}),c.jsx("div",{ref:g,className:"overflow-auto",style:{height:typeof s=="number"?`${s}px`:s,contain:"strict"},children:c.jsx("div",{style:{height:`${m.getTotalSize()}px`,width:"100%",position:"relative"},children:E.map(u=>{const b=n[u.index],_=typeof o=="function"?o(b,u.index):o||"";return c.jsx("div",{className:`grid border-b hover:bg-muted/50 cursor-pointer ${_}`,style:{position:"absolute",top:0,left:0,width:"100%",height:`${u.size}px`,transform:`translateY(${u.start}px)`,gridTemplateColumns:a.map(v=>v.width||"1fr").join(" ")},onClick:()=>l?.(b,u.index),children:a.map((v,w)=>{const z=b[v.accessorKey],O=v.cell?v.cell(z,b):z;return c.jsx("div",{className:`px-4 py-2 text-sm flex items-center ${v.align==="center"?"text-center justify-center":v.align==="right"?"text-right justify-end":"text-left justify-start"}`,children:O},w)})},u.key)})})}),c.jsxs("div",{className:"px-4 py-2 text-xs text-muted-foreground border-t",children:["Showing ",E.length," of ",n.length," rows (virtual scrolling enabled)"]})]})},fe=({schema:n,...a})=>{const{dataSource:e}=B.useSchemaContext()||{};return c.jsx(ye,{schema:n,dataSource:e,...a})};he.ComponentRegistry.register("object-grid",fe,{namespace:"plugin-grid",label:"Object Grid",category:"plugin",inputs:[{name:"objectName",type:"string",label:"Object Name",required:!0},{name:"columns",type:"array",label:"Columns"},{name:"filters",type:"array",label:"Filters"}]}),he.ComponentRegistry.register("grid",fe,{namespace:"view",label:"Data Grid",category:"view",inputs:[{name:"objectName",type:"string",label:"Object Name",required:!0},{name:"columns",type:"array",label:"Columns"},{name:"filters",type:"array",label:"Filters"}]}),A.ObjectGrid=ye,A.ObjectGridRenderer=fe,A.VirtualGrid=Ye,Object.defineProperty(A,Symbol.toStringTag,{value:"Module"})}));
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -8,7 +8,13 @@ export interface ObjectGridProps {
|
|
|
8
8
|
onEdit?: (record: any) => void;
|
|
9
9
|
onDelete?: (record: any) => void;
|
|
10
10
|
onBulkDelete?: (records: any[]) => void;
|
|
11
|
-
onCellChange?: (rowIndex: number, columnKey: string, newValue: any) => void;
|
|
11
|
+
onCellChange?: (rowIndex: number, columnKey: string, newValue: any, row: any) => void;
|
|
12
|
+
onRowSave?: (rowIndex: number, changes: Record<string, any>, row: any) => void | Promise<void>;
|
|
13
|
+
onBatchSave?: (changes: Array<{
|
|
14
|
+
rowIndex: number;
|
|
15
|
+
changes: Record<string, any>;
|
|
16
|
+
row: any;
|
|
17
|
+
}>) => void | Promise<void>;
|
|
12
18
|
onRowSelect?: (selectedRows: any[]) => void;
|
|
13
19
|
}
|
|
14
20
|
export declare const ObjectGrid: React.FC<ObjectGridProps>;
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export interface VirtualGridColumn {
|
|
3
|
+
header: string;
|
|
4
|
+
accessorKey: string;
|
|
5
|
+
cell?: (value: any, row: any) => React.ReactNode;
|
|
6
|
+
width?: number | string;
|
|
7
|
+
align?: 'left' | 'center' | 'right';
|
|
8
|
+
}
|
|
9
|
+
export interface VirtualGridProps {
|
|
10
|
+
data: any[];
|
|
11
|
+
columns: VirtualGridColumn[];
|
|
12
|
+
rowHeight?: number;
|
|
13
|
+
height?: number | string;
|
|
14
|
+
className?: string;
|
|
15
|
+
headerClassName?: string;
|
|
16
|
+
rowClassName?: string | ((row: any, index: number) => string);
|
|
17
|
+
onRowClick?: (row: any, index: number) => void;
|
|
18
|
+
overscan?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Virtual scrolling grid component
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <VirtualGrid
|
|
26
|
+
* data={items}
|
|
27
|
+
* columns={[
|
|
28
|
+
* { header: 'Name', accessorKey: 'name' },
|
|
29
|
+
* { header: 'Age', accessorKey: 'age' },
|
|
30
|
+
* ]}
|
|
31
|
+
* rowHeight={40}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const VirtualGrid: React.FC<VirtualGridProps>;
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { ObjectGrid } from './ObjectGrid';
|
|
3
|
+
import { VirtualGrid } from './VirtualGrid';
|
|
4
|
+
export { ObjectGrid, VirtualGrid };
|
|
5
|
+
export type { ObjectGridProps } from './ObjectGrid';
|
|
6
|
+
export type { VirtualGridProps, VirtualGridColumn } from './VirtualGrid';
|
|
7
|
+
export declare const ObjectGridRenderer: React.FC<{
|
|
8
|
+
schema: any;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-grid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Grid plugin for Object UI",
|
|
@@ -15,22 +15,25 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@tanstack/react-virtual": "^3.11.3",
|
|
18
19
|
"lucide-react": "^0.563.0",
|
|
19
|
-
"@object-ui/components": "0.
|
|
20
|
-
"@object-ui/core": "0.
|
|
21
|
-
"@object-ui/fields": "0.
|
|
22
|
-
"@object-ui/react": "0.
|
|
23
|
-
"@object-ui/types": "0.
|
|
20
|
+
"@object-ui/components": "2.0.0",
|
|
21
|
+
"@object-ui/core": "2.0.0",
|
|
22
|
+
"@object-ui/fields": "2.0.0",
|
|
23
|
+
"@object-ui/react": "2.0.0",
|
|
24
|
+
"@object-ui/types": "2.0.0"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
27
|
"react": "^18.0.0 || ^19.0.0",
|
|
27
28
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@vitejs/plugin-react": "^
|
|
31
|
+
"@vitejs/plugin-react": "^5.1.3",
|
|
32
|
+
"msw": "^2.12.9",
|
|
31
33
|
"typescript": "^5.9.3",
|
|
32
34
|
"vite": "^7.3.1",
|
|
33
|
-
"vite-plugin-dts": "^4.5.4"
|
|
35
|
+
"vite-plugin-dts": "^4.5.4",
|
|
36
|
+
"@object-ui/data-objectstack": "2.0.0"
|
|
34
37
|
},
|
|
35
38
|
"scripts": {
|
|
36
39
|
"build": "vite build",
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListColumn Extensions Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for link, action, hidden, type, wrap, and resizable properties
|
|
5
|
+
* on ListColumn when rendered through ObjectGrid → data-table.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { ObjectGrid } from './ObjectGrid';
|
|
12
|
+
import { registerAllFields } from '@object-ui/fields';
|
|
13
|
+
import { ActionProvider } from '@object-ui/react';
|
|
14
|
+
import type { ListColumn } from '@object-ui/types';
|
|
15
|
+
|
|
16
|
+
registerAllFields();
|
|
17
|
+
|
|
18
|
+
// --- Mock Data ---
|
|
19
|
+
const mockData = [
|
|
20
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com', amount: 1500, status: 'active' },
|
|
21
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com', amount: 2300, status: 'inactive' },
|
|
22
|
+
{ _id: '3', name: 'Charlie', email: 'charlie@test.com', amount: 800, status: 'active' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// --- Helper: Render ObjectGrid with static data and ListColumn[] ---
|
|
26
|
+
function renderGrid(columns: ListColumn[], opts?: { onNavigate?: any; navigation?: any }) {
|
|
27
|
+
const schema: any = {
|
|
28
|
+
type: 'object-grid' as const,
|
|
29
|
+
objectName: 'test_object',
|
|
30
|
+
columns,
|
|
31
|
+
data: { provider: 'value', items: mockData },
|
|
32
|
+
navigation: opts?.navigation,
|
|
33
|
+
onNavigate: opts?.onNavigate,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return render(
|
|
37
|
+
<ActionProvider>
|
|
38
|
+
<ObjectGrid schema={schema} />
|
|
39
|
+
</ActionProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =========================================================================
|
|
44
|
+
// 1. Hidden columns
|
|
45
|
+
// =========================================================================
|
|
46
|
+
describe('ListColumn: hidden', () => {
|
|
47
|
+
it('should not render hidden columns', async () => {
|
|
48
|
+
renderGrid([
|
|
49
|
+
{ field: 'name', label: 'Name' },
|
|
50
|
+
{ field: 'email', label: 'Email', hidden: true },
|
|
51
|
+
{ field: 'amount', label: 'Amount' },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
expect(screen.getByText('Amount')).toBeInTheDocument();
|
|
58
|
+
// Email column should NOT be rendered
|
|
59
|
+
expect(screen.queryByText('Email')).not.toBeInTheDocument();
|
|
60
|
+
expect(screen.queryByText('alice@test.com')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should render non-hidden columns normally', async () => {
|
|
64
|
+
renderGrid([
|
|
65
|
+
{ field: 'name', label: 'Name', hidden: false },
|
|
66
|
+
{ field: 'email', label: 'Email' },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// =========================================================================
|
|
77
|
+
// 2. Link columns
|
|
78
|
+
// =========================================================================
|
|
79
|
+
describe('ListColumn: link', () => {
|
|
80
|
+
it('should render link columns as clickable text', async () => {
|
|
81
|
+
renderGrid([
|
|
82
|
+
{ field: 'name', label: 'Name', link: true },
|
|
83
|
+
{ field: 'email', label: 'Email' },
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// The name cells should be rendered as buttons (clickable links)
|
|
91
|
+
const aliceLink = screen.getByRole('button', { name: 'Alice' });
|
|
92
|
+
expect(aliceLink).toBeInTheDocument();
|
|
93
|
+
expect(aliceLink).toHaveClass('text-primary');
|
|
94
|
+
|
|
95
|
+
const bobLink = screen.getByRole('button', { name: 'Bob' });
|
|
96
|
+
expect(bobLink).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should trigger navigation when link column is clicked', async () => {
|
|
100
|
+
const onNavigate = vi.fn();
|
|
101
|
+
|
|
102
|
+
renderGrid(
|
|
103
|
+
[
|
|
104
|
+
{ field: 'name', label: 'Name', link: true },
|
|
105
|
+
{ field: 'email', label: 'Email' },
|
|
106
|
+
],
|
|
107
|
+
{
|
|
108
|
+
navigation: { mode: 'page' },
|
|
109
|
+
onNavigate,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(screen.getByRole('button', { name: 'Alice' })).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
fireEvent.click(screen.getByRole('button', { name: 'Alice' }));
|
|
118
|
+
|
|
119
|
+
expect(onNavigate).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(onNavigate).toHaveBeenCalledWith('1', 'view');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should not render non-link columns as buttons', async () => {
|
|
124
|
+
renderGrid([
|
|
125
|
+
{ field: 'name', label: 'Name', link: true },
|
|
126
|
+
{ field: 'email', label: 'Email' },
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Email values should NOT be buttons
|
|
134
|
+
expect(screen.queryByRole('button', { name: 'alice@test.com' })).not.toBeInTheDocument();
|
|
135
|
+
// But the text should still render
|
|
136
|
+
expect(screen.getByText('alice@test.com')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// =========================================================================
|
|
141
|
+
// 3. Action columns
|
|
142
|
+
// =========================================================================
|
|
143
|
+
describe('ListColumn: action', () => {
|
|
144
|
+
it('should render action columns as clickable text', async () => {
|
|
145
|
+
renderGrid([
|
|
146
|
+
{ field: 'name', label: 'Name' },
|
|
147
|
+
{ field: 'status', label: 'Status', action: 'toggleStatus' },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Status cells should be buttons
|
|
155
|
+
const activeBtn = screen.getAllByRole('button', { name: 'active' });
|
|
156
|
+
expect(activeBtn.length).toBeGreaterThanOrEqual(1);
|
|
157
|
+
expect(activeBtn[0]).toHaveClass('text-primary');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should execute action when action column is clicked', async () => {
|
|
161
|
+
const actionHandler = vi.fn().mockResolvedValue({ success: true });
|
|
162
|
+
|
|
163
|
+
const schema: any = {
|
|
164
|
+
type: 'object-grid' as const,
|
|
165
|
+
objectName: 'test_object',
|
|
166
|
+
columns: [
|
|
167
|
+
{ field: 'name', label: 'Name' },
|
|
168
|
+
{ field: 'status', label: 'Status', action: 'toggleStatus' },
|
|
169
|
+
],
|
|
170
|
+
data: { provider: 'value', items: mockData },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
render(
|
|
174
|
+
<ActionProvider handlers={{ toggleStatus: actionHandler }}>
|
|
175
|
+
<ObjectGrid schema={schema} />
|
|
176
|
+
</ActionProvider>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const statusBtns = screen.getAllByRole('button', { name: 'active' });
|
|
184
|
+
fireEvent.click(statusBtns[0]);
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(actionHandler).toHaveBeenCalledTimes(1);
|
|
188
|
+
});
|
|
189
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
190
|
+
expect.objectContaining({
|
|
191
|
+
type: 'toggleStatus',
|
|
192
|
+
params: expect.objectContaining({
|
|
193
|
+
field: 'status',
|
|
194
|
+
value: 'active',
|
|
195
|
+
record: expect.objectContaining({ _id: '1', name: 'Alice' }),
|
|
196
|
+
}),
|
|
197
|
+
}),
|
|
198
|
+
expect.any(Object) // ActionCtx
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// =========================================================================
|
|
204
|
+
// 4. Type-based cell rendering
|
|
205
|
+
// =========================================================================
|
|
206
|
+
describe('ListColumn: type', () => {
|
|
207
|
+
it('should use getCellRenderer for typed columns', async () => {
|
|
208
|
+
renderGrid([
|
|
209
|
+
{ field: 'name', label: 'Name' },
|
|
210
|
+
{ field: 'email', label: 'Email', type: 'email' },
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Email type should render as a mailto link
|
|
218
|
+
const emailLink = screen.getByText('alice@test.com');
|
|
219
|
+
expect(emailLink).toBeInTheDocument();
|
|
220
|
+
// The email cell renderer wraps in an anchor
|
|
221
|
+
expect(emailLink.closest('a')).toHaveAttribute('href', 'mailto:alice@test.com');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should render boolean type columns correctly', async () => {
|
|
225
|
+
const boolData = [
|
|
226
|
+
{ _id: '1', name: 'Alice', active: true },
|
|
227
|
+
{ _id: '2', name: 'Bob', active: false },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const schema: any = {
|
|
231
|
+
type: 'object-grid' as const,
|
|
232
|
+
objectName: 'test_object',
|
|
233
|
+
columns: [
|
|
234
|
+
{ field: 'name', label: 'Name' },
|
|
235
|
+
{ field: 'active', label: 'Active', type: 'boolean' },
|
|
236
|
+
],
|
|
237
|
+
data: { provider: 'value', items: boolData },
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
render(
|
|
241
|
+
<ActionProvider>
|
|
242
|
+
<ObjectGrid schema={schema} />
|
|
243
|
+
</ActionProvider>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Boolean renderer should show check/x icons or text representation
|
|
251
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// =========================================================================
|
|
256
|
+
// 5. Combined: link + type
|
|
257
|
+
// =========================================================================
|
|
258
|
+
describe('ListColumn: link + type', () => {
|
|
259
|
+
it('should render typed content inside a clickable link', async () => {
|
|
260
|
+
const onNavigate = vi.fn();
|
|
261
|
+
|
|
262
|
+
renderGrid(
|
|
263
|
+
[
|
|
264
|
+
{ field: 'email', label: 'Email', link: true, type: 'email' },
|
|
265
|
+
{ field: 'name', label: 'Name' },
|
|
266
|
+
],
|
|
267
|
+
{
|
|
268
|
+
navigation: { mode: 'page' },
|
|
269
|
+
onNavigate,
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Should be a button wrapping the email content
|
|
278
|
+
const emailBtn = screen.getByRole('button', { name: /alice@test.com/ });
|
|
279
|
+
expect(emailBtn).toBeInTheDocument();
|
|
280
|
+
expect(emailBtn).toHaveClass('text-primary');
|
|
281
|
+
|
|
282
|
+
fireEvent.click(emailBtn);
|
|
283
|
+
expect(onNavigate).toHaveBeenCalledWith('1', 'view');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// =========================================================================
|
|
288
|
+
// 6. Column properties passthrough
|
|
289
|
+
// =========================================================================
|
|
290
|
+
describe('ListColumn: property passthrough', () => {
|
|
291
|
+
it('should auto-generate header from field name if no label', async () => {
|
|
292
|
+
renderGrid([
|
|
293
|
+
{ field: 'first_name' },
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
// Should convert snake_case to title case
|
|
298
|
+
expect(screen.getByText('First name')).toBeInTheDocument();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should use label when provided', async () => {
|
|
303
|
+
renderGrid([
|
|
304
|
+
{ field: 'name', label: 'Full Name' },
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.getByText('Full Name')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should handle all columns hidden gracefully', async () => {
|
|
313
|
+
const { container } = renderGrid([
|
|
314
|
+
{ field: 'name', hidden: true },
|
|
315
|
+
{ field: 'email', hidden: true },
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
// Should render without error, just no columns
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
expect(container).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// =========================================================================
|
|
326
|
+
// 7. Type definitions alignment
|
|
327
|
+
// =========================================================================
|
|
328
|
+
describe('ListColumn type definitions', () => {
|
|
329
|
+
it('should accept link property on ListColumn', () => {
|
|
330
|
+
const col: ListColumn = {
|
|
331
|
+
field: 'name',
|
|
332
|
+
link: true,
|
|
333
|
+
};
|
|
334
|
+
expect(col.link).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should accept action property on ListColumn', () => {
|
|
338
|
+
const col: ListColumn = {
|
|
339
|
+
field: 'status',
|
|
340
|
+
action: 'toggleStatus',
|
|
341
|
+
};
|
|
342
|
+
expect(col.action).toBe('toggleStatus');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should accept both link and action together', () => {
|
|
346
|
+
const col: ListColumn = {
|
|
347
|
+
field: 'name',
|
|
348
|
+
link: true,
|
|
349
|
+
action: 'viewDetail',
|
|
350
|
+
};
|
|
351
|
+
expect(col.link).toBe(true);
|
|
352
|
+
expect(col.action).toBe('viewDetail');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should accept all ListColumn properties', () => {
|
|
356
|
+
const col: ListColumn = {
|
|
357
|
+
field: 'amount',
|
|
358
|
+
label: 'Total Amount',
|
|
359
|
+
width: 150,
|
|
360
|
+
align: 'right',
|
|
361
|
+
hidden: false,
|
|
362
|
+
sortable: true,
|
|
363
|
+
resizable: true,
|
|
364
|
+
wrap: false,
|
|
365
|
+
type: 'currency',
|
|
366
|
+
link: false,
|
|
367
|
+
action: 'editAmount',
|
|
368
|
+
};
|
|
369
|
+
expect(col.field).toBe('amount');
|
|
370
|
+
expect(col.type).toBe('currency');
|
|
371
|
+
expect(col.link).toBe(false);
|
|
372
|
+
expect(col.action).toBe('editAmount');
|
|
373
|
+
});
|
|
374
|
+
});
|