From 7b1f16a1d78c1ced0acc9c085b02c666e8b31a84 Mon Sep 17 00:00:00 2001 From: Tomas Machalek Date: Wed, 31 May 2023 18:27:03 +0200 Subject: [PATCH] Fix handling of the 'ReloadConc' action ... ... in relation to conc. summary and query replay. This also fixes misc. browser history interaction issues. Still TODO: Minimize (globally) using of layoutModel.getConcArgs and prefer waiting for ReadyToAddNewOperation which provides truly actual value of the most recent conc ID. --- package-lock.json | 14 +- package.json | 2 +- public/files/js/app/navigation/history.ts | 4 +- public/files/js/app/page.ts | 2 +- public/files/js/models/asyncTask/actions.ts | 1 + public/files/js/models/coll/result.ts | 2 +- public/files/js/models/coll/save.ts | 2 +- public/files/js/models/concordance/actions.ts | 22 ++ public/files/js/models/concordance/main.ts | 55 +++-- public/files/js/models/concordance/summary.ts | 12 + .../js/models/concordance/ttdist/model.ts | 4 +- .../js/models/freqs/regular/freqCharts.ts | 2 +- public/files/js/models/freqs/regular/save.ts | 4 +- public/files/js/models/freqs/regular/table.ts | 2 +- .../files/js/models/options/structsAttrs.ts | 2 +- public/files/js/models/pquery/result.ts | 2 +- public/files/js/models/pquery/save.ts | 4 +- public/files/js/models/query/actions.ts | 9 +- public/files/js/models/query/filter.ts | 29 ++- public/files/js/models/query/first.ts | 1 - public/files/js/models/query/firstHits.ts | 29 ++- public/files/js/models/query/replay/index.ts | 208 +++++++++++------- public/files/js/models/query/sample.ts | 2 +- public/files/js/models/query/shuffle.ts | 27 ++- public/files/js/models/query/sort/multi.ts | 35 ++- public/files/js/models/query/sort/single.ts | 37 +++- public/files/js/models/subcorp/edit.ts | 5 +- public/files/js/models/wordlist/main.ts | 2 +- public/files/js/models/wordlist/save.ts | 4 +- public/files/js/pages/view.ts | 8 +- .../defaultTaghelper/positional/models.ts | 2 +- public/files/js/types/kontext.ts | 4 +- 32 files changed, 382 insertions(+), 156 deletions(-) diff --git a/package-lock.json b/package-lock.json index b11f805681..fa4c49330e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "d3": "^7.6.1", "d3-color": "^3.1.0", "diff": "^5.1.0", - "kombo": "^0.95.2", + "kombo": "^0.96.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-is": "^18.2.0", @@ -5171,9 +5171,9 @@ } }, "node_modules/kombo": { - "version": "0.95.2", - "resolved": "https://registry.npmjs.org/kombo/-/kombo-0.95.2.tgz", - "integrity": "sha512-iqOMViaXCfO07fz0Qk1D6ms1A8sLY8TxMOFMPFcxpucNdo0x1NiIIUCAktFxqD8xPVbVv9VYOSC4ES3rn/ilHg==", + "version": "0.96.5", + "resolved": "https://registry.npmjs.org/kombo/-/kombo-0.96.5.tgz", + "integrity": "sha512-+wu0wHvhBfcsDIq3sfF7gPPCe9PHNcSfq7wqswIZtwhHK7aiijV6dRHlKo0W8XBCgoZ1OIKQ/arHfy7G6IPXsA==", "dependencies": { "immer": "^9.0.12" }, @@ -12299,9 +12299,9 @@ "dev": true }, "kombo": { - "version": "0.95.2", - "resolved": "https://registry.npmjs.org/kombo/-/kombo-0.95.2.tgz", - "integrity": "sha512-iqOMViaXCfO07fz0Qk1D6ms1A8sLY8TxMOFMPFcxpucNdo0x1NiIIUCAktFxqD8xPVbVv9VYOSC4ES3rn/ilHg==", + "version": "0.96.5", + "resolved": "https://registry.npmjs.org/kombo/-/kombo-0.96.5.tgz", + "integrity": "sha512-+wu0wHvhBfcsDIq3sfF7gPPCe9PHNcSfq7wqswIZtwhHK7aiijV6dRHlKo0W8XBCgoZ1OIKQ/arHfy7G6IPXsA==", "requires": { "immer": "^9.0.12" } diff --git a/package.json b/package.json index a68c992dcb..b0f989a344 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "d3": "^7.6.1", "d3-color": "^3.1.0", "diff": "^5.1.0", - "kombo": "^0.95.2", + "kombo": "^0.96.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-is": "^18.2.0", diff --git a/public/files/js/app/navigation/history.ts b/public/files/js/app/navigation/history.ts index 4c3e7db442..3c95f4e0c4 100644 --- a/public/files/js/app/navigation/history.ts +++ b/public/files/js/app/navigation/history.ts @@ -87,7 +87,9 @@ export class History implements Kontext.IHistory { } setOnPopState(fn:(event:PopStateEvent)=>void):void { - window.onpopstate = fn; + window.onpopstate = (event:PopStateEvent) => { + fn(event); + }; } } diff --git a/public/files/js/app/page.ts b/public/files/js/app/page.ts index 917903a5c2..32642e2310 100644 --- a/public/files/js/app/page.ts +++ b/public/files/js/app/page.ts @@ -577,7 +577,7 @@ export abstract class PageModel implements Kontext.IURLHandler, IConcArgsHandler const concIds = pipe( value, List.filter(v => v[0] === '~'), - List.map(v => v.substr(1)) + List.map(v => v.substring(1)) ); if (!List.empty(concIds)) { this.setConf('concPersistenceOpId', List.head(concIds)); diff --git a/public/files/js/models/asyncTask/actions.ts b/public/files/js/models/asyncTask/actions.ts index 953b99d13f..7169396dbb 100644 --- a/public/files/js/models/asyncTask/actions.ts +++ b/public/files/js/models/asyncTask/actions.ts @@ -22,6 +22,7 @@ import { Action } from 'kombo'; import * as Kontext from '../../types/kontext'; + export class Actions { static InboxToggleOverviewVisibility:Action<{ diff --git a/public/files/js/models/coll/result.ts b/public/files/js/models/coll/result.ts index fcd3ae0d69..ee8d32698d 100644 --- a/public/files/js/models/coll/result.ts +++ b/public/files/js/models/coll/result.ts @@ -306,7 +306,7 @@ export class CollResultModel extends StatelessModel { } private processDataReload(state:CollResultModelState):Observable<[AjaxResponse, CollServerArgs]> { - return this.suspend({}, (action, syncData) => { + return this.waitForAction({}, (action, syncData) => { if (action.name === Actions.FormPrepareSubmitArgsDone.name) { return null; } diff --git a/public/files/js/models/coll/save.ts b/public/files/js/models/coll/save.ts index d5160dc2e8..73db68436e 100644 --- a/public/files/js/models/coll/save.ts +++ b/public/files/js/models/coll/save.ts @@ -185,7 +185,7 @@ export class CollResultsSaveModel extends StatelessModel { + this.waitForAction({}, (action, syncData) => { if (action.name === Actions.FormPrepareSubmitArgsDone.name) { return null; } diff --git a/public/files/js/models/concordance/actions.ts b/public/files/js/models/concordance/actions.ts index 40da955eb3..3bcb082e03 100644 --- a/public/files/js/models/concordance/actions.ts +++ b/public/files/js/models/concordance/actions.ts @@ -42,6 +42,16 @@ export interface PublishLineSelectionPayload { export class Actions { + static ReadyToAddNewOperation:Action<{ + lastConcId:string; + }> = { + name: 'CONCORDANCE_READY_TO_ADD_NEW_OPERATION' + }; + + static isReadyToAddNewOperation(a:Action):a is typeof Actions.ReadyToAddNewOperation { + return a.name === Actions.ReadyToAddNewOperation.name; + }; + static AddedNewOperation:Action<{ concId:string; data:AjaxConcResponse; @@ -98,8 +108,16 @@ export class Actions { return a.name === Actions.ChangePage.name; } + /** + * defines a reload of an already known operation + */ static ReloadConc:Action<{ concId:string; + arf:number; + concSize:number; + fullSize:number; + corpusIpm:number; + queryChainSize:number; isPopState?:boolean; viewMode?:ConcViewMode; }> = { @@ -434,6 +452,10 @@ export class Actions { name: 'CONCORDANCE_PUBLISH_STORED_LINE_SELECTIONS' }; + static isPublishStoredLineSelections(a:Action):a is typeof Actions.PublishStoredLineSelections { + return a.name == Actions.PublishStoredLineSelections.name; + } + static DownloadSelectionOverview:Action<{ format:string; }> = { diff --git a/public/files/js/models/concordance/main.ts b/public/files/js/models/concordance/main.ts index b4e20348c2..7cd9360d55 100644 --- a/public/files/js/models/concordance/main.ts +++ b/public/files/js/models/concordance/main.ts @@ -326,7 +326,12 @@ export class ConcordanceModel extends StatefulModel { this.pushHistoryState({ name: Actions.ReloadConc.name, payload: { - concId: action.payload.data.conc_persistence_op_id + concId: action.payload.data.conc_persistence_op_id, + arf: action.payload.data.result_arf, + concSize: action.payload.data.concsize, + corpusIpm: action.payload.data.result_relative_freq, + fullSize: action.payload.data.fullsize, + queryChainSize: List.size(action.payload.data.query_overview) } }); Dict.forEach( @@ -409,7 +414,7 @@ export class ConcordanceModel extends StatefulModel { } forkJoin([ this.waitForAction({}, (action, syncData) => { - return action.name === Actions.PublishStoredLineSelections.name ? + return Actions.isPublishStoredLineSelections(action) ? null : syncData; }).pipe( map(v => (v as typeof Actions.PublishStoredLineSelections).payload) @@ -432,6 +437,8 @@ export class ConcordanceModel extends StatefulModel { this.state.highlightWordsStore ); + this.layoutModel.updateConcArgs({q: [action.payload.concId]}); + } else { Dict.forEach( (_, kcAttr) => { @@ -474,11 +481,16 @@ export class ConcordanceModel extends StatefulModel { concatMap(v => this.loadConcPage()) ).subscribe({ - next: ([concId,]) => { + next: ([resp, ]) => { this.pushHistoryState({ name: Actions.ReloadConc.name, payload: { - concId + concId: resp.conc_persistence_op_id, + arf: resp.result_arf, + concSize: resp.concsize, + fullSize: resp.fullsize, + corpusIpm: resp.result_relative_freq, + queryChainSize: List.size(resp.query_overview) } }); this.emitChange(); @@ -596,11 +608,16 @@ export class ConcordanceModel extends StatefulModel { state.attrViewMode = action.payload.attrVmode; }); this.loadConcPage().subscribe({ - next: ([concId,]) => { + next: ([resp,]) => { this.pushHistoryState({ name: Actions.ReloadConc.name, payload: { - concId + concId: resp.conc_persistence_op_id, + arf: resp.result_arf, + corpusIpm: resp.result_relative_freq, + concSize: resp.concsize, + fullSize: resp.fullsize, + queryChainSize: List.size(resp.query_overview) } }); this.emitChange(); @@ -622,11 +639,16 @@ export class ConcordanceModel extends StatefulModel { state.currentPage = 1; }); this.loadConcPage().subscribe({ - next: ([concId,]) => { + next: ([resp,]) => { this.pushHistoryState({ name: Actions.ReloadConc.name, payload: { - concId + concId: resp.conc_persistence_op_id, + arf: resp.result_arf, + corpusIpm: resp.result_relative_freq, + concSize: resp.concsize, + fullSize: resp.fullsize, + queryChainSize: List.size(resp.query_overview) } }); this.emitChange(); @@ -959,7 +981,7 @@ export class ConcordanceModel extends StatefulModel { * @param concId if non-empty then a specific concordance is loaded * @return a 2-tuple [actual conc. ID, page num] */ - private loadConcPage(concId?:string):Observable<[string, number]> { + private loadConcPage(concId?:string):Observable<[AjaxConcResponse, number]> { return this.changePage('customPage', 1, concId ? `~${concId}` : undefined); } @@ -983,7 +1005,7 @@ export class ConcordanceModel extends StatefulModel { action:PaginationActions, pageNumber?:number, concId?:string - ):Observable<[string, number]> { + ):Observable<[AjaxConcResponse, number]> { const pageNum:number = action === 'customPage' ? pageNumber : this.state.pagination[action]; @@ -1020,7 +1042,7 @@ export class ConcordanceModel extends StatefulModel { }); } ), - map(resp => tuple(resp.conc_persistence_op_id, pageNum)) + map(resp => tuple(resp, pageNum)) ); } @@ -1226,14 +1248,19 @@ export class ConcordanceModel extends StatefulModel { ).pipe( tap( - data => { + resp => { this.changeState(state => { - this.importData(state, data); + this.importData(state, resp); }); this.pushHistoryState({ name: Actions.ReloadConc.name, payload: { - concId: data.conc_persistence_op_id, + concId: resp.conc_persistence_op_id, + arf: resp.result_arf, + corpusIpm: resp.result_relative_freq, + concSize: resp.concsize, + fullSize: resp.fullsize, + queryChainSize: List.size(resp.query_overview), viewMode } }); diff --git a/public/files/js/models/concordance/summary.ts b/public/files/js/models/concordance/summary.ts index d910b918b2..c7a97e5d28 100644 --- a/public/files/js/models/concordance/summary.ts +++ b/public/files/js/models/concordance/summary.ts @@ -109,6 +109,18 @@ export class ConcSummaryModel extends StatelessModel { } ); + this.addActionHandler( + Actions.ReloadConc, + (state, action) => { + state.arf = action.payload.arf; + state.concSize = action.payload.concSize; + state.fullSize = action.payload.fullSize; + state.ipm = null; + state.queryChainSize = action.payload.queryChainSize; + state.corpusIpm = action.payload.corpusIpm; + } + ) + this.addActionHandler( Actions.AddedNewOperation, (state, action) => { diff --git a/public/files/js/models/concordance/ttdist/model.ts b/public/files/js/models/concordance/ttdist/model.ts index 6fa4c738e3..a011145a9e 100644 --- a/public/files/js/models/concordance/ttdist/model.ts +++ b/public/files/js/models/concordance/ttdist/model.ts @@ -90,7 +90,7 @@ export class TextTypesDistModel extends StatefulModel { state.blockedByAsyncConc = !action.payload.finished; } ); - this.suspendWithTimeout(5000, {}, (action, syncData) => { + this.waitForActionWithTimeout(5000, {}, (action, syncData) => { if (ConcActions.isConcordanceRecalculationReady(action)) { return null; } @@ -113,7 +113,7 @@ export class TextTypesDistModel extends StatefulModel { ConcActions.LoadTTDictOverview.name, action => { if (this.state.blocks.length === 0) { - this.suspendWithTimeout(5000, {}, (action, syncData) => { + this.waitForActionWithTimeout(5000, {}, (action, syncData) => { if (ConcActions.isConcordanceRecalculationReady(action)) { return null; } diff --git a/public/files/js/models/freqs/regular/freqCharts.ts b/public/files/js/models/freqs/regular/freqCharts.ts index 1c3ae1d4d8..5d04d7b836 100644 --- a/public/files/js/models/freqs/regular/freqCharts.ts +++ b/public/files/js/models/freqs/regular/freqCharts.ts @@ -490,7 +490,7 @@ export class FreqChartsModel extends StatelessModel { Actions.ResultSetMinFreqVal, null, (state, action, dispatch) => { - this.suspendWithTimeout( + this.waitForActionWithTimeout( 5000, {}, (action, syncData) => { diff --git a/public/files/js/models/freqs/regular/save.ts b/public/files/js/models/freqs/regular/save.ts index c376f74e19..1a9f1758ac 100644 --- a/public/files/js/models/freqs/regular/save.ts +++ b/public/files/js/models/freqs/regular/save.ts @@ -93,7 +93,7 @@ export class FreqResultsSaveModel extends StatefulModel + this.waitForAction({}, (action, syncData) => action.name === Actions.ResultPrepareSubmitArgsDone.name ? null : syncData ).subscribe( @@ -151,7 +151,7 @@ export class FreqResultsSaveModel extends StatefulModel {state.formIsActive = false}); - this.suspend({}, (action, syncData) => { + this.waitForAction({}, (action, syncData) => { return action.name === Actions.ResultPrepareSubmitArgsDone.name ? null : syncData }).subscribe( (action) => { diff --git a/public/files/js/models/freqs/regular/table.ts b/public/files/js/models/freqs/regular/table.ts index 946f0e39f9..f41a1c3d65 100644 --- a/public/files/js/models/freqs/regular/table.ts +++ b/public/files/js/models/freqs/regular/table.ts @@ -336,7 +336,7 @@ export class FreqDataRowsModel extends StatelessModel { Actions.ResultSetMinFreqVal, null, (state, action, dispatch) => { - this.suspendWithTimeout( + this.waitForActionWithTimeout( 5000, {}, (action, syncData) => { diff --git a/public/files/js/models/options/structsAttrs.ts b/public/files/js/models/options/structsAttrs.ts index 71b91e01b3..cbb8f9b6c1 100644 --- a/public/files/js/models/options/structsAttrs.ts +++ b/public/files/js/models/options/structsAttrs.ts @@ -122,7 +122,7 @@ export class CorpusViewOptionsModel extends StatelessModel { if (!state.hasLoadedData) { - this.suspendWithTimeout(20000, {}, (action , syncData) => { + this.waitForActionWithTimeout(20000, {}, (action , syncData) => { return null; }).pipe( diff --git a/public/files/js/models/pquery/result.ts b/public/files/js/models/pquery/result.ts index 423b23aa9b..6aa5c44e6c 100644 --- a/public/files/js/models/pquery/result.ts +++ b/public/files/js/models/pquery/result.ts @@ -171,7 +171,7 @@ export class PqueryResultModel extends StatefulModel { this.addActionHandler( Actions.ResultApplyQuickFilter.name, action => { - this.suspendWithTimeout( + this.waitForActionWithTimeout( 1000, {}, (action2, syncData) => Actions.isResultApplyQuickFilterArgsReady(action2) ? null : syncData diff --git a/public/files/js/models/pquery/save.ts b/public/files/js/models/pquery/save.ts index 60be0766d9..1ef2c088b5 100644 --- a/public/files/js/models/pquery/save.ts +++ b/public/files/js/models/pquery/save.ts @@ -93,7 +93,7 @@ export class PqueryResultsSaveModel extends StatefulModel + this.waitForAction({}, (action, syncData) => action.name === Actions.SaveFormPrepareSubmitArgsDone.name ? null : syncData ).subscribe( @@ -147,7 +147,7 @@ export class PqueryResultsSaveModel extends StatefulModel {state.formIsActive = false}); - this.suspend({}, (action, syncData) => { + this.waitForAction({}, (action, syncData) => { return action.name === Actions.SaveFormPrepareSubmitArgsDone.name ? null : syncData }).subscribe( (action) => { diff --git a/public/files/js/models/query/actions.ts b/public/files/js/models/query/actions.ts index 84a20a17b5..e57e29ec81 100644 --- a/public/files/js/models/query/actions.ts +++ b/public/files/js/models/query/actions.ts @@ -69,14 +69,13 @@ export class Actions { name: 'TRIM_QUERY' }; - static SliceQueryChain: Action<{ - operationIdx:number; - concId:string; + static UpdateOperations:Action<{ + operations:Array; }> = { - name: 'QUERY_REPLAY_SLICE_QUERY_CHAIN' + name: 'QUERY_REPLAY_UPDATE_OPERATIONS' }; - static QuerySetStopAfterIdx: Action<{ + static QuerySetStopAfterIdx:Action<{ value:number; }> = { name: 'QUERY_SET_STOP_AFTER_IDX' diff --git a/public/files/js/models/query/filter.ts b/public/files/js/models/query/filter.ts index 0a1d1f0dbe..a718fa0d70 100644 --- a/public/files/js/models/query/filter.ts +++ b/public/files/js/models/query/filter.ts @@ -20,7 +20,7 @@ import { IFullActionControl } from 'kombo'; import { Observable, of as rxOf } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { concatMap, tap } from 'rxjs/operators'; import { tuple, pipe, Dict, List, HTTP, id } from 'cnc-tskit'; import * as Kontext from '../../types/kontext'; @@ -505,14 +505,35 @@ export class FilterFormModel extends QueryFormModel { } err = this.testQueryTypeMismatch(); }); + if (!err) { this.changeState(state => { state.isBusy = true; }); - this.submitQuery( - action.payload.filterId, - List.head(this.pageModel.getConcArgs().q).substr(1) + + this.waitForActionWithTimeout( + 5000, + {}, + (action, syncData) => { + if (ConcActions.isReadyToAddNewOperation(action)) { + return null; + } + return syncData; + } ).pipe( + concatMap( + wAction => { + if (ConcActions.isReadyToAddNewOperation(wAction)) { + return this.submitQuery( + action.payload.filterId, + wAction.payload.lastConcId + ); + + } else { + throw new Error('failed to handle filter submit - unexpected action ' + wAction.name); + } + } + ), tap( (data) => { this.pageModel.updateConcPersistenceId(data.conc_persistence_op_id); diff --git a/public/files/js/models/query/first.ts b/public/files/js/models/query/first.ts index b30356963c..73049c623c 100644 --- a/public/files/js/models/query/first.ts +++ b/public/files/js/models/query/first.ts @@ -171,7 +171,6 @@ function importUserQueries( simpleQueryDefaultAttrs:{[sourceId:string]:Array>}, attrList:Array ):{[corpus:string]:AnyQuery} { - return pipe( corpora, List.filter(corpus => Dict.hasKey(corpus, data.currQueryTypes)), diff --git a/public/files/js/models/query/firstHits.ts b/public/files/js/models/query/firstHits.ts index 024e496726..7eb5b9d553 100644 --- a/public/files/js/models/query/firstHits.ts +++ b/public/files/js/models/query/firstHits.ts @@ -20,7 +20,7 @@ import { IFullActionControl, StatefulModel } from 'kombo'; import { Observable, of as rxOf } from 'rxjs'; -import { tap, map } from 'rxjs/operators'; +import { tap, map, concatMap } from 'rxjs/operators'; import { PageModel } from '../../app/page'; import { Actions as MainMenuActions } from '../mainMenu/actions'; @@ -73,9 +73,30 @@ export class FirstHitsModel extends StatefulModel { this.addActionHandler( Actions.FilterFirstHitsSubmit.name, action => { - const concId = List.head(this.layoutModel.getConcArgs().q).substr(1); - this.submitForm( - action.payload.opKey, concId + + this.waitForActionWithTimeout( + 5000, + {}, + (action, syncData) => { + if (ConcActions.isReadyToAddNewOperation(action)) { + return null; + } + return syncData; + } + ).pipe( + concatMap( + wAction => { + if (ConcActions.isReadyToAddNewOperation(wAction)) { + return this.submitForm( + action.payload.opKey, + wAction.payload.lastConcId + ); + + } else { + throw new Error('failed to handle firstHits submit - unexpected action ' + wAction.name); + } + } + ) ).subscribe({ next: data => { diff --git a/public/files/js/models/query/replay/index.ts b/public/files/js/models/query/replay/index.ts index c436e082ae..b35b4ab6b0 100644 --- a/public/files/js/models/query/replay/index.ts +++ b/public/files/js/models/query/replay/index.ts @@ -36,6 +36,7 @@ import { Actions } from '../actions'; import { Actions as ConcActions } from '../../concordance/actions'; import { Actions as MainMenuActions } from '../../mainMenu/actions'; import { Actions as TTActions } from '../../textTypes/actions'; +import { Actions as QueryActions } from '../../query/actions'; import { PersistentQueryOperation, importEncodedOperation, QueryPipelineResponse, QueryPipelineResponseItem } from './common'; @@ -87,10 +88,12 @@ export interface ReplayModelDeps { export interface QueryReplayModelState { + /** + * This property contains a query operations pipeline. The last item + * should be always the one user sees on the concordance view page. + */ operations:Array; - lastOperationKey:string; - /** * Contains args used by different input forms involved in the current query operations. * The used key is the one used by conc_persistence to store operations to db. @@ -98,7 +101,7 @@ export interface QueryReplayModelState { * operation which will be submitted and appended to the current query (e.g. we add a * filter/sort/...) */ - concArgsCache:{[key:string]:ConcFormArgs}; + concFormsCache:{[key:string]:ConcFormArgs}; branchReplayIsRunning:boolean; @@ -182,9 +185,8 @@ export class QueryReplayModel extends QueryInfoModel { dispatcher, pageModel, { - lastOperationKey: pageModel.getConf('concPersistenceOpId'), operations: List.map(importEncodedOperation, currentOperations), - concArgsCache: {...concArgsCache}, + concFormsCache: {...concArgsCache}, branchReplayIsRunning: false, editedOperationIdx: null, stopAfterOpIdx: null, @@ -206,10 +208,9 @@ export class QueryReplayModel extends QueryInfoModel { (state, action) => { state.branchReplayIsRunning = false; if (!action.error) { - state.lastOperationKey = action.payload.data.conc_persistence_op_id; state.operations = List.map( importEncodedOperation, action.payload.data.query_overview); - state.concArgsCache = {}; + state.concFormsCache = {}; } } ); @@ -323,7 +324,7 @@ export class QueryReplayModel extends QueryInfoModel { } else { state.editedOperationIdx = action.payload.operationIdx; - state.concArgsCache[action.payload.data.op_key] = action.payload.data; + state.concFormsCache[action.payload.data.op_key] = action.payload.data; state.operations[action.payload.operationIdx].concPersistenceId = action.payload.data.op_key; } @@ -443,10 +444,9 @@ export class QueryReplayModel extends QueryInfoModel { ); this.addActionHandler( - Actions.SliceQueryChain, + Actions.UpdateOperations, (state, action) => { - state.operations = state.operations.slice(0, action.payload.operationIdx + 1); - state.lastOperationKey = action.payload.concId; + state.operations = List.map(importEncodedOperation, action.payload.operations); } ); @@ -471,31 +471,32 @@ export class QueryReplayModel extends QueryInfoModel { (state, action, dispatch) => { const args = { ...this.pageModel.getConcArgs(), - q: '~' + state.lastOperationKey + q: '~' + action.payload.concId }; - this.pageModel.ajax$( - HTTP.Method.GET, - this.pageModel.createActionUrl('load_query_pipeline'), - args + const opIdx = List.findIndex( + x => x.concPersistenceId === action.payload.concId, + state.operations + ); + ( + this.allOperationsInCache(state) && opIdx > -1 ? + this.loadQeryPipelineFromCache(state) : + this.pageModel.ajax$( + HTTP.Method.GET, + this.pageModel.createActionUrl('load_query_pipeline'), + args + ) ).subscribe( resp => { - if (action.payload.concId) { - const operationIdx = pipe( - resp.ops, - List.zip(state.operations), - List.findIndex(([op,]) => op.id === action.payload.concId) - ); // TODO kind of a weak mapping here - if (operationIdx < state.operations.length - 1) { - dispatch({ - name: Actions.SliceQueryChain.name, - payload: { - operationIdx, - concId: action.payload.concId - } - }); + dispatch( + Actions.UpdateOperations, + { + operations: this.insertOperationKeys( + resp.query_overview, + resp.ops + ) } - } + ) } ); } @@ -508,6 +509,27 @@ export class QueryReplayModel extends QueryInfoModel { List.last(state.operations).fullSize = action.payload.fullsize; } ); + + this.addActionHandler( + [ + QueryActions.ApplyFilter, + QueryActions.FilterFirstHitsSubmit, + QueryActions.SampleFormSubmit, + QueryActions.ShuffleFormSubmit, + QueryActions.SortFormSubmit, + QueryActions.MLSortFormSubmit + ], + null, + (state, action, dispatch) => { + dispatch( + ConcActions.ReadyToAddNewOperation, + { + lastConcId: this.getLastOperationId(state) + } + ) + } + + ) } private getActualCorpname():string { @@ -525,11 +547,36 @@ export class QueryReplayModel extends QueryInfoModel { ):string|undefined { if (state.operations[idx]) { const key = state.operations[idx].concPersistenceId; - return state.concArgsCache[key] ? key : undefined; + return state.concFormsCache[key] ? key : undefined; } return undefined; } + private getLastOperationId(state:QueryReplayModelState):string { + return List.last(state.operations).concPersistenceId; + } + + /** + * Update conc_persistence_op_id properties within + * the array of QueryOperation instances based on + * conc ID (= opKeys) from the 'pipeline'. + */ + private insertOperationKeys( + opList:Array, + pipeline:Array + ):Array { + return pipe( + opList, + List.zipAll(pipeline), + List.map( + ([op, pipeOp]) => ({ + ...op, + conc_persistence_op_id: pipeOp.id + }) + ) + ); + } + /** * Generate a function representing an operation within query pipeline. Such * an operation typically consists of: @@ -772,6 +819,42 @@ export class QueryReplayModel extends QueryInfoModel { } } + private allOperationsInCache(state:QueryReplayModelState):boolean { + return List.every( + x => Dict.hasKey(x.concPersistenceId, state.concFormsCache), + state.operations + ); + } + + private loadQeryPipelineFromCache(state:QueryReplayModelState):Observable { + return rxOf<{ + messages: [], + ops:Array; + query_overview:Array; + }>({ + messages: [], + ops: pipe( + state.operations, + List.map(op => ({ + form_args: state.concFormsCache[op.concPersistenceId], + id: op.concPersistenceId + })) + ), + query_overview: pipe( + state.operations, + List.map(op => ({ + op: op.op, + opid: op.opid, + nicearg: null, + arg: op.encodedArgs, + size: op.size, + fullsize: op.fullSize, + conc_persistence_op_id: op.concPersistenceId + })) + ) + }); + } + /** * Process a query pipeline with the operation with index [changedOpIdx] updated. * The function must load a list of all operations a pipeline is composed of (the @@ -793,35 +876,12 @@ export class QueryReplayModel extends QueryInfoModel { ):Observable { const args = { ...this.pageModel.getConcArgs(), - q: '~' + state.lastOperationKey + q: '~' + this.getLastOperationId(state) }; return ( - List.size(state.operations) === Dict.size(state.concArgsCache) ? - rxOf<{ - ops:Array; - query_overview:Array; - }>({ - ops: pipe( - state.operations, - List.map(op => ({ - form_args: state.concArgsCache[op.concPersistenceId], - id: op.concPersistenceId - })) - ), - query_overview: pipe( - state.operations, - List.map(op => ({ - op: op.op, - opid: op.opid, - nicearg: null, - arg: op.encodedArgs, - size: op.size, - fullsize: op.fullSize, - conc_persistence_op_id: op.concPersistenceId - })) - ) - }) : + this.allOperationsInCache(state) ? + this.loadQeryPipelineFromCache(state) : this.pageModel.ajax$( HTTP.Method.GET, this.pageModel.createActionUrl('load_query_pipeline'), @@ -899,7 +959,7 @@ export class QueryReplayModel extends QueryInfoModel { return (queryKey !== undefined ? // cache hit this.queryModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as QueryFormArgs) + rxOf(state.concFormsCache[queryKey] as QueryFormArgs) ) : this.queryModel.syncFrom( this.pageModel.ajax$( @@ -907,7 +967,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ) @@ -932,7 +992,7 @@ export class QueryReplayModel extends QueryInfoModel { return (queryKey !== undefined ? // cache hit this.filterModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as FilterFormArgs) + rxOf(state.concFormsCache[queryKey] as FilterFormArgs) ) : this.filterModel.syncFrom( this.pageModel.ajax$( @@ -940,7 +1000,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ) @@ -953,7 +1013,7 @@ export class QueryReplayModel extends QueryInfoModel { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? this.sortModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as SortFormArgs)).pipe( + rxOf(state.concFormsCache[queryKey] as SortFormArgs)).pipe( concatMap(data => this.mlConcSortModel.syncFrom(rxOf(data))) ) : this.sortModel.syncFrom( @@ -962,7 +1022,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ).pipe( @@ -979,7 +1039,7 @@ export class QueryReplayModel extends QueryInfoModel { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? this.sampleModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as SampleFormArgs) + rxOf(state.concFormsCache[queryKey] as SampleFormArgs) ) : this.sampleModel.syncFrom( this.pageModel.ajax$( @@ -987,7 +1047,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ) @@ -998,13 +1058,13 @@ export class QueryReplayModel extends QueryInfoModel { opIdx:number):Observable { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? - rxOf(state.concArgsCache[queryKey]) : + rxOf(state.concFormsCache[queryKey]) : this.pageModel.ajax$( HTTP.Method.GET, this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } @@ -1015,13 +1075,13 @@ export class QueryReplayModel extends QueryInfoModel { opIdx:number):Observable { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? - rxOf(state.concArgsCache[queryKey]) : + rxOf(state.concFormsCache[queryKey]) : this.pageModel.ajax$( HTTP.Method.GET, this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ); @@ -1032,7 +1092,7 @@ export class QueryReplayModel extends QueryInfoModel { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? this.firstHitsModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as FirstHitsFormArgs) + rxOf(state.concFormsCache[queryKey] as FirstHitsFormArgs) ) : this.firstHitsModel.syncFrom( this.pageModel.ajax$( @@ -1040,7 +1100,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ) @@ -1055,7 +1115,7 @@ export class QueryReplayModel extends QueryInfoModel { const queryKey = this.getOpCacheKey(state, opIdx); return queryKey !== undefined ? this.switchMcModel.syncFrom( - rxOf(state.concArgsCache[queryKey] as SwitchMainCorpArgs) + rxOf(state.concFormsCache[queryKey] as SwitchMainCorpArgs) ) : this.switchMcModel.syncFrom( this.pageModel.ajax$( @@ -1063,7 +1123,7 @@ export class QueryReplayModel extends QueryInfoModel { this.pageModel.createActionUrl('ajax_fetch_conc_form_args'), { corpname: this.getActualCorpname(), - last_key: state.lastOperationKey, + last_key: this.getLastOperationId(state), idx: opIdx } ) diff --git a/public/files/js/models/query/sample.ts b/public/files/js/models/query/sample.ts index 7d89f3ded5..ad81b94b4c 100644 --- a/public/files/js/models/query/sample.ts +++ b/public/files/js/models/query/sample.ts @@ -21,7 +21,7 @@ import { IFullActionControl, StatefulModel } from 'kombo'; import { Observable, of as rxOf } from 'rxjs'; import { tap, map } from 'rxjs/operators'; -import { Dict, HTTP, List, tuple } from 'cnc-tskit'; +import { Dict, HTTP, List } from 'cnc-tskit'; import { PageModel } from '../../app/page'; import { SampleServerArgs } from './common'; diff --git a/public/files/js/models/query/shuffle.ts b/public/files/js/models/query/shuffle.ts index 0b24ab241b..5170a3f29c 100644 --- a/public/files/js/models/query/shuffle.ts +++ b/public/files/js/models/query/shuffle.ts @@ -20,7 +20,7 @@ import { IFullActionControl, StatefulModel } from 'kombo'; import { Observable, of as rxOf } from 'rxjs'; -import { tap, map } from 'rxjs/operators'; +import { tap, map, concatMap } from 'rxjs/operators'; import { PageModel } from '../../app/page'; import { Actions as MainMenuActions } from '../mainMenu/actions'; @@ -73,9 +73,28 @@ export class ShuffleModel extends StatefulModel { this.addActionHandler( Actions.ShuffleFormSubmit, action => { - const concId = List.head(this.layoutModel.getConcArgs().q).substring(1); - this.submitForm( - action.payload.opKey, concId + this.waitForActionWithTimeout( + 5000, + {}, + (action, syncData) => { + if (ConcActions.isReadyToAddNewOperation(action)) { + return null; + } + return syncData; + } + ).pipe( + concatMap( + wAction => { + if (ConcActions.isReadyToAddNewOperation(wAction)) { + return this.submitForm( + action.payload.opKey, wAction.payload.lastConcId + ); + + } else { + throw new Error('failed to handle shuffle submit - unexpected action ' + wAction.name); + } + } + ) ).subscribe({ next: data => { diff --git a/public/files/js/models/query/sort/multi.ts b/public/files/js/models/query/sort/multi.ts index 73a4f47d52..fe94ed3961 100644 --- a/public/files/js/models/query/sort/multi.ts +++ b/public/files/js/models/query/sort/multi.ts @@ -27,7 +27,7 @@ import { SortFormProperties, importMultiLevelArg } from './common'; import { PageModel } from '../../../app/page'; import { Actions as MainMenuActions } from '../../mainMenu/actions'; import { Actions } from '../actions'; -import { tap, map } from 'rxjs/operators'; +import { tap, map, concatMap } from 'rxjs/operators'; import { MLSortServerArgs } from '../common'; import { AjaxConcResponse } from '../../concordance/common'; import { Actions as ConcActions } from '../../concordance/actions'; @@ -115,11 +115,26 @@ export class MultiLevelConcSortModel extends StatefulModel( Actions.MLSortFormSubmit.name, action => { - this.submitQuery( - action.payload.sortId, - this.pageModel.getConf('concPersistenceOpId') - + this.waitForActionWithTimeout( + 5000, + {}, + (action, syncData) => { + if (ConcActions.isReadyToAddNewOperation(action)) { + return null; + } + return syncData; + } ).pipe( + concatMap( + wAction => { + if (ConcActions.isReadyToAddNewOperation(wAction)) { + return this.submitQuery( + action.payload.sortId, + wAction.payload.lastConcId + ); + } + } + ), tap( (data) => { this.pageModel.updateConcPersistenceId(data.conc_persistence_op_id); @@ -128,8 +143,8 @@ export class MultiLevelConcSortModel extends StatefulModel { + ).subscribe({ + next: data => { dispatcher.dispatch({ name: ConcActions.AddedNewOperation.name, payload: { @@ -139,10 +154,10 @@ export class MultiLevelConcSortModel extends StatefulModel { - this.pageModel.showMessage('error', err); + error: error => { + this.pageModel.showMessage('error', error); } - ); + }); } ); diff --git a/public/files/js/models/query/sort/single.ts b/public/files/js/models/query/sort/single.ts index bf9d470b87..7576375604 100644 --- a/public/files/js/models/query/sort/single.ts +++ b/public/files/js/models/query/sort/single.ts @@ -20,7 +20,7 @@ import { IFullActionControl, StatefulModel } from 'kombo'; import { Observable, of as rxOf } from 'rxjs'; -import { tap, map } from 'rxjs/operators'; +import { tap, map, concatMap } from 'rxjs/operators'; import * as Kontext from '../../../types/kontext'; import { PageModel } from '../../../app/page'; @@ -104,10 +104,29 @@ export class ConcSortModel extends StatefulModel { this.changeState(state => { state.isBusy = true; }); - this.submitQuery( - action.payload.sortId, - this.pageModel.getConf('concPersistenceOpId') + this.waitForActionWithTimeout( + 5000, + {}, + (action, syncData) => { + if (ConcActions.isReadyToAddNewOperation(action)) { + return null; + } + return syncData; + } ).pipe( + concatMap( + wAction => { + if (ConcActions.isReadyToAddNewOperation(wAction)) { + return this.submitQuery( + action.payload.sortId, + wAction.payload.lastConcId + ); + + } else { + throw new Error('failed to handle sorting submit - unexpected action ' + wAction.name); + } + } + ), tap( (data) => { this.pageModel.updateConcPersistenceId(data.conc_persistence_op_id); @@ -116,8 +135,8 @@ export class ConcSortModel extends StatefulModel { }); } ) - ).subscribe( - (data) => { + ).subscribe({ + next: data => { dispatcher.dispatch({ name: ConcActions.AddedNewOperation.name, payload: { @@ -127,10 +146,10 @@ export class ConcSortModel extends StatefulModel { }); }, - (err) => { - this.pageModel.showMessage('error', err); + error: error => { + this.pageModel.showMessage('error', error); } - ); + }); } ); diff --git a/public/files/js/models/subcorp/edit.ts b/public/files/js/models/subcorp/edit.ts index 7026d52ee6..06f1e61630 100644 --- a/public/files/js/models/subcorp/edit.ts +++ b/public/files/js/models/subcorp/edit.ts @@ -19,7 +19,7 @@ */ import { concatMap, map, Observable, tap, throwError } from 'rxjs'; -import { IActionQueue, SEDispatcher, StatelessModel } from 'kombo'; +import { Action, IActionQueue, SEDispatcher, StatelessModel } from 'kombo'; import { PageModel } from '../../app/page'; import { Actions } from './actions'; @@ -50,6 +50,7 @@ export interface SubcorpusEditModelState { prevRawDescription:string|undefined; } +type PayloadType = T extends Action ? P : never; export class SubcorpusEditModel extends StatelessModel { @@ -440,7 +441,7 @@ export class SubcorpusEditModel extends StatelessModel error: task.error, args: task.args, url: undefined - } + } as PayloadType ); if ((args.form_type === 'tt-sel' || args.form_type === 'within') && args.usesubcorp) { diff --git a/public/files/js/models/wordlist/main.ts b/public/files/js/models/wordlist/main.ts index 0fe0d81339..2733c55d0b 100644 --- a/public/files/js/models/wordlist/main.ts +++ b/public/files/js/models/wordlist/main.ts @@ -146,7 +146,7 @@ export class WordlistResultModel extends StatelessModel { - this.suspend({}, (otherAction, syncData) => { + this.waitForAction({}, (otherAction, syncData) => { if (otherAction.name === Actions.WordlistFormSubmitReady.name) { return null; } diff --git a/public/files/js/models/wordlist/save.ts b/public/files/js/models/wordlist/save.ts index 2a1c7b8e7c..d438c33cb0 100644 --- a/public/files/js/models/wordlist/save.ts +++ b/public/files/js/models/wordlist/save.ts @@ -122,7 +122,7 @@ export class WordlistSaveModel extends StatelessModel { } }, (state, action, dispatch) => { - this.suspend({}, (action, syncData) => { + this.waitForAction({}, (action, syncData) => { if (action.name === Actions.WordlistFormSubmitReady.name) { return null; } @@ -166,7 +166,7 @@ export class WordlistSaveModel extends StatelessModel { state.toLine.value = ''; }, (state, action, dispatch) => { - this.suspend({}, (action, syncData) => { + this.waitForAction({}, (action, syncData) => { if (action.name === Actions.WordlistFormSubmitReady.name) { return null; } diff --git a/public/files/js/pages/view.ts b/public/files/js/pages/view.ts index 6af203a18f..bf550b2c10 100644 --- a/public/files/js/pages/view.ts +++ b/public/files/js/pages/view.ts @@ -321,7 +321,13 @@ export class ViewPage { payload: { concId: this.layoutModel.getConf('concPersistenceOpId'), viewMode: this.layoutModel.getConcArgs().viewmode, - isPopState: true + arf: this.layoutModel.getConf('ResultArf'), + concSize: this.layoutModel.getConf('ConcSize'), + fullSize: this.layoutModel.getConf('FullSize'), + corpusIpm: this.layoutModel.getConf('ResultIpm'), + queryChainSize: 1, // TODO size 1 even for attached default shuffle? + isPopState: true, + } } }, diff --git a/public/files/js/plugins/defaultTaghelper/positional/models.ts b/public/files/js/plugins/defaultTaghelper/positional/models.ts index 217be047ae..dadcac491a 100644 --- a/public/files/js/plugins/defaultTaghelper/positional/models.ts +++ b/public/files/js/plugins/defaultTaghelper/positional/models.ts @@ -44,7 +44,7 @@ export class DataInitSyncModel extends StatelessModel<{}> { action => action.payload.value === 'tag', null, (state, action, dispatch) => { - this.suspendWithTimeout( + this.waitForActionWithTimeout( 5000, {}, (wAction, syncData) => { diff --git a/public/files/js/types/kontext.ts b/public/files/js/types/kontext.ts index 4d6ead0cc4..bb5e408ac2 100644 --- a/public/files/js/types/kontext.ts +++ b/public/files/js/types/kontext.ts @@ -453,8 +453,10 @@ export interface QueryOperation { /** * A persistent key of the operation + * Note: this is currently attached on the clinet from the pipeline + * op list. */ - conc_persistence_op_id:string; + conc_persistence_op_id:string|null; } export type VirtualKeys = Array>;