From ec1611244ed7bf85c0f2c184ba7e8e34093e3d08 Mon Sep 17 00:00:00 2001 From: heswell Date: Mon, 18 Dec 2023 09:01:19 +0000 Subject: [PATCH] Filter ux fixes (#1078) * UX fixes in FilterCLauseEditor * Filterbar improvements * add basic Filterbar tests for both mouse and keyboard * add tooltips for filter pills * tooltip style fix * fix layout issues * enab;e typeahead on TickingDataSource * save renamed filter * add first cut of filterbarmenu --- vuu-ui/cypress.config.ts | 5 + .../src/hooks/useTypeaheadSuggestions.ts | 1 - .../src/TickingArrayDataSource.ts | 12 +- .../vuu-data-test/src/makeSuggestions.ts | 30 +- .../vuu-data-test/src/simul/simul-module.ts | 2 +- .../array-data-source/array-data-source.ts | 21 +- vuu-ui/packages/vuu-data/src/data-source.ts | 26 +- .../packages/vuu-data/src/inlined-worker.js | 6 +- .../vuu-data/src/remote-data-source.ts | 28 +- .../__component__/Filterbar/Filterbar.cy.tsx | 323 ++++++++++++++++++ .../vuu-filters/src/filter-bar/FilterBar.tsx | 25 +- .../src/filter-bar/FilterBarMenu.css | 0 .../src/filter-bar/FilterBarMenu.tsx | 19 ++ .../src/filter-bar/useFilterBar.ts | 117 ++++++- .../src/filter-bar/useFilterBarMenu.ts | 33 ++ .../vuu-filters/src/filter-bar/useFilters.ts | 105 +++++- .../filter-builder-menu/FilterBuilderMenu.css | 24 +- .../filter-builder-menu/FilterBuilderMenu.tsx | 15 +- .../src/filter-clause/ExpandoCombobox.tsx | 4 +- .../src/filter-clause/FilterClauseEditor.tsx | 37 +- .../src/filter-clause/TextInput.tsx | 37 +- .../src/filter-clause/filterClauseTypes.ts | 5 +- .../filter-clause/useFilterClauseEditor.ts | 156 +++++++-- .../src/filter-pill/FilterPill.css | 1 + .../src/filter-pill/FilterPill.tsx | 35 +- .../src/filter-pill/filterAsReactNode.tsx | 32 ++ .../packages/vuu-filters/src/filter-utils.ts | 37 +- .../OverflowContainer.cy.tsx | 0 .../{__e2e__ => __component__}/tsconfig.json | 0 .../src/layout-reducer/layoutTypes.ts | 7 + .../vuu-layout/src/layout-view/View.tsx | 3 +- .../packages/vuu-layout/src/stack/Stack.css | 4 + .../vuu-layout/src/toolbar/useToolbar.ts | 1 - .../packages/vuu-popups/src/portal/Portal.css | 6 +- .../vuu-popups/src/tooltip/Tooltip.css | 14 +- .../vuu-popups/src/tooltip/Tooltip.tsx | 6 +- .../layout-management/useLayoutManager.tsx | 34 +- .../buildContextMenuDescriptors.ts | 26 -- .../src/combo-box/ComboBox.tsx | 12 +- .../src/combo-box/useCombobox.ts | 43 ++- .../src/common-hooks/collectionTypes.ts | 1 + .../src/common-hooks/useCollectionItems.ts | 5 +- .../common-hooks/useKeyboardNavigation.ts | 1 + .../vuu-ui-controls/src/list/useList.ts | 2 +- vuu-ui/packages/vuu-utils/src/filter-utils.ts | 4 +- .../src/useFilterTable.tsx | 24 ++ .../src/useSessionDataSource.ts | 2 +- .../Filters/FilterBar/FilterBar.examples.tsx | 67 +++- .../examples/UiControls/Combobox.examples.tsx | 19 ++ vuu-ui/tsconfig.json | 3 +- 50 files changed, 1223 insertions(+), 197 deletions(-) create mode 100644 vuu-ui/packages/vuu-filters/src/__tests__/__component__/Filterbar/Filterbar.cy.tsx create mode 100644 vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.css create mode 100644 vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.tsx create mode 100644 vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts create mode 100644 vuu-ui/packages/vuu-filters/src/filter-pill/filterAsReactNode.tsx rename vuu-ui/packages/vuu-layout/src/__tests__/{__e2e__ => __component__}/OverflowContainer/OverflowContainer.cy.tsx (100%) rename vuu-ui/packages/vuu-layout/src/__tests__/{__e2e__ => __component__}/tsconfig.json (100%) diff --git a/vuu-ui/cypress.config.ts b/vuu-ui/cypress.config.ts index 5d93ecd14..978b48e13 100644 --- a/vuu-ui/cypress.config.ts +++ b/vuu-ui/cypress.config.ts @@ -14,6 +14,11 @@ const viteConfig: UserConfig = { build: { sourcemap: true, }, + define: { + "process.env.NODE_DEBUG": false, + "process.env.LOCAL": true, + "process.env.LAYOUT_BASE_URL": `"http://127.0.0.1:8081/api"`, + }, resolve: { alias: { "cypress/react18": reactVersion.startsWith("18") diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts index 42cb853f8..69fa72802 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts @@ -44,6 +44,5 @@ export const useTypeaheadSuggestions = () => params, ...TYPEAHEAD_MESSAGE_CONSTANTS, } as ClientToServerGetUniqueValuesStartingWith); - return makeRpcCall(rpcMessage); }, []); diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index 291d5aa8d..9432b3ed1 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -19,6 +19,7 @@ import { } from "@finos/vuu-protocol-types"; import { SelectionItem } from "@finos/vuu-table-types"; import { metadataKeys } from "@finos/vuu-utils"; +import { makeSuggestions } from "./makeSuggestions"; import { Table } from "./Table"; export type RpcService = { @@ -109,7 +110,6 @@ export class TickingArrayDataSource extends ArrayDataSource { columnName: string, value: VuuRowDataItemType ): Promise { - console.log(`applyEdit ${columnName} ${value}`); const key = row[metadataKeys.KEY]; this.#table?.update(key, columnName, value); return Promise.resolve(true); @@ -160,4 +160,14 @@ export class TickingArrayDataSource extends ArrayDataSource { } return super.menuRpcCall(rpcRequest); } + + getTypeaheadSuggestions(column: string, pattern?: string): Promise { + if (this.#table) { + return makeSuggestions(this.#table, column, pattern); + } else { + throw Error( + "cannot call getTypeaheadSuggestions on TickingDataSOurce if table has not been provided" + ); + } + } } diff --git a/vuu-ui/packages/vuu-data-test/src/makeSuggestions.ts b/vuu-ui/packages/vuu-data-test/src/makeSuggestions.ts index 842ac2cb8..c4e147911 100644 --- a/vuu-ui/packages/vuu-data-test/src/makeSuggestions.ts +++ b/vuu-ui/packages/vuu-data-test/src/makeSuggestions.ts @@ -20,6 +20,7 @@ const getUniqueValues = (table: Table, column: string, pattern = "") => { uniqueValues.push(value); } } + uniqueValues.sort(); if (cachedEntry) { cachedEntry.set(column, uniqueValues); } else { @@ -31,15 +32,28 @@ const getUniqueValues = (table: Table, column: string, pattern = "") => { : uniqueValues; }; +// export const makeSuggestions = ( +// table: Table, +// column: string, +// pattern?: string +// ) => { +// const uniqueValues = getUniqueValues(table, column, pattern); +// if (uniqueValues.length > 20) { +// return uniqueValues?.slice(0, 20).map((v) => v.toString()); +// } else { +// return uniqueValues.map((v) => v.toString()); +// } +// }; export const makeSuggestions = ( table: Table, column: string, pattern?: string -) => { - const uniqueValues = getUniqueValues(table, column, pattern); - if (uniqueValues.length > 20) { - return uniqueValues?.slice(0, 20).map((v) => v.toString()); - } else { - return uniqueValues.map((v) => v.toString()); - } -}; +): Promise => + new Promise((resolve) => { + const uniqueValues = getUniqueValues(table, column, pattern); + const result = + uniqueValues.length > 20 + ? uniqueValues?.slice(0, 20).map((v) => v.toString()) + : uniqueValues.map((v) => v.toString()); + setTimeout(() => resolve(result), 100); + }); diff --git a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts index 4369ea277..c300a3c11 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts @@ -52,7 +52,7 @@ const createDataSource = (tableName: SimulTableName) => { }); }; -const suggestionFetcher: SuggestionFetcher = async ([ +const suggestionFetcher: SuggestionFetcher = ([ vuuTable, column, pattern, diff --git a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts index 16a219642..32db5b321 100644 --- a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts @@ -320,7 +320,7 @@ export class ArrayDataSource } set config(config: DataSourceConfig) { - if (configChanged(this.#config, config)) { + if (this.applyConfig(config)) { if (config) { const originalConfig = this.#config; const newConfig: DataSourceConfig = @@ -406,6 +406,25 @@ export class ArrayDataSource } } + applyConfig(config: DataSourceConfig) { + if (configChanged(this.#config, config)) { + if (config) { + const newConfig: DataSourceConfig = + config?.filter?.filter && config?.filter.filterStruct === undefined + ? { + ...config, + filter: { + filter: config.filter.filter, + filterStruct: parseFilter(config.filter.filter), + }, + } + : config; + this.#config = withConfigDefaults(newConfig); + return true; + } + } + } + get selectedRowsCount() { return this.#selectedRowsCount; } diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index 3752de6d3..6fcd40d5e 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -514,9 +514,33 @@ export type DataSourceStatus = | "suspended" | "unsubscribed"; -export interface DataSource extends EventEmitter { +export interface TypeaheadSuggestionProvider { + getTypeaheadSuggestions: ( + columnName: string, + pattern?: string + ) => Promise; +} + +export const isTypeaheadSuggestionProvider = ( + source: unknown +): source is TypeaheadSuggestionProvider => + typeof (source as TypeaheadSuggestionProvider)["getTypeaheadSuggestions"] === + "function"; + +export interface DataSource + extends EventEmitter, + Partial { aggregations: VuuAggregation[]; applyEdit: DataSourceEditHandler; + /** + * set config without triggering config event. Use this method when initialising + * a dataSource that has been restored from session state. The dataSource will + * not yet be subscribed. Triggering the config event is unnecessary and might + * cause a React exception if the event were to cause a render. + * @param config DataSourceConfig + * @returns true if config has been applied (will not be if existig config is same) + */ + applyConfig: (config: DataSourceConfig) => true | undefined; closeTreeNode: (key: string, cascade?: boolean) => void; columns: string[]; config: DataSourceConfig; diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index aa2e538e6..3d014b01e 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1,8 +1,8 @@ export const workerSourceCode = ` -var pe=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var d=(s,e,t)=>(pe(s,e,"read from private field"),t?t.call(s):e.get(s)),O=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},de=(s,e,t,n)=>(pe(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function ge(s,e,t=[],n=[]){for(let r=0,o=s.length;r{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${s}=\`)))==null?void 0:t.split("=")[1]};function J({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ttypeof s=="string"&&ot.includes(s),at="error",k=()=>{},ut="error",{loggingLevel:A=ut}=lt(),w=s=>{let e=A==="debug",t=e||A==="info",n=t||A==="warn",r=n||A==="error",o=t?p=>console.info(\`[\${s}] \${p}\`):k,i=n?p=>console.warn(\`[\${s}] \${p}\`):k,u=e?p=>console.debug(\`[\${s}] \${p}\`):k;return{errorEnabled:r,error:r?p=>console.error(\`[\${s}] \${p}\`):k}};function lt(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:ct()}}function ct(){let s=fe("vuu-logging-level");return it(s)?s:at}var{debug:pt,debugEnabled:dt}=w("range-monitor"),U=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)dt&&pt(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function me(s){return Array.isArray(s)}function gt(s){return!Array.isArray(s)}var R,he=class{constructor(){O(this,R,new Map)}addListener(e,t){let n=d(this,R).get(e);n?me(n)?n.push(t):gt(n)&&d(this,R).set(e,[n,t]):d(this,R).set(e,t)}removeListener(e,t){if(!d(this,R).has(e))return;let n=d(this,R).get(e),r=-1;if(n===t)d(this,R).delete(e);else if(Array.isArray(n)){for(let o=length;o-- >0;)if(n[o]===t){r=o;break}if(r<0)return;n.length===1?(n.length=0,d(this,R).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&d(this,R).has(e)?d(this,R).delete(e):e===void 0&&d(this,R).clear()}emit(e,...t){if(d(this,R)){let n=d(this,R).get(e);n&&this.invokeHandler(n,t)}}once(e,t){let n=(...r)=>{this.removeListener(e,n),t(...r)};this.on(e,n)}on(e,t){this.addListener(e,t)}hasListener(e,t){let n=d(this,R).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(me(e))e.slice().forEach(n=>this.invokeHandler(n,t));else switch(t.length){case 0:e();break;case 1:e(t[0]);break;case 2:e(t[0],t[1]);break;default:e.call(null,...t)}}};R=new WeakMap;var F=String.fromCharCode(8200),f=String.fromCharCode(8199);var yn={DIGIT:f,TWO_DIGITS:f+f,THREE_DIGITS:f+f+f,FULL_PADDING:[null,F+f,F+f+f,F+f+f+f,F+f+f+f+f]};var wn=f+f+f+f+f+f+f+f+f;var{COUNT:qn}=V;var N=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((r,o)=>{(o=t)&&(this.free.push(r),this.keys.delete(o))});let n=t-e;this.keys.size+this.free.length>n&&(this.free.length=Math.max(0,n-this.keys.size));for(let r=e;rthis.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 de=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var d=(s,e,t)=>(de(s,e,"read from private field"),t?t.call(s):e.get(s)),k=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},ge=(s,e,t,n)=>(de(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function fe(s,e,t=[],n=[]){for(let r=0,o=s.length;r{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${s}=\`)))==null?void 0:t.split("=")[1]};function K({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ts.replaceAll("/",".")},"dd/mm/yyyy":{locale:"en-GB",options:{...I}},"dd MMM yyyy":{locale:"en-GB",options:{...I,month:"short"}},"dd MMMM yyyy":{locale:"en-GB",options:{...I,month:"long"}},"mm/dd/yyyy":{locale:"en-US",options:{...I}},"MMM dd, yyyy":{locale:"en-US",options:{...I,month:"short"}},"MMMM dd, yyyy":{locale:"en-US",options:{...I,month:"long"}}},Rn={...ut,...at};var ct=["error","warn","info","debug"],pt=s=>typeof s=="string"&&ct.includes(s),dt="error",A=()=>{},gt="error",{loggingLevel:U=gt}=ft(),w=s=>{let e=U==="debug",t=e||U==="info",n=t||U==="warn",r=n||U==="error",o=t?p=>console.info(\`[\${s}] \${p}\`):A,i=n?p=>console.warn(\`[\${s}] \${p}\`):A,u=e?p=>console.debug(\`[\${s}] \${p}\`):A;return{errorEnabled:r,error:r?p=>console.error(\`[\${s}] \${p}\`):A}};function ft(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:mt()}}function mt(){let s=me("vuu-logging-level");return pt(s)?s:dt}var{debug:ht,debugEnabled:Ct}=w("range-monitor"),F=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)Ct&&ht(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function be(s){return Array.isArray(s)}function bt(s){return!Array.isArray(s)}var R,Re=class{constructor(){k(this,R,new Map)}addListener(e,t){let n=d(this,R).get(e);n?be(n)?n.push(t):bt(n)&&d(this,R).set(e,[n,t]):d(this,R).set(e,t)}removeListener(e,t){if(!d(this,R).has(e))return;let n=d(this,R).get(e),r=-1;if(n===t)d(this,R).delete(e);else if(Array.isArray(n)){for(let o=length;o-- >0;)if(n[o]===t){r=o;break}if(r<0)return;n.length===1?(n.length=0,d(this,R).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&d(this,R).has(e)?d(this,R).delete(e):e===void 0&&d(this,R).clear()}emit(e,...t){if(d(this,R)){let n=d(this,R).get(e);n&&this.invokeHandler(n,t)}}once(e,t){let n=(...r)=>{this.removeListener(e,n),t(...r)};this.on(e,n)}on(e,t){this.addListener(e,t)}hasListener(e,t){let n=d(this,R).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(be(e))e.slice().forEach(n=>this.invokeHandler(n,t));else switch(t.length){case 0:e();break;case 1:e(t[0]);break;case 2:e(t[0],t[1]);break;default:e.call(null,...t)}}};R=new WeakMap;var N=String.fromCharCode(8200),f=String.fromCharCode(8199);var Ln={DIGIT:f,TWO_DIGITS:f+f,THREE_DIGITS:f+f+f,FULL_PADDING:[null,N+f,N+f+f,N+f+f+f,N+f+f+f+f]};var _n=f+f+f+f+f+f+f+f+f;var{COUNT:rr}=V;var W=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((r,o)=>{(o=t)&&(this.free.push(r),this.keys.delete(o))});let n=t-e;this.keys.size+this.free.length>n&&(this.free.length=Math.max(0,n-this.keys.size));for(let r=e;rthis.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 keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{IDX:Kn}=V;var{SELECTED:Qn}=V,M={False:0,True:1,First:2,Last:4};var ft=(s,e)=>e>=s[0]&&e<=s[1],mt=M.True+M.First+M.Last,ht=M.True+M.First,Ct=M.True+M.Last,K=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return mt}else if(ft(t,e))return e===t[0]?ht:e===t[1]?Ct:M.True;return M.False};var Ce=s=>{if(s.every(t=>typeof t=="number"))return s;let e=[];for(let t of s)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var bt=(()=>{let s=0,e=()=>\`0000\${(Math.random()*36**4<<0).toString(36)}\`.slice(-4);return()=>(s+=1,\`u\${e()}\${s}\`)})();var{debug:_s,debugEnabled:Os,error:Te,info:E,infoEnabled:wt,warn:D}=w("websocket-connection"),Se="ws",Et=s=>s.startsWith(Se+"://")||s.startsWith(Se+"s://"),Ee={},Q=Symbol("setWebsocket"),W=Symbol("connectionCallback");async function Ve(s,e,t,n=10,r=5){return Ee[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},Me(s,e,t)}async function Z(s){throw Error("connection broken")}async function Me(s,e,t,n){let{status:r,connect:o,reconnect:i}=Ee[s],u=r==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",p=await Mt(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[Q](p);let a=n!=null?n:new X(p,s,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 Vt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var Vt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(Me(s,e,t,n))},r)}),Mt=(s,e)=>new Promise((t,n)=>{let r=Et(s)?s:\`wss://\${s}\`;wt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=i=>n(i)}),ye=()=>{D==null||D("Connection cannot be closed, socket not yet opened")},we=s=>{D==null||D(\`Message cannot be sent, socket closed \${s.body.type}\`)},xt=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},X=class{constructor(e,t,n,r){this.close=ye;this.requiresLogin=!0;this.send=we;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=xt(e.data);this.messagesCount+=1,this[W](t)};this.url=t,this.protocol=n,this[W]=r,this[Q](e)}reconnect(){Z(this)}[(W,Q)](e){let t=this[W];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=()=>{Te("\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"?Te("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=r)},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=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=ye,this.send=we,E==null||E("close websocket")}}};var vt=["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"],xe=s=>vt.includes(s.type),ve=s=>s.type==="VIEW_PORT_RPC_CALL",q=({requestId:s,...e})=>[s,e],Ie=s=>{let e=s.at(0);if(e.updateType==="SIZE"){if(s.length===1)return s;e=s.at(1)}let t=s.at(-1);return[e,t]},De=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var ee=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Pe=s=>s.type==="connection-status",Le=s=>s.type==="connection-metrics";var _e=s=>"viewport"in s,Oe=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&\$(s.action.table),\$=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var ke="CHANGE_VP_SUCCESS";var Ae="CLOSE_TREE_NODE",Ue="CLOSE_TREE_SUCCESS";var Fe="CREATE_VP",Ne="DISABLE_VP",We="DISABLE_VP_SUCCESS";var qe="ENABLE_VP",\$e="ENABLE_VP_SUCCESS";var te="GET_VP_VISUAL_LINKS",Ge="GET_VIEW_PORT_MENUS";var Be="HB",He="HB_RESP",je="LOGIN",ze="OPEN_TREE_NODE",Je="OPEN_TREE_SUCCESS";var Ke="REMOVE_VP";var Ye="SET_SELECTION_SUCCESS";var Qe=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var Xe=[],T=w("array-backed-moving-window");function It(s,e){if(!e||e.data.length!==s.data.length||e.sel!==s.sel)return!1;for(let t=0;t{var t;if((t=T.info)==null||t.call(T,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return d(this,h).to-t0&&e-d(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:r}=this.range;if(t=r)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-d(this,h).from;if(It(e,this.internalData[n]))return!1;let r=this.isWithinClientRange(t);return(r||this.isWithinRange(t))&&(!this.internalData[n]&&r&&(this.rowsWithinRange+=1),this.internalData[n]=e),r}getAtIndex(e){return d(this,h).isWithin(e)&&this.internalData[e-d(this,h).from]!=null?this.internalData[e-d(this,h).from]:void 0}isWithinRange(e){return d(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=T.debug)==null||p.call(T,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,r=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===r)return[!1,Xe];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,r;if(e!==d(this,h).from||t!==d(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,i]=d(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){r=e[o];break}return n&&r?[n.rowIndex,r.rowIndex]:[-1,-1]}};h=new WeakMap;var Dt=[],{debug:C,debugEnabled:B,error:Pt,info:g,infoEnabled:Lt,warn:P}=w("viewport"),_t=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),H=[void 0,void 0],Ot={count:0,mode:void 0,size:0,ts:0},j=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},m){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=Ot;this.updateThrottleTimer=void 0;this.rangeMonitor=new U("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,r=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,r=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,r=0;return this.lastUpdateStatus.mode=e,r};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:r}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(r>=o&&r{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)?(g==null||g("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=r,this.groupBy=o,this.keys=new N(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,Lt&&(g==null||g(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new G(this.clientRange,u,this.bufferSize),this.postMessageToClient=m}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:Fe,table:this.table,range:J(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:r,range:o,sort:i,groupBy:u},c){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:r,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:r}=this,o=r.get(e);if(!o){Pt(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(g==null||g(\`completeOperation \${i}\`),r.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 P==null||P("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:m}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=m,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),C==null||C(\`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,C==null||C(\`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[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){B&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...J(t,this.bufferSize,u)}:null;if(c){B&&(C==null||C(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});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:m}=a;this.dataWindow.outOfRange(l,m)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:P==null||P("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?re:ne;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:ze,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ae,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,r){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:r,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,g==null||g("suspend")}resume(){return this.suspended=!1,B&&(C==null||C(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?re:ne;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),g==null||g(\`enable: \${this.serverViewportId}\`),{type:qe,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),g==null||g(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Ne,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),C==null||C(\`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 g==null||g(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...r}=t;return this.useBatchMode&&(this.batchMode=!0),B?C==null||C(\`setConfig \${JSON.stringify(t)}\`):g==null||g("setConfig"),this.createRequest({...r,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}),g==null||g(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),g==null||g(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=Dt){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}),g==null||g(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:Ce(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],i=!0;if(e>=r&&er&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,n,r));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,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?H:[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}}},ne=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?K(o,s):0].concat(n),re=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[i,u,,c,,p,...a]=n;return[s,r.keyFor(s),c,u,i,p,e,t?K(o,s):0].concat(a)};var et=1;var{debug:x,debugEnabled:se,error:L,info:y,infoEnabled:kt,warn:oe}=w("server-proxy"),b=()=>\`\${et++}\`,At={},Ut=s=>s.disabled!==!0&&s.suspended!==!0,Ft={type:"NO_ACTION"},Nt=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function Wt(s,e){return s.map(t=>{let{parentVpId:n}=t,r=e.get(n);if(r)return{...t,parentClientVpId:r.clientViewportId,label:r.title};throw Error("addLabelsToLinks viewport not found")})}var z=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]=ge(Array.from(this.viewports.values()),Ut);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.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,r)=>{this.sendMessageToServer({type:je,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&L("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))L(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),n=new j(e,this.postMessageToClient);this.viewports.set(e.viewport,n);let r=this.awaitResponseToMessage(n.subscribe(),e.viewport);Promise.all([r,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=n;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,n)),this.mapClientToServerViewport.set(e.viewport,c);let a=n.handleSubscribed(i,u);a&&(this.postMessageToClient(a),se&&x(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),n.disabled&&this.disableViewport(n),this.queuedRequests.length>0&&this.processQueuedRequests(),p==="subscribing"&&!\$(n.table)&&(this.sendMessageToServer({type:te,vpId:c}),this.sendMessageToServer({type:Ge,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:m}])=>l!==c&&!m).forEach(([l])=>{this.sendMessageToServer({type:te,vpId:l})}))})}}processQueuedRequests(){let e={};for(;this.queuedRequests.length;){let t=this.queuedRequests.pop();if(t){let{clientViewportId:n,message:r,requestId:o}=t;if(r.type==="CHANGE_VP_RANGE"){if(e.CHANGE_VP_RANGE)continue;e.CHANGE_VP_RANGE=!0;let i=this.mapClientToServerViewport.get(n);i&&this.sendMessageToServer({...r,viewPortId:i},o)}}}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(y==null||y(\`Unsubscribe Message (Client to Server): - \${t}\`),this.sendMessageToServer({type:Ke,viewPortId:t})):L(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let r=this.viewports.get(n);if(r)return r;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=b(),[r,o,i]=e.rangeRequest(n,t.range);y==null||y(\`setViewRange \${t.range.from} - \${t.range.to}\`),r&&(this.sendIfReady(r,n,e.status==="subscribed")||this.queuedRequests.push({clientViewportId:t.viewport,message:r,requestId:n})),o?(y==null||y(\`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=b(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=b(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=b(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=b(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=b(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=b(),{columns:r}=t,o=e.columnRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=b(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=b(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=b(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{y==null||y("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(x==null||x("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,n]=e.resume();x==null||x(\`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=b();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,i=b(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(i,o,u,r);this.sendMessageToServer(c,i)}else L("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=b(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var r;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(r=o.links)!=null&&r.some(i=>i.parentVpId===t)){let[i]=o.setLinks(Nt(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:r})=>r===e)){let[r]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(r)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=q(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=q(e);this.sendMessageToServer({...r,vpId:t.serverViewportId,namedParams:{}},n)}}rpcCall(e){let[t,n]=q(e),r=Qe(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){var t;if(_e(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(ve(e))return this.viewportRpcCall(e);if(xe(e))return this.menuRpcCall(e);{let{type:n,requestId:r}=e;switch(n){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:n},r)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:r})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,r).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:r})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}L(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=b()){if(\$(e))return Promise.resolve(void 0);let n=\`\${e.module}:\${e.table}\`,r=this.cachedTableMetaRequests.get(n);return r||(r=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(n,r)),r==null?void 0:r.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=b()){return new Promise((n,r)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:r,resolve:n})})}sendIfReady(e,t,n=!0){return n&&this.sendMessageToServer(e,t),n}sendMessageToServer(e,t=\`\${et++}\`,n=At){let{module:r="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:r,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:r}=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 Be:this.sendMessageToServer({type:He,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(r)this.sessionId=r,(u=this.pendingLogin)==null||u.resolve(r),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 Ye:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(n)}break;case ke:case We:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),se&&x(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case \$e:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);if(l){this.postMessageToClient(l);let[m,S]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:S,size:m,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=De(t.rows);for(let[l,m]of Object.entries(a)){let S=i.get(l);S?S.updateRows(m):oe==null||oe(\`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:m}=t;a.completeOperation(n,l,m)}}break;case Je:case Ue:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:m,parentColumnName:S}=t,_=a.completeOperation(n,m,l.clientViewportId,S);_&&this.postMessageToClient(_)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(n);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 m=Wt(a,this.viewports),[S,_]=l.setLinks(m);if(this.postMessageToClient(S),_){let{link:ue,parentClientVpId:tt}=_,le=b(),ce=this.mapClientToServerViewport.get(tt);if(ce){let nt=l.createLink(le,ue.fromColumn,ce,ue.toColumn);this.sendMessageToServer(nt,le)}}}}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: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:l,vpId:m}=t,S=this.viewports.get(m);S&&this.postMessageToClient({clientViewportId:S.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:n});break}case"VIEW_PORT_MENU_RESP":if(Oe(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(m=>{let S=ee(m);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:S},tableAlreadyOpen:this.isTableOpen(a.table),requestId:n})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Ft,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:n})}break;case"RPC_RESP":{let{method:a,result:l}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:l,requestId:n})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:l}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:l,requestId:n})}break;case"ERROR":L(t.msg);break;default:kt&&y(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=ee(e),this.cachedTableSchemas.set(r,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!==H){let[r,o]=n,i=e.getNewRowCount();(i!==void 0||r&&r.length>0)&&(se&&x(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:i,type:"viewport-update"}))}}})}};var I,{info:ie,infoEnabled:ae}=w("worker");async function qt(s,e,t,n,r,o,i){let u=await Ve(s,e,c=>{Le(c)?postMessage({type:"connection-metrics",messages:c}):Pe(c)?(r(c),c.status==="reconnected"&&I.reconnect()):I.handleMessageFromServer(c)},o,i);I=new z(u,c=>\$t(c)),u.requiresLogin&&await I.login(t,n)}function \$t(s){postMessage(s)}var Gt=async({data:s})=>{switch(s.type){case"connect":await qt(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":ae&&ie(\`client subscribe: \${JSON.stringify(s)}\`),I.subscribe(s);break;case"unsubscribe":ae&&ie(\`client unsubscribe: \${JSON.stringify(s)}\`),I.unsubscribe(s.viewport);break;default:ae&&ie(\`client message: \${JSON.stringify(s)}\`),I.handleMessageFromClient(s)}};self.addEventListener("message",Gt);postMessage({type:"ready"}); + \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{IDX:pr}=V;var{SELECTED:fr}=V,M={False:0,True:1,First:2,Last:4};var Rt=(s,e)=>e>=s[0]&&e<=s[1],Tt=M.True+M.First+M.Last,yt=M.True+M.First,St=M.True+M.Last,Q=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return Tt}else if(Rt(t,e))return e===t[0]?yt:e===t[1]?St:M.True;return M.False};var Te=s=>{if(s.every(t=>typeof t=="number"))return s;let e=[];for(let t of s)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var wt=(()=>{let s=0,e=()=>\`0000\${(Math.random()*36**4<<0).toString(36)}\`.slice(-4);return()=>(s+=1,\`u\${e()}\${s}\`)})();var{debug:Ks,debugEnabled:Qs,error:we,info:E,infoEnabled:vt,warn:P}=w("websocket-connection"),Ee="ws",It=s=>s.startsWith(Ee+"://")||s.startsWith(Ee+"s://"),xe={},Z=Symbol("setWebsocket"),q=Symbol("connectionCallback");async function ve(s,e,t,n=10,r=5){return xe[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},Ie(s,e,t)}async function Y(s){throw Error("connection broken")}async function Ie(s,e,t,n){let{status:r,connect:o,reconnect:i}=xe[s],u=r==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",p=await Pt(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[Z](p);let a=n!=null?n:new ee(p,s,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 Dt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var Dt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(Ie(s,e,t,n))},r)}),Pt=(s,e)=>new Promise((t,n)=>{let r=It(s)?s:\`wss://\${s}\`;vt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=i=>n(i)}),Ve=()=>{P==null||P("Connection cannot be closed, socket not yet opened")},Me=s=>{P==null||P(\`Message cannot be sent, socket closed \${s.body.type}\`)},Lt=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},ee=class{constructor(e,t,n,r){this.close=Ve;this.requiresLogin=!0;this.send=Me;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=Lt(e.data);this.messagesCount+=1,this[q](t)};this.url=t,this.protocol=n,this[q]=r,this[Z](e)}reconnect(){Y(this)}[(q,Z)](e){let t=this[q];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=()=>{we("\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"?we("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Y(this),this.send=r)},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"&&(Y(this),this.send=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=Ve,this.send=Me,E==null||E("close websocket")}}};var _t=["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"],De=s=>_t.includes(s.type),Pe=s=>s.type==="VIEW_PORT_RPC_CALL",\$=({requestId:s,...e})=>[s,e],Le=s=>{let e=s.at(0);if(e.updateType==="SIZE"){if(s.length===1)return s;e=s.at(1)}let t=s.at(-1);return[e,t]},_e=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var te=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Oe=s=>s.type==="connection-status",ke=s=>s.type==="connection-metrics";var Ae=s=>"viewport"in s,Ue=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&G(s.action.table),G=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var Fe="CHANGE_VP_SUCCESS";var Ne="CLOSE_TREE_NODE",We="CLOSE_TREE_SUCCESS";var qe="CREATE_VP",\$e="DISABLE_VP",Ge="DISABLE_VP_SUCCESS";var Be="ENABLE_VP",He="ENABLE_VP_SUCCESS";var ne="GET_VP_VISUAL_LINKS",je="GET_VIEW_PORT_MENUS";var ze="HB",Je="HB_RESP",Ke="LOGIN",Qe="OPEN_TREE_NODE",Xe="OPEN_TREE_SUCCESS";var Ye="REMOVE_VP";var Ze="SET_SELECTION_SUCCESS";var tt=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var nt=[],T=w("array-backed-moving-window");function Ot(s,e){if(!e||e.data.length!==s.data.length||e.sel!==s.sel)return!1;for(let t=0;t{var t;if((t=T.info)==null||t.call(T,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return d(this,h).to-t0&&e-d(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:r}=this.range;if(t=r)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-d(this,h).from;if(Ot(e,this.internalData[n]))return!1;let r=this.isWithinClientRange(t);return(r||this.isWithinRange(t))&&(!this.internalData[n]&&r&&(this.rowsWithinRange+=1),this.internalData[n]=e),r}getAtIndex(e){return d(this,h).isWithin(e)&&this.internalData[e-d(this,h).from]!=null?this.internalData[e-d(this,h).from]:void 0}isWithinRange(e){return d(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=T.debug)==null||p.call(T,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,r=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===r)return[!1,nt];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,r;if(e!==d(this,h).from||t!==d(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,i]=d(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){r=e[o];break}return n&&r?[n.rowIndex,r.rowIndex]:[-1,-1]}};h=new WeakMap;var kt=[],{debug:C,debugEnabled:H,error:At,info:g,infoEnabled:Ut,warn:L}=w("viewport"),Ft=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),j=[void 0,void 0],Nt={count:0,mode:void 0,size:0,ts:0},z=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},m){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=Nt;this.updateThrottleTimer=void 0;this.rangeMonitor=new F("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,r=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,r=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,r=0;return this.lastUpdateStatus.mode=e,r};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:r}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(r>=o&&r{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)?(g==null||g("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=r,this.groupBy=o,this.keys=new W(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,Ut&&(g==null||g(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new B(this.clientRange,u,this.bufferSize),this.postMessageToClient=m}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:qe,table:this.table,range:K(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:r,range:o,sort:i,groupBy:u},c){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:r,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:r}=this,o=r.get(e);if(!o){At(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(g==null||g(\`completeOperation \${i}\`),r.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 L==null||L("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:m}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=m,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),C==null||C(\`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,C==null||C(\`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[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){H&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...K(t,this.bufferSize,u)}:null;if(c){H&&(C==null||C(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});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:m}=a;this.dataWindow.outOfRange(l,m)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:L==null||L("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?se:re;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:Qe,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ne,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,r){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:r,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,g==null||g("suspend")}resume(){return this.suspended=!1,H&&(C==null||C(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?se:re;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),g==null||g(\`enable: \${this.serverViewportId}\`),{type:Be,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),g==null||g(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:\$e,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),C==null||C(\`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 g==null||g(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...r}=t;return this.useBatchMode&&(this.batchMode=!0),H?C==null||C(\`setConfig \${JSON.stringify(t)}\`):g==null||g("setConfig"),this.createRequest({...r,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}),g==null||g(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),g==null||g(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=kt){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}),g==null||g(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:Te(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],i=!0;if(e>=r&&er&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,n,r));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,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?j:[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}}},re=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?Q(o,s):0].concat(n),se=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[i,u,,c,,p,...a]=n;return[s,r.keyFor(s),c,u,i,p,e,t?Q(o,s):0].concat(a)};var rt=1;var{debug:x,debugEnabled:oe,error:_,info:S,infoEnabled:Wt,warn:ie}=w("server-proxy"),b=()=>\`\${rt++}\`,qt={},\$t=s=>s.disabled!==!0&&s.suspended!==!0,Gt={type:"NO_ACTION"},Bt=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function Ht(s,e){return s.map(t=>{let{parentVpId:n}=t,r=e.get(n);if(r)return{...t,parentClientVpId:r.clientViewportId,label:r.title};throw Error("addLabelsToLinks viewport not found")})}var J=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]=fe(Array.from(this.viewports.values()),\$t);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.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,r)=>{this.sendMessageToServer({type:Ke,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&_("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))_(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),n=new z(e,this.postMessageToClient);this.viewports.set(e.viewport,n);let r=this.awaitResponseToMessage(n.subscribe(),e.viewport);Promise.all([r,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=n;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,n)),this.mapClientToServerViewport.set(e.viewport,c);let a=n.handleSubscribed(i,u);a&&(this.postMessageToClient(a),oe&&x(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),n.disabled&&this.disableViewport(n),this.queuedRequests.length>0&&this.processQueuedRequests(),p==="subscribing"&&!G(n.table)&&(this.sendMessageToServer({type:ne,vpId:c}),this.sendMessageToServer({type:je,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:m}])=>l!==c&&!m).forEach(([l])=>{this.sendMessageToServer({type:ne,vpId:l})}))})}}processQueuedRequests(){let e={};for(;this.queuedRequests.length;){let t=this.queuedRequests.pop();if(t){let{clientViewportId:n,message:r,requestId:o}=t;if(r.type==="CHANGE_VP_RANGE"){if(e.CHANGE_VP_RANGE)continue;e.CHANGE_VP_RANGE=!0;let i=this.mapClientToServerViewport.get(n);i&&this.sendMessageToServer({...r,viewPortId:i},o)}}}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(S==null||S(\`Unsubscribe Message (Client to Server): + \${t}\`),this.sendMessageToServer({type:Ye,viewPortId:t})):_(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let r=this.viewports.get(n);if(r)return r;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=b(),[r,o,i]=e.rangeRequest(n,t.range);S==null||S(\`setViewRange \${t.range.from} - \${t.range.to}\`),r&&(this.sendIfReady(r,n,e.status==="subscribed")||this.queuedRequests.push({clientViewportId:t.viewport,message:r,requestId:n})),o?(S==null||S(\`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=b(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=b(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=b(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=b(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=b(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=b(),{columns:r}=t,o=e.columnRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=b(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=b(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=b(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{S==null||S("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(x==null||x("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,n]=e.resume();x==null||x(\`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=b();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,i=b(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(i,o,u,r);this.sendMessageToServer(c,i)}else _("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=b(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var r;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(r=o.links)!=null&&r.some(i=>i.parentVpId===t)){let[i]=o.setLinks(Bt(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:r})=>r===e)){let[r]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(r)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=\$(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=\$(e);this.sendMessageToServer({...r,vpId:t.serverViewportId,namedParams:{}},n)}}rpcCall(e){let[t,n]=\$(e),r=tt(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){var t;if(Ae(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(Pe(e))return this.viewportRpcCall(e);if(De(e))return this.menuRpcCall(e);{let{type:n,requestId:r}=e;switch(n){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:n},r)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:r})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,r).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:r})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}_(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=b()){if(G(e))return Promise.resolve(void 0);let n=\`\${e.module}:\${e.table}\`,r=this.cachedTableMetaRequests.get(n);return r||(r=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(n,r)),r==null?void 0:r.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=b()){return new Promise((n,r)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:r,resolve:n})})}sendIfReady(e,t,n=!0){return n&&this.sendMessageToServer(e,t),n}sendMessageToServer(e,t=\`\${rt++}\`,n=qt){let{module:r="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:r,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:r}=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 ze:this.sendMessageToServer({type:Je,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(r)this.sessionId=r,(u=this.pendingLogin)==null||u.resolve(r),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 Ze:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(n)}break;case Fe:case Ge:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),oe&&x(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case He:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);if(l){this.postMessageToClient(l);let[m,y]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:y,size:m,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=_e(t.rows);for(let[l,m]of Object.entries(a)){let y=i.get(l);y?y.updateRows(m):ie==null||ie(\`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:m}=t;a.completeOperation(n,l,m)}}break;case Xe:case We:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:m,parentColumnName:y}=t,O=a.completeOperation(n,m,l.clientViewportId,y);O&&this.postMessageToClient(O)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(n);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 m=Ht(a,this.viewports),[y,O]=l.setLinks(m);if(this.postMessageToClient(y),O){let{link:le,parentClientVpId:st}=O,ce=b(),pe=this.mapClientToServerViewport.get(st);if(pe){let ot=l.createLink(ce,le.fromColumn,pe,le.toColumn);this.sendMessageToServer(ot,ce)}}}}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: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:l,vpId:m}=t,y=this.viewports.get(m);y&&this.postMessageToClient({clientViewportId:y.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:n});break}case"VIEW_PORT_MENU_RESP":if(Ue(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(m=>{let y=te(m);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:y},tableAlreadyOpen:this.isTableOpen(a.table),requestId:n})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Gt,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:n})}break;case"RPC_RESP":{let{method:a,result:l}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:l,requestId:n})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:l}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:l,requestId:n})}break;case"ERROR":_(t.msg);break;default:Wt&&S(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=te(e),this.cachedTableSchemas.set(r,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!==j){let[r,o]=n,i=e.getNewRowCount();(i!==void 0||r&&r.length>0)&&(oe&&x(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:i,type:"viewport-update"}))}}})}};var D,{info:ae,infoEnabled:ue}=w("worker");async function jt(s,e,t,n,r,o,i){let u=await ve(s,e,c=>{ke(c)?postMessage({type:"connection-metrics",messages:c}):Oe(c)?(r(c),c.status==="reconnected"&&D.reconnect()):D.handleMessageFromServer(c)},o,i);D=new J(u,c=>zt(c)),u.requiresLogin&&await D.login(t,n)}function zt(s){postMessage(s)}var Jt=async({data:s})=>{switch(s.type){case"connect":await jt(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":ue&&ae(\`client subscribe: \${JSON.stringify(s)}\`),D.subscribe(s);break;case"unsubscribe":ue&&ae(\`client unsubscribe: \${JSON.stringify(s)}\`),D.unsubscribe(s.viewport);break;default:ue&&ae(\`client message: \${JSON.stringify(s)}\`),D.handleMessageFromClient(s)}};self.addEventListener("message",Jt);postMessage({type:"ready"}); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data/src/remote-data-source.ts b/vuu-ui/packages/vuu-data/src/remote-data-source.ts index e64a9b613..119b690c3 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -437,6 +437,21 @@ export class RemoteDataSource } set config(config: DataSourceConfig) { + if (this.applyConfig(config)) { + if (this.#config && this.viewport && this.server) { + if (config) { + this.server?.send({ + viewport: this.viewport, + type: "config", + config: this.#config, + }); + } + } + this.emit("config", this.#config); + } + } + + applyConfig(config: DataSourceConfig) { if (configChanged(this.#config, config)) { if (config) { const newConfig: DataSourceConfig = @@ -449,19 +464,8 @@ export class RemoteDataSource }, } : config; - this.#config = withConfigDefaults(newConfig); - - if (this.#config && this.viewport && this.server) { - if (config) { - this.server?.send({ - viewport: this.viewport, - type: "config", - config: this.#config, - }); - } - } - this.emit("config", this.#config); + return true; } } } diff --git a/vuu-ui/packages/vuu-filters/src/__tests__/__component__/Filterbar/Filterbar.cy.tsx b/vuu-ui/packages/vuu-filters/src/__tests__/__component__/Filterbar/Filterbar.cy.tsx new file mode 100644 index 000000000..513480a40 --- /dev/null +++ b/vuu-ui/packages/vuu-filters/src/__tests__/__component__/Filterbar/Filterbar.cy.tsx @@ -0,0 +1,323 @@ +import { clear } from "console"; +import React from "react"; +// TODO try and get TS path alias working to avoid relative paths like this +import { DefaultFilterBar } from "../../../../../../showcase/src/examples/Filters/FilterBar/FilterBar.examples"; + +// Common selectors +const OVERFLOW_CONTAINER = ".vuuOverflowContainer-wrapContainer"; +const OVERFLOW_INDICATOR = ".vuuOverflowContainer-OverflowIndicator"; +const ADD_BUTTON = ".vuuFilterBar-add"; +const FILTER_CLAUSE = ".vuuFilterClause"; +const FILTER_CLAUSE_FIELD = ".vuuFilterClauseField"; + +const findOverflowItem = (className: string) => + cy.get(OVERFLOW_CONTAINER).find(className); + +const clickListItem = (label: string) => { + cy.findByText(label).realHover(); + cy.findByText(label).realClick(); +}; + +const clickListItems = (...labels: string[]) => { + for (const label of labels) { + clickListItem(label); + } +}; + +const clickButton = (label: string) => { + cy.findByText(label).should("be.visible"); + cy.findByText(label).realClick(); +}; + +const waitUntilEditableLabelIsFocused = (overflowItemClassName: string) => + findOverflowItem(overflowItemClassName) + .find(".vuuEditableLabel") + .find("input") + .should("be.focused"); + +const assertInputValue = (className: string, value: string) => + cy.get(`${className} input`).should("have.attr", "value", value); + +describe("WHEN it initially renders", () => { + it("THEN expected classname is present", () => { + cy.mount(); + const container = cy.findByTestId("filterbar"); + container.should("have.class", "vuuFilterBar"); + }); + it("THEN content container is empty, except for non-visible overflow indicator", () => { + cy.mount(); + const container = cy.findByTestId("filterbar"); + container.get(OVERFLOW_CONTAINER).find("> *").should("have.length", 1); + container.get(OVERFLOW_INDICATOR).should("exist"); + container.get(OVERFLOW_INDICATOR).should("have.css", "width", "0px"); + }); +}); + +describe("The mouse user", () => { + describe("WHEN user click Add button on empty Filterbar", () => { + it("THEN new FilterClause is initiated", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + cy.get(OVERFLOW_CONTAINER).find("> *").should("have.length", 3); + cy.get(OVERFLOW_CONTAINER) + .find('[data-index="0"] > *') + .should("have.class", "vuuFilterClause"); + + cy.get(OVERFLOW_CONTAINER) + .find('[data-index="1"] > *') + .should("have.class", "vuuFilterBar-remove"); + + cy.get(OVERFLOW_INDICATOR).should("exist"); + cy.get(OVERFLOW_INDICATOR).should("have.css", "width", "0px"); + }); + + it("THEN column combobox is focused and the dropdown shown", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + cy.findByRole("combobox").should("be.focused"); + cy.findByRole("combobox").should("have.attr", "aria-expanded", "true"); + + // make sure columns list has renderered + cy.findByText("currency").should("exist"); + }); + }); + + describe("WHEN user selects a column", () => { + it("THEN focus moves to operator field", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItem("currency"); + cy.get(FILTER_CLAUSE).should("have.length", 1); + cy.get(FILTER_CLAUSE_FIELD).should("have.length", 2); + + assertInputValue(".vuuFilterClauseColumn", "currency"); + + cy.get(".vuuFilterClauseOperator input").should("be.focused"); + cy.get(".vuuFilterClauseOperator input").should( + "have.attr", + "aria-expanded", + "true" + ); + + // make sure operators list has renderered + cy.findByText("=").should("exist"); + }); + }); + describe("WHEN user selects an operator", () => { + it("THEN focus moves to value field", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "="); + + cy.get(FILTER_CLAUSE).should("have.length", 1); + cy.get(FILTER_CLAUSE_FIELD).should("have.length", 3); + + cy.get(".vuuFilterClauseValue input").should("be.focused"); + cy.get(".vuuFilterClauseValue input").should( + "have.attr", + "aria-expanded", + "true" + ); + cy.findByText("USD").should("exist"); + }); + }); + + describe("WHEN user selects a value", () => { + it("THEN Save menu is shown", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + cy.get(FILTER_CLAUSE).should("have.length", 1); + cy.get(`${FILTER_CLAUSE} ${FILTER_CLAUSE}-clearButton`).should( + "have.length", + 1 + ); + cy.get(".vuuFilterBuilderMenuList").should("be.visible"); + }); + }); + + describe("WHEN user clicks APPLY AND SAVE", () => { + it("THEN filtersChangedHandler callback is invoked", () => { + const onFiltersChanged = cy.stub().as("filtersChangedHandler"); + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + clickButton("APPLY AND SAVE"); + cy.get("@filtersChangedHandler").should("be.calledWith", [ + { column: "currency", op: "=", value: "USD" }, + ]); + }); + + it("THEN filter is applied", () => { + const onFiltersChanged = cy.stub().as("filtersChangedHandler"); + const onFilterApplied = cy.stub().as("onFilterApplied"); + cy.mount( + + ); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + clickButton("APPLY AND SAVE"); + cy.get("@filtersChangedHandler").should("be.calledWith", [ + { column: "currency", op: "=", value: "USD" }, + ]); + cy.get("@onFilterApplied").should("be.calledWith", { + filter: 'currency = "USD"', + filterStruct: { column: "currency", op: "=", value: "USD" }, + }); + }); + + it("THEN filter pill is displayed, label is in edit state and focused", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + clickButton("APPLY AND SAVE"); + cy.get(OVERFLOW_CONTAINER).find("> *").should("have.length", 2); + findOverflowItem(".vuuFilterPill").should("have.length", 1); + findOverflowItem(".vuuFilterPill") + .find(".vuuEditableLabel") + .should("have.class", "vuuEditableLabel-editing"); + findOverflowItem(".vuuFilterPill") + .find(".vuuEditableLabel") + .find("input") + .should("be.focused"); + }); + + describe("WHEN user overtypes label and presses ENTER", () => { + it("THEN label is applied and exits edit mode", () => { + const onFiltersChanged = cy.stub().as("filtersChangedHandler"); + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + clickButton("APPLY AND SAVE"); + waitUntilEditableLabelIsFocused(".vuuFilterPill"); + cy.realType("test"); + cy.realPress("Enter"); + findOverflowItem(".vuuFilterPill") + .find(".vuuEditableLabel") + .should("not.have.class", "vuuEditableLabel-editing"); + cy.get("@filtersChangedHandler").should("be.calledWith", [ + { column: "currency", op: "=", value: "USD", name: "test" }, + ]); + }); + + it("THEN filter pill has focus", () => { + cy.mount(); + cy.get(ADD_BUTTON).realClick(); + clickListItems("currency", "=", "USD"); + clickButton("APPLY AND SAVE"); + waitUntilEditableLabelIsFocused(".vuuFilterPill"); + cy.realType("test"); + cy.realPress("Enter"); + findOverflowItem(".vuuFilterPill").should("be.focused"); + }); + }); + }); +}); + +describe("The keyboard user", () => { + describe("WHEN user navigates with keyboard to empty Filterbar", () => { + it("THEN add button is focussed", () => { + cy.mount(); + cy.findByTestId("pre-filterbar").find("input").focus(); + cy.realPress("Tab"); + cy.get(ADD_BUTTON).should("be.focused"); + }); + + describe("WHEN user presses ADD then uses keyboard to select currency", () => { + it("THEN currency is selected and focus moves to operator", () => { + cy.mount(); + + cy.findByTestId("pre-filterbar").find("input").focus(); + cy.realPress("Tab"); + cy.get(ADD_BUTTON).should("be.focused"); + cy.realPress("Enter"); + cy.findByRole("combobox").should("be.focused"); + + // make sure columns list has renderered + cy.findByText("currency").should("exist"); + cy.realPress("ArrowDown"); + cy.get(".vuuListItem.vuuHighlighted").should("have.text", "currency"); + cy.realPress("Enter"); + + assertInputValue(".vuuFilterClauseColumn", "currency"); + + cy.get(".vuuFilterClauseOperator input").should("be.focused"); + cy.get(".vuuFilterClauseOperator input").should( + "have.attr", + "aria-expanded", + "true" + ); + }); + }); + describe("THEN WHEN user uses keyboard to select =", () => { + it("THEN = is selected and focus moves to value", () => { + cy.mount(); + + cy.findByTestId("pre-filterbar").find("input").focus(); + cy.realPress("Tab"); + cy.get(ADD_BUTTON).should("be.focused"); + cy.realPress("Enter"); + cy.findByRole("combobox").should("be.focused"); + + // make sure columns list has renderered + cy.findByText("currency").should("exist"); + cy.realPress("ArrowDown"); + cy.get(".vuuListItem.vuuHighlighted").should("have.text", "currency"); + cy.realPress("Enter"); + + cy.findByText("=").should("exist"); + cy.get(".vuuListItem.vuuHighlighted").should("have.text", "="); + cy.realPress("Enter"); + + assertInputValue(".vuuFilterClauseOperator", "="); + + cy.get(".vuuFilterClauseValue input").should("be.focused"); + cy.get(".vuuFilterClauseValue input").should( + "have.attr", + "aria-expanded", + "true" + ); + }); + describe("THEN WHEN user uses keyboard to select USD", () => { + it("THEN USD is selected, and focus moves to Menu", () => { + cy.mount(); + + cy.findByTestId("pre-filterbar").find("input").focus(); + cy.realPress("Tab"); + cy.get(ADD_BUTTON).should("be.focused"); + cy.realPress("Enter"); + cy.findByRole("combobox").should("be.focused"); + + // make sure columns list has renderered + cy.findByText("currency").should("exist"); + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + cy.findByText("=").should("exist"); + cy.realPress("Enter"); + + cy.findByText("USD").should("exist"); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + assertInputValue(".vuuFilterClauseValue", "USD"); + + cy.get(FILTER_CLAUSE).should("have.length", 1); + cy.get(`${FILTER_CLAUSE} ${FILTER_CLAUSE}-clearButton`).should( + "have.length", + 1 + ); + cy.get(".vuuFilterBuilderMenuList") + .should("be.visible") + .should("be.focused"); + }); + }); + }); + }); +}); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx index 7186d9d31..02e7f71f6 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx @@ -11,6 +11,7 @@ import { FilterClauseEditor, FilterClauseEditorProps } from "../filter-clause"; import { FilterPill } from "../filter-pill"; import { filterClauses as getFilterClauses } from "../filter-utils"; import { useFilterBar } from "./useFilterBar"; +import { FilterBarMenu } from "./FilterBarMenu"; import "./FilterBar.css"; @@ -45,12 +46,16 @@ export const FilterBar = ({ addButtonProps, editFilter, filters, + onBlurFilterClause, + onCancelFilterClause, onClickAddFilter, onClickRemoveFilter, onChangeFilterClause, onChangeActiveFilterIndex, + onFocusFilterClause, onNavigateOutOfBounds, - onKeyDown, + onKeyDownFilterbar, + onKeyDownMenu, onMenuAction, pillProps, promptProps, @@ -63,6 +68,7 @@ export const FilterBar = ({ onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp, + tableSchema, }); const className = cx(classBase, classNameProp, { @@ -70,8 +76,6 @@ export const FilterBar = ({ [`${classBase}-edit`]: editFilter !== undefined, }); - const onClose = () => console.log("Closing filter component"); - const getChildren = () => { const items: ReactElement[] = []; if (editFilter === undefined) { @@ -91,15 +95,21 @@ export const FilterBar = ({ {...FilterClauseEditorProps} filterClause={filterClause} key={`editor-${i}`} + onCancel={onCancelFilterClause} onChange={onChangeFilterClause} - onClose={onClose} + onBlur={onBlurFilterClause} + onFocus={onFocusFilterClause} tableSchema={tableSchema} /> ); }); if (showMenu) { items.push( - + ); } items.push( @@ -121,10 +131,11 @@ export const FilterBar = ({
- + + {/* */} { + const classBase = "vuuFilterBarMenu"; + + const { menuBuilder, menuActionHandler } = useFilterBarMenu(); + + return ( +
+ +
+ ); +}; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts index 130e75557..e424935fc 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts @@ -4,14 +4,19 @@ import { FilterClause, FilterWithPartialClause, } from "@finos/vuu-filter-types"; -import { PromptProps } from "@finos/vuu-popups"; -import { dispatchMouseEvent, filterAsQuery } from "@finos/vuu-utils"; -import { EditableLabelProps } from "@salt-ds/lab"; import { ActiveItemChangeHandler, NavigationOutOfBoundsHandler, } from "@finos/vuu-layout"; +import { PromptProps } from "@finos/vuu-popups"; +import { + dispatchMouseEvent, + filterAsQuery, + isMultiClauseFilter, +} from "@finos/vuu-utils"; +import { EditableLabelProps } from "@salt-ds/lab"; import { + FocusEventHandler, KeyboardEvent, KeyboardEventHandler, RefObject, @@ -21,9 +26,10 @@ import { useRef, useState, } from "react"; +import { FilterClauseCancelHandler } from "../filter-clause/useFilterClauseEditor"; import { FilterPillProps } from "../filter-pill"; import { FilterMenuOptions } from "../filter-pill-menu"; -import { addClause, replaceClause } from "../filter-utils"; +import { addClause, removeLastClause, replaceClause } from "../filter-utils"; import { FilterBarProps } from "./FilterBar"; import { useFilters } from "./useFilters"; @@ -36,6 +42,7 @@ export interface FilterBarHookProps | "onChangeActiveFilterIndex" | "onFiltersChanged" | "showMenu" + | "tableSchema" > { containerRef: RefObject; } @@ -50,6 +57,7 @@ export const useFilterBar = ({ onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp, + tableSchema, }: FilterBarHookProps) => { const addButtonRef = useRef(null); const editingFilter = useRef(); @@ -70,6 +78,7 @@ export const useFilterBar = ({ } = useFilters({ filters: filtersProp, onFiltersChanged, + tableSchema, }); const editPillLabel = useCallback( @@ -269,12 +278,17 @@ export const useFilterBar = ({ return true; } - case "and-clause": - setEditFilter((filter) => - addClause(filter as Filter, EMPTY_FILTER_CLAUSE) + case "and-clause": { + const newFilter = addClause( + editFilter as Filter, + EMPTY_FILTER_CLAUSE ); + console.log({ newFilter }); + setEditFilter(newFilter); setShowMenu(false); return true; + } + case "or-clause": setEditFilter((filter) => addClause(filter as Filter, {}, { combineWith: "or" }) @@ -317,7 +331,7 @@ export const useFilterBar = ({ const handleClickAddFilter = useCallback(() => { setEditFilter({}); - }, []); + }, [setEditFilter]); const handleClickRemoveFilter = useCallback(() => { setEditFilter(undefined); @@ -329,14 +343,50 @@ export const useFilterBar = ({ onExitEditMode: handleExitEditFilterName, }; - const handleChangeFilterClause = (filterClause: Partial) => { - if (filterClause !== undefined) { - setEditFilter((filter) => replaceClause(filter, filterClause)); - setShowMenu(true); + const handleChangeFilterClause = useCallback( + (filterClause: Partial) => { + console.log(`handleCHangeFilterClause ${JSON.stringify(filterClause)}`); + if (filterClause !== undefined) { + const newFilter = replaceClause(editFilter, filterClause); + setEditFilter(newFilter); + setShowMenu(true); + } + }, + [editFilter] + ); + + const handleCancelFilterClause = useCallback( + (reason) => { + if (reason === "Backspace" && isMultiClauseFilter(editFilter)) { + setEditFilter(removeLastClause(editFilter)); + } + }, + [editFilter] + ); + + const handleBlurFilterClause = useCallback((e) => { + const target = e.target as HTMLElement; + const relatedTarget = e.relatedTarget as HTMLElement; + const filterClause = target.closest(".vuuFilterClause"); + if (filterClause?.contains(relatedTarget)) { + // do nothing + } else { + const dropdownId = target.getAttribute("aria-owns"); + const dropDown = dropdownId ? document.getElementById(dropdownId) : null; + if (dropDown?.contains(relatedTarget)) { + // do nothing + } else { + // if clause is complete + setShowMenu(true); + } } - }; + }, []); + + const handleFocusFilterClause = useCallback(() => { + setShowMenu(false); + }, []); - const onKeyDown = useCallback( + const handleKeyDownFilterbar = useCallback( (evt: KeyboardEvent) => { if (evt.key === "Escape" && editFilter !== undefined) { // TODO confirm if edits applied ? @@ -349,6 +399,39 @@ export const useFilterBar = ({ [editFilter] ); + const handleKeyDownMenu = useCallback( + (evt) => { + console.log(`keydown from List ${evt.key}`); + const { current: container } = containerRef; + if (evt.key === "Backspace" && container) { + evt.preventDefault(); + const fields = Array.from( + container.querySelectorAll(".vuuFilterClauseField") + ); + if (fields.length > 0) { + const field = fields.at(-1) as HTMLElement; + field?.querySelector("input")?.focus(); + } + setShowMenu(false); + } else if (evt.key === "Tab") { + if (evt.shiftKey && container) { + const clearButtons = Array.from( + container.querySelectorAll(".vuuFilterClause-clearButton") + ) as HTMLButtonElement[]; + if (clearButtons.length > 0) { + const clearButton = clearButtons.at(-1) as HTMLButtonElement; + setTimeout(() => { + clearButton.focus(); + }, 100); + } + } else { + console.log("apply current selection"); + } + } + }, + [containerRef] + ); + const handleAddButtonKeyDown = useCallback((evt) => { if (evt.key === "ArrowLeft") { console.log("navgiate to the Toolbar"); @@ -372,11 +455,15 @@ export const useFilterBar = ({ addButtonProps, editFilter, filters, + onBlurFilterClause: handleBlurFilterClause, + onCancelFilterClause: handleCancelFilterClause, onChangeActiveFilterIndex: handleChangeActiveFilterIndex, onClickAddFilter: handleClickAddFilter, onClickRemoveFilter: handleClickRemoveFilter, onChangeFilterClause: handleChangeFilterClause, - onKeyDown, + onFocusFilterClause: handleFocusFilterClause, + onKeyDownFilterbar: handleKeyDownFilterbar, + onKeyDownMenu: handleKeyDownMenu, onMenuAction: handleMenuAction, onNavigateOutOfBounds: handlePillNavigationOutOfBounds, pillProps, diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts new file mode 100644 index 000000000..19d3ef33a --- /dev/null +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts @@ -0,0 +1,33 @@ +import { useCallback, useMemo } from "react"; +import { + ContextMenuItemDescriptor, + MenuActionHandler, + MenuBuilder, +} from "@finos/vuu-data-types"; +import { MenuActionClosePopup } from "@finos/vuu-popups"; + +export const useFilterBarMenu = () => { + const menuBuilder = useCallback(() => { + return [ + { + label: `You have no saved filters for this table`, + action: `no-action`, + } as ContextMenuItemDescriptor, + ]; + }, []); + + const menuActionHandler = useMemo( + () => (action: MenuActionClosePopup) => { + console.log(`invoke menuId `, { + action, + }); + return false; + }, + [] + ); + + return { + menuBuilder, + menuActionHandler, + }; +}; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts index 86b5d2f9f..1456b6b9c 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts @@ -1,17 +1,21 @@ import { useControlled } from "@finos/vuu-ui-controls"; -import { Filter } from "@finos/vuu-filter-types"; +import { Filter, NamedFilter } from "@finos/vuu-filter-types"; import { useCallback } from "react"; +import { TableSchema } from "packages/vuu-data/src"; +import { useLayoutManager } from "@finos/vuu-shell"; export interface FiltersHookProps { defaultFilters?: Filter[]; filters?: Filter[]; onFiltersChanged?: (filters: Filter[]) => void; + tableSchema?: TableSchema; } export const useFilters = ({ defaultFilters, filters: filtersProp, onFiltersChanged, + tableSchema, }: FiltersHookProps) => { const [filters, setFilters] = useControlled({ controlled: filtersProp, @@ -20,6 +24,94 @@ export const useFilters = ({ state: "Filters", }); + const { getApplicationSettings, saveApplicationSettings } = + useLayoutManager(); + + type SavedFilterMap = { + [key: string]: NamedFilter[]; + }; + + const hasFilter = (filters: NamedFilter[], name: string) => + filters.findIndex((f) => f.name === name) !== -1; + + const saveFilterToSettings = useCallback( + (filter: Filter, name?: string) => { + if (tableSchema && name) { + const savedFilters = getApplicationSettings( + "filters" + ) as SavedFilterMap; + let newFilters = savedFilters; + const { module, table } = tableSchema.table; + const key = `${module}:${table}`; + if (savedFilters) { + if (savedFilters[key]) { + if (hasFilter(savedFilters[key], name)) { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].map((f) => + f.name === name ? { ...filter, name } : f + ), + }; + } else if ( + filter?.name && + filter?.name !== name && + hasFilter(savedFilters[key], filter.name) + ) { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].map((f) => + f.name === filter.name ? { ...filter, name } : f + ), + }; + } else { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].concat({ ...filter, name }), + }; + } + } else { + newFilters = { + ...savedFilters, + [key]: [{ ...filter, name }], + }; + } + } else { + newFilters = { + [key]: [{ ...filter, name }], + }; + } + if (newFilters !== savedFilters) { + saveApplicationSettings(newFilters, "filters"); + } + } + }, + [getApplicationSettings, saveApplicationSettings, tableSchema] + ); + + const removeFilterFromSettings = useCallback( + (filter: Filter | NamedFilter) => { + if (tableSchema && filter.name) { + const savedFilters = getApplicationSettings( + "filters" + ) as SavedFilterMap; + + const { module, table } = tableSchema.table; + const key = `${module}:${table}`; + + if ( + savedFilters[key]?.findIndex((f) => f.name === filter.name) !== -1 + ) { + const newSavedFilters = { + ...savedFilters, + [key]: savedFilters[key].filter((f) => f.name !== filter.name), + }; + saveApplicationSettings(newSavedFilters, "filters"); + } + } + }, + [getApplicationSettings, saveApplicationSettings, tableSchema] + ); + const handleAddFilter = useCallback( (filter: Filter) => { const index = filters.length; @@ -33,6 +125,10 @@ export const useFilters = ({ const handleDeleteFilter = useCallback( (filter: Filter) => { + console.log(`handleDeleteFilter`, { + filter, + }); + let index = -1; const newFilters = filters.filter((f, i) => { if (f !== filter) { @@ -44,9 +140,10 @@ export const useFilters = ({ }); setFilters(newFilters); onFiltersChanged?.(newFilters); + removeFilterFromSettings(filter); return index; }, - [filters, onFiltersChanged, setFilters] + [filters, onFiltersChanged, removeFilterFromSettings, setFilters] ); const handleRenameFilter = useCallback( @@ -62,9 +159,11 @@ export const useFilters = ({ }); setFilters(newFilters); onFiltersChanged?.(newFilters); + saveFilterToSettings(filter, name); + return index; }, - [filters, onFiltersChanged, setFilters] + [filters, onFiltersChanged, saveFilterToSettings, setFilters] ); const handleChangeFilter = useCallback( diff --git a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.css b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.css index 734a5ff04..8587b3a1e 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.css +++ b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.css @@ -12,19 +12,17 @@ --vuuList-borderStyle: none; } -.vuuListItem.vuuMenuButton { + + .vuuListItem:has(.vuuMenuButton){ + justify-content: center; + } + + .vuuMenuButton { + background-color: white; + border: solid 1px var(--salt-actionable-primary-foreground); + border-radius: 6px; color: var(--vuu-color-gray-50); font-size: 9px; - padding: 0 8px; + padding: 1px 6px; } - - .vuuListItem.vuuMenuButton:after { - border-radius: 6px; - content: ''; - position: absolute; - height: 16px; - background-color: transparent; - left: 4px; - right: 4px; - border: solid 1px var(--salt-actionable-primary-foreground); - } \ No newline at end of file + diff --git a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx index c1c211e31..652d1966e 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-builder-menu/FilterBuilderMenu.tsx @@ -2,7 +2,7 @@ import { ContextMenuProps } from "@finos/vuu-popups"; import { MenuActionHandler } from "@finos/vuu-data-types"; import { ReactElement, useCallback, useRef } from "react"; import { PopupComponent as Popup, Portal } from "@finos/vuu-popups"; -import { List, ListItem } from "@finos/vuu-ui-controls"; +import { List, ListItem, ListProps } from "@finos/vuu-ui-controls"; import "./FilterBuilderMenu.css"; @@ -10,10 +10,14 @@ const classBase = "vuuFilterBuilderMenu"; export interface FilterBuilderMenuProps extends Omit { + ListProps?: Pick; onMenuAction: MenuActionHandler; } -export const FilterBuilderMenu = ({ onMenuAction }: FilterBuilderMenuProps) => { +export const FilterBuilderMenu = ({ + ListProps, + onMenuAction, +}: FilterBuilderMenuProps) => { const ref = useRef(null); const listRef = useCallback((el: HTMLDivElement | null) => { if (el) { @@ -39,16 +43,17 @@ export const FilterBuilderMenu = ({ onMenuAction }: FilterBuilderMenuProps) => { - - APPLY AND SAVE + + APPLY AND SAVE AND OR diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx b/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx index 03f201514..5c16d43c1 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-clause/ExpandoCombobox.tsx @@ -67,7 +67,6 @@ export const ExpandoCombobox = forwardRef(function ExpandoCombobox< const handleInputChange = useCallback( (evt: FormEvent) => { const { value } = evt.target as HTMLInputElement; - console.log(`onInputChange ${value}`); setText(value); onInputChange?.(evt); }, @@ -130,8 +129,7 @@ export const ExpandoCombobox = forwardRef(function ExpandoCombobox< const popupProps = { minWidth: "fit-content", }; - - return props.source?.length === 0 ? null : ( + return (
, "onChange"> { filterClause: Partial; + onCancel?: FilterClauseCancelHandler; onChange: (filterClause: Partial) => void; - onClose: () => void; + onDropdownClose?: (closeReason: CloseReason) => void; + onDropdownOpen?: () => void; suggestionProvider?: () => SuggestionFetcher; tableSchema: TableSchema; } @@ -26,8 +32,10 @@ const classBase = "vuuFilterClause"; export const FilterClauseEditor = ({ className, + onCancel, onChange, - onClose, + onDropdownClose, + onDropdownOpen, filterClause, suggestionProvider, tableSchema, @@ -39,6 +47,9 @@ export const FilterClauseEditor = ({ InputProps, columnRef, onChangeValue, + onClear, + onClearKeyDown, + onDeselectValue, onSelectionChangeColumn, onSelectionChangeOperator, operator, @@ -48,6 +59,7 @@ export const FilterClauseEditor = ({ valueRef, } = useFilterClauseEditor({ filterClause, + onCancel, onChange, tableSchema, }); @@ -64,7 +76,9 @@ export const FilterClauseEditor = ({ InputProps={InputProps} className={cx(`${classBase}Field`, `${classBase}Value`)} column={selectedColumn} + data-field="value" filterClause={filterClause} + onDeselect={onDeselectValue} onInputComplete={onChangeValue} operator={operator} ref={valueRef} @@ -109,35 +123,40 @@ export const FilterClauseEditor = ({ return (
- title="column" InputProps={InputProps} + allowBackspaceClearsSelection className={cx(`${classBase}Field`, `${classBase}Column`)} + data-field="column" initialHighlightedIndex={0} itemToString={(column) => column.name} + onSelectionChange={onSelectionChangeColumn} ref={columnRef} source={columns} - onSelectionChange={onSelectionChangeColumn} + title="column" value={selectedColumn?.name ?? ""} /> {selectedColumn?.name ? ( - title="operator" InputProps={InputProps} + allowBackspaceClearsSelection className={cx(`${classBase}Field`, `${classBase}Operator`, { [`${classBase}Operator-hidden`]: selectedColumn === null, })} + data-field="operator" initialHighlightedIndex={0} + onSelectionChange={onSelectionChangeOperator} ref={operatorRef} source={getOperators(selectedColumn)} - onSelectionChange={onSelectionChangeOperator} + title="operator" value={operator ?? ""} /> ) : null} {getInputElement()} {value !== undefined ? (