diff --git a/vuu-ui/cypress.config.ts b/vuu-ui/cypress.config.ts index d0e74de28..47eb6a986 100644 --- a/vuu-ui/cypress.config.ts +++ b/vuu-ui/cypress.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ viewportHeight: 1024, video: false, component: { + scrollBehavior: false, setupNodeEvents(on, config) { // installCoverageTask(on, config); //Setting up a log task to allow logging to the console during an axe test because console.log() does not work directly in a test diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts index c39de166d..73c3ec499 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts @@ -504,8 +504,8 @@ export class ArrayDataSource private setRange(range: VuuRange, forceFullRefresh = false) { if (range.from !== this.#range.from || range.to !== this.#range.to) { this.#range = range; - this.keys.reset(range); - this.sendRowsToClient(forceFullRefresh); + const keysResequenced = this.keys.reset(range); + this.sendRowsToClient(forceFullRefresh || keysResequenced); } else if (forceFullRefresh) { this.sendRowsToClient(forceFullRefresh); } diff --git a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js index 74da92e03..dced23288 100644 --- a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js @@ -1,8 +1,10 @@ export const workerSourceCode = ` -var ie=(r,e,t)=>{if(!e.has(r))throw TypeError("Cannot "+t)};var m=(r,e,t)=>(ie(r,e,"read from private field"),t?t.call(r):e.get(r)),ae=(r,e,t)=>{if(e.has(r))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(r):e.set(r,t)},ue=(r,e,t,s)=>(ie(r,e,"write to private field"),s?s.call(r,t):e.set(r,t),t);function le(r,e,t=[],s=[]){for(let n=0,o=r.length;n{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(s=>s.startsWith(\`\${r}=\`)))==null?void 0:t.split("=")[1]};function G({from:r,to:e},t=0,s=Number.MAX_SAFE_INTEGER){if(t===0)return sr>=e&&r=this.to||tr.type==="connection-status",de=r=>r.type==="connection-metrics";var ge=r=>"viewport"in r;var Qe=["error","warn","info","debug"],Ye=r=>typeof r=="string"&&Qe.includes(r),Ze="error",P=()=>{},Xe="error",{loggingLevel:L=Xe}=et(),b=r=>{let e=L==="debug",t=e||L==="info",s=t||L==="warn",n=s||L==="error",o=t?p=>console.info(\`[\${r}] \${p}\`):P,i=s?p=>console.warn(\`[\${r}] \${p}\`):P,u=e?p=>console.debug(\`[\${r}] \${p}\`):P;return{errorEnabled:n,error:n?p=>console.error(\`[\${r}] \${p}\`):P}};function et(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:tt()}}function tt(){let r=ce("vuu-logging-level");return Ye(r)?r:Ze}var{debug:st,debugEnabled:nt}=b("range-monitor"),k=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:s}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),s)nt&&st(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-s).toFixed(0)} ms elapsed\`);else return 0}};var O=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.shift():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((n,o)=>{(o=t)&&(this.free.push(n),this.keys.delete(o))});let s=t-e;this.keys.size+this.free.length>s&&(this.free.length=Math.max(0,s-this.keys.size));for(let n=e;nthis.keys.size&&(this.nextKeyValue=this.keys.size)}keyFor(e){let t=this.keys.get(e);if(t===void 0)throw console.log(\`key not found +var ie=(r,e,t)=>{if(!e.has(r))throw TypeError("Cannot "+t)};var m=(r,e,t)=>(ie(r,e,"read from private field"),t?t.call(r):e.get(r)),ae=(r,e,t)=>{if(e.has(r))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(r):e.set(r,t)},ue=(r,e,t,n)=>(ie(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t);function le(r,e,t=[],n=[]){for(let s=0,o=r.length;s{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${r}=\`)))==null?void 0:t.split("=")[1]};function G({from:r,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(r===0&&e===0)return{from:r,to:e};if(t===0)return nr>=e&&r=this.to||tr.type==="connection-status",de=r=>r.type==="connection-metrics";var ge=r=>"viewport"in r;var Qe=["error","warn","info","debug"],Ye=r=>typeof r=="string"&&Qe.includes(r),Ze="error",P=()=>{},Xe="error",{loggingLevel:L=Xe}=et(),b=r=>{let e=L==="debug",t=e||L==="info",n=t||L==="warn",s=n||L==="error",o=t?p=>console.info(\`[\${r}] \${p}\`):P,i=n?p=>console.warn(\`[\${r}] \${p}\`):P,u=e?p=>console.debug(\`[\${r}] \${p}\`):P;return{errorEnabled:s,error:s?p=>console.error(\`[\${r}] \${p}\`):P}};function et(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:tt()}}function tt(){let r=ce("vuu-logging-level");return Ye(r)?r:Ze}var{debug:nt,debugEnabled:st}=b("range-monitor"),k=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:n}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),n)st&&nt(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};var rt=[],O=class{constructor(e){this.keys=new Map;this.nextKeyValue=0;this.range=e,this.init(e)}next(e=rt){return e.length>0?e.shift():this.nextKeyValue++}init({from:e,to:t}){this.keys.clear(),this.nextKeyValue=0;for(let n=e;ns)return this.init(e);let i=[];this.keys.forEach((u,l)=>{(l=n)&&(i.push(u),this.keys.delete(l))});for(let u=t;u\`\${e}=>\${t}\`).join(",")}};var{SELECTED:\$t}=\$,w={False:0,True:1,First:2,Last:4};var rt=(r,e)=>e>=r[0]&&e<=r[1],ot=w.True+w.First+w.Last,it=w.True+w.First,at=w.True+w.Last,z=(r,e)=>{for(let t of r)if(typeof t=="number"){if(t===e)return ot}else if(rt(t,e))return e===t[0]?it:e===t[1]?at:w.True;return w.False};var he=r=>{if(r.every(t=>typeof t=="number"))return r;let e=[];for(let t of r)if(typeof t=="number")e.push(t);else for(let s=t[0];s<=t[1];s++)e.push(s);return e};var fe=r=>r.type==="VIEW_PORT_MENU_RESP"&&r.action!==null&&x(r.action.table),x=r=>r!==null&&typeof r=="object"&&"table"in r&&"module"in r?r.table.startsWith("session"):!1;var ut=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],me=r=>ut.includes(r.type),Ce=r=>r.type==="VIEW_PORT_RPC_CALL",A=({requestId:r,...e})=>[r,e],Re=r=>{let e=r.at(0);if(e.updateType==="SIZE"){if(r.length===1)return r;e=r.at(1)}let t=r.at(-1);return[e,t]},Se=r=>{let e={};for(let t of r)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e},H=({columns:r,dataTypes:e,key:t,table:s})=>({table:s,columns:r.map((n,o)=>({name:n,serverDataType:e[o]})),key:t});var Te="CHANGE_VP_SUCCESS";var be="CLOSE_TREE_NODE",Ee="CLOSE_TREE_SUCCESS";var we="CREATE_VP",Ve="DISABLE_VP",ve="DISABLE_VP_SUCCESS";var ye="ENABLE_VP",Me="ENABLE_VP_SUCCESS";var K="GET_VP_VISUAL_LINKS",_e="GET_VIEW_PORT_MENUS";var Ie="HB",De="HB_RESP",Pe="LOGIN",Le="OPEN_TREE_NODE",ke="OPEN_TREE_SUCCESS";var Oe="REMOVE_VP";var xe="SET_SELECTION_SUCCESS";var Ne=r=>{switch(r){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var Ue=[],R=b("array-backed-moving-window");function lt(r,e){if(!e||e.data.length!==r.data.length||e.sel!==r.sel)return!1;for(let t=0;t{var t;if((t=R.info)==null||t.call(R,\`setRowCount \${e}\`),e{let s=this.bufferSize*.25;return m(this,h).to-t0&&e-m(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:s,to:n}=this.range;if(t=n)return!0}setAtIndex(e){let{rowIndex:t}=e,s=t-m(this,h).from;if(lt(e,this.internalData[s]))return!1;let n=this.isWithinClientRange(t);return(n||this.isWithinRange(t))&&(!this.internalData[s]&&n&&(this.rowsWithinRange+=1),this.internalData[s]=e),n}getAtIndex(e){return m(this,h).isWithin(e)&&this.internalData[e-m(this,h).from]!=null?this.internalData[e-m(this,h).from]:void 0}isWithinRange(e){return m(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=R.debug)==null||p.call(R,\`setClientRange \${e} - \${t}\`);let s=this.clientRange.from,n=Math.min(this.clientRange.to,this.rowCount);if(e===s&&t===n)return[!1,Ue];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let a=e;ao.to){let a=Math.max(e,o.to);i=this.internalData.slice(a-u,t-u)}else{let a=Math.min(o.from,t);i=this.internalData.slice(e-u,a-u)}return[this.bufferBreakout(e,t),i]}setRange(e,t){var s,n;if(e!==m(this,h).from||t!==m(this,h).to){(s=R.debug)==null||s.call(R,\`setRange \${e} - \${t}\`);let[o,i]=m(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let c=o;c=0;o--)if(e[o]!==void 0){n=e[o];break}return s&&n?[s.rowIndex,n.rowIndex]:[-1,-1]}};h=new WeakMap;var ct=[],{debug:f,debugEnabled:U,error:pt,info:d,infoEnabled:dt,warn:y}=b("viewport"),gt=({rowKey:r,updateType:e})=>e==="U"&&!r.startsWith("\$root"),W=[void 0,void 0],ht={count:0,mode:void 0,size:0,ts:0},q=class{constructor({aggregations:e,bufferSize:t=50,columns:s,filter:n,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},g){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=ht;this.updateThrottleTimer=void 0;this.rangeMonitor=new k("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:s}=this.lastUpdateStatus,n=0;if(s===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,n=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,n=0;return this.lastUpdateStatus.mode=e,n};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,s=t*.25,{from:n}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(n>=o&&n{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(d==null||d("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=a,this.columns=s,this.filter=n,this.groupBy=o,this.keys=new O(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,dt&&(d==null||d(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new N(this.clientRange,u,this.bufferSize),this.postMessageToClient=g}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:we,table:this.table,range:G(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:s,filterSpec:n,range:o,sort:i,groupBy:u},c){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=s,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:s,filter:n,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:s,pendingOperations:n}=this,o=n.get(e);if(!o){pt(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(d==null||d(\`completeOperation \${i}\`),n.delete(e),i==="CHANGE_VP_RANGE"){let[c,p]=t;(u=this.dataWindow)==null||u.setRange(c,p);for(let a=this.pendingRangeRequests.length-1;a>=0;a--){let l=this.pendingRangeRequests[a];if(l.requestId===e){l.acked=!0;break}else y==null||y("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:g}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=g,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),f==null||f(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:s,type:i,config:o.data}}else{if(i==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,f==null||f(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:s,type:i,groupBy:o.data};if(i==="columns")return this.columns=o.data,{clientViewportId:s,type:i,columns:o.data};if(i==="filter")return this.filter=o.data,{clientViewportId:s,type:i,filter:o.data};if(i==="aggregate")return this.aggregations=o.data,{clientViewportId:s,type:"aggregate",aggregations:this.aggregations};if(i==="sort")return this.sort=o.data,{clientViewportId:s,type:i,sort:this.sort};if(i!=="selection"){if(i==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:s};if(i==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:s};if(i==="CREATE_VISUAL_LINK"){let[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:s,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:s}}}}rangeRequest(e,t){U&&this.rangeMonitor.set(t);let s="CHANGE_VP_RANGE";if(this.dataWindow){let[n,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=n&&!this.rangeRequestAlreadyPending(t)?{type:s,viewPortId:this.serverViewportId,...G(t,this.bufferSize,u)}:null;if(c){U&&(f==null||f(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:s});let a=this.pendingRangeRequests.at(-1);if(a)if(a.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:g}=a;this.dataWindow.outOfRange(l,g)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:y==null||y("Range Request before previous request is acked")}this.pendingRangeRequests.push({...c,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);this.keys.reset(this.dataWindow.clientRange);let p=this.isTree?j:J;return o.length?[c,o.map(a=>p(a,this.keys,this.selectedRows))]:i?[c,void 0,i]:[c]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Le,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:be,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,s,n){let o={type:"CREATE_VISUAL_LINK",parentVpId:s,childVpId:this.serverViewportId,parentColumnName:n,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,d==null||d("suspend")}resume(){return this.suspended=!1,U&&(f==null||f(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:s}=this,n=this.isTree?j:J;for(let o of t)o&&e.push(n(o,s,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),d==null||d(\`enable: \${this.serverViewportId}\`),{type:ye,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),d==null||d(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Ve,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),f==null||f(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:s}=t;return d==null||d(\`filterRequest: \${s}\`),this.createRequest({filterSpec:{filter:s}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:s,...n}=t;return this.useBatchMode&&(this.batchMode=!0),U?f==null||f(\`setConfig \${JSON.stringify(t)}\`):d==null||d("setConfig"),this.createRequest({...n,filterSpec:typeof(s==null?void 0:s.filter)=="string"?{filter:s.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),d==null||d(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),d==null||d(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=ct){var s;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(s=this.dataWindow)==null||s.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),d==null||d(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:he(t)}}removePendingRangeRequest(e,t){for(let s=this.pendingRangeRequests.length-1;s>=0;s--){let{from:n,to:o}=this.pendingRangeRequests[s],i=!0;if(e>=n&&en&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,s,n));this.pendingUpdates.length=0}else{let i=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of i)e.push(o(u,s,n));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?W:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},J=({rowIndex:r,rowKey:e,sel:t,data:s},n,o)=>[r,n.keyFor(r),!0,!1,0,0,e,t?z(o,r):0].concat(s),j=({rowIndex:r,rowKey:e,sel:t,data:s},n,o)=>{let[i,u,,c,,p,...a]=s;return[r,n.keyFor(r),c,u,i,p,e,t?z(o,r):0].concat(a)};var We=1;var{debug:V,debugEnabled:Q,error:M,info:T,infoEnabled:ft,warn:Y}=b("server-proxy"),C=()=>\`\${We++}\`,mt={},Ct=r=>r.disabled!==!0&&r.suspended!==!0,Rt={type:"NO_ACTION"},St=(r,e,t)=>r.map(s=>s.parentVpId===e?{...s,label:t}:s);function Tt(r,e){return r.map(t=>{let{parentVpId:s}=t,n=e.get(s);if(n)return{...t,parentClientVpId:n.clientViewportId,label:n.title};throw Error("addLabelsToLinks viewport not found")})}var F=class{constructor(e,t){this.authToken="";this.user="user";this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableMetaRequests=new Map;this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=le(Array.from(this.viewports.values()),Ct);this.viewports.clear(),this.mapClientToServerViewport.clear();let s=n=>{n.forEach(o=>{let{clientViewportId:i}=o;this.viewports.set(i,o),this.sendMessageToServer(o.subscribe(),i)})};s(e),setTimeout(()=>{s(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((s,n)=>{this.sendMessageToServer({type:Pe,token:this.authToken,user:t},""),this.pendingLogin={resolve:s,reject:n}});this.authToken===""&&M("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))M(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),s=new q(e,this.postMessageToClient);this.viewports.set(e.viewport,s);let n=this.awaitResponseToMessage(s.subscribe(),e.viewport);Promise.all([n,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=s;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,s)),this.mapClientToServerViewport.set(e.viewport,c);let a=s.handleSubscribed(i,u);a&&(this.postMessageToClient(a),Q&&V(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),s.disabled&&this.disableViewport(s),this.queuedRequests.length>0&&this.processQueuedRequests(),p==="subscribing"&&!x(s.table)&&(this.sendMessageToServer({type:K,vpId:c}),this.sendMessageToServer({type:_e,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:g}])=>l!==c&&!g).forEach(([l])=>{this.sendMessageToServer({type:K,vpId:l})}))})}}processQueuedRequests(){let e={};for(;this.queuedRequests.length;){let t=this.queuedRequests.pop();if(t){let{clientViewportId:s,message:n,requestId:o}=t;if(n.type==="CHANGE_VP_RANGE"){if(e.CHANGE_VP_RANGE)continue;e.CHANGE_VP_RANGE=!0;let i=this.mapClientToServerViewport.get(s);i&&this.sendMessageToServer({...n,viewPortId:i},o)}}}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(T==null||T(\`Unsubscribe Message (Client to Server): - \${t}\`),this.sendMessageToServer({type:Oe,viewPortId:t})):M(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let s=this.mapClientToServerViewport.get(e);if(s){let n=this.viewports.get(s);if(n)return n;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let s=C(),[n,o,i]=e.rangeRequest(s,t.range);T==null||T(\`setViewRange \${t.range.from} - \${t.range.to}\`),n&&(this.sendIfReady(n,s,e.status==="subscribed")||this.queuedRequests.push({clientViewportId:t.viewport,message:n,requestId:s})),o?(T==null||T(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):i&&this.postMessageToClient(i)}setConfig(e,t){let s=C(),n=e.setConfig(s,t.config);this.sendIfReady(n,s,e.status==="subscribed")}aggregate(e,t){let s=C(),n=e.aggregateRequest(s,t.aggregations);this.sendIfReady(n,s,e.status==="subscribed")}sort(e,t){let s=C(),n=e.sortRequest(s,t.sort);this.sendIfReady(n,s,e.status==="subscribed")}groupBy(e,t){let s=C(),n=e.groupByRequest(s,t.groupBy);this.sendIfReady(n,s,e.status==="subscribed")}filter(e,t){let s=C(),{filter:n}=t,o=e.filterRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}setColumns(e,t){let s=C(),{columns:n}=t,o=e.columnRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let s=C(),{selected:n}=t,o=e.selectRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}disableViewport(e){let t=C(),s=e.disable(t);this.sendIfReady(s,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=C(),s=e.enable(t);this.sendIfReady(s,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{T==null||T("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(V==null||V("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,s]=e.resume();V==null||V(\`resumeViewport size \${t}, \${s.length} rows sent to client\`),this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:s,size:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let s=C();this.sendIfReady(e.openTreeNode(s,t),s,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let s=C();this.sendIfReady(e.closeTreeNode(s,t),s,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:s,parentColumnName:n,childColumnName:o}=t,i=C(),u=this.mapClientToServerViewport.get(s);if(u){let c=e.createLink(i,o,u,n);this.sendMessageToServer(c,i)}else M("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=C(),s=e.removeLink(t);this.sendMessageToServer(s,t)}updateTitleOnVisualLinks(e){var n;let{serverViewportId:t,title:s}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&s&&(n=o.links)!=null&&n.some(i=>i.parentVpId===t)){let[i]=o.setLinks(St(o.links,t,s));this.postMessageToClient(i)}}removeViewportFromVisualLinks(e){var t;for(let s of this.viewports.values())if((t=s.links)!=null&&t.some(({parentVpId:n})=>n===e)){let[n]=s.setLinks(s.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(n)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[s,n]=A(e);this.sendMessageToServer({...n,vpId:t.serverViewportId},s)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[s,n]=A(e);this.sendMessageToServer({...n,vpId:t.serverViewportId,namedParams:{}},s)}}rpcCall(e){let[t,s]=A(e),n=Ne(s.service);this.sendMessageToServer(s,t,{module:n})}handleMessageFromClient(e){var t;if(ge(e))if(e.type==="disable"){let s=this.getViewportForClient(e.viewport,!1);return s!==null?this.disableViewport(s):void 0}else{let s=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(s,e);case"config":return this.setConfig(s,e);case"aggregate":return this.aggregate(s,e);case"sort":return this.sort(s,e);case"groupBy":return this.groupBy(s,e);case"filter":return this.filter(s,e);case"select":return this.select(s,e);case"suspend":return this.suspendViewport(s);case"resume":return this.resumeViewport(s);case"enable":return this.enableViewport(s);case"openTreeNode":return this.openTreeNode(s,e);case"closeTreeNode":return this.closeTreeNode(s,e);case"createLink":return this.createLink(s,e);case"removeLink":return this.removeLink(s);case"setColumns":return this.setColumns(s,e);case"setTitle":return this.setTitle(s,e);default:}}else{if(Ce(e))return this.viewportRpcCall(e);if(me(e))return this.menuRpcCall(e);{let{type:s,requestId:n}=e;switch(s){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:s},n)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:n})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,n).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:n})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}M(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=C()){if(x(e))return Promise.resolve(void 0);let s=\`\${e.module}:\${e.table}\`,n=this.cachedTableMetaRequests.get(s);return n||(n=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(s,n)),n==null?void 0:n.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=C()){return new Promise((s,n)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:n,resolve:s})})}sendIfReady(e,t,s=!0){return s&&this.sendMessageToServer(e,t),s}sendMessageToServer(e,t=\`\${We++}\`,s=mt){let{module:n="CORE"}=s;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:n,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:s,sessionId:n}=e,o=this.pendingRequests.get(s);if(o){let{resolve:a}=o;this.pendingRequests.delete(s),a(t);return}let{viewports:i}=this;switch(t.type){case Ie:this.sendMessageToServer({type:De,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(n)this.sessionId=n,(u=this.pendingLogin)==null||u.resolve(n),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case"REMOVE_VP_SUCCESS":{let a=i.get(t.viewPortId);a&&(this.mapClientToServerViewport.delete(a.clientViewportId),i.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case xe:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(s)}break;case Te:case ve:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(s);l!==void 0&&(this.postMessageToClient(l),Q&&V(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case Me:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(s);if(l){this.postMessageToClient(l);let[g,S]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:S,size:g,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=Se(t.rows);for(let[l,g]of Object.entries(a)){let S=i.get(l);S?S.updateRows(g):Y==null||Y(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case"CHANGE_VP_RANGE_SUCCESS":{let a=this.viewports.get(t.viewPortId);if(a){let{from:l,to:g}=t;a.completeOperation(s,l,g)}}break;case ke:case Ee:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:g,parentColumnName:S}=t,I=a.completeOperation(s,g,l.clientViewportId,S);I&&this.postMessageToClient(I)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(s);l&&this.postMessageToClient(l)}}break;case"VP_VISUAL_LINKS_RESP":{let a=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(a.length&&l){let g=Tt(a,this.viewports),[S,I]=l.setLinks(g);if(this.postMessageToClient(S),I){let{link:ne,parentClientVpId:Ke}=I,re=C(),oe=this.mapClientToServerViewport.get(Ke);if(oe){let Je=l.createLink(re,ne.fromColumn,oe,ne.toColumn);this.sendMessageToServer(Je,re)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let a=this.viewports.get(t.vpId);if(a){let l=a.setMenu(t.menu);this.postMessageToClient(l)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:s,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:s,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_REJ":{console.log("send menu error back to client");let{error:a,rpcName:l,vpId:g}=t,S=this.viewports.get(g);S&&this.postMessageToClient({clientViewportId:S.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:s});break}case"VIEW_PORT_MENU_RESP":if(fe(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(g=>{let S=H(g);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:S},tableAlreadyOpen:this.isTableOpen(a.table),requestId:s})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Rt,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:s})}break;case"RPC_RESP":{let{method:a,result:l}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:l,requestId:s})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:l}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:l,requestId:s})}break;case"ERROR":M(t.msg);break;default:ft&&T(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:s}=e.table,n=\`\${t}:\${s}\`,o=this.cachedTableSchemas.get(n);return o||(o=H(e),this.cachedTableSchemas.set(n,o)),o}isTableOpen(e){if(e){let t=e.table;for(let s of this.viewports.values())if(!s.suspended&&s.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let s=this.viewports.get(t.parentVpId);return s&&!s.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let s=e.getClientRows();if(s!==W){let[n,o]=s,i=e.getNewRowCount();(i!==void 0||n&&n.length>0)&&(Q&&V(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=n==null?void 0:n.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:n,size:i,type:"viewport-update"}))}}})}};var{debug:us,debugEnabled:ls,error:qe,info:E,infoEnabled:bt,warn:_}=b("websocket-connection"),Fe="ws",Et=r=>r.startsWith(Fe+"://")||r.startsWith(Fe+"s://"),Ge={},X=Symbol("setWebsocket"),B=Symbol("connectionCallback");async function ze(r,e,t,s=10,n=5){return Ge[r]={status:"connecting",connect:{allowed:n,remaining:n},reconnect:{allowed:s,remaining:s}},He(r,e,t)}async function Z(r){throw Error("connection broken")}async function He(r,e,t,s){let{status:n,connect:o,reconnect:i}=Ge[r],u=n==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof s<"u",p=await Vt(r,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),s!==void 0&&s[X](p);let a=s!=null?s:new ee(p,r,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),a.status=l,u.remaining=u.allowed,a}catch{let p=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:p}),p)return wt(r,e,t,s,2e3);throw Error("Failed to establish connection")}}var wt=(r,e,t,s,n)=>new Promise(o=>{setTimeout(()=>{o(He(r,e,t,s))},n)}),Vt=(r,e)=>new Promise((t,s)=>{let n=Et(r)?r:\`wss://\${r}\`;bt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(n,e);o.onopen=()=>t(o),o.onerror=i=>s(i)}),Be=()=>{_==null||_("Connection cannot be closed, socket not yet opened")},\$e=r=>{_==null||_(\`Message cannot be sent, socket closed \${r.body.type}\`)},vt=r=>{try{return JSON.parse(r)}catch{throw Error(\`Error parsing JSON response from server \${r}\`)}},ee=class{constructor(e,t,s,n){this.close=Be;this.requiresLogin=!0;this.send=\$e;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=vt(e.data);this.messagesCount+=1,this[B](t)};this.url=t,this.protocol=s,this[B]=n,this[X](e)}reconnect(){Z(this)}[(B,X)](e){let t=this[B];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{qe("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?qe("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=n)},e.onclose=()=>{E==null||E("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Z(this),this.send=n)};let s=o=>{e.send(JSON.stringify(o))},n=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=s,this.close=()=>{this.status="closed",e.close(),this.close=Be,this.send=\$e,E==null||E("close websocket")}}};var v,{info:te,infoEnabled:se}=b("worker");async function yt(r,e,t,s,n,o,i){let u=await ze(r,e,c=>{de(c)?postMessage({type:"connection-metrics",messages:c}):pe(c)?(n(c),c.status==="reconnected"&&v.reconnect()):v.handleMessageFromServer(c)},o,i);v=new F(u,c=>Mt(c)),u.requiresLogin&&await v.login(t,s)}function Mt(r){postMessage(r)}var _t=async({data:r})=>{switch(r.type){case"connect":await yt(r.url,r.protocol,r.token,r.username,postMessage,r.retryLimitDisconnect,r.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":se&&te(\`client subscribe: \${JSON.stringify(r)}\`),v.subscribe(r);break;case"unsubscribe":se&&te(\`client unsubscribe: \${JSON.stringify(r)}\`),v.unsubscribe(r.viewport);break;default:se&&te(\`client message: \${JSON.stringify(r)}\`),v.handleMessageFromClient(r)}};self.addEventListener("message",_t);postMessage({type:"ready"}); + \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return\`\${this.keys.size} keys +\${Array.from(this.keys.entries()).sort(([e],[t])=>e-t).map(([e,t])=>\`\${e}=>\${t}\`).join(",")}] +\`}};var{SELECTED:Gt}=B,E={False:0,True:1,First:2,Last:4};var ot=(r,e)=>e>=r[0]&&e<=r[1],it=E.True+E.First+E.Last,at=E.True+E.First,ut=E.True+E.Last,H=(r,e)=>{for(let t of r)if(typeof t=="number"){if(t===e)return it}else if(ot(t,e))return e===t[0]?at:e===t[1]?ut:E.True;return E.False};var he=r=>{if(r.every(t=>typeof t=="number"))return r;let e=[];for(let t of r)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var fe=r=>r.type==="VIEW_PORT_MENU_RESP"&&r.action!==null&&x(r.action.table),x=r=>r!==null&&typeof r=="object"&&"table"in r&&"module"in r?r.table.startsWith("session"):!1;var lt=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],me=r=>lt.includes(r.type),Ce=r=>r.type==="VIEW_PORT_RPC_CALL",A=({requestId:r,...e})=>[r,e],Re=r=>{let e=r.at(0);if(e.updateType==="SIZE"){if(r.length===1)return r;e=r.at(1)}let t=r.at(-1);return[e,t]},Se=r=>{let e={};for(let t of r)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e},z=({columns:r,dataTypes:e,key:t,table:n})=>({table:n,columns:r.map((s,o)=>({name:s,serverDataType:e[o]})),key:t});var Te="CHANGE_VP_SUCCESS";var be="CLOSE_TREE_NODE",we="CLOSE_TREE_SUCCESS";var Ee="CREATE_VP",Ve="DISABLE_VP",ve="DISABLE_VP_SUCCESS";var ye="ENABLE_VP",Me="ENABLE_VP_SUCCESS";var K="GET_VP_VISUAL_LINKS",_e="GET_VIEW_PORT_MENUS";var Ie="HB",De="HB_RESP",Pe="LOGIN",Le="OPEN_TREE_NODE",ke="OPEN_TREE_SUCCESS";var Oe="REMOVE_VP";var xe="SET_SELECTION_SUCCESS";var Ne=r=>{switch(r){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var Ue=[],S=b("array-backed-moving-window");function ct(r,e){if(!e||e.data.length!==r.data.length||e.sel!==r.sel)return!1;for(let t=0;t{var t;if((t=S.info)==null||t.call(S,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return m(this,h).to-t0&&e-m(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:s}=this.range;if(t=s)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-m(this,h).from;if(ct(e,this.internalData[n]))return!1;let s=this.isWithinClientRange(t);return(s||this.isWithinRange(t))&&(!this.internalData[n]&&s&&(this.rowsWithinRange+=1),this.internalData[n]=e),s}getAtIndex(e){return m(this,h).isWithin(e)&&this.internalData[e-m(this,h).from]!=null?this.internalData[e-m(this,h).from]:void 0}isWithinRange(e){return m(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=S.debug)==null||p.call(S,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,s=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===s)return[!1,Ue];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let a=e;ao.to){let a=Math.max(e,o.to);i=this.internalData.slice(a-u,t-u)}else{let a=Math.min(o.from,t);i=this.internalData.slice(e-u,a-u)}return[this.bufferBreakout(e,t),i]}setRange(e,t){var n,s;if(e!==m(this,h).from||t!==m(this,h).to){(n=S.debug)==null||n.call(S,\`setRange \${e} - \${t}\`);let[o,i]=m(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let l=o;l=0;o--)if(e[o]!==void 0){s=e[o];break}return n&&s?[n.rowIndex,s.rowIndex]:[-1,-1]}};h=new WeakMap;var pt=[],{debug:f,debugEnabled:U,error:dt,info:d,infoEnabled:gt,warn:y}=b("viewport"),ht=({rowKey:r,updateType:e})=>e==="U"&&!r.startsWith("\$root"),W=[void 0,void 0],ft={count:0,mode:void 0,size:0,ts:0},q=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:s,groupBy:o=[],table:i,range:u,sort:l,title:p,viewport:a,visualLink:c},g){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=ft;this.updateThrottleTimer=void 0;this.rangeMonitor=new k("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:n}=this.lastUpdateStatus,s=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,s=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,s=0;return this.lastUpdateStatus.mode=e,s};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:s}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(s>=o&&s{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(d==null||d("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=a,this.columns=n,this.filter=s,this.groupBy=o,this.keys=new O(u),this.pendingLinkedParent=c,this.table=i,this.sort=l,this.title=p,gt&&(d==null||d(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new N(this.clientRange,u,this.bufferSize),this.postMessageToClient=g}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:Ee,table:this.table,range:G(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:n,filterSpec:s,range:o,sort:i,groupBy:u},l){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=n,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:n,filter:s,groupBy:u,range:o,sort:i,tableSchema:l}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:s}=this,o=s.get(e);if(!o){dt(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(d==null||d(\`completeOperation \${i}\`),s.delete(e),i==="CHANGE_VP_RANGE"){let[l,p]=t;(u=this.dataWindow)==null||u.setRange(l,p);for(let a=this.pendingRangeRequests.length-1;a>=0;a--){let c=this.pendingRangeRequests[a];if(c.requestId===e){c.acked=!0;break}else y==null||y("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:l,columns:p,filter:a,groupBy:c,sort:g}=o.data;return this.aggregations=l,this.columns=p,this.filter=a,this.groupBy=c,this.sort=g,c.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),f==null||f(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,config:o.data}}else{if(i==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,f==null||f(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,groupBy:o.data};if(i==="columns")return this.columns=o.data,{clientViewportId:n,type:i,columns:o.data};if(i==="filter")return this.filter=o.data,{clientViewportId:n,type:i,filter:o.data};if(i==="aggregate")return this.aggregations=o.data,{clientViewportId:n,type:"aggregate",aggregations:this.aggregations};if(i==="sort")return this.sort=o.data,{clientViewportId:n,type:i,sort:this.sort};if(i!=="selection"){if(i==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:n};if(i==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:n};if(i==="CREATE_VISUAL_LINK"){let[l,p,a]=t;return this.linkedParent={colName:l,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:l,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){U&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[s,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,l=s&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...G(t,this.bufferSize,u)}:null;if(l){U&&(f==null||f(\`create CHANGE_VP_RANGE: [\${l.from} - \${l.to}]\`)),this.awaitOperation(e,{type:n});let c=this.pendingRangeRequests.at(-1);if(c)if(c.acked)console.warn("Range Request before previous request is filled");else{let{from:g,to:R}=c;this.dataWindow.outOfRange(g,R)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:y==null||y("Range Request before previous request is acked")}this.pendingRangeRequests.push({...l,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);let p=this.keys.reset(this.dataWindow.clientRange);console.log(\`keys reset, resequenced \${p}\`);let a=this.isTree?j:J;return o.length?[l,o.map(c=>a(c,this.keys,this.selectedRows))]:i?[l,void 0,i]:[l]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Le,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:be,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,s){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:s,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,d==null||d("suspend")}resume(){return this.suspended=!1,U&&(f==null||f(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,s=this.isTree?j:J;for(let o of t)o&&e.push(s(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),d==null||d(\`enable: \${this.serverViewportId}\`),{type:ye,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),d==null||d(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Ve,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),f==null||f(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:n}=t;return d==null||d(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...s}=t;return this.useBatchMode&&(this.batchMode=!0),U?f==null||f(\`setConfig \${JSON.stringify(t)}\`):d==null||d("setConfig"),this.createRequest({...s,filterSpec:typeof(n==null?void 0:n.filter)=="string"?{filter:n.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),d==null||d(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),d==null||d(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=pt){var n;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(n=this.dataWindow)==null||n.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),d==null||d(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:he(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:s,to:o}=this.pendingRangeRequests[n],i=!0;if(e>=s&&es&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,n,s));this.pendingUpdates.length=0}else{let i=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of i)e.push(o(u,n,s));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?W:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},J=({rowIndex:r,rowKey:e,sel:t,data:n},s,o)=>[r,s.keyFor(r),!0,!1,0,0,e,t?H(o,r):0].concat(n),j=({rowIndex:r,rowKey:e,sel:t,data:n},s,o)=>{let[i,u,,l,,p,...a]=n;return[r,s.keyFor(r),l,u,i,p,e,t?H(o,r):0].concat(a)};var We=1;var{debug:V,debugEnabled:Q,error:M,info:T,infoEnabled:mt,warn:Y}=b("server-proxy"),C=()=>\`\${We++}\`,Ct={},Rt=r=>r.disabled!==!0&&r.suspended!==!0,St={type:"NO_ACTION"},Tt=(r,e,t)=>r.map(n=>n.parentVpId===e?{...n,label:t}:n);function bt(r,e){return r.map(t=>{let{parentVpId:n}=t,s=e.get(n);if(s)return{...t,parentClientVpId:s.clientViewportId,label:s.title};throw Error("addLabelsToLinks viewport not found")})}var \$=class{constructor(e,t){this.authToken="";this.user="user";this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableMetaRequests=new Map;this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=le(Array.from(this.viewports.values()),Rt);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=s=>{s.forEach(o=>{let{clientViewportId:i}=o;this.viewports.set(i,o),this.sendMessageToServer(o.subscribe(),i)})};n(e),setTimeout(()=>{n(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((n,s)=>{this.sendMessageToServer({type:Pe,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:s}});this.authToken===""&&M("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))M(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),n=new q(e,this.postMessageToClient);this.viewports.set(e.viewport,n);let s=this.awaitResponseToMessage(n.subscribe(),e.viewport);Promise.all([s,t]).then(([i,u])=>{let{viewPortId:l}=i,{status:p}=n;e.viewport!==l&&(this.viewports.delete(e.viewport),this.viewports.set(l,n)),this.mapClientToServerViewport.set(e.viewport,l);let a=n.handleSubscribed(i,u);a&&(this.postMessageToClient(a),Q&&V(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),n.disabled&&this.disableViewport(n),this.queuedRequests.length>0&&this.processQueuedRequests(),p==="subscribing"&&!x(n.table)&&(this.sendMessageToServer({type:K,vpId:l}),this.sendMessageToServer({type:_e,vpId:l}),Array.from(this.viewports.entries()).filter(([c,{disabled:g}])=>c!==l&&!g).forEach(([c])=>{this.sendMessageToServer({type:K,vpId:c})}))})}}processQueuedRequests(){let e={};for(;this.queuedRequests.length;){let t=this.queuedRequests.pop();if(t){let{clientViewportId:n,message:s,requestId:o}=t;if(s.type==="CHANGE_VP_RANGE"){if(e.CHANGE_VP_RANGE)continue;e.CHANGE_VP_RANGE=!0;let i=this.mapClientToServerViewport.get(n);i&&this.sendMessageToServer({...s,viewPortId:i},o)}}}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(T==null||T(\`Unsubscribe Message (Client to Server): + \${t}\`),this.sendMessageToServer({type:Oe,viewPortId:t})):M(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let s=this.viewports.get(n);if(s)return s;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let n=C(),[s,o,i]=e.rangeRequest(n,t.range);T==null||T(\`setViewRange \${t.range.from} - \${t.range.to}\`),s&&(this.sendIfReady(s,n,e.status==="subscribed")||this.queuedRequests.push({clientViewportId:t.viewport,message:s,requestId:n})),o?(T==null||T(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):i&&this.postMessageToClient(i)}setConfig(e,t){let n=C(),s=e.setConfig(n,t.config);this.sendIfReady(s,n,e.status==="subscribed")}aggregate(e,t){let n=C(),s=e.aggregateRequest(n,t.aggregations);this.sendIfReady(s,n,e.status==="subscribed")}sort(e,t){let n=C(),s=e.sortRequest(n,t.sort);this.sendIfReady(s,n,e.status==="subscribed")}groupBy(e,t){let n=C(),s=e.groupByRequest(n,t.groupBy);this.sendIfReady(s,n,e.status==="subscribed")}filter(e,t){let n=C(),{filter:s}=t,o=e.filterRequest(n,s);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=C(),{columns:s}=t,o=e.columnRequest(n,s);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=C(),{selected:s}=t,o=e.selectRequest(n,s);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=C(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=C(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{T==null||T("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(V==null||V("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,n]=e.resume();V==null||V(\`resumeViewport size \${t}, \${n.length} rows sent to client\`),this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:n,size:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let n=C();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=C();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:s,childColumnName:o}=t,i=C(),u=this.mapClientToServerViewport.get(n);if(u){let l=e.createLink(i,o,u,s);this.sendMessageToServer(l,i)}else M("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=C(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var s;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(s=o.links)!=null&&s.some(i=>i.parentVpId===t)){let[i]=o.setLinks(Tt(o.links,t,n));this.postMessageToClient(i)}}removeViewportFromVisualLinks(e){var t;for(let n of this.viewports.values())if((t=n.links)!=null&&t.some(({parentVpId:s})=>s===e)){let[s]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(s)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,s]=A(e);this.sendMessageToServer({...s,vpId:t.serverViewportId},n)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,s]=A(e);this.sendMessageToServer({...s,vpId:t.serverViewportId,namedParams:{}},n)}}rpcCall(e){let[t,n]=A(e),s=Ne(n.service);this.sendMessageToServer(n,t,{module:s})}handleMessageFromClient(e){var t;if(ge(e))if(e.type==="disable"){let n=this.getViewportForClient(e.viewport,!1);return n!==null?this.disableViewport(n):void 0}else{let n=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(n,e);case"config":return this.setConfig(n,e);case"aggregate":return this.aggregate(n,e);case"sort":return this.sort(n,e);case"groupBy":return this.groupBy(n,e);case"filter":return this.filter(n,e);case"select":return this.select(n,e);case"suspend":return this.suspendViewport(n);case"resume":return this.resumeViewport(n);case"enable":return this.enableViewport(n);case"openTreeNode":return this.openTreeNode(n,e);case"closeTreeNode":return this.closeTreeNode(n,e);case"createLink":return this.createLink(n,e);case"removeLink":return this.removeLink(n);case"setColumns":return this.setColumns(n,e);case"setTitle":return this.setTitle(n,e);default:}}else{if(Ce(e))return this.viewportRpcCall(e);if(me(e))return this.menuRpcCall(e);{let{type:n,requestId:s}=e;switch(n){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:n},s)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:s})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,s).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:s})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}M(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=C()){if(x(e))return Promise.resolve(void 0);let n=\`\${e.module}:\${e.table}\`,s=this.cachedTableMetaRequests.get(n);return s||(s=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(n,s)),s==null?void 0:s.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=C()){return new Promise((n,s)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:s,resolve:n})})}sendIfReady(e,t,n=!0){return n&&this.sendMessageToServer(e,t),n}sendMessageToServer(e,t=\`\${We++}\`,n=Ct){let{module:s="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:s,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:s}=e,o=this.pendingRequests.get(n);if(o){let{resolve:a}=o;this.pendingRequests.delete(n),a(t);return}let{viewports:i}=this;switch(t.type){case Ie:this.sendMessageToServer({type:De,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(s)this.sessionId=s,(u=this.pendingLogin)==null||u.resolve(s),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case"REMOVE_VP_SUCCESS":{let a=i.get(t.viewPortId);a&&(this.mapClientToServerViewport.delete(a.clientViewportId),i.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case xe:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(n)}break;case Te:case ve:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let c=a.completeOperation(n);c!==void 0&&(this.postMessageToClient(c),Q&&V(\`postMessageToClient \${JSON.stringify(c)}\`))}}break;case Me:{let a=this.viewports.get(t.viewPortId);if(a){let c=a.completeOperation(n);if(c){this.postMessageToClient(c);let[g,R]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:R,size:g,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=Se(t.rows);for(let[c,g]of Object.entries(a)){let R=i.get(c);R?R.updateRows(g):Y==null||Y(\`TABLE_ROW message received for non registered viewport \${c}\`)}this.processUpdates()}break;case"CHANGE_VP_RANGE_SUCCESS":{let a=this.viewports.get(t.viewPortId);if(a){let{from:c,to:g}=t;a.completeOperation(n,c,g)}}break;case ke:case we:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),c=this.viewports.get(t.parentVpId);if(a&&c){let{childColumnName:g,parentColumnName:R}=t,I=a.completeOperation(n,g,c.clientViewportId,R);I&&this.postMessageToClient(I)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let c=a.completeOperation(n);c&&this.postMessageToClient(c)}}break;case"VP_VISUAL_LINKS_RESP":{let a=this.getActiveLinks(t.links),c=this.viewports.get(t.vpId);if(a.length&&c){let g=bt(a,this.viewports),[R,I]=c.setLinks(g);if(this.postMessageToClient(R),I){let{link:se,parentClientVpId:Ke}=I,re=C(),oe=this.mapClientToServerViewport.get(Ke);if(oe){let Je=c.createLink(re,se.fromColumn,oe,se.toColumn);this.sendMessageToServer(Je,re)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let a=this.viewports.get(t.vpId);if(a){let c=a.setMenu(t.menu);this.postMessageToClient(c)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:n,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:n,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_REJ":{console.log("send menu error back to client");let{error:a,rpcName:c,vpId:g}=t,R=this.viewports.get(g);R&&this.postMessageToClient({clientViewportId:R.clientViewportId,error:a,rpcName:c,type:"VIEW_PORT_MENU_REJ",requestId:n});break}case"VIEW_PORT_MENU_RESP":if(fe(t)){let{action:a,rpcName:c}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(g=>{let R=z(g);this.postMessageToClient({rpcName:c,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:R},tableAlreadyOpen:this.isTableOpen(a.table),requestId:n})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||St,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:n})}break;case"RPC_RESP":{let{method:a,result:c}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:c,requestId:n})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:c}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:c,requestId:n})}break;case"ERROR":M(t.msg);break;default:mt&&T(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:n}=e.table,s=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(s);return o||(o=z(e),this.cachedTableSchemas.set(s,o)),o}isTableOpen(e){if(e){let t=e.table;for(let n of this.viewports.values())if(!n.suspended&&n.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let n=this.viewports.get(t.parentVpId);return n&&!n.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let n=e.getClientRows();if(n!==W){let[s,o]=n,i=e.getNewRowCount();(i!==void 0||s&&s.length>0)&&(Q&&V(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=s==null?void 0:s.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:s,size:i,type:"viewport-update"}))}}})}};var{debug:cn,debugEnabled:pn,error:qe,info:w,infoEnabled:wt,warn:_}=b("websocket-connection"),\$e="ws",Et=r=>r.startsWith(\$e+"://")||r.startsWith(\$e+"s://"),Ge={},X=Symbol("setWebsocket"),F=Symbol("connectionCallback");async function He(r,e,t,n=10,s=5){return Ge[r]={status:"connecting",connect:{allowed:s,remaining:s},reconnect:{allowed:n,remaining:n}},ze(r,e,t)}async function Z(r){throw Error("connection broken")}async function ze(r,e,t,n){let{status:s,connect:o,reconnect:i}=Ge[r],u=s==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let l=typeof n<"u",p=await vt(r,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[X](p);let a=n!=null?n:new ee(p,r,e,t),c=l?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:c}),a.status=c,u.remaining=u.allowed,a}catch{let p=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:p}),p)return Vt(r,e,t,n,2e3);throw Error("Failed to establish connection")}}var Vt=(r,e,t,n,s)=>new Promise(o=>{setTimeout(()=>{o(ze(r,e,t,n))},s)}),vt=(r,e)=>new Promise((t,n)=>{let s=Et(r)?r:\`wss://\${r}\`;wt&&e!==void 0&&w(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(s,e);o.onopen=()=>t(o),o.onerror=i=>n(i)}),Fe=()=>{_==null||_("Connection cannot be closed, socket not yet opened")},Be=r=>{_==null||_(\`Message cannot be sent, socket closed \${r.body.type}\`)},yt=r=>{try{return JSON.parse(r)}catch{throw Error(\`Error parsing JSON response from server \${r}\`)}},ee=class{constructor(e,t,n,s){this.close=Fe;this.requiresLogin=!0;this.send=Be;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=yt(e.data);this.messagesCount+=1,this[F](t)};this.url=t,this.protocol=n,this[F]=s,this[X](e)}reconnect(){Z(this)}[(F,X)](e){let t=this[F];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{qe("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?qe("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=s)},e.onclose=()=>{w==null||w("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Z(this),this.send=s)};let n=o=>{e.send(JSON.stringify(o))},s=o=>{w==null||w(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=Fe,this.send=Be,w==null||w("close websocket")}}};var v,{info:te,infoEnabled:ne}=b("worker");async function Mt(r,e,t,n,s,o,i){let u=await He(r,e,l=>{de(l)?postMessage({type:"connection-metrics",messages:l}):pe(l)?(s(l),l.status==="reconnected"&&v.reconnect()):v.handleMessageFromServer(l)},o,i);v=new \$(u,l=>_t(l)),u.requiresLogin&&await v.login(t,n)}function _t(r){postMessage(r)}var It=async({data:r})=>{switch(r.type){case"connect":await Mt(r.url,r.protocol,r.token,r.username,postMessage,r.retryLimitDisconnect,r.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":ne&&te(\`client subscribe: \${JSON.stringify(r)}\`),v.subscribe(r);break;case"unsubscribe":ne&&te(\`client unsubscribe: \${JSON.stringify(r)}\`),v.unsubscribe(r.viewport);break;default:ne&&te(\`client message: \${JSON.stringify(r)}\`),v.handleMessageFromClient(r)}};self.addEventListener("message",It);postMessage({type:"ready"}); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts index cc12240b8..1db4966d9 100644 --- a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts @@ -499,15 +499,15 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], - [12,1,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,2,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,3,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], - [15,4,true,false,0,0,"key-15",0,"key-15","name 15",1015,true,], - [16,5,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,6,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,7,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,8,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [12,2,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,3,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,4,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [15,5,true,false,0,0,"key-15",0,"key-15","name 15",1015,true,], + [16,6,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,7,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,8,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,9,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], ], }); }); @@ -829,8 +829,8 @@ describe("ServerProxy", () => { body: { viewPortId: "server-vp-1", type: "CHANGE_VP_RANGE", - from: 3, - to: 23, + from: 0, + to: 28, }, module: "CORE", requestId: "1", @@ -1172,8 +1172,8 @@ describe("ServerProxy", () => { body: { viewPortId: "server-vp-1", type: "CHANGE_VP_RANGE", - from: 6, - to: 36, + from: 0, + to: 46, }, module: "CORE", requestId: "1", @@ -1247,8 +1247,8 @@ describe("ServerProxy", () => { body: { viewPortId: "server-vp-1", type: "CHANGE_VP_RANGE", - from: 6, - to: 36, + from: 0, + to: 46, }, module: "CORE", requestId: "1", @@ -1263,8 +1263,8 @@ describe("ServerProxy", () => { body: { type: "CHANGE_VP_RANGE_SUCCESS", viewPortId: "server-vp-1", - from: 6, - to: 36, + from: 0, + to: 46, }, }); @@ -1295,22 +1295,22 @@ describe("ServerProxy", () => { range: { from: 24, to: 34 }, }); - expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledTimes(0); expect(postMessageToClient).toHaveBeenCalledTimes(0); - expect(connection.send).toHaveBeenCalledWith({ - user: "user", - body: { - viewPortId: "server-vp-1", - type: "CHANGE_VP_RANGE", - from: 14, - to: 44, - }, - module: "CORE", - requestId: "1", - sessionId: "dsdsd", - token: "test", - }); + // expect(connection.send).toHaveBeenCalledWith({ + // user: "user", + // body: { + // viewPortId: "server-vp-1", + // type: "CHANGE_VP_RANGE", + // from: 14, + // to: 44, + // }, + // module: "CORE", + // requestId: "1", + // sessionId: "dsdsd", + // token: "test", + // }); }); }); diff --git a/vuu-ui/packages/vuu-data-remote/test/test-utils.ts b/vuu-ui/packages/vuu-data-remote/test/test-utils.ts index 589293e13..2ec2105d1 100644 --- a/vuu-ui/packages/vuu-data-remote/test/test-utils.ts +++ b/vuu-ui/packages/vuu-data-remote/test/test-utils.ts @@ -13,6 +13,7 @@ import { PostMessageToClientCallback, ServerProxySubscribeMessage, } from "../src"; +import { TableSchema } from "@finos/vuu-data-types"; export const COMMON_ATTRS = { module: "TEST", @@ -161,7 +162,7 @@ export const updateTableRow = ( }; }; -export const testSchema = { +export const testSchema: TableSchema = { columns: [ { name: "col-1", serverDataType: "string" }, { name: "col-2", serverDataType: "string" }, diff --git a/vuu-ui/packages/vuu-data-remote/test/viewport.test.ts b/vuu-ui/packages/vuu-data-remote/test/viewport.test.ts index 2e7b54be8..c2369a4d9 100644 --- a/vuu-ui/packages/vuu-data-remote/test/viewport.test.ts +++ b/vuu-ui/packages/vuu-data-remote/test/viewport.test.ts @@ -1,8 +1,8 @@ -import "./global-mocks"; +import { ServerProxySubscribeMessage } from "@finos/vuu-data-types"; import { ServerToClientCreateViewPortSuccess } from "@finos/vuu-protocol-types"; import { describe, expect, it } from "vitest"; -import { ServerProxySubscribeMessage } from "../src"; import { Viewport } from "../src/server-proxy/viewport"; +import "./global-mocks"; import { createSubscription, createTableRows, @@ -61,7 +61,7 @@ describe("Viewport", () => { type: "CREATE_VP", ...rest, filterSpec: { filter }, - range: { from: 0, to: 50 }, + range: { from: 0, to: 0 }, }); }); it("sets status to subscribing", () => { @@ -91,7 +91,7 @@ describe("Viewport", () => { type: "CREATE_VP", ...rest, filterSpec: { filter }, - range: { from: 0, to: 100 }, + range: { from: 0, to: 0 }, }); }); it("applies bufferSize to existing range", () => { @@ -139,7 +139,7 @@ describe("Viewport", () => { type: "CREATE_VP", ...rest, filterSpec: { filter }, - range: { from: 50, to: 250 }, + range: { from: 0, to: 300 }, }); }); }); @@ -154,7 +154,7 @@ describe("Viewport", () => { table: vuu_table.table, viewPortId: "server-vp1", }; - vp.handleSubscribed(vuuMessageBody); + vp.handleSubscribed(vuuMessageBody, testSchema); expect(vp.status).toEqual("subscribed"); }); @@ -191,7 +191,7 @@ describe("Viewport", () => { ); const [, serverSubscription] = createSubscription(); - vp.handleSubscribed(serverSubscription.body); + vp.handleSubscribed(serverSubscription.body, testSchema); vp.updateRows([sizeRow(), ...createTableRows("server-vp-1", 0, 20)]); @@ -222,7 +222,7 @@ describe("Viewport", () => { noop ); const [, serverSubscription] = createSubscription(); - vp.handleSubscribed(serverSubscription.body); + vp.handleSubscribed(serverSubscription.body, testSchema); vp.updateRows([sizeRow(), ...createTableRows("server-vp-1", 0, 20)]); // bufferSize = 10, so range will be expanded +/-5 => [45-65] @@ -243,7 +243,7 @@ describe("Viewport", () => { ); // this one breaches the bufferSize 25% threshold expect(vp["rangeRequestAlreadyPending"]({ from: 52, to: 63 })).toEqual( - false + true ); expect(vp["rangeRequestAlreadyPending"]({ from: 60, to: 70 })).toEqual( false @@ -260,7 +260,7 @@ describe("Viewport", () => { noop ); const [, serverSubscription] = createSubscription(); - vp.handleSubscribed(serverSubscription.body); + vp.handleSubscribed(serverSubscription.body, testSchema); vp.updateRows([sizeRow(), ...createTableRows("server-vp-1", 0, 20)]); // bufferSize = 10, so range will be expanded +/-5 => [45-65] @@ -285,7 +285,7 @@ describe("Viewport", () => { true ); expect(vp["rangeRequestAlreadyPending"]({ from: 50, to: 73 })).toEqual( - false + true ); expect(vp["rangeRequestAlreadyPending"]({ from: 70, to: 80 })).toEqual( false @@ -305,7 +305,7 @@ describe("Viewport", () => { ); const [, serverSubscription] = createSubscription(); - vp.handleSubscribed(serverSubscription.body); + vp.handleSubscribed(serverSubscription.body, testSchema); vp.updateRows([sizeRow(), ...createTableRows("server-vp-1", 0, 20)]); diff --git a/vuu-ui/packages/vuu-table/src/Row.tsx b/vuu-ui/packages/vuu-table/src/Row.tsx index 122b9f09f..e7ff5fd12 100644 --- a/vuu-ui/packages/vuu-table/src/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/Row.tsx @@ -92,7 +92,6 @@ export const Row = memo( return (
({ + name: (_: string, el: Element) => el.ariaRowIndex === `${index}`, +}); + +describe("WHEN it initially renders", () => { + const RENDER_BUFFER = 5; + const ROW_COUNT = 1000; + const tableConfig = { + renderBufferSize: RENDER_BUFFER, + headerHeight: 25, + height: 625, + rowCount: ROW_COUNT, + rowHeight: 20, + width: 1000, + }; + + it("THEN expected classname is present", () => { + cy.mount( + + ); + const container = cy.findByTestId("table"); + container.should("have.class", "vuuTable"); + }); + + it("THEN expected number of rows are present, with buffered rows, all with correct aria index", () => { + cy.mount(); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); +}); diff --git a/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx new file mode 100644 index 000000000..c520a6b93 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx @@ -0,0 +1,184 @@ +// TODO try and get TS path alias working to avoid relative paths like this +import { TestTable } from "../../../../../showcase/src/examples/Table/Table.examples"; +import { assertRenderedRows, withAriaIndex } from "./table-test-utils"; + +describe("Table scrolling and keyboard navigation", () => { + const RENDER_BUFFER = 5; + const ROW_COUNT = 1000; + const tableConfig = { + renderBufferSize: RENDER_BUFFER, + headerHeight: 25, + height: 625, + rowCount: ROW_COUNT, + rowHeight: 20, + width: 1000, + }; + describe("Page Keys", () => { + describe("WHEN first cell is focussed and page down pressed", () => { + it("THEN table scrolls down and next page of rows are rendered, first cell of new page is focussed", () => { + cy.mount(); + + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + cy.realPress("PageDown"); + + cy.findByRole("row", withAriaIndex(25)).should("not.exist"); + cy.findByRole("row", withAriaIndex(26)).should("exist"); + + cy.get(".vuuTable-contentContainer") + .then((el) => el[0].scrollTop) + .should("equal", 600); + + // row 31 should be top row in viewport + cy.findByRole("row", withAriaIndex(31)).should( + "have.css", + "transform", + "matrix(1, 0, 0, 1, 0, 600)" + ); + + cy.findByRole("cell", { name: "row 31" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 31" }).should("be.focused"); + }); + + describe("AND WHEN page up is then pressed", () => { + it("THEN table is back to original state, and first cell is once again focussed", () => { + cy.mount(); + + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + + cy.realPress("PageDown"); + cy.wait(60); + cy.realPress("PageUp"); + + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); + }); + + describe("Home / End Keys", () => { + describe("WHEN topmost rows are in viewport, first cell is focussed and Home key pressed ", () => { + it("THEN nothing changes", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("Home"); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + describe("WHEN topmost rows are in viewport, cell in middle of viewport is focussed and Home key pressed ", () => { + it("THEN no scrolling, but focus moves to first cell", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 5" }).click(); + cy.realPress("Home"); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + + describe("WHEN topmost rows are in viewport, first cell is focussed and End key pressed ", () => { + it("THEN scrolls to end of data, last cell is focussed (same column)", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("End"); + cy.findByRole("cell", { name: "row 1,000" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1,000" }).should("be.focused"); + assertRenderedRows({ from: 970, to: 1000 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + + describe("WHEN topmost rows are in viewport, cell mid viewport focussed and End key pressed ", () => { + it("THEN scrolls to end of data, last cell is focussed (same column)", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 10" }).click(); + cy.realPress("End"); + cy.findByRole("cell", { name: "row 1,000" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1,000" }).should("be.focused"); + assertRenderedRows({ from: 970, to: 1000 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); + + describe("Arrow Up / Down Keys", () => { + describe("WHEN topmost rows are in viewport, first cell is focussed and Down Arrow key pressed ", () => { + it("THEN no scrolling, focus moved down to next cell", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("ArrowDown"); + cy.findByRole("cell", { name: "row 2" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 2" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + describe("WHEN topmost rows are in viewport, first cell in last row is focussed and Down Arrow key pressed ", () => { + it("THEN scroll down by 1 row, cell in bottom row has focus", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 30" }).click(); + cy.realPress("ArrowDown"); + cy.findByRole("cell", { name: "row 31" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 31" }).should("be.focused"); + assertRenderedRows({ from: 1, to: 31 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); + + describe("scrolling with Scrollbar", () => { + describe("WHEN scrolled down by a distance equating to 500 rows", () => { + it("THEN correct rows are within viewport", () => { + cy.mount(); + cy.get(".vuuTable-scrollbarContainer").scrollTo(0, 10000); + assertRenderedRows({ from: 500, to: 530 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); +}); diff --git a/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts b/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts new file mode 100644 index 000000000..e566d3068 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts @@ -0,0 +1,43 @@ +import { VuuRange } from "packages/vuu-protocol-types"; + +export const withAriaIndex = (index: number) => ({ + name: (_: string, el: Element) => el.ariaRowIndex === `${index}`, +}); + +export const assertRenderedRows = ( + { from, to }: VuuRange, + renderBufferSize: number, + totalRowCount: number +) => { + const leadingBufferedRows = from < renderBufferSize ? from : renderBufferSize; + const offsetFromEnd = totalRowCount - to; + const trailingBufferedRows = + offsetFromEnd < renderBufferSize + ? Math.min(0, offsetFromEnd) + : renderBufferSize; + const renderedRowCount = + to - from + leadingBufferedRows + trailingBufferedRows; + + // Note the Table Headers row is included in count, hence the + 1 + cy.findAllByRole("row").should("have.length", renderedRowCount + 1); + + // we use the aria index for locators, which is 1 based + const firstRenderedRow = from - leadingBufferedRows + 1; + const firstVisibleRow = from + 1; + const lastVisibleRow = to; + const lastRenderedRow = to + trailingBufferedRows; + + cy.findByRole("row", withAriaIndex(firstRenderedRow - 1)).should("not.exist"); + cy.findByRole("row", withAriaIndex(firstVisibleRow)).should("be.visible"); + cy.findByRole("row", withAriaIndex(lastVisibleRow)).should("be.visible"); + + if (trailingBufferedRows > 0) { + cy.findByRole("row", withAriaIndex(lastVisibleRow + 1)).should( + "not.be.visible" + ); + cy.findByRole("row", withAriaIndex(lastRenderedRow)).should( + "not.be.visible" + ); + } + cy.findByRole("row", withAriaIndex(lastRenderedRow + 1)).should("not.exist"); +}; diff --git a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts index 94b547439..df685e111 100644 --- a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts @@ -1,4 +1,5 @@ import { RefObject } from "react"; +import { ScrollDirection } from "./useTableScroll"; /** * [rowIndex, colIndex @@ -33,8 +34,8 @@ export const getTableCell = ( } }; -export const cellIsEditable = (cell: HTMLDivElement) => - cell.classList.contains("vuuTableCell-editable"); +export const cellIsEditable = (cell: HTMLDivElement | null) => + cell?.classList.contains("vuuTableCell-editable"); export const cellIsTextInput = (cell: HTMLElement) => cell.querySelector(".vuuTableInputCell") !== null; @@ -53,3 +54,32 @@ const closestRow = (el: HTMLElement) => el.closest('[role="row"]') as HTMLElement; export const closestRowIndex = (el: HTMLElement) => getRowIndex(closestRow(el)); + +const NO_SCROLL_NECESSARY = [undefined, undefined] as const; + +export const howFarIsRowOutsideViewport = ( + rowEl: HTMLElement, + totalHeaderHeight: number, + contentContainer = rowEl.closest(".vuuTable-contentContainer") +): readonly [ScrollDirection | undefined, number | undefined] => { + //TODO lots of scope for optimisation here + if (contentContainer) { + // TODO take totalHeaderHeight into consideration + const viewport = contentContainer?.getBoundingClientRect(); + const upperBoundary = viewport.top + totalHeaderHeight; + const row = rowEl.getBoundingClientRect(); + if (row) { + if (row.bottom > viewport.bottom) { + return ["down", row.bottom - viewport.bottom]; + } else if (row.top < upperBoundary) { + return ["up", row.top - upperBoundary]; + } else { + return NO_SCROLL_NECESSARY; + } + } else { + throw Error("Whats going on, row not found"); + } + } else { + throw Error("Whats going on, scrollbar container not found"); + } +}; diff --git a/vuu-ui/packages/vuu-table/src/table-header/TableHeader.tsx b/vuu-ui/packages/vuu-table/src/table-header/TableHeader.tsx index 8ec2dfbe8..3c42e9942 100644 --- a/vuu-ui/packages/vuu-table/src/table-header/TableHeader.tsx +++ b/vuu-ui/packages/vuu-table/src/table-header/TableHeader.tsx @@ -8,6 +8,7 @@ import { } from "@finos/vuu-table-types"; import { isGroupColumn, isNotHidden } from "@finos/vuu-utils"; import cx from "clsx"; +import { memo } from "react"; import { GroupHeaderCellNext, HeaderCell } from "../header-cell"; import { useTableHeader } from "./useTableHeader"; @@ -30,70 +31,73 @@ export interface TableHeaderProps { tableId: string; } -export const TableHeader = ({ - classBase = "vuuTable", - columns, - headings, - onMoveColumn, - onMoveGroupColumn, - onRemoveGroupColumn, - onResizeColumn, - onSortColumn, - tableConfig, - tableId, -}: TableHeaderProps) => { - const { - containerRef, - draggableColumn, - draggedColumnIndex, - onClick, - onMouseDown, - } = useTableHeader({ +export const TableHeader = memo( + ({ + classBase = "vuuTable", columns, + headings, onMoveColumn, + onMoveGroupColumn, + onRemoveGroupColumn, + onResizeColumn, onSortColumn, tableConfig, - }); + tableId, + }: TableHeaderProps) => { + const { + containerRef, + draggableColumn, + draggedColumnIndex, + onClick, + onMouseDown, + } = useTableHeader({ + columns, + onMoveColumn, + onSortColumn, + tableConfig, + }); - return ( -
- {headings.map((colHeaders, i) => ( -
- {colHeaders.map(({ label, width }, j) => ( -
- {label} -
- ))} + return ( +
+ {headings.map((colHeaders, i) => ( +
+ {colHeaders.map(({ label, width }, j) => ( +
+ {label} +
+ ))} +
+ ))} +
+ {columns.filter(isNotHidden).map((col, i) => + isGroupColumn(col) ? ( + + ) : ( + + ) + )} + {draggableColumn}
- ))} -
- {columns.filter(isNotHidden).map((col, i) => - isGroupColumn(col) ? ( - - ) : ( - - ) - )} - {draggableColumn}
-
- ); -}; + ); + } +); +TableHeader.displayName = "TableHeader"; diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index 078ebb9e4..90d6559e5 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -6,7 +6,7 @@ import { VuuFeatureInvocationMessage, } from "@finos/vuu-data-types"; import { VuuRange } from "@finos/vuu-protocol-types"; -import { getFullRange, NULL_RANGE } from "@finos/vuu-utils"; +import { getFullRange, NULL_RANGE, rangesAreSame } from "@finos/vuu-utils"; import { GridAction } from "@finos/vuu-table-types"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MovingWindow } from "./moving-window"; @@ -37,7 +37,7 @@ export const useDataSource = ({ const data = useRef([]); const isMounted = useRef(true); const hasUpdated = useRef(false); - const rangeRef = useRef(NULL_RANGE); + const rangeRef = useRef(range); const dataWindow = useMemo( () => new MovingWindow(getFullRange(range, renderBufferSize)), @@ -112,12 +112,13 @@ export const useDataSource = ({ const setRange = useCallback( (range: VuuRange) => { - // TODO can we directly call setData here when we do an - // in-situ row scroll ? - const fullRange = getFullRange(range, renderBufferSize); - dataWindow.setRange(fullRange); - dataSource.range = rangeRef.current = fullRange; - dataSource.emit("range", range); + console.log(`set Range ${range.from} ${range.to}`); + if (!rangesAreSame(range, rangeRef.current)) { + const fullRange = getFullRange(range, renderBufferSize); + dataWindow.setRange(fullRange); + dataSource.range = rangeRef.current = fullRange; + dataSource.emit("range", range); + } }, [dataSource, dataWindow, renderBufferSize] ); diff --git a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts index 0d5872696..d7e8eed8b 100644 --- a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts @@ -184,7 +184,6 @@ NavigationHookProps) => { activeCell.setAttribute("tabindex", "0"); } // TODO needs to be scroll cell - console.log(`scroll row ${cellPos[0]}`); requestScroll?.({ type: "scroll-row", rowIndex: cellPos[0] }); activeCell.focus({ preventScroll: true }); } @@ -219,26 +218,48 @@ NavigationHookProps) => { new Promise((resolve) => { let newRowIdx = rowIdx; switch (key) { - case "PageDown": + case "PageDown": { newRowIdx = Math.min(rowCount - 1, rowIdx + viewportRowCount); - requestScroll?.({ type: "scroll-page", direction: "down" }); + if (newRowIdx !== rowIdx) { + requestScroll?.({ type: "scroll-page", direction: "down" }); + } break; - case "PageUp": + } + case "PageUp": { newRowIdx = Math.max(0, rowIdx - viewportRowCount); - requestScroll?.({ type: "scroll-page", direction: "up" }); + if (newRowIdx !== rowIdx) { + requestScroll?.({ type: "scroll-page", direction: "up" }); + } break; - case "Home": + } + case "Home": { newRowIdx = 0; - requestScroll?.({ type: "scroll-end", direction: "home" }); + if (newRowIdx !== rowIdx) { + requestScroll?.({ type: "scroll-end", direction: "home" }); + } break; - case "End": + } + case "End": { newRowIdx = rowCount - 1; - requestScroll?.({ type: "scroll-end", direction: "end" }); + if (newRowIdx !== rowIdx) { + requestScroll?.({ type: "scroll-end", direction: "end" }); + } break; + } } + // Introduce a delay to allow the scroll operation to complete, + // which will trigger a range reset and rerender of rows. We + // might need to tweak how this works. If we introduce too big + // a delay, we risk seeing the newly rendered rows, with the focus + // still on the old cell, which will be apparent as a brief flash + // of the old cell focus before switching to correct cell. If we were + // to change the way re assign keys such that we can guarantee that + // when we page down, rows in same position get same keys, then same + // cell would be focussed in new page as previous and issue would not + // arise. setTimeout(() => { resolve([newRowIdx, colIdx]); - }, 90); + }, 35); }), [requestScroll, rowCount, viewportRowCount] ); diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index a9d11ae4f..7d01e316a 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -206,10 +206,25 @@ export const useTable = ({ [dataSource.columns] ); + const onSubscribed = useCallback( + ({ tableSchema }: DataSourceSubscribedMessage) => { + if (tableSchema) { + dispatchColumnAction({ + type: "setTableSchema", + tableSchema, + }); + } else { + console.log("subscription message with no schema"); + } + }, + [dispatchColumnAction] + ); + const { getRowAtPosition, getRowOffset, - setPctScrollTop, + setInSituRowOffset: viewportHookSetInSituRowOffset, + setScrollTop: viewportHookSetScrollTop, ...viewportMeasurements } = useTableViewport({ columns, @@ -222,26 +237,9 @@ export const useTable = ({ const initialRange = useInitialValue({ from: 0, - to: - viewportMeasurements.rowCount === 0 - ? 0 - : viewportMeasurements.rowCount + 1, + to: viewportMeasurements.rowCount, }); - const onSubscribed = useCallback( - ({ tableSchema }: DataSourceSubscribedMessage) => { - if (tableSchema) { - dispatchColumnAction({ - type: "setTableSchema", - tableSchema, - }); - } else { - console.log("subscription message with no schema"); - } - }, - [dispatchColumnAction] - ); - const { data, dataRef, getSelectedRows, range, setRange } = useDataSource({ dataSource, // We need to factor this out of Table @@ -252,6 +250,17 @@ export const useTable = ({ range: initialRange, }); + const { requestScroll, ...scrollProps } = useTableScroll({ + getRowAtPosition, + rowHeight, + scrollingApiRef, + setRange, + onVerticalScroll: viewportHookSetScrollTop, + onVerticalScrollInSitu: viewportHookSetInSituRowOffset, + viewportMeasurements, + }); + + // TODO does this belong here ? const handleConfigEditedInSettingsPanel = useCallback( (tableConfig: TableConfig) => { dispatchColumnAction({ @@ -463,22 +472,6 @@ export const useTable = ({ [columns, dataSource, dispatchColumnAction] ); - const handleVerticalScroll = useCallback( - (_: number, pctScrollTop: number) => { - setPctScrollTop(pctScrollTop); - }, - [setPctScrollTop] - ); - - const { requestScroll, ...scrollProps } = useTableScroll({ - getRowAtPosition, - rowHeight, - scrollingApiRef, - setRange, - onVerticalScroll: handleVerticalScroll, - viewportMeasurements, - }); - const { highlightedIndexRef, navigate, diff --git a/vuu-ui/packages/vuu-table/src/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/useTableScroll.ts index 180a6fc10..746bab21c 100644 --- a/vuu-ui/packages/vuu-table/src/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/useTableScroll.ts @@ -9,6 +9,7 @@ import { useRef, } from "react"; import { ViewportMeasurements } from "./useTableViewport"; +import { howFarIsRowOutsideViewport } from "./table-dom-utils"; export type ScrollDirectionVertical = "up" | "down"; export type ScrollDirectionHorizontal = "left" | "right"; @@ -45,12 +46,59 @@ export interface ScrollingAPI { scrollToKey: (rowKey: string) => void; } -const getPctScroll = (container: HTMLElement) => { - const { scrollLeft, scrollTop } = container; +/** + * Return the maximum scroll positions for gioven container + * @param container + * @returns [maxScrollLeft, maxScrollTop] + */ +const getMaxScroll = (container: HTMLElement) => { const { clientHeight, clientWidth, scrollHeight, scrollWidth } = container; + return [scrollWidth - clientWidth, scrollHeight - clientHeight]; +}; + +const getScrollDirection = ( + prevScrollPositions: ScrollPos | undefined, + scrollPos: number +) => { + if (prevScrollPositions === undefined) { + return undefined; + } else { + const { scrollTop: prevTop } = prevScrollPositions; + return scrollPos > prevTop ? "fwd" : "bwd"; + } +}; + +const getPctScroll = (container: HTMLElement, currentScrollPos?: ScrollPos) => { + const { + clientHeight, + clientWidth, + scrollHeight, + scrollLeft, + scrollTop, + scrollWidth, + } = container; + + const maxScrollLeft = scrollWidth - clientWidth; const pctScrollLeft = scrollLeft / (scrollWidth - clientWidth); - const pctScrollTop = scrollTop / (scrollHeight - clientHeight); - return [pctScrollLeft, pctScrollTop]; + const maxScrollTop = scrollHeight - clientHeight; + let pctScrollTop = scrollTop / (scrollHeight - clientHeight); + + const scrollDirection = getScrollDirection(currentScrollPos, scrollTop); + + if (scrollDirection === "fwd" && pctScrollTop > 0.99) { + pctScrollTop = 1; + } else if (scrollDirection === "bwd" && pctScrollTop < 0.02) { + pctScrollTop = 0; + } + + return [ + scrollLeft, + pctScrollLeft, + maxScrollLeft, + scrollTop, + pctScrollTop, + maxScrollTop, + ]; }; export const noScrolling: ScrollingAPI = { @@ -64,35 +112,6 @@ interface CallbackRefHookProps { label?: string; } -const NO_SCROLL_NECESSARY = [undefined, undefined] as const; - -export const howFarIsRowOutsideViewport = ( - rowEl: HTMLElement, - totalHeaderHeight: number, - contentContainer = rowEl.closest(".vuuTable-contentContainer") -): readonly [ScrollDirection | undefined, number | undefined] => { - //TODO lots of scope for optimisation here - if (contentContainer) { - // TODO take totalHeaderHeight into consideration - const viewport = contentContainer?.getBoundingClientRect(); - const upperBoundary = viewport.top + totalHeaderHeight; - const row = rowEl.getBoundingClientRect(); - if (row) { - if (row.bottom > viewport.bottom) { - return ["down", row.bottom - viewport.bottom]; - } else if (row.top < upperBoundary) { - return ["up", row.top - upperBoundary]; - } else { - return NO_SCROLL_NECESSARY; - } - } else { - throw Error("Whats going on, row not found"); - } - } else { - throw Error("Whats going on, scrollbar container not found"); - } -}; - const useCallbackRef = ({ onAttach, onDetach, @@ -114,10 +133,21 @@ const useCallbackRef = ({ return callbackRef; }; +type ScrollPos = { + scrollLeft: number; + scrollTop: number; +}; + export interface TableScrollHookProps { getRowAtPosition: RowAtPositionFunc; onHorizontalScroll?: (scrollLeft: number) => void; onVerticalScroll?: (scrollTop: number, pctScrollTop: number) => void; + /** + * When we have a virtualized scroll container, keyboard navigation is + * performed `in situ`. We shift the range of rows rendered within the + * viewport, whithout actually moving the scroll position + */ + onVerticalScrollInSitu?: (rowIndexOffsetCount: number) => void; rowHeight: number; scrollingApiRef?: ForwardedRef; setRange: (range: VuuRange) => void; @@ -128,21 +158,28 @@ export const useTableScroll = ({ getRowAtPosition, onHorizontalScroll, onVerticalScroll, + onVerticalScrollInSitu, scrollingApiRef, setRange, viewportMeasurements, }: TableScrollHookProps) => { const firstRowRef = useRef(0); const contentContainerScrolledRef = useRef(false); - const scrollPosRef = useRef({ scrollTop: 0, scrollLeft: 0 }); + const contentContainerPosRef = useRef({ + scrollTop: 0, + scrollLeft: 0, + }); + const scrollbarContainerScrolledRef = useRef(false); + const scrollbarContainerPosRef = useRef({ + scrollTop: 0, + scrollLeft: 0, + }); const scrollbarContainerRef = useRef(null); const contentContainerRef = useRef(null); const { appliedPageSize, isVirtualScroll, - maxScrollContainerScrollHorizontal: maxScrollLeft, - maxScrollContainerScrollVertical: maxScrollTop, rowCount: viewportRowCount, totalHeaderHeight, } = viewportMeasurements; @@ -153,7 +190,7 @@ export const useTableScroll = ({ const firstRow = getRowAtPosition(scrollTop); if (firstRow !== firstRowRef.current) { firstRowRef.current = firstRow; - setRange({ from: firstRow, to: firstRow + viewportRowCount + 1 }); + setRange({ from: firstRow, to: firstRow + viewportRowCount }); } }, [getRowAtPosition, onVerticalScroll, setRange, viewportRowCount] @@ -163,42 +200,70 @@ export const useTableScroll = ({ const { current: contentContainer } = contentContainerRef; const { current: scrollbarContainer } = scrollbarContainerRef; const { current: contentContainerScrolled } = contentContainerScrolledRef; + const { current: scrollPos } = scrollbarContainerPosRef; + if (contentContainerScrolled) { contentContainerScrolledRef.current = false; } else if (contentContainer && scrollbarContainer) { - const [pctScrollLeft, pctScrollTop] = getPctScroll(scrollbarContainer); - const rootScrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - const rootScrollTop = pctScrollTop * maxScrollTop; + scrollbarContainerScrolledRef.current = true; + const [scrollLeft, pctScrollLeft, , scrollTop, pctScrollTop] = + getPctScroll(scrollbarContainer, scrollPos); + + scrollPos.scrollLeft = scrollLeft; + scrollPos.scrollTop = scrollTop; + + const [maxScrollLeft, maxScrollTop] = getMaxScroll(scrollbarContainer); + const contentScrollLeft = Math.round(pctScrollLeft * maxScrollLeft); + const contentScrollTop = pctScrollTop * maxScrollTop; + contentContainer.scrollTo({ - left: rootScrollLeft, - top: rootScrollTop, + left: contentScrollLeft, + top: contentScrollTop, behavior: "auto", }); } - }, [maxScrollLeft, maxScrollTop]); + onVerticalScrollInSitu?.(0); + }, [onVerticalScrollInSitu]); const handleContentContainerScroll = useCallback(() => { + const { current: scrollbarContainerScrolled } = + scrollbarContainerScrolledRef; const { current: contentContainer } = contentContainerRef; const { current: scrollbarContainer } = scrollbarContainerRef; - const { current: scrollPos } = scrollPosRef; + const { current: scrollPos } = contentContainerPosRef; if (contentContainer && scrollbarContainer) { - const { scrollLeft, scrollTop } = contentContainer; - const [pctScrollLeft, pctScrollTop] = getPctScroll(contentContainer); + const [ + scrollLeft, + pctScrollLeft, + maxScrollLeft, + scrollTop, + pctScrollTop, + maxScrollTop, + ] = getPctScroll(contentContainer); + contentContainerScrolledRef.current = true; - scrollbarContainer.scrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - scrollbarContainer.scrollTop = pctScrollTop * maxScrollTop; + + if (scrollbarContainerScrolled) { + scrollbarContainerScrolledRef.current = false; + } else { + scrollbarContainer.scrollLeft = Math.round( + pctScrollLeft * maxScrollLeft + ); + scrollbarContainer.scrollTop = pctScrollTop * maxScrollTop; + } if (scrollPos.scrollTop !== scrollTop) { scrollPos.scrollTop = scrollTop; handleVerticalScroll(scrollTop, pctScrollTop); + onVerticalScrollInSitu?.(0); } if (scrollPos.scrollLeft !== scrollLeft) { scrollPos.scrollLeft = scrollLeft; onHorizontalScroll?.(scrollLeft); } } - }, [handleVerticalScroll, maxScrollLeft, maxScrollTop, onHorizontalScroll]); + }, [handleVerticalScroll, onHorizontalScroll, onVerticalScrollInSitu]); const handleAttachScrollbarContainer = useCallback( (el: HTMLDivElement) => { @@ -248,13 +313,14 @@ export const useTableScroll = ({ const requestScroll: ScrollRequestHandler = useCallback( (scrollRequest) => { - const { current: scrollbarContainer } = contentContainerRef; - if (scrollbarContainer) { - const { scrollLeft, scrollTop } = scrollbarContainer; + const { current: contentContainer } = contentContainerRef; + if (contentContainer) { + const [maxScrollLeft, maxScrollTop] = getMaxScroll(contentContainer); + const { scrollLeft, scrollTop } = contentContainer; contentContainerScrolledRef.current = false; if (scrollRequest.type === "scroll-row") { const activeRow = getRowElementAtIndex( - scrollbarContainer, + contentContainer, scrollRequest.rowIndex ); if (activeRow !== null) { @@ -264,13 +330,14 @@ export const useTableScroll = ({ ); if (direction && distance) { if (isVirtualScroll) { - console.log( - `virtual scroll row required ${direction} ${distance} - first Row ${firstRowRef.current}` - ); - // const from = firstRowRef.current + 1; - // console.log(`setRange from ${from}`); - // setRange({ from, to: from + viewportRowCount + 1 }); + const offset = direction === "down" ? 1 : -1; + onVerticalScrollInSitu?.(offset); + const firstRow = firstRowRef.current + offset; + firstRowRef.current = firstRow; + setRange({ + from: firstRow, + to: firstRow + viewportRowCount, + }); } else { let newScrollLeft = scrollLeft; let newScrollTop = scrollTop; @@ -285,7 +352,7 @@ export const useTableScroll = ({ maxScrollLeft ); } - scrollbarContainer.scrollTo({ + contentContainer.scrollTo({ top: newScrollTop, left: newScrollLeft, behavior: "smooth", @@ -296,7 +363,12 @@ export const useTableScroll = ({ } else if (scrollRequest.type === "scroll-page") { const { direction } = scrollRequest; if (isVirtualScroll) { - console.log(`need a virtual page scroll`); + const offset = + direction === "down" ? viewportRowCount : -viewportRowCount; + onVerticalScrollInSitu?.(offset); + const firstRow = firstRowRef.current + offset; + firstRowRef.current = firstRow; + setRange({ from: firstRow, to: firstRow + viewportRowCount }); } else { const scrollBy = direction === "down" ? appliedPageSize : -appliedPageSize; @@ -304,7 +376,7 @@ export const useTableScroll = ({ Math.max(0, scrollTop + scrollBy), maxScrollTop ); - scrollbarContainer.scrollTo({ + contentContainer.scrollTo({ top: newScrollTop, left: scrollLeft, behavior: "auto", @@ -313,9 +385,9 @@ export const useTableScroll = ({ } else if (scrollRequest.type === "scroll-end") { const { direction } = scrollRequest; const scrollTo = direction === "end" ? maxScrollTop : 0; - scrollbarContainer.scrollTo({ + contentContainer.scrollTo({ top: scrollTo, - left: scrollbarContainer.scrollLeft, + left: contentContainer.scrollLeft, behavior: "auto", }); } @@ -324,8 +396,7 @@ export const useTableScroll = ({ [ appliedPageSize, isVirtualScroll, - maxScrollLeft, - maxScrollTop, + onVerticalScrollInSitu, setRange, totalHeaderHeight, viewportRowCount, @@ -361,7 +432,7 @@ export const useTableScroll = ({ useEffect(() => { const { current: from } = firstRowRef; - const rowRange = { from, to: from + viewportRowCount + 1 }; + const rowRange = { from, to: from + viewportRowCount }; setRange(rowRange); }, [setRange, viewportRowCount]); diff --git a/vuu-ui/packages/vuu-table/src/useTableViewport.ts b/vuu-ui/packages/vuu-table/src/useTableViewport.ts index 544d141b5..802a067f0 100644 --- a/vuu-ui/packages/vuu-table/src/useTableViewport.ts +++ b/vuu-ui/packages/vuu-table/src/useTableViewport.ts @@ -7,6 +7,7 @@ import { RuntimeColumnDescriptor, TableHeadings } from "@finos/vuu-table-types"; import { MeasuredSize } from "@finos/vuu-ui-controls"; import { actualRowPositioning, + measurePinnedColumns, RowAtPositionFunc, RowOffsetFunc, RowPositioning, @@ -20,6 +21,10 @@ export interface TableViewportHookProps { headings: TableHeadings; rowCount: number; rowHeight: number; + /** + * this is the solid left/right `border` rendered on the selection block + */ + selectionEndSize?: number; size: MeasuredSize | undefined; } @@ -28,8 +33,6 @@ export interface ViewportMeasurements { contentHeight: number; horizontalScrollbarHeight: number; isVirtualScroll: boolean; - maxScrollContainerScrollHorizontal: number; - maxScrollContainerScrollVertical: number; pinnedWidthLeft: number; pinnedWidthRight: number; rowCount: number; @@ -42,12 +45,12 @@ export interface ViewportMeasurements { export interface TableViewportHookResult extends ViewportMeasurements { getRowAtPosition: RowAtPositionFunc; getRowOffset: RowOffsetFunc; - setPctScrollTop: (scrollPct: number) => void; + setInSituRowOffset: (rowIndexOffset: number) => void; + setScrollTop: (scrollTop: number, scrollPct: number) => void; } // Too simplistic, it depends on rowHeight -// const MAX_RAW_ROWS = 1_000_000; -const MAX_RAW_ROWS = 100_000; +const MAX_PIXEL_HEIGHT = 10_000_000; const UNMEASURED_VIEWPORT: TableViewportHookResult = { appliedPageSize: 0, @@ -57,56 +60,35 @@ const UNMEASURED_VIEWPORT: TableViewportHookResult = { getRowOffset: () => -1, horizontalScrollbarHeight: 0, isVirtualScroll: false, - maxScrollContainerScrollHorizontal: 0, - maxScrollContainerScrollVertical: 0, pinnedWidthLeft: 0, pinnedWidthRight: 0, rowCount: 0, - setPctScrollTop: () => undefined, + setInSituRowOffset: () => undefined, + setScrollTop: () => undefined, totalHeaderHeight: 0, verticalScrollbarWidth: 0, viewportBodyHeight: 0, }; -const measurePinnedColumns = (columns: RuntimeColumnDescriptor[]) => { - let pinnedWidthLeft = 0; - let pinnedWidthRight = 0; - let unpinnedWidth = 0; - for (const column of columns) { - const { hidden, pin, width } = column; - const visibleWidth = hidden ? 0 : width; - if (pin === "left") { - pinnedWidthLeft += visibleWidth; - } else if (pin === "right") { - pinnedWidthRight += visibleWidth; - } else { - unpinnedWidth += visibleWidth; - } - } - return { - pinnedWidthLeft: pinnedWidthLeft + 4, - pinnedWidthRight: pinnedWidthRight + 4, - unpinnedWidth, - }; -}; - export const useTableViewport = ({ columns, headerHeight, headings, rowCount, rowHeight, + selectionEndSize = 4, size, }: TableViewportHookProps): TableViewportHookResult => { + const inSituRowOffsetRef = useRef(0); const pctScrollTopRef = useRef(0); // TODO we are limited by pixels not an arbitraty number of rows - const pixelContentHeight = rowHeight * Math.min(rowCount, MAX_RAW_ROWS); + const pixelContentHeight = Math.min(rowHeight * rowCount, MAX_PIXEL_HEIGHT); const virtualContentHeight = rowCount * rowHeight; const virtualisedExtent = virtualContentHeight - pixelContentHeight; const { pinnedWidthLeft, pinnedWidthRight, unpinnedWidth } = useMemo( - () => measurePinnedColumns(columns), - [columns] + () => measurePinnedColumns(columns, selectionEndSize), + [columns, selectionEndSize] ); const totalHeaderHeightRef = useRef(headerHeight); @@ -117,20 +99,38 @@ export const useTableViewport = ({ const [getRowOffset, getRowAtPosition, isVirtualScroll] = useMemo(() => { if (virtualisedExtent) { - return virtualRowPositioning( - rowHeight, - virtualisedExtent, - pctScrollTopRef - ); + const [_getRowOffset, getRowAtPosition, _isVirtual] = + virtualRowPositioning(rowHeight, virtualisedExtent, pctScrollTopRef); + const getOffset: RowOffsetFunc = (row) => { + return _getRowOffset(row, inSituRowOffsetRef.current); + }; + return [getOffset, getRowAtPosition, _isVirtual]; } else { return actualRowPositioning(rowHeight); } }, [virtualisedExtent, rowHeight]); - const setPctScrollTop = useCallback((scrollPct: number) => { + const setScrollTop = useCallback((_: number, scrollPct: number) => { pctScrollTopRef.current = scrollPct; }, []); + /** + * The inSituRowOffset is used to simulate scrolling through a very large dataset + * without actually moving the scroll position. It is triggered by keyboard + * navigation. A simulated scroll operation will always be of one or more rows. + * A value of zero is a request to reset the offset. + */ + const setInSituRowOffset = useCallback((rowIndexOffset: number) => { + if (rowIndexOffset === 0) { + inSituRowOffsetRef.current = 0; + } else { + inSituRowOffsetRef.current = Math.max( + 0, + inSituRowOffsetRef.current + rowIndexOffset + ); + } + }, []); + return useMemo(() => { if (size) { const { current: totalHeaderHeight } = totalHeaderHeightRef; @@ -139,12 +139,6 @@ export const useTableViewport = ({ const contentWidth = pinnedWidthLeft + unpinnedWidth + pinnedWidthRight; const horizontalScrollbarHeight = contentWidth > size.width ? scrollbarSize : 0; - const maxScrollContainerScrollVertical = - pixelContentHeight - - ((size?.height ?? 0) - horizontalScrollbarHeight) + - totalHeaderHeight; - const maxScrollContainerScrollHorizontal = - contentWidth - size.width + pinnedWidthLeft; const visibleRows = (size.height - headerHeight) / rowHeight; const count = Number.isInteger(visibleRows) ? visibleRows @@ -164,12 +158,11 @@ export const useTableViewport = ({ getRowOffset, isVirtualScroll, horizontalScrollbarHeight, - maxScrollContainerScrollHorizontal, - maxScrollContainerScrollVertical, pinnedWidthLeft, pinnedWidthRight, rowCount: count, - setPctScrollTop, + setInSituRowOffset, + setScrollTop, totalHeaderHeight, verticalScrollbarWidth, viewportBodyHeight, @@ -187,7 +180,8 @@ export const useTableViewport = ({ pinnedWidthRight, pixelContentHeight, rowHeight, - setPctScrollTop, + setInSituRowOffset, + setScrollTop, size, virtualContentHeight, ]); diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index ac836b923..f29cb2fae 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -358,7 +358,7 @@ export const sortPinnedColumns = ( const leftPinnedColumns: RuntimeColumnDescriptor[] = []; const rightPinnedColumns: RuntimeColumnDescriptor[] = []; const restColumns: RuntimeColumnDescriptor[] = []; - // let pinnedWidthLeft = 0; + // 4 is the selectionEndSize, need to consider how we make this available let pinnedWidthLeft = 4; for (const column of columns) { // prettier-ignore @@ -406,6 +406,31 @@ export const sortPinnedColumns = ( } }; +export const measurePinnedColumns = ( + columns: RuntimeColumnDescriptor[], + selectionEndSize: number +) => { + let pinnedWidthLeft = 0; + let pinnedWidthRight = 0; + let unpinnedWidth = 0; + for (const column of columns) { + const { hidden, pin, width } = column; + const visibleWidth = hidden ? 0 : width; + if (pin === "left") { + pinnedWidthLeft += visibleWidth; + } else if (pin === "right") { + pinnedWidthRight += visibleWidth; + } else { + unpinnedWidth += visibleWidth; + } + } + return { + pinnedWidthLeft: pinnedWidthLeft + selectionEndSize, + pinnedWidthRight: pinnedWidthRight + selectionEndSize, + unpinnedWidth, + }; +}; + export const getTableHeadings = ( columns: RuntimeColumnDescriptor[] ): TableHeadings => { @@ -439,6 +464,8 @@ export const getTableHeadings = ( export const getColumnStyle = ({ pin, + // the 4 is `selectionEndSize`, unfortunate if we need to be passed it from cell + // need to think about how to make this available pinnedOffset = pin === "left" ? 0 : 4, width, }: RuntimeColumnDescriptor) => diff --git a/vuu-ui/packages/vuu-utils/src/keyset.ts b/vuu-ui/packages/vuu-utils/src/keyset.ts index 883ba358d..4a62c9551 100644 --- a/vuu-ui/packages/vuu-utils/src/keyset.ts +++ b/vuu-ui/packages/vuu-utils/src/keyset.ts @@ -1,48 +1,66 @@ import { VuuRange } from "@finos/vuu-protocol-types"; +const EMPTY: number[] = []; export class KeySet { - private keys: Map; - private free: number[]; - private nextKeyValue: number; + private keys = new Map(); + private nextKeyValue = 0; + private range: VuuRange; constructor(range: VuuRange) { - this.keys = new Map(); - this.free = []; - this.nextKeyValue = 0; - this.reset(range); + this.range = range; + this.init(range); } - public next(): number { - if (this.free.length > 0) { - return this.free.shift() as number; + public next(free: number[] = EMPTY): number { + if (free.length > 0) { + return free.shift() as number; } else { return this.nextKeyValue++; } } - public reset({ from, to }: VuuRange) { + private init({ from, to }: VuuRange) { + this.keys.clear(); + this.nextKeyValue = 0; + + for (let rowIndex = from; rowIndex < to; rowIndex++) { + const nextKeyValue = this.next(); + this.keys.set(rowIndex, nextKeyValue); + } + + return true; + } + + public reset(range: VuuRange) { + const { from, to } = range; + + const newSize = to - from; + const currentSize = this.range.to - this.range.from; + this.range = range; + + if (currentSize > newSize) { + // We re-initialize the range when the range size reduces, even though this will + // potentially re-render all items. + return this.init(range); + } + + const freeKeys: number[] = []; + this.keys.forEach((keyValue, rowIndex) => { if (rowIndex < from || rowIndex >= to) { - this.free.push(keyValue); + freeKeys.push(keyValue); this.keys.delete(rowIndex); } }); - const size = to - from; - if (this.keys.size + this.free.length > size) { - this.free.length = Math.max(0, size - this.keys.size); - } - for (let rowIndex = from; rowIndex < to; rowIndex++) { if (!this.keys.has(rowIndex)) { - const nextKeyValue = this.next(); + const nextKeyValue = this.next(freeKeys); this.keys.set(rowIndex, nextKeyValue); } } - if (this.nextKeyValue > this.keys.size) { - this.nextKeyValue = this.keys.size; - } + return false; } public keyFor(rowIndex: number): number { @@ -50,7 +68,6 @@ export class KeySet { if (key === undefined) { console.log(`key not found keys: ${this.toDebugString()} - free : ${this.free.join(",")} `); throw Error(`KeySet, no key found for rowIndex ${rowIndex}`); } @@ -58,8 +75,10 @@ export class KeySet { } public toDebugString() { - return Array.from(this.keys.entries()) - .map(([k, v]) => `${k}=>${v}`) - .join(","); + return `${this.keys.size} keys +${Array.from(this.keys.entries()) + .sort(([key1], [key2]) => key1 - key2) + .map(([k, v]) => `${k}=>${v}`) + .join(",")}]\n`; } } diff --git a/vuu-ui/packages/vuu-utils/src/range-utils.ts b/vuu-ui/packages/vuu-utils/src/range-utils.ts index 4756c3b5c..98443d44b 100644 --- a/vuu-ui/packages/vuu-utils/src/range-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/range-utils.ts @@ -12,36 +12,42 @@ interface FromToRange { export const NULL_RANGE: VuuRange = { from: 0, to: 0 } as const; +export const rangesAreSame = ( + r1: VuuRange | undefined, + r2: VuuRange | undefined +) => { + return r1?.from === r2?.from && r1?.to === r2?.to; +}; + export function getFullRange( { from, to }: VuuRange, bufferSize = 0, - rowCount: number = Number.MAX_SAFE_INTEGER + totalRowCount: number = Number.MAX_SAFE_INTEGER ): FromToRange { - if (bufferSize === 0) { - if (rowCount < from) { + if (from === 0 && to === 0) { + return { from, to }; + } else if (bufferSize === 0) { + if (totalRowCount < from) { return { from: 0, to: 0 }; } else { - return { from, to: Math.min(to, rowCount) }; + return { from, to: Math.min(to, totalRowCount) }; } } else if (from === 0) { - return { from, to: Math.min(to + bufferSize, rowCount) }; + return { from, to: Math.min(to + bufferSize, totalRowCount) }; } else { - const rangeSize = to - from; - const buff = Math.round(bufferSize / 2); - const shortfallBefore = from - buff < 0; - const shortFallAfter = rowCount - (to + buff) < 0; - - if (shortfallBefore && shortFallAfter) { - return { from: 0, to: rowCount }; + const shortfallBefore = from - bufferSize < 0; + const shortfallAfter = totalRowCount - (to + bufferSize) < 0; + if (shortfallBefore && shortfallAfter) { + return { from: 0, to: totalRowCount }; } else if (shortfallBefore) { - return { from: 0, to: rangeSize + bufferSize }; - } else if (shortFallAfter) { + return { from: 0, to: to + bufferSize }; + } else if (shortfallAfter) { return { - from: Math.max(0, rowCount - (rangeSize + bufferSize)), - to: rowCount, + from: Math.max(0, from - bufferSize), + to: totalRowCount, }; } else { - return { from: from - buff, to: to + buff }; + return { from: from - bufferSize, to: to + bufferSize }; } } } diff --git a/vuu-ui/packages/vuu-utils/src/row-utils.ts b/vuu-ui/packages/vuu-utils/src/row-utils.ts index 9196e2424..3191c0495 100644 --- a/vuu-ui/packages/vuu-utils/src/row-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/row-utils.ts @@ -38,9 +38,9 @@ export const virtualRowPositioning = ( virtualisedExtent: number, pctScrollTop: MutableRefObject ): RowPositioning => [ - (row) => { + (row, offset = 0) => { const rowOffset = pctScrollTop.current * virtualisedExtent; - return row[IDX] * rowHeight - rowOffset; + return (row[IDX] - offset) * rowHeight - rowOffset; }, /* Return index position of closest row diff --git a/vuu-ui/packages/vuu-utils/test/keyset.test.js b/vuu-ui/packages/vuu-utils/test/keyset.test.js index fd25f6e99..de81867b0 100644 --- a/vuu-ui/packages/vuu-utils/test/keyset.test.js +++ b/vuu-ui/packages/vuu-utils/test/keyset.test.js @@ -29,7 +29,6 @@ describe("KeySet", () => { expect(keySet.keys.size).toEqual(9); expect([...keySet.keys.keys()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]); expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]); - expect(keySet.free).toEqual([]); }); it("re-initialises a keyset, reducing size to zero, then resets to non zero value", () => { @@ -37,7 +36,6 @@ describe("KeySet", () => { keySet.reset({ from: 0, to: 0 }); expect(keySet.keys.size).toEqual(0); expect([...keySet.keys.keys()]).toEqual([]); - expect(keySet.free).toEqual([]); keySet.reset({ from: 0, to: 10 }); expect(keySet.keys.size).toEqual(10); expect([...keySet.keys.keys()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); @@ -79,6 +77,41 @@ describe("KeySet", () => { expect([...keySet.keys.keys()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); + + it("Bug repro, page down x2, page up x 2 page down", () => { + const keySet = new KeySet({ from: 0, to: 0 }); + + keySet.reset({ from: 0, to: 35 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + + keySet.reset({ from: 25, to: 66 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + + keySet.reset({ from: 55, to: 96 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + + keySet.reset({ from: 25, to: 66 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + + keySet.reset({ from: 0, to: 36 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + + keySet.reset({ from: 25, to: 66 }); + console.log(keySet.toDebugString()); + console.log(` nextKeyValue ${keySet["nextKeyValue"]}`); + // prettier-ignore + expect([...keySet.keys.entries()]).toEqual([ + [25,25], [26,26], [27,27], [28,28], [29,29], [30,30], [31,31], [32,32], [33,33], [34,34], [35,35], [36,0], [37,1], + [38,2], [39,3], [40,4], [41,5], [42,6], [43,7], [44,8], [45,9], [46,10], [47,11], [48,12], + [49,13], [50,14], [51,15], [52,16], [53,17], [54,18], + [55,19], [56,20], [57,21], [58,22], [59,23], [60,24], [61,36], [62,37], [63,38], [64,39], [65,40], + ]); + }); }); // it("handles erratic resets without ever producing a duplicate key", () => { diff --git a/vuu-ui/packages/vuu-utils/test/range-utils.test.ts b/vuu-ui/packages/vuu-utils/test/range-utils.test.ts index 2a82682a5..604db5f04 100644 --- a/vuu-ui/packages/vuu-utils/test/range-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/range-utils.test.ts @@ -1,64 +1,120 @@ import { describe, expect, it } from "vitest"; -import { rangeNewItems } from "../src/range-utils"; +import { getFullRange, rangeNewItems } from "../src/range-utils"; -describe("rangeNewItems", () => { - it("returns new range when ranges do not overlap", () => { - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 20, to: 30}) - ).toEqual({from: 20, to: 30}); - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 10, to: 20}) - ).toEqual({from: 10, to: 20}); - // prettier-ignore - expect( - rangeNewItems({from: 20, to: 30},{from: 0, to: 10}) - ).toEqual({from: 0, to: 10}); - // prettier-ignore - expect( - rangeNewItems({from: 20, to: 30},{from: 10, to: 20}) - ).toEqual({from: 10, to: 20}); +describe("range-utils", () => { + describe("rangeNewItems", () => { + it("returns new range when ranges do not overlap", () => { + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 20, to: 30}) + ).toEqual({from: 20, to: 30}); + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 10, to: 20}) + ).toEqual({from: 10, to: 20}); + // prettier-ignore + expect( + rangeNewItems({from: 20, to: 30},{from: 0, to: 10}) + ).toEqual({from: 0, to: 10}); + // prettier-ignore + expect( + rangeNewItems({from: 20, to: 30},{from: 10, to: 20}) + ).toEqual({from: 10, to: 20}); + }); + it("returns items when new range overlaps end of existing range", () => { + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 1, to: 11}) + ).toEqual({from: 10, to: 11}); + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 3, to: 13}) + ).toEqual({from: 10, to: 13}); + }); + it("returns items when new range overlaps start of existing range", () => { + // prettier-ignore + expect( + rangeNewItems({from: 10, to: 20},{from: 2, to: 12}) + ).toEqual({from: 2, to: 10}); + // prettier-ignore + expect( + rangeNewItems({from: 5, to: 15},{from: 0, to: 10}) + ).toEqual({from: 0, to: 5}); + }); + it("returns items when new range extends existing range", () => { + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 0, to: 12}) + ).toEqual({from: 10, to: 12}); + // prettier-ignore + expect( + rangeNewItems({from: 0, to: 10},{from: 0, to: 20}) + ).toEqual({from: 10, to: 20}); + // prettier-ignore + expect( + rangeNewItems({from: 5, to: 15},{from: 0, to: 15}) + ).toEqual({from: 0, to: 5}); + }); + it("returns new range when original range is subset", () => { + // prettier-ignore + expect( + rangeNewItems({from: 5, to: 15},{from: 0, to: 20}) + ).toEqual({from: 0, to: 20}); + // prettier-ignore + }); }); - it("returns items when new range overlaps end of existing range", () => { - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 1, to: 11}) - ).toEqual({from: 10, to: 11}); - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 3, to: 13}) - ).toEqual({from: 10, to: 13}); - }); - it("returns items when new range overlaps start of existing range", () => { - // prettier-ignore - expect( - rangeNewItems({from: 10, to: 20},{from: 2, to: 12}) - ).toEqual({from: 2, to: 10}); - // prettier-ignore - expect( - rangeNewItems({from: 5, to: 15},{from: 0, to: 10}) - ).toEqual({from: 0, to: 5}); - }); - it("returns items when new range extends existing range", () => { - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 0, to: 12}) - ).toEqual({from: 10, to: 12}); - // prettier-ignore - expect( - rangeNewItems({from: 0, to: 10},{from: 0, to: 20}) - ).toEqual({from: 10, to: 20}); - // prettier-ignore - expect( - rangeNewItems({from: 5, to: 15},{from: 0, to: 15}) - ).toEqual({from: 0, to: 5}); - }); - it("returns new range when original range is subset", () => { - // prettier-ignore - expect( - rangeNewItems({from: 5, to: 15},{from: 0, to: 20}) - ).toEqual({from: 0, to: 20}); - // prettier-ignore + + describe("getFullRange", () => { + describe("WHEN passed the null range", () => { + it("THEN it returns a null range", () => { + expect(getFullRange({ from: 0, to: 0 })).toEqual({ from: 0, to: 0 }); + }); + }); + describe("WHEN a buffersize is provided", () => { + describe("WHEN range starts from 0", () => { + it("THEN buffer is applied to range end", () => { + expect(getFullRange({ from: 0, to: 30 }, 10)).toEqual({ + from: 0, + to: 40, + }); + }); + }); + describe("WHEN range starts from a value less than bufferSize", () => { + it("THEN range start will be 0", () => { + expect(getFullRange({ from: 5, to: 35 }, 10)).toEqual({ + from: 0, + to: 45, + }); + }); + }); + describe("WHEN away from start end of range", () => { + it("THEN bufferSize will be applied to each end of range", () => { + expect(getFullRange({ from: 20, to: 50 }, 10)).toEqual({ + from: 10, + to: 60, + }); + }); + }); + describe("WHEN close to end of dataset", () => { + it("THEN range will be clipped to dataset size", () => { + expect(getFullRange({ from: 65, to: 95 }, 10, 100)).toEqual({ + from: 55, + to: 100, + }); + expect(getFullRange({ from: 70, to: 100 }, 10, 100)).toEqual({ + from: 60, + to: 100, + }); + }); + }); + describe("WHEN range spans full rowcount", () => { + it("THEN bufferSize is irrelevant", () => { + expect(getFullRange({ from: 0, to: 8 }, 10, 8)).toEqual({ + from: 0, + to: 8, + }); + }); + }); + }); }); }); diff --git a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx index e544771e6..ba5a438fc 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx @@ -272,8 +272,7 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { onAvailableColumnsChange: handleAvailableColumnsChange, onConfigChange: handleTableConfigChange, onFeatureInvocation: handleVuuFeatureInvoked, - // renderBufferSize: 50, - renderBufferSize: 0, + renderBufferSize: 10, }; // It is important that these values are not assigned in advance. They diff --git a/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx index 987af8975..8fda79be3 100644 --- a/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx @@ -1,9 +1,8 @@ import { ArrayDataSource } from "@finos/vuu-data-local"; -import { ArrayProxy, RowAtIndexFunc } from "@finos/vuu-data-test"; +import { ArrayProxy } from "@finos/vuu-data-test"; import { DataSource } from "@finos/vuu-data-types"; -import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; import { noScrolling, ScrollingAPI, Table } from "@finos/vuu-table"; -import { ColumnDescriptor, TableConfig } from "@finos/vuu-table-types"; +import { TableConfig } from "@finos/vuu-table-types"; import { Toolbar } from "@finos/vuu-ui-controls"; import { Button, Input } from "@salt-ds/core"; import { @@ -13,38 +12,16 @@ import { useRef, useState, } from "react"; +import { columnGenerator, rowGenerator } from "./SimpleTableDataGenerator"; let displaySequence = 1; -type RowGenerator = ( - columns: string[] -) => RowAtIndexFunc; - -export type ColumnGenerator = (count: number) => ColumnDescriptor[]; - -const columnGenerator: ColumnGenerator = (count) => { - return [{ name: "row number", width: 150 }].concat( - Array(count) - .fill(true) - .map((_, i) => { - const name = `column ${i + 1}`; - return { name, width: 150 }; - }) - ); -}; - -const rowGenerator: RowGenerator = (columns: string[]) => (index) => { - return [`row ${index + 1}`].concat( - Array(columns.length) - .fill(true) - .map((v, j) => `value ${j + 1} @ ${index + 1}`) - ); -}; - export const SimpleTable = () => { const config = useMemo( () => ({ columns: columnGenerator(5), + rowSeparators: true, + zebraStripes: true, }), [] ); diff --git a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx index 8784e60e1..7c83ef83f 100644 --- a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx @@ -56,10 +56,13 @@ const getDefaultColumnConfig = ( } }; -const SimulTable = ({ +export const SimulTable = ({ getDefaultColumnConfig, - tableName, -}: { + height = 625, + renderBufferSize = 0, + tableName = "instruments", + ...props +}: Partial & { getDefaultColumnConfig?: DefaultColumnConfiguration; tableName: SimulTableName; }) => { @@ -85,6 +88,7 @@ const SimulTable = ({ const { buildViewserverMenuOptions, handleMenuAction } = useVuuMenuActions({ dataSource: tableProps.dataSource, }); + return ( ); }; - -export const Instruments = () => ; -Instruments.displaySequence = displaySequence++; +SimulTable.displaySequence = displaySequence++; export const InstrumentsExtended = () => ( = (index: number) => T[]; + +type RowGenerator = ( + columns: string[] +) => DataRowAtIndexFunc; + +export type ColumnGenerator = (count: number) => ColumnDescriptor[]; + +export const columnGenerator: ColumnGenerator = (count) => { + return [{ name: "row number", width: 150 }].concat( + Array(count) + .fill(true) + .map((_, i) => { + const name = `column ${i + 1}`; + return { name, width: 150 }; + }) + ); +}; + +export const rowGenerator: RowGenerator = + (columns: string[]) => (index) => { + const rowIndex = index + 1; + return [`row ${rowIndex.toLocaleString()}`].concat( + Array(columns.length) + .fill(true) + .map((v, j) => `value ${j + 1} @ ${index + 1}`) + ); + }; diff --git a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx index 6b42c2c3c..e0aed3f8f 100644 --- a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx @@ -31,6 +31,8 @@ import { registerComponent as registerCellRenderer, } from "@finos/vuu-utils"; import { Button } from "@salt-ds/core"; +import { ArrayDataSource } from "@finos/vuu-data-local"; +import { DataSource } from "@finos/vuu-data-types"; import { CSSProperties, MouseEventHandler, @@ -39,11 +41,54 @@ import { useState, } from "react"; import { useTestDataSource } from "../utils"; +import { columnGenerator, rowGenerator } from "./SimpleTableDataGenerator"; import "./Table.examples.css"; let displaySequence = 1; +export const TestTable = ({ + headerHeight = 25, + height = 625, + renderBufferSize = 5, + rowCount = 1000, + rowHeight = 20, + width = 1000, +}) => { + const config = useMemo( + () => ({ + columns: columnGenerator(5), + rowSeparators: true, + zebraStripes: true, + }), + [] + ); + + const dataSource = useMemo(() => { + const generateRow = rowGenerator(config.columns.map((col) => col.name)); + const data = new Array(rowCount) + .fill(0) + .map((_, index) => generateRow(index)); + return new ArrayDataSource({ + columnDescriptors: config.columns, + data, + }); + }, [config.columns, rowCount]); + + return ( +
+ ); +}; +TestTable.displaySequence = displaySequence++; + export const NavigationStyle = () => { const tableProps = useMemo>(() => { const tableName: SimulTableName = "instruments"; @@ -237,8 +282,8 @@ export const TableNextVuuInstruments = () => {
); diff --git a/vuu-ui/tsconfig-typecheck.json b/vuu-ui/tsconfig-typecheck.json index 931d4ab37..947c935a2 100644 --- a/vuu-ui/tsconfig-typecheck.json +++ b/vuu-ui/tsconfig-typecheck.json @@ -27,5 +27,5 @@ "sample-apps/*/src", "global.d.ts" ], - "exclude": ["**/*.cy.*", "**/*.test.*"] + "exclude": ["**/*test-utils.ts", "**/*.cy.*", "**/*.test.*"] }