@object-ui/plugin-grid 0.3.0 → 0.5.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 +21 -0
- package/CHANGELOG.md +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1018 -295
- package/dist/index.umd.cjs +5 -2
- 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/index.d.ts +10 -0
- package/dist/packages/plugin-grid/src/index.test.d.ts +1 -0
- package/package.json +11 -8
- package/src/ObjectGrid.msw.test.tsx +107 -0
- package/src/ObjectGrid.tsx +81 -21
- package/src/VirtualGrid.test.tsx +23 -0
- package/src/VirtualGrid.tsx +183 -0
- package/src/index.test.tsx +29 -0
- package/src/index.tsx +23 -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/{plugin-grid → packages/plugin-grid}/src/ObjectGrid.d.ts +0 -0
package/dist/index.umd.cjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(A,j){typeof exports=="object"&&typeof module<"u"?j(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"],j):(A=typeof globalThis<"u"?globalThis:A||self,j(A.ObjectUIPluginGrid={},A.React,A.core,A.react,A.fields,A.components,A.lucideReact,A.ReactDOM))})(this,(function(A,j,re,X,pe,P,Z,be){"use strict";function ve(t){const a=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const e in t)if(e!=="default"){const s=Object.getOwnPropertyDescriptor(t,e);Object.defineProperty(a,e,s.get?s:{enumerable:!0,get:()=>t[e]})}}return a.default=t,Object.freeze(a)}const U=ve(j);var G={exports:{}},W={};var ie;function ye(){if(ie)return W;ie=1;var t=Symbol.for("react.transitional.element"),a=Symbol.for("react.fragment");function e(s,n,i){var o=null;if(i!==void 0&&(o=""+i),n.key!==void 0&&(o=""+n.key),"key"in n){i={};for(var l in n)l!=="key"&&(i[l]=n[l])}else i=n;return n=i.ref,{$$typeof:t,type:s,key:o,ref:n!==void 0?n:null,props:i}}return W.Fragment=a,W.jsx=e,W.jsxs=e,W}var L={};var oe;function Ee(){return oe||(oe=1,process.env.NODE_ENV!=="production"&&(function(){function t(r){if(r==null)return null;if(typeof r=="function")return r.$$typeof===ee?null:r.displayName||r.name||null;if(typeof r=="string")return r;switch(r){case _:return"Fragment";case p:return"Profiler";case z:return"StrictMode";case B:return"Suspense";case N:return"SuspenseList";case q: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 g:return"Portal";case k:return r.displayName||"Context";case C:return(r._context.displayName||"Context")+".Consumer";case D:var u=r.render;return r=r.displayName,r||(r=u.displayName||u.name||"",r=r!==""?"ForwardRef("+r+")":"ForwardRef"),r;case H:return u=r.displayName||null,u!==null?u:t(r.type)||"Memo";case V:u=r._payload,r=r._init;try{return t(r(u))}catch{}}return null}function a(r){return""+r}function e(r){try{a(r);var u=!1}catch{u=!0}if(u){u=console;var x=u.error,w=typeof Symbol=="function"&&Symbol.toStringTag&&r[Symbol.toStringTag]||r.constructor.name||"Object";return x.call(u,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",w),a(r)}}function s(r){if(r===_)return"<>";if(typeof r=="object"&&r!==null&&r.$$typeof===V)return"<...>";try{var u=t(r);return u?"<"+u+">":"<...>"}catch{return"<...>"}}function n(){var r=Y.A;return r===null?null:r.getOwner()}function i(){return Error("react-stack-top-frame")}function o(r){if(J.call(r,"key")){var u=Object.getOwnPropertyDescriptor(r,"key").get;if(u&&u.isReactWarning)return!1}return r.key!==void 0}function l(r,u){function x(){M||(M=!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)",u))}x.isReactWarning=!0,Object.defineProperty(r,"key",{get:x,configurable:!0})}function f(){var r=t(this.type);return R[r]||(R[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 v(r,u,x,w,K,ne){var O=x.ref;return r={$$typeof:T,type:r,key:u,props:x,_owner:w},(O!==void 0?O:null)!==null?Object.defineProperty(r,"ref",{enumerable:!1,get:f}):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:K}),Object.defineProperty(r,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:ne}),Object.freeze&&(Object.freeze(r.props),Object.freeze(r)),r}function d(r,u,x,w,K,ne){var O=u.children;if(O!==void 0)if(w)if(te(O)){for(w=0;w<O.length;w++)S(O[w]);Object.freeze&&Object.freeze(O)}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 S(O);if(J.call(u,"key")){O=t(r);var $=Object.keys(u).filter(function(De){return De!=="key"});w=0<$.length?"{key: someKey, "+$.join(": ..., ")+": ...}":"{key: someKey}",I[O+w]||($=0<$.length?"{"+$.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} />`,w,O,$,O),I[O+w]=!0)}if(O=null,x!==void 0&&(e(x),O=""+x),o(u)&&(e(u.key),O=""+u.key),"key"in u){x={};for(var se in u)se!=="key"&&(x[se]=u[se])}else x=u;return O&&l(x,typeof r=="function"?r.displayName||r.name||"Unknown":r),v(r,O,x,n(),K,ne)}function S(r){c(r)?r._store&&(r._store.validated=1):typeof r=="object"&&r!==null&&r.$$typeof===V&&(r._payload.status==="fulfilled"?c(r._payload.value)&&r._payload.value._store&&(r._payload.value._store.validated=1):r._store&&(r._store.validated=1))}function c(r){return typeof r=="object"&&r!==null&&r.$$typeof===T}var m=j,T=Symbol.for("react.transitional.element"),g=Symbol.for("react.portal"),_=Symbol.for("react.fragment"),z=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),C=Symbol.for("react.consumer"),k=Symbol.for("react.context"),D=Symbol.for("react.forward_ref"),B=Symbol.for("react.suspense"),N=Symbol.for("react.suspense_list"),H=Symbol.for("react.memo"),V=Symbol.for("react.lazy"),q=Symbol.for("react.activity"),ee=Symbol.for("react.client.reference"),Y=m.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,J=Object.prototype.hasOwnProperty,te=Array.isArray,E=console.createTask?console.createTask:function(){return null};m={react_stack_bottom_frame:function(r){return r()}};var M,R={},y=m.react_stack_bottom_frame.bind(m,i)(),h=E(s(i)),I={};L.Fragment=_,L.jsx=function(r,u,x){var w=1e4>Y.recentlyCreatedOwnerStacks++;return d(r,u,x,!1,w?Error("react-stack-top-frame"):y,w?E(s(r)):h)},L.jsxs=function(r,u,x){var w=1e4>Y.recentlyCreatedOwnerStacks++;return d(r,u,x,!0,w?Error("react-stack-top-frame"):y,w?E(s(r)):h)}})()),L}var le;function xe(){return le||(le=1,process.env.NODE_ENV==="production"?G.exports=ye():G.exports=Ee()),G.exports}var b=xe();function Se(t){return t.data?Array.isArray(t.data)?{provider:"value",items:t.data}:t.data:t.staticData?{provider:"value",items:t.staticData}:t.objectName?{provider:"object",object:t.objectName}:null}function ae(t){if(!(!t||t.length===0))return typeof t[0]=="object"&&t[0]!==null,t}const ce=({schema:t,dataSource:a,onEdit:e,onDelete:s,onRowSelect:n,onRowClick:i,...o})=>{const[l,f]=j.useState([]),[v,d]=j.useState(!0),[S,c]=j.useState(null),[m,T]=j.useState(null),g=o.data,_=X.useDataScope(t.bind),z=Se(t),p=j.useMemo(()=>g&&Array.isArray(g)?{provider:"value",items:g}:_&&Array.isArray(_)?{provider:"value",items:_}:z,[JSON.stringify(z),_,g]),C=p?.provider==="value";j.useEffect(()=>{C&&p?.provider==="value"&&(f(E=>{const M=p.items;return JSON.stringify(E)!==JSON.stringify(M)?M:E}),d(!1))},[C,p]),j.useEffect(()=>{const E=async()=>{try{if(!a)throw new Error("DataSource required");const R=p?.provider==="object"&&"object"in p?p.object:t.objectName;if(!R)throw new Error("Object name required for object provider");const y=await a.getObjectSchema(R);T(y)}catch(R){c(R)}},M=ae(t.columns)||t.fields;C&&M?T({name:t.objectName,fields:{}}):t.objectName&&!C&&a&&E()},[t.objectName,t.columns,t.fields,a,C,p]);const k=j.useCallback(()=>{const E=ae(t.columns);if(E){if(E.length>0&&typeof E[0]=="object"&&E[0]!==null){const y=E[0];if("accessorKey"in y)return E;if("field"in y)return E.filter(h=>h?.field&&typeof h.field=="string").map(h=>({header:h.label||h.field.charAt(0).toUpperCase()+h.field.slice(1).replace(/_/g," "),accessorKey:h.field,...h.width&&{width:h.width},...h.align&&{align:h.align},sortable:h.sortable!==!1}))}return E.filter(y=>typeof y=="string"&&y.trim().length>0).map(y=>({header:m?.fields?.[y]?.label||y.charAt(0).toUpperCase()+y.slice(1).replace(/_/g," "),accessorKey:y}))}if(C){const y=p?.provider==="value"?p.items:[];if(y.length>0)return(t.fields||Object.keys(y[0])).map(I=>({header:I.charAt(0).toUpperCase()+I.slice(1).replace(/_/g," "),accessorKey:I}))}if(!m)return[];const M=[];return(t.fields||Object.keys(m.fields||{})).forEach(y=>{const h=m.fields?.[y];if(!h||h.permissions&&h.permissions.read===!1)return;const I=pe.getCellRenderer(h.type);M.push({header:h.label||y,accessorKey:y,cell:r=>b.jsx(I,{value:r,field:h}),sortable:h.sortable!==!1})}),M},[m,t.fields,t.columns,p,C]),D=j.useCallback(async()=>{if(!(C||!a)){d(!0);try{const E=p?.provider==="object"&&"object"in p?p.object:t.objectName;if(!E)throw new Error("Object name required for data fetching");const R={$select:(()=>{if(t.fields)return t.fields;if(t.columns&&Array.isArray(t.columns))return t.columns.map(h=>typeof h=="string"?h:h.field)})(),$top:t.pagination?.pageSize||t.pageSize||50};t.filter&&Array.isArray(t.filter)?R.$filter=t.filter:"defaultFilters"in t&&t.defaultFilters&&(R.$filter=t.defaultFilters),t.sort?typeof t.sort=="string"?R.$orderby=t.sort:Array.isArray(t.sort)&&(R.$orderby=t.sort.map(h=>`${h.field} ${h.order}`).join(", ")):"defaultSort"in t&&t.defaultSort&&(R.$orderby=`${t.defaultSort.field} ${t.defaultSort.order}`);const y=await a.find(E,R);f(y.data||[])}catch(E){c(E)}finally{d(!1)}}},[t,a,C,p]);if(j.useEffect(()=>{(m||C)&&D()},[m,C,D]),S)return b.jsxs("div",{className:"p-4 border border-red-300 bg-red-50 rounded-md",children:[b.jsx("h3",{className:"text-red-800 font-semibold",children:"Error loading grid"}),b.jsx("p",{className:"text-red-600 text-sm mt-1",children:S.message})]});if(v&&l.length===0)return b.jsxs("div",{className:"p-8 text-center",children:[b.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"}),b.jsx("p",{className:"mt-2 text-sm text-gray-600",children:"Loading grid..."})]});const B=k(),N="operations"in t?t.operations:void 0,H=N&&(N.update||N.delete),V=H?[...B,{header:"Actions",accessorKey:"_actions",cell:(E,M)=>b.jsxs(P.DropdownMenu,{children:[b.jsx(P.DropdownMenuTrigger,{asChild:!0,children:b.jsxs(P.Button,{variant:"ghost",size:"icon",className:"h-8 w-8",children:[b.jsx(Z.MoreVertical,{className:"h-4 w-4"}),b.jsx("span",{className:"sr-only",children:"Open menu"})]})}),b.jsxs(P.DropdownMenuContent,{align:"end",children:[N?.update&&e&&b.jsxs(P.DropdownMenuItem,{onClick:()=>e(M),children:[b.jsx(Z.Edit,{className:"mr-2 h-4 w-4"}),"Edit"]}),N?.delete&&s&&b.jsxs(P.DropdownMenuItem,{onClick:()=>s(M),children:[b.jsx(Z.Trash2,{className:"mr-2 h-4 w-4"}),"Delete"]})]})]}),sortable:!1}]:B;let q=!1;t.selection?.type?q=t.selection.type==="none"?!1:t.selection.type:t.selectable!==void 0&&(q=t.selectable);const ee=t.pagination!==void 0?!0:t.showPagination!==void 0?t.showPagination:!0,Y=t.pagination?.pageSize||t.pageSize||10,J=t.searchableFields!==void 0?t.searchableFields.length>0:t.showSearch!==void 0?t.showSearch:!0,te={type:"data-table",caption:t.label||t.title,columns:V,data:l,pagination:ee,pageSize:Y,searchable:J,selectable:q,sortable:!0,exportable:N?.export,rowActions:H,resizableColumns:t.resizable??t.resizableColumns??!0,reorderableColumns:t.reorderableColumns??!1,className:t.className,onSelectionChange:n,onRowClick:i};return b.jsx(X.SchemaRenderer,{schema:te})};function F(t,a,e){let s=e.initialDeps??[],n,i=!0;function o(){var l,f,v;let d;e.key&&((l=e.debug)!=null&&l.call(e))&&(d=Date.now());const S=t();if(!(S.length!==s.length||S.some((T,g)=>s[g]!==T)))return n;s=S;let m;if(e.key&&((f=e.debug)!=null&&f.call(e))&&(m=Date.now()),n=a(...S),e.key&&((v=e.debug)!=null&&v.call(e))){const T=Math.round((Date.now()-d)*100)/100,g=Math.round((Date.now()-m)*100)/100,_=g/16,z=(p,C)=>{for(p=String(p);p.length<C;)p=" "+p;return p};console.info(`%c⏱ ${z(g,5)} /${z(T,5)} ms`,`
|
|
7
|
+
font-size: .6rem;
|
|
8
|
+
font-weight: bold;
|
|
9
|
+
color: hsl(${Math.max(0,Math.min(120-120*_,120))}deg 100% 31%);`,e?.key)}return e?.onChange&&!(i&&e.skipInitialOnChange)&&e.onChange(n),i=!1,n}return o.updateDeps=l=>{s=l},o}function ue(t,a){if(t===void 0)throw new Error("Unexpected undefined");return t}const we=(t,a)=>Math.abs(t-a)<1.01,Oe=(t,a,e)=>{let s;return function(...n){t.clearTimeout(s),s=t.setTimeout(()=>a.apply(this,n),e)}},de=t=>{const{offsetWidth:a,offsetHeight:e}=t;return{width:a,height:e}},je=t=>t,_e=t=>{const a=Math.max(t.startIndex-t.overscan,0),e=Math.min(t.endIndex+t.overscan,t.count-1),s=[];for(let n=a;n<=e;n++)s.push(n);return s},Ce=(t,a)=>{const e=t.scrollElement;if(!e)return;const s=t.targetWindow;if(!s)return;const n=o=>{const{width:l,height:f}=o;a({width:Math.round(l),height:Math.round(f)})};if(n(de(e)),!s.ResizeObserver)return()=>{};const i=new s.ResizeObserver(o=>{const l=()=>{const f=o[0];if(f?.borderBoxSize){const v=f.borderBoxSize[0];if(v){n({width:v.inlineSize,height:v.blockSize});return}}n(de(e))};t.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(l):l()});return i.observe(e,{box:"border-box"}),()=>{i.unobserve(e)}},fe={passive:!0},he=typeof window>"u"?!0:"onscrollend"in window,Te=(t,a)=>{const e=t.scrollElement;if(!e)return;const s=t.targetWindow;if(!s)return;let n=0;const i=t.options.useScrollendEvent&&he?()=>{}:Oe(s,()=>{a(n,!1)},t.options.isScrollingResetDelay),o=d=>()=>{const{horizontal:S,isRtl:c}=t.options;n=S?e.scrollLeft*(c&&-1||1):e.scrollTop,i(),a(n,d)},l=o(!0),f=o(!1);e.addEventListener("scroll",l,fe);const v=t.options.useScrollendEvent&&he;return v&&e.addEventListener("scrollend",f,fe),()=>{e.removeEventListener("scroll",l),v&&e.removeEventListener("scrollend",f)}},Ae=(t,a,e)=>{if(a?.borderBoxSize){const s=a.borderBoxSize[0];if(s)return Math.round(s[e.options.horizontal?"inlineSize":"blockSize"])}return t[e.options.horizontal?"offsetWidth":"offsetHeight"]},Re=(t,{adjustments:a=0,behavior:e},s)=>{var n,i;const o=t+a;(i=(n=s.scrollElement)==null?void 0:n.scrollTo)==null||i.call(n,{[s.options.horizontal?"left":"top"]:o,behavior:e})};class Me{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(n=>{n.forEach(i=>{const o=()=>{this._measureElement(i.target,i)};this.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(o):o()})}));return{disconnect:()=>{var n;(n=s())==null||n.disconnect(),e=null},observe:n=>{var i;return(i=s())==null?void 0:i.observe(n,{box:"border-box"})},unobserve:n=>{var i;return(i=s())==null?void 0:i.unobserve(n)}}})(),this.range=null,this.setOptions=e=>{Object.entries(e).forEach(([s,n])=>{typeof n>"u"&&delete e[s]}),this.options={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:je,rangeExtractor:_e,onChange:()=>{},measureElement:Ae,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,n;(n=(s=this.options).onChange)==null||n.call(s,this,e)},this.maybeNotify=F(()=>(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(n=>{this.observer.observe(n)}),this.unsubs.push(this.options.observeElementRect(this,n=>{this.scrollRect=n,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(n,i)=>{this.scrollAdjustments=0,this.scrollDirection=i?this.getScrollOffset()<n?"forward":"backward":null,this.scrollOffset=n,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 n=new Map,i=new Map;for(let o=s-1;o>=0;o--){const l=e[o];if(n.has(l.lane))continue;const f=i.get(l.lane);if(f==null||l.end>f.end?i.set(l.lane,l):l.end<f.end&&n.set(l.lane,!0),n.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=F(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes],(e,s,n,i,o,l)=>(this.prevLanes!==void 0&&this.prevLanes!==l&&(this.lanesChangedFlag=!0),this.prevLanes=l,this.pendingMeasuredCacheIndexes=[],{count:e,paddingStart:s,scrollMargin:n,getItemKey:i,enabled:o,lanes:l}),{key:!1}),this.getMeasurements=F(()=>[this.getMeasurementOptions(),this.itemSizeCache],({count:e,paddingStart:s,scrollMargin:n,getItemKey:i,enabled:o,lanes:l},f)=>{if(!o)return this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>e)for(const c of this.laneAssignments.keys())c>=e&&this.laneAssignments.delete(c);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(c=>{this.itemSizeCache.set(c.key,c.size)}));const v=this.lanesSettling?0:this.pendingMeasuredCacheIndexes.length>0?Math.min(...this.pendingMeasuredCacheIndexes):0;this.pendingMeasuredCacheIndexes=[],this.lanesSettling&&this.measurementsCache.length===e&&(this.lanesSettling=!1);const d=this.measurementsCache.slice(0,v),S=new Array(l).fill(void 0);for(let c=0;c<v;c++){const m=d[c];m&&(S[m.lane]=c)}for(let c=v;c<e;c++){const m=i(c),T=this.laneAssignments.get(c);let g,_;if(T!==void 0&&this.options.lanes>1){g=T;const k=S[g],D=k!==void 0?d[k]:void 0;_=D?D.end+this.options.gap:s+n}else{const k=this.options.lanes===1?d[c-1]:this.getFurthestMeasurement(d,c);_=k?k.end+this.options.gap:s+n,g=k?k.lane:c%this.options.lanes,this.options.lanes>1&&this.laneAssignments.set(c,g)}const z=f.get(m),p=typeof z=="number"?z:this.options.estimateSize(c),C=_+p;d[c]={index:c,start:_,size:p,end:C,key:m,lane:g},S[g]=c}return this.measurementsCache=d,d},{key:process.env.NODE_ENV!=="production"&&"getMeasurements",debug:()=>this.options.debug}),this.calculateRange=F(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes],(e,s,n,i)=>this.range=e.length>0&&s>0?ze({measurements:e,outerSize:s,scrollOffset:n,lanes:i}):null,{key:process.env.NODE_ENV!=="production"&&"calculateRange",debug:()=>this.options.debug}),this.getVirtualIndexes=F(()=>{let e=null,s=null;const n=this.calculateRange();return n&&(e=n.startIndex,s=n.endIndex),this.maybeNotify.updateDeps([this.isScrolling,e,s]),[this.options.rangeExtractor,this.options.overscan,this.options.count,e,s]},(e,s,n,i,o)=>i===null||o===null?[]:e({startIndex:i,endIndex:o,overscan:s,count:n}),{key:process.env.NODE_ENV!=="production"&&"getVirtualIndexes",debug:()=>this.options.debug}),this.indexFromElement=e=>{const s=this.options.indexAttribute,n=e.getAttribute(s);return n?parseInt(n,10):(console.warn(`Missing attribute name '${s}={index}' on measured element.`),-1)},this._measureElement=(e,s)=>{const n=this.indexFromElement(e),i=this.measurementsCache[n];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(n,this.options.measureElement(e,s,this))},this.resizeItem=(e,s)=>{const n=this.measurementsCache[e];if(!n)return;const i=this.itemSizeCache.get(n.key)??n.size,o=s-i;o!==0&&((this.shouldAdjustScrollPositionOnItemSizeChange!==void 0?this.shouldAdjustScrollPositionOnItemSizeChange(n,o,this):n.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(n.index),this.itemSizeCache=new Map(this.itemSizeCache.set(n.key,s)),this.notify(!1))},this.measureElement=e=>{if(!e){this.elementsCache.forEach((s,n)=>{s.isConnected||(this.observer.unobserve(s),this.elementsCache.delete(n))});return}this._measureElement(e,void 0)},this.getVirtualItems=F(()=>[this.getVirtualIndexes(),this.getMeasurements()],(e,s)=>{const n=[];for(let i=0,o=e.length;i<o;i++){const l=e[i],f=s[l];n.push(f)}return n},{key:process.env.NODE_ENV!=="production"&&"getVirtualItems",debug:()=>this.options.debug}),this.getVirtualItemForOffset=e=>{const s=this.getMeasurements();if(s.length!==0)return ue(s[me(0,s.length-1,n=>ue(s[n]).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,n=0)=>{if(!this.scrollElement)return 0;const i=this.getSize(),o=this.getScrollOffset();s==="auto"&&(s=e>=o+i?"end":"start"),s==="center"?e+=(n-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 n=this.measurementsCache[e];if(!n)return;const i=this.getSize(),o=this.getScrollOffset();if(s==="auto")if(n.end>=o+i-this.options.scrollPaddingEnd)s="end";else if(n.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"?n.end+this.options.scrollPaddingEnd:n.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(l,s,n.size),s]},this.isDynamicMode=()=>this.elementsCache.size>0,this.scrollToOffset=(e,{align:s="start",behavior:n}={})=>{n==="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:n})},this.scrollToIndex=(e,{align:s="auto",behavior:n}={})=>{n==="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=v=>{if(!this.targetWindow)return;const d=this.getOffsetForIndex(e,v);if(!d){console.warn("Failed to get offset for index:",e);return}const[S,c]=d;this._scrollToOffset(S,{adjustments:void 0,behavior:n}),this.targetWindow.requestAnimationFrame(()=>{const m=()=>{if(this.currentScrollToIndex!==e)return;const T=this.getScrollOffset(),g=this.getOffsetForIndex(e,c);if(!g){console.warn("Failed to get offset for index:",e);return}we(g[0],T)||f(c)};this.isDynamicMode()?this.targetWindow.requestAnimationFrame(m):m()})},f=v=>{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(v))):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 n;if(s.length===0)n=this.options.paddingStart;else if(this.options.lanes===1)n=((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--}n=Math.max(...i.filter(l=>l!==null))}return Math.max(n-this.options.scrollMargin+this.options.paddingEnd,0)},this._scrollToOffset=(e,{adjustments:s,behavior:n})=>{this.options.scrollToFn(e,{behavior:n,adjustments:s},this)},this.measure=()=>{this.itemSizeCache=new Map,this.laneAssignments=new Map,this.notify(!1)},this.setOptions(a)}}const me=(t,a,e,s)=>{for(;t<=a;){const n=(t+a)/2|0,i=e(n);if(i<s)t=n+1;else if(i>s)a=n-1;else return n}return t>0?t-1:0};function ze({measurements:t,outerSize:a,scrollOffset:e,lanes:s}){const n=t.length-1,i=f=>t[f].start;if(t.length<=s)return{startIndex:0,endIndex:n};let o=me(0,n,i,e),l=o;if(s===1)for(;l<n&&t[l].end<e+a;)l++;else if(s>1){const f=Array(s).fill(0);for(;l<n&&f.some(d=>d<e+a);){const d=t[l];f[d.lane]=d.end,l++}const v=Array(s).fill(e+a);for(;o>=0&&v.some(d=>d>=e);){const d=t[o];v[d.lane]=d.start,o--}o=Math.max(0,o-o%s),l=Math.min(n,l+(s-1-l%s))}return{startIndex:o,endIndex:l}}const ge=typeof document<"u"?U.useLayoutEffect:U.useEffect;function ke({useFlushSync:t=!0,...a}){const e=U.useReducer(()=>({}),{})[1],s={...a,onChange:(i,o)=>{var l;t&&o?be.flushSync(e):e(),(l=a.onChange)==null||l.call(a,i,o)}},[n]=U.useState(()=>new Me(s));return n.setOptions(s),ge(()=>n._didMount(),[]),ge(()=>n._willUpdate()),n}function Ne(t){return ke({observeElementRect:Ce,observeElementOffset:Te,scrollToFn:Re,...t})}const Ie=({data:t,columns:a,rowHeight:e=40,height:s=600,className:n="",headerClassName:i="",rowClassName:o,onRowClick:l,overscan:f=5})=>{const v=j.useRef(null),d=Ne({count:t.length,getScrollElement:()=>v.current,estimateSize:()=>e,overscan:f}),S=d.getVirtualItems();return b.jsxs("div",{className:n,children:[b.jsx("div",{className:`grid border-b sticky top-0 bg-background z-10 ${i}`,style:{gridTemplateColumns:a.map(c=>c.width||"1fr").join(" ")},children:a.map((c,m)=>b.jsx("div",{className:`px-4 py-2 font-semibold text-sm ${c.align==="center"?"text-center":c.align==="right"?"text-right":"text-left"}`,children:c.header},m))}),b.jsx("div",{ref:v,className:"overflow-auto",style:{height:typeof s=="number"?`${s}px`:s,contain:"strict"},children:b.jsx("div",{style:{height:`${d.getTotalSize()}px`,width:"100%",position:"relative"},children:S.map(c=>{const m=t[c.index],T=typeof o=="function"?o(m,c.index):o||"";return b.jsx("div",{className:`grid border-b hover:bg-muted/50 cursor-pointer ${T}`,style:{position:"absolute",top:0,left:0,width:"100%",height:`${c.size}px`,transform:`translateY(${c.start}px)`,gridTemplateColumns:a.map(g=>g.width||"1fr").join(" ")},onClick:()=>l?.(m,c.index),children:a.map((g,_)=>{const z=m[g.accessorKey],p=g.cell?g.cell(z,m):z;return b.jsx("div",{className:`px-4 py-2 text-sm flex items-center ${g.align==="center"?"text-center justify-center":g.align==="right"?"text-right justify-end":"text-left justify-start"}`,children:p},_)})},c.key)})})}),b.jsxs("div",{className:"px-4 py-2 text-xs text-muted-foreground border-t",children:["Showing ",S.length," of ",t.length," rows (virtual scrolling enabled)"]})]})},Q=({schema:t,...a})=>{const{dataSource:e}=X.useSchemaContext()||{};return b.jsx(ce,{schema:t,dataSource:e,...a})};re.ComponentRegistry.register("object-grid",Q,{namespace:"plugin-grid",label:"Object Grid",category:"plugin"}),re.ComponentRegistry.register("grid",Q,{namespace:"view",label:"Data Grid",category:"view"}),A.ObjectGrid=ce,A.ObjectGridRenderer=Q,A.VirtualGrid=Ie,Object.defineProperty(A,Symbol.toStringTag,{value:"Module"})}));
|
|
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>;
|
|
@@ -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": "0.5.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": "0.5.0",
|
|
21
|
+
"@object-ui/core": "0.5.0",
|
|
22
|
+
"@object-ui/fields": "0.5.0",
|
|
23
|
+
"@object-ui/react": "0.5.0",
|
|
24
|
+
"@object-ui/types": "0.5.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.7",
|
|
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": "0.5.0"
|
|
34
37
|
},
|
|
35
38
|
"scripts": {
|
|
36
39
|
"build": "vite build",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { ObjectGrid } from './ObjectGrid';
|
|
5
|
+
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
6
|
+
import { setupServer } from 'msw/node';
|
|
7
|
+
import { http, HttpResponse } from 'msw';
|
|
8
|
+
import { registerAllFields } from '@object-ui/fields';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { ContactObject } from '../../../examples/crm/src/objects/contact.object';
|
|
11
|
+
|
|
12
|
+
registerAllFields();
|
|
13
|
+
|
|
14
|
+
const BASE_URL = process.env.OBJECTSTACK_API_URL || 'http://localhost';
|
|
15
|
+
|
|
16
|
+
// --- Mock Data ---
|
|
17
|
+
|
|
18
|
+
const mockSchema = ContactObject;
|
|
19
|
+
|
|
20
|
+
const mockData = {
|
|
21
|
+
value: [
|
|
22
|
+
{ _id: '1', name: 'John Doe', email: 'john@example.com', company: 'Acme Inc' },
|
|
23
|
+
{ _id: '2', name: 'Jane Smith', email: 'jane@test.com', company: 'Globex' },
|
|
24
|
+
{ _id: '3', name: 'Bob Wilson', email: 'bob@test.com', company: 'Acme Inc' }
|
|
25
|
+
],
|
|
26
|
+
count: 3,
|
|
27
|
+
'@odata.count': 3
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// --- MSW Setup ---
|
|
31
|
+
|
|
32
|
+
const handlers = [
|
|
33
|
+
// OPTIONS handler for CORS preflight
|
|
34
|
+
http.options(`${BASE_URL}/*`, () => {
|
|
35
|
+
return new HttpResponse(null, {
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: {
|
|
38
|
+
'Access-Control-Allow-Origin': '*',
|
|
39
|
+
'Access-Control-Allow-Methods': 'GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH',
|
|
40
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
// Health check / Connection check
|
|
46
|
+
http.get(`${BASE_URL}/api/v1`, () => {
|
|
47
|
+
return HttpResponse.json({ status: 'ok', version: '1.0.0' });
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
// Schema: /api/v1/meta/object/:name
|
|
51
|
+
http.get(`${BASE_URL}/api/v1/meta/object/contact`, () => {
|
|
52
|
+
return HttpResponse.json(mockSchema);
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
// Data Query: /api/v1/data/contact
|
|
56
|
+
http.get(`${BASE_URL}/api/v1/data/contact`, () => {
|
|
57
|
+
return HttpResponse.json(mockData);
|
|
58
|
+
})
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const server = setupServer(...handlers);
|
|
62
|
+
|
|
63
|
+
// --- Test Suite ---
|
|
64
|
+
|
|
65
|
+
describe('ObjectGrid with ObjectStack/MSW', () => {
|
|
66
|
+
// Only start MSW if we are NOT using a real server
|
|
67
|
+
if (!process.env.OBJECTSTACK_API_URL) {
|
|
68
|
+
beforeAll(() => server.listen());
|
|
69
|
+
afterEach(() => server.resetHandlers());
|
|
70
|
+
afterAll(() => server.close());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dataSource = new ObjectStackAdapter({
|
|
74
|
+
baseUrl: BASE_URL,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('loads schema and data rows', async () => {
|
|
78
|
+
render(
|
|
79
|
+
<ObjectGrid
|
|
80
|
+
schema={{
|
|
81
|
+
type: 'object-grid',
|
|
82
|
+
objectName: 'contact',
|
|
83
|
+
columns: ['name', 'email', 'company'] // Explicit columns or auto-derived
|
|
84
|
+
}}
|
|
85
|
+
dataSource={dataSource}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Verify Column Headers (from schema or props)
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
// Changed from 'Full Name' to 'Name' to match CRM example schema
|
|
92
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
95
|
+
expect(screen.getByText('Company')).toBeInTheDocument();
|
|
96
|
+
|
|
97
|
+
// Verify Data Rows
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
expect(screen.getByText('jane@test.com')).toBeInTheDocument();
|
|
102
|
+
expect(screen.getByText('Globex')).toBeInTheDocument();
|
|
103
|
+
|
|
104
|
+
// Check Row Count (if grid displays it)
|
|
105
|
+
// expect(screen.getByText(/3 records/i)).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/ObjectGrid.tsx
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
25
25
|
import type { ObjectGridSchema, DataSource, ListColumn, ViewData } from '@object-ui/types';
|
|
26
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
26
|
+
import { SchemaRenderer, useDataScope } from '@object-ui/react';
|
|
27
27
|
import { getCellRenderer } from '@object-ui/fields';
|
|
28
28
|
import { Button } from '@object-ui/components';
|
|
29
29
|
import {
|
|
@@ -53,6 +53,15 @@ export interface ObjectGridProps {
|
|
|
53
53
|
function getDataConfig(schema: ObjectGridSchema): ViewData | null {
|
|
54
54
|
// New format: explicit data configuration
|
|
55
55
|
if (schema.data) {
|
|
56
|
+
// Check if data is an array (shorthand format) or already a ViewData object
|
|
57
|
+
if (Array.isArray(schema.data)) {
|
|
58
|
+
// Convert array shorthand to proper ViewData format
|
|
59
|
+
return {
|
|
60
|
+
provider: 'value',
|
|
61
|
+
items: schema.data,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Already in ViewData format
|
|
56
65
|
return schema.data;
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -99,20 +108,55 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
99
108
|
onEdit,
|
|
100
109
|
onDelete,
|
|
101
110
|
onRowSelect,
|
|
111
|
+
onRowClick,
|
|
112
|
+
...rest
|
|
102
113
|
}) => {
|
|
103
114
|
const [data, setData] = useState<any[]>([]);
|
|
104
115
|
const [loading, setLoading] = useState(true);
|
|
105
116
|
const [error, setError] = useState<Error | null>(null);
|
|
106
117
|
const [objectSchema, setObjectSchema] = useState<any>(null);
|
|
107
118
|
|
|
119
|
+
// Check if data is passed directly (from ListView)
|
|
120
|
+
const passedData = (rest as any).data;
|
|
121
|
+
|
|
122
|
+
// Resolve bound data if 'bind' property exists
|
|
123
|
+
const boundData = useDataScope(schema.bind);
|
|
124
|
+
|
|
108
125
|
// Get data configuration (supports both new and legacy formats)
|
|
109
|
-
const
|
|
126
|
+
const rawDataConfig = getDataConfig(schema);
|
|
127
|
+
// Memoize dataConfig using deep comparison to prevent infinite loops
|
|
128
|
+
const dataConfig = React.useMemo(() => {
|
|
129
|
+
// If we have passed data (highest priority), treat it as value provider
|
|
130
|
+
if (passedData && Array.isArray(passedData)) {
|
|
131
|
+
return {
|
|
132
|
+
provider: 'value',
|
|
133
|
+
items: passedData
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If we have bound data, it takes precedence as inline value
|
|
138
|
+
if (boundData && Array.isArray(boundData)) {
|
|
139
|
+
return {
|
|
140
|
+
provider: 'value',
|
|
141
|
+
items: boundData
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return rawDataConfig;
|
|
145
|
+
}, [JSON.stringify(rawDataConfig), boundData, passedData]);
|
|
146
|
+
|
|
110
147
|
const hasInlineData = dataConfig?.provider === 'value';
|
|
111
148
|
|
|
112
149
|
useEffect(() => {
|
|
113
150
|
if (hasInlineData && dataConfig?.provider === 'value') {
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
// Only update if data is different to avoid infinite loop
|
|
152
|
+
setData(prev => {
|
|
153
|
+
const newItems = dataConfig.items as any[];
|
|
154
|
+
if (JSON.stringify(prev) !== JSON.stringify(newItems)) {
|
|
155
|
+
return newItems;
|
|
156
|
+
}
|
|
157
|
+
return prev;
|
|
158
|
+
});
|
|
159
|
+
setLoading(false);
|
|
116
160
|
}
|
|
117
161
|
}, [hasInlineData, dataConfig]);
|
|
118
162
|
|
|
@@ -124,7 +168,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
// For object provider, get the object name
|
|
127
|
-
const objectName = dataConfig?.provider === 'object'
|
|
171
|
+
const objectName = dataConfig?.provider === 'object' && 'object' in dataConfig
|
|
128
172
|
? dataConfig.object
|
|
129
173
|
: schema.objectName;
|
|
130
174
|
|
|
@@ -154,26 +198,40 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
154
198
|
const cols = normalizeColumns(schema.columns);
|
|
155
199
|
|
|
156
200
|
if (cols) {
|
|
157
|
-
//
|
|
201
|
+
// Check if columns are already in data-table format (have 'accessorKey')
|
|
202
|
+
// vs ListColumn format (have 'field')
|
|
158
203
|
if (cols.length > 0 && typeof cols[0] === 'object' && cols[0] !== null) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
const firstCol = cols[0] as any;
|
|
205
|
+
|
|
206
|
+
// Already in data-table format - use as-is
|
|
207
|
+
if ('accessorKey' in firstCol) {
|
|
208
|
+
return cols;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ListColumn format - convert to data-table format
|
|
212
|
+
if ('field' in firstCol) {
|
|
213
|
+
return (cols as ListColumn[])
|
|
214
|
+
.filter((col) => col?.field && typeof col.field === 'string') // Filter out invalid column objects
|
|
215
|
+
.map((col) => ({
|
|
216
|
+
header: col.label || col.field.charAt(0).toUpperCase() + col.field.slice(1).replace(/_/g, ' '),
|
|
217
|
+
accessorKey: col.field,
|
|
218
|
+
...(col.width && { width: col.width }),
|
|
219
|
+
...(col.align && { align: col.align }),
|
|
220
|
+
sortable: col.sortable !== false,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
168
223
|
}
|
|
169
224
|
|
|
170
225
|
// String array format - filter out invalid entries
|
|
171
226
|
return (cols as string[])
|
|
172
227
|
.filter((fieldName) => typeof fieldName === 'string' && fieldName.trim().length > 0)
|
|
173
|
-
.map((fieldName) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
.map((fieldName) => {
|
|
229
|
+
const fieldLabel = objectSchema?.fields?.[fieldName]?.label;
|
|
230
|
+
return {
|
|
231
|
+
header: fieldLabel || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/_/g, ' '),
|
|
232
|
+
accessorKey: fieldName,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
177
235
|
}
|
|
178
236
|
|
|
179
237
|
// Legacy support: use 'fields' if columns not provided
|
|
@@ -217,7 +275,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
217
275
|
setLoading(true);
|
|
218
276
|
try {
|
|
219
277
|
// Get object name from data config or schema
|
|
220
|
-
const objectName = dataConfig?.provider === 'object'
|
|
278
|
+
const objectName = dataConfig?.provider === 'object' && 'object' in dataConfig
|
|
221
279
|
? dataConfig.object
|
|
222
280
|
: schema.objectName;
|
|
223
281
|
|
|
@@ -321,7 +379,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
321
379
|
</DropdownMenuItem>
|
|
322
380
|
)}
|
|
323
381
|
{operations?.delete && onDelete && (
|
|
324
|
-
<DropdownMenuItem
|
|
382
|
+
<DropdownMenuItem onClick={() => onDelete(row)}>
|
|
325
383
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
326
384
|
Delete
|
|
327
385
|
</DropdownMenuItem>
|
|
@@ -369,8 +427,10 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
|
|
|
369
427
|
exportable: operations?.export,
|
|
370
428
|
rowActions: hasActions,
|
|
371
429
|
resizableColumns: schema.resizable ?? schema.resizableColumns ?? true,
|
|
430
|
+
reorderableColumns: schema.reorderableColumns ?? false,
|
|
372
431
|
className: schema.className,
|
|
373
432
|
onSelectionChange: onRowSelect,
|
|
433
|
+
onRowClick: onRowClick,
|
|
374
434
|
};
|
|
375
435
|
|
|
376
436
|
return <SchemaRenderer schema={dataTableSchema} />;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { VirtualGrid } from './VirtualGrid';
|
|
11
|
+
|
|
12
|
+
describe('VirtualGrid', () => {
|
|
13
|
+
it('should be exported', () => {
|
|
14
|
+
expect(VirtualGrid).toBeDefined();
|
|
15
|
+
expect(typeof VirtualGrid).toBe('function');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should have the correct display name', () => {
|
|
19
|
+
// Verify it's a React component
|
|
20
|
+
expect(VirtualGrid.name).toBe('VirtualGrid');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|