diff --git a/frontend/app/components/draw/axis-ticks-selected.js b/frontend/app/components/draw/axis-ticks-selected.js index c62ee757..c07a70cf 100644 --- a/frontend/app/components/draw/axis-ticks-selected.js +++ b/frontend/app/components/draw/axis-ticks-selected.js @@ -18,12 +18,13 @@ const dLog = console.debug; const CompName = 'components/axis-ticks-selected'; /*----------------------------------------------------------------------------*/ -/** @return true if feature's block is not viewed and its dataset +/** @return true if feature's block's dataset * has tag transient. */ function featureIsTransient(f) { - let isTransient = ! f.get('blockId.isViewed'); - if (isTransient) { + /** transient blocks are viewed, after c739c7cd. */ + let isTransient; + { let d = f.get('blockId.datasetId'); d = d.get('content') || d; isTransient = d.hasTag('transient'); @@ -46,6 +47,13 @@ export default Component.extend(AxisEvents, { selected : service('data/selected'), controls : service(), + /** set up a component reference for use in the Web Inspector console */ + develRefnSetup : on('init', function () { + // used in development only, in Web Inspector console. + if (window.PretzelFrontend) { + window.PretzelFrontend.axisTicksSelected = this; + } + }), resized : function(widthChanged, heightChanged, useTransition) { /* useTransition could be passed down to showTickLocations() @@ -107,6 +115,7 @@ export default Component.extend(AxisEvents, { 'selected.shiftClickedFeatures.length', 'selected.labelledFeatures.length', 'selected.features.length', + 'axis1d.blocks.length', function () { this.renderTicksThrottle(); }), diff --git a/frontend/app/components/matrix-view.js b/frontend/app/components/matrix-view.js index 008a3d64..ce7a87fb 100644 --- a/frontend/app/components/matrix-view.js +++ b/frontend/app/components/matrix-view.js @@ -319,8 +319,12 @@ export default Component.extend({ return () => ! this.isDestroying && this.createOrUpdateTable(); }), + get useHandsOnTable() { + return !!config.handsOnTableLicenseKey; + }, + createOrUpdateTable() { - if (! this.table) { + if (! this.table && this.useHandsOnTable) { this.createTable(); } if (! this.noData) { @@ -630,7 +634,7 @@ export default Component.extend({ /** related : cornerClones */ topLeftDialog = $('#observational-table .ht_clone_top_left_corner .colHeader.cornerHeader')[0]; // dLog(fnName, topLeftDialog); - Ember_set(this, 'tableApi.topLeftDialog', topLeftDialog); + later(() => Ember_set(this, 'tableApi.topLeftDialog', topLeftDialog)); this.addComponentClass(); }, @@ -1680,7 +1684,9 @@ export default Component.extend({ let length_checker = $("#length_checker"); length_checker.css('font-weight', 'bold'); this.get('columnNames').forEach(function(col_name) { - let w = length_checker.text(col_name).width(); + /** Just the sampleName is displayed, not datasetId. Similar in colHeaders, uses split(). */ + const sampleName = columnName2SampleName(col_name); + let w = length_checker.text(sampleName).width(); if (w > longest) { longest = w; } diff --git a/frontend/app/components/panel/manage-explorer.js b/frontend/app/components/panel/manage-explorer.js index 1244bd7c..a0359f48 100644 --- a/frontend/app/components/panel/manage-explorer.js +++ b/frontend/app/components/panel/manage-explorer.js @@ -321,7 +321,7 @@ export default ManageBase.extend({ this.set('blockFeatureOntologiesTreeEmbeddedKeyLength', keyLength); // perhaps rename both to keysLength. return valueTree; }) - .catch(error => { dLog(fnName, error.responseJSON.error || error); return Promise.resolve({}); }); + .catch(error => { dLog(fnName, error.responseJSON?.error || error); return Promise.resolve({}); }); return promise; }), diff --git a/frontend/app/components/panel/manage-genotype.hbs b/frontend/app/components/panel/manage-genotype.hbs index 5f4ae5c3..a6c47cf2 100644 --- a/frontend/app/components/panel/manage-genotype.hbs +++ b/frontend/app/components/panel/manage-genotype.hbs @@ -1,4 +1,6 @@ {{this.selectedSampleEffect}} +{{this.sampleFiltersCopyEffect}} +{{!-- this.gtBlockViewEffect --}} {{will-destroy this.onWillDestroy}} {{#if this.showInputDialog}} diff --git a/frontend/app/components/panel/manage-genotype.js b/frontend/app/components/panel/manage-genotype.js index 6440976e..b301b295 100644 --- a/frontend/app/components/panel/manage-genotype.js +++ b/frontend/app/components/panel/manage-genotype.js @@ -805,18 +805,23 @@ export default class PanelManageGenotypeComponent extends Component { abBlocks.forEach((abBlock, i) => { const block = abBlock.block, - selected = block[sampleFiltersSymbol][filterTypeName], - blocksTypeFilters = 'blocks' + toTitleCase(filterTypeName) + 'Filters'; - /** selected is equal to one of : - * this.blocksFeatureFilters[i].sampleFilters.feature - * this.blocksHaplotypeFilters[i].sampleFilters.haplotype - */ - if (selected !== this[blocksTypeFilters][i].sampleFilters[filterTypeName]) { - dLog(fnName, abBlock, this[blocksTypeFilters][i], selected); - } - if (selected.length) { - selected.removeAt(0, selected.length); + sampleFilters = block[sampleFiltersSymbol]; + if (sampleFilters) { + const + selected = sampleFilters[filterTypeName], + blocksTypeFilters = 'blocks' + toTitleCase(filterTypeName) + 'Filters'; + /** selected is equal to one of : + * this.blocksFeatureFilters[i].sampleFilters.feature + * this.blocksHaplotypeFilters[i].sampleFilters.haplotype + */ + if (selected !== this[blocksTypeFilters][i].sampleFilters[filterTypeName]) { + dLog(fnName, abBlock, this[blocksTypeFilters][i], selected); + } + if (selected?.length) { + selected.removeAt(0, selected.length); + } } + const referenceSamples = block[referenceSamplesSymbol]; if (referenceSamples) { arrayClear(referenceSamples); @@ -838,7 +843,140 @@ export default class PanelManageGenotypeComponent extends Component { later(() => this.matrixView.table.render(), 2000); } } - + + //---------------------------------------------------------------------------- + /** Retain or reset selected SNPs when another dataset is added in Genotype Table. + * retain and reset are alternatives, and are implemented by gtBlockViewEffect + * and sampleFiltersCopyEffect respectively. + */ + + /** Retain previous value of .gtBlocks seen by gtBlockViewEffect to detect added blocks */ + gtBlocksPrevious = null; + /** Side Effect : clear sampleFilters if a new VCF Genotype block is viewed. + */ + @computed('gtBlocks') + get gtBlockViewEffect() { + const + fnName = 'gtBlockViewEffect', + blocks = this.gtBlocks, + gtBlocksPrevious = this.gtBlocksPrevious, + newBlocks = gtBlocksPrevious ? blocks.filter(block => ! gtBlocksPrevious.includes(block)) : blocks, + added = newBlocks.length; + this.gtBlocksPrevious = blocks; + if (added) { + dLog(fnName, newBlocks.mapBy('brushName'), blocks.mapBy('brushName'), gtBlocksPrevious?.mapBy('brushName')); + /* The Clear button calls sampleFiltersClear() and haplotypeFiltersApply() + * sampleFiltersClear() clears the filters for .sampleFilterTypeName, so + * set that and call Clear again. + */ + this.sampleFiltersClear(); + this.setSelectedSampleFilter('feature', /*i*/undefined); + this.sampleFiltersClear(); + this.haplotypeFiltersApply(); + } + } + /** Side Effect : copy sampleFilters of another block if a new block is viewed. */ + @computed('gtBlocks') + get sampleFiltersCopyEffect() { + /** allow time for brushed features to be loaded. + * Later, we can update after additional features are loaded, and also + * sampleFiltersCopyType() is intended to map the selected features to the + * added block when called again subsequently. + */ + later(() => ! this.isDestroying && this.sampleFiltersCopy(), 3000); + } + /** If a new VCF block is viewed, and it does not have [sampleFiltersSymbol] + * copy this from another block. + * Copy each of the attributes, whose keys are sampleFilterKeys. + */ + sampleFiltersCopy() { + const fnName = 'sampleFiltersCopy'; + this.sampleFilterKeys.forEach(sampleFilterTypeName => this.sampleFiltersCopyType(sampleFilterTypeName)); + dLog(fnName, this.sampleFiltersCheck()); + + // update selectedSampleEffect + later(() => { + this.selectedSampleRefreshDisplay(/*sampleFilterTypeName*/undefined); + // Repeat - will work out the requirements display to be refreshed. + later(() => { + ! this.isDestroying && this.selectedSampleRefreshDisplay(/*sampleFilterTypeName*/undefined); + }, 1000); + }); + } + /** If a new VCF block is viewed, and it does not have [sampleFiltersSymbol] + * copy this from another block. + * + * This has been tested on the case where there is 1 block displayed with + * selected SNPs on the same referenceBlock as a block which is added / viewed. + * This handles what may be the most useful and common case, and suits a user + * story which is being documented, but will have to be + * re-thought in other contexts, e.g. if there are 2 blocks with + * [sampleFiltersSymbol] (currently chooses the first one), or if the selected + * SNPs are not in this block. + */ + sampleFiltersCopyType(sampleFilterTypeName) { + const fnName = 'sampleFiltersCopyType'; + { + const + blocks = this.gtBlocks, + filters = Object.groupBy(blocks, block => { + const + sampleFilters = block[sampleFiltersSymbol], + sampleFilter = sampleFilters?.[sampleFilterTypeName], + /** for sampleFilterTypeName 'feature' check if a feature is in this block. + * If the features' blocks !== block, then we want to do, below: + * map from copyThis features to block.features ... + */ + ok = sampleFilter?.length && + ((sampleFilterTypeName !== 'feature') || + sampleFilter.find(feature => contentOf(feature.get('blockId')) === block)); + return !!ok; + }), + newBlocks = filters['false'], + haveFilters = filters['true']; + if (haveFilters && newBlocks) { + newBlocks.forEach(block => { + const + /** Only match SNPs on the same reference block. + * There could be multiple haveFilters on the same .referenceBlock; + * just using the first. */ + copyThis = haveFilters?.find(b => b.referenceBlock === block.referenceBlock), + copyThisFilter = copyThis?.[sampleFiltersSymbol], + copy = + Object.entries(copyThisFilter).reduce((O, [k,v]) => {O[k] = v.slice(0); return O;}, {}); + dLog(fnName, 'copyThisFilter', copyThisFilter, copyThis.brushName, copy); + + /** map from copyThis features to block.features, matching SNP position, i.e. .value_0 */ + copy.feature?.forEach((f, i) => { + const f2 = block.get('features').findBy('value_0', f.value_0); + dLog(fnName, i, f, f2); + if (f2) { + f2[matchRefSymbol] = f[matchRefSymbol]; + copy.feature[i] = f2; + } + /* else could remove copy.features[i], or search again later after + * more features are loaded. Could use API to request those features. */ + }); + block[sampleFiltersSymbol] = copy; + dLog(fnName, block); + }); + } + } + } + /** Refresh display to show result of sampleFiltersCopy(). + */ + selectedSampleRefreshDisplay(sampleFilterTypeName) { + if (! sampleFilterTypeName) { + sampleFilterTypeName = this.args.userSettings.sampleFilterTypeName; + } + // There is probably some redundancy here which can be reduced. + this.sampleFiltersSet(sampleFilterTypeName); + this.datasetPositionFilterChangeCount++; + this.haplotypeFiltersApply(); + /* referenceSamples are separate from sampleFilters - could have a + * separate dependency/count for sampleFilters. */ + this.referenceSamplesCount++; + } /** Use Ember_set() to signal update of tracked properties and trigger re-render. */ sampleFiltersSet(filterTypeName) { @@ -1060,6 +1198,26 @@ export default class PanelManageGenotypeComponent extends Component { dLog(fnName, filterTypeName, axisBrushes, blocksF); return blocksF; } + /** Surface the details of the selected SNPs, to check result of + * sampleFiltersCopyType(). + * This is similar to blocksSampleFilters(), but has a different use. + */ + sampleFiltersCheck() { + const + blocks = this.gtBlocks, + sampleFilters = blocks.map(block => { + const + sf = block[sampleFiltersSymbol], + features = this.blockSampleFilters(block, 'feature'), + featuresDesc = features.map(feature => [ + feature.get('blockId.brushName'), + feature.value_0, + feature[matchRefSymbol], + ]); + return featuresDesc; + }); + return sampleFilters; + } /** @return array of blocks and the features selected on them for filtering samples. */ @computed('brushedOrViewedVCFBlocks') @@ -1543,11 +1701,22 @@ export default class PanelManageGenotypeComponent extends Component { */ const lookupBlock = this.args.userSettings.lookupBlock; if (lookupBlock !== undefined) { - this.axisBrushBlockIndex = blocks.findIndex((abb) => abb.block === lookupBlock); + let axisBrushBlockIndex = blocks.findIndex((abb) => abb.block === lookupBlock); + /** .axisBrushBlockIndex may be -1, when data block is un-viewed while brushed. + * In this case, if there is another viewed block on the same axis, choose that, + * else if there are viewed blocks, choose the first one, because + * .axisBrushBlockIndex === -1 leads to .lookupBlock undefined, which + * gets undefined block in vcfGenotypeSamples(). + */ + if (axisBrushBlockIndex === -1) { + axisBrushBlockIndex = blocks.findIndex((abb) => abb.block.referenceBlock === lookupBlock.referenceBlock); + } else if (blocks.length) { + axisBrushBlockIndex = 0; + } + this.axisBrushBlockIndex = axisBrushBlockIndex; if (this.axisBrushBlockIndex === undefined) { this.args.userSettings.lookupBlock = undefined; } - /** .axisBrushBlockIndex maybe -1, when data block is un-viewed while brushed. */ dLog(fnName, this.axisBrushBlockIndex, blocks[this.axisBrushBlockIndex]?.block.id, blocks, lookupBlock.id); } else if ((this.axisBrushBlockIndex === undefined) || (this.axisBrushBlockIndex > blocks.length-1)) { /* first value is selected. if only 1 value then select onchange action will not be called. */ diff --git a/frontend/app/components/panel/upload/blast-results-view.js b/frontend/app/components/panel/upload/blast-results-view.js index 83f03075..487e21cc 100644 --- a/frontend/app/components/panel/upload/blast-results-view.js +++ b/frontend/app/components/panel/upload/blast-results-view.js @@ -5,6 +5,8 @@ import { later as run_later, bind } from '@ember/runloop'; import { alias } from '@ember/object/computed'; +import { isEqual } from 'lodash/lang'; + import config from '../../../config/environment'; import { nowOrLater } from '../../../utils/ember-devel'; @@ -95,6 +97,11 @@ export default Component.extend({ didReceiveAttrs() { this._super(...arguments); + // used in development only, in Web Inspector console. + if (window.PretzelFrontend) { + window.PretzelFrontend.blastResultsView = this; + } + let promise = this.get('search.promise'); if (promise) { promise.catch(() => { @@ -238,7 +245,8 @@ export default Component.extend({ /** based on dataFeatures - see comments there. */ let names = data - .map((row) => /*chrName2Pretzel*/(row[c_chr])); + .map((row) => /*chrName2Pretzel*/(row[c_chr])) + .uniq(); dLog('blockNames', names.length, names[0]); return names; }), @@ -330,10 +338,14 @@ export default Component.extend({ dLog(fnName, viewFeaturesFlag); this.viewFeaturesAll(viewFeaturesFlag); }, - viewFeaturesEffect : computed('dataFeaturesForStore.[]', 'viewRow', 'active', function () { - // viewFeatures() uses the dependencies : dataFeaturesForStore, viewRow, active. - this.viewFeatures(); - }), + viewFeaturesEffect : computed( + 'dataFeaturesForStore.[]', 'viewRow', 'active', + /** update after the blastResults_ block is viewed */ + 'block.viewed.length', + function () { + // viewFeatures() uses the dependencies : dataFeaturesForStore, viewRow, active. + this.viewFeatures(); + }), /** if .viewAllResultAxesFlag, narrowAxesToViewed() */ narrowAxesToViewedEffect : computed( @@ -392,7 +404,7 @@ export default Component.extend({ (viewFlag) => toView[viewFlag] && this.get('viewDataset')(parentName, viewFlag, toView[viewFlag])); }, - parentBlocks : computed('search.parent', function () { + parentBlocks : computed('search.parent', 'blockNames', function () { const fnName = 'parentBlocks'; let parentName = this.get('search.parent'); let @@ -413,10 +425,11 @@ export default Component.extend({ /** blockScopes is parallel to blockNames, and enables a mapping * from name (result) to scope (axis reference) */ this.blockScopes = blockNames.map(name => blocks.findBy('name', name)?.scope); + dLog(fnName, blockNames, blocks, 'blockScopes', this.blockScopes); return blocks; }), resultParentBlocksByName : computed('parentBlocks', 'blockNames.[]', function () { - const fnName = 'resultParentBlocks'; + const fnName = 'resultParentBlocksByName'; let blockNames = this.get('blockNames'); const @@ -429,7 +442,7 @@ export default Component.extend({ } return blocks; }), - /** View the blocks of the parent which are identifeid by .blockNames. + /** View the blocks of the parent which are identified by .blockNames. * @param viewFlag true/false for view/unview */ viewParent(viewFlag) { @@ -487,13 +500,28 @@ export default Component.extend({ const blocksByName = this.get('resultParentBlocksByName'), scopesForNames = this.get('blockNames').map(name => blocksByName[name]?.scope || name); + /* from a quick look, blockScopes and scopesForNames are probably similar + * solutions to the same problem, from different branches, and likely the same. + * from these branches / commits : + * blockScopes : [feature/ongoingGenotype ade64e58] Load blast search results with block .scope matching reference + * scopesForNames : [feature/upgradeFrontend 8c8d63c2] update axis drag, feature search results display + */ + if (! isEqual(this.blockScopes, scopesForNames) || + (this.blockScopes.length < this.get('blockNames.length')) || + (scopesForNames.length < this.get('blockNames.length')) ) { + dLog(fnName, 'blockNames', this.get('blockNames'), 'blockScopes', this.blockScopes, 'scopesForNames', scopesForNames); + } let blocks = transient.blocksForSearch( datasetName, - this.blockScopes, this.get('blockNames'), + // this.blockScopes, scopesForNames, namespace ); + transient.datasetBlocksResolveProxies(dataset, blocks); + this.dataset = dataset; // for development + this.blocks = blocks; // + /** change features[].blockId to match blocks[], which has dataset.id prefixed to make them distinct. */ let featuresU = features.map((f) => { let {blockId, ...rest} = f; rest.blockId = dataset.id + '-' + blockId; return rest; }); /** When changing between 2 blast-results tabs, this function will be @@ -515,7 +543,10 @@ export default Component.extend({ active, /* when switching tabs got : this.isDestroyed===true, this.viewRow and this.get('viewRow') undefined * but this.search.viewRow OK */ - () => transient.showFeatures(dataset, blocks, featuresU, active, this.get('viewRow') || this.search.viewRow)); + () => { + this.get('viewDataset')(datasetName, active, blocks.mapBy('name')); + run_later(() => transient.showFeatures(dataset, blocks, featuresU, active, this.get('viewRow') || this.search.viewRow)); + }); } }, diff --git a/frontend/app/components/table-brushed.js b/frontend/app/components/table-brushed.js index c232dc78..03e62041 100644 --- a/frontend/app/components/table-brushed.js +++ b/frontend/app/components/table-brushed.js @@ -325,6 +325,9 @@ export default Component.extend({ this._super(...arguments); }, + get useHandsOnTable() { + return !!config.handsOnTableLicenseKey; + }, didRender() { this._super(...arguments); @@ -335,7 +338,7 @@ export default Component.extend({ // flag that createTable() has started. this.set('table', null); later(() => { - if (! this.isDestroying) { + if (! this.isDestroying && this.useHandsOnTable) { this.createTable(this); } }); diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index 66ad7080..2f7eaba6 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -302,8 +302,10 @@ export default Controller.extend(Evented, componentQueryParams.Mixin, { let related = this.get('view').viewRelatedBlocks(block); // load related[] before block. related.push(block); - // or send('getSummaryAndData', block); - related.forEach((block) => this.actions.getSummaryAndData.apply(this, [block])); + if (! block.hasTag('transient')) { + // or send('getSummaryAndData', block); + related.forEach((block) => this.actions.getSummaryAndData.apply(this, [block])); + } }, getSummaryAndData(block) { /* Before progressive loading this would load the data (features) of the block. @@ -351,12 +353,12 @@ export default Controller.extend(Evented, componentQueryParams.Mixin, { } // this.send('setTab', 'right', 'block'); - /** Don't switch to the dataset tab if the Genotype Table is displayed */ - const rightTab = this.get('layout.right.tab'); + /** Don't switch to the dataset tab if the Genotype Table is displayed */ + const rightTab = this.get('layout.right.tab'); let queryParams = this.get('model.params'); /* if the block tab in right panel is not displayed then select the block's dataset. */ if ((rightTab !== "genotype") && - ! (queryParams.options && queryParams.parsedOptions.blockTab)) { + ! (queryParams.options && queryParams.parsedOptions.blockTab)) { this.send('selectDataset', block.datasetId); } } @@ -386,10 +388,15 @@ export default Controller.extend(Evented, componentQueryParams.Mixin, { * un-ergonomic to constantly switch to the dataset tab, closing the * features or paths table and losing the users' column width adjustments * etc. This condition excepts that case. + * + * Also don't change to dataset tab if current tab is Genotype Table + * - it is distracting and unergonomic, probably not useful, and + * (currently) disrupts column/sample sorting by selected SNPS (sampleFilters) */ let changed = this.get('selectedDataset.id') !== ds.get('id'); + const tabIsGenotype = this.get('layout.right.tab') === "genotype"; this.set('selectedDataset', ds); - if (changed) { + if (changed && ! tabIsGenotype) { this.send('setTab', 'right', 'dataset'); } }, diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index 87431310..6a6e2d79 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -88,6 +88,20 @@ export default Model.extend({ features: hasMany('feature', {async: false, inverse : 'blockId'}), range: attr('array'), scope: attr('string'), + /** rename scope to _scope, so it can be wrapped by get/set scope. + scope: computed('_scope', { + get() { + // dLog('Getting scope:', this._scope); + return this._scope; + }, + set(key, value) { + dLog('Setting scope:', value); + this.set('_scope', value); + return value; + } + }), + */ + name: attr('string'), namespace: attr('string'), featureType: attr(), diff --git a/frontend/app/routes/application.js b/frontend/app/routes/application.js index 6ec32751..ca25e6a0 100644 --- a/frontend/app/routes/application.js +++ b/frontend/app/routes/application.js @@ -12,10 +12,14 @@ export default Route.extend(/*ApplicationRouteMixin,*/ { init() { this._super(...arguments); console.log(this, this.session, this.session.on); - this.session.session.on('authenticationSucceeded', () => this.sessionAuthenticated()); - this.session.session.on('invalidationSucceeded', () => this.session.handleInvalidation()); - // no this.session.triggerAuthentication, maybe this.session.session.requireAuthentication('login') - this.session.session.on('authenticationRequested', () => this.session.triggerAuthentication('login')); + /** Same comment as in app/utils/ember-simple-auth-mixin-replacements/application-route-mixin.js : + * Registrations for session.on authenticationSucceeded and + * invalidationSucceeded which were here until 83f6bda4 are already done by + * ember-simple-auth/addon/services/session.js : _setupHandlers(), which + * uses Configuration.{routeAfterAuthentication,rootURL}. + * That does not include authenticationRequested : triggerAuthentication(), + * which seems no longer applicable. + */ }, async beforeModel() { diff --git a/frontend/app/routes/mapview.js b/frontend/app/routes/mapview.js index 90fb49e0..54da497d 100644 --- a/frontend/app/routes/mapview.js +++ b/frontend/app/routes/mapview.js @@ -66,6 +66,14 @@ let config = { this.get('auth').runtimeConfig().then((config) => { dLog('getHoTLicenseKey', config, ENV); ENV.handsOnTableLicenseKey = config.handsOnTableLicenseKey; + /** May also want to signify that the runtimeConfig has been received, e.g. + * ENV.runtimeConfigReceived = true; + * or ENV.runtimeConfig = config; + * This would allow delayed instantiation of some components which + * depend on that config. + * runtimeConfig might fit better within services/controls.js, instead + * of modifying ENV. + */ }); } }, diff --git a/frontend/app/serializers/block.js b/frontend/app/serializers/block.js index cb1430d3..1fcaa64b 100644 --- a/frontend/app/serializers/block.js +++ b/frontend/app/serializers/block.js @@ -40,6 +40,7 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { annotations: { embedded: 'always' }, intervals: { embedded: 'always' }, features: { embedded: 'always' }, + // _scope : 'scope', /** Rename Pretzel Block .meta to ._meta to avoid clash with ember-data object * .meta, as commented in ./dataset.js : attrs : _meta */ diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index e5fdf1b4..1c532df9 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -686,10 +686,21 @@ export default Service.extend(Evented, { */ peekBlock(blockId) { + if ((blockId === undefined) || (blockId === null)) { + return undefined; + } let apiServers = this.get('apiServers'), store = apiServers.id2Store(blockId), - block = store && store.peekRecord('block', blockId); + block; + /** transient blocks are not present in id2Server[] (services/api-servers), + * so fall back to searching each server store. */ + if (store) { + block = store && store.peekRecord('block', blockId); + } else { + block = Object.values(apiServers.servers).find( + apiServer => apiServer.store.peekRecord('block', blockId)); + } return block; }, @@ -898,6 +909,15 @@ export default Service.extend(Evented, { * undefined and limits are requested for all blocks. */ getBlocksLimits(blockId) { + /** don't call taskGetLimits for a transient block. */ + if (blockId && this.peekBlock(blockId)?.hasTag('transient')) { + // Limits of the parent / reference are applicable. + /** taskGetLimits() sets .featureLimits and .featureValueCount; perhaps + * calculate those here. + */ + return Promise.resolve([]); + } + const fnName = 'getBlocksLimits'; let taskGet = this.get('taskGetLimits'); console.log("getBlocksLimits", blockId); @@ -923,6 +943,15 @@ export default Service.extend(Evented, { getBlocksSummary(blockIds) { + /** Filter out the transient blocks; set their .featureCount, and don't call + * taskGetSummary for them. */ + const + blocks = blockIds.map(blockId => this.peekBlock(blockId)), + /** result of hasTag() is true or falsey (undefined / 0 / false) */ + transientBlocks = Object.groupBy(blocks, block => !!block?.hasTag('transient')); + transientBlocks['true']?.forEach(block => block.set('featureCount', block.features.length)); + blockIds = transientBlocks['false']?.map(block => block.id) ?? []; + let taskGet = this.get('taskGetSummary'); console.log("getBlocksSummary", blockIds); let p = new Promise(function(resolve, reject){ @@ -1171,7 +1200,7 @@ export default Service.extend(Evented, { * blocks[0] is reserved for the reference block, so a new array is * [undefined], and blocks[0] is set by the caller. * - * @param map contains the datasetId:scope:blocks heirarchy. + * @param map contains the datasetId:scope:blocks hierarchy. * @param create added to indicate whether to create a map entry if it doesn't exist. * The 2nd pass of mapBlocksByReferenceAndScope() passes create===false - see * comment there. diff --git a/frontend/app/services/data/transient.js b/frontend/app/services/data/transient.js index 0baa15fc..f7c16ea5 100644 --- a/frontend/app/services/data/transient.js +++ b/frontend/app/services/data/transient.js @@ -2,6 +2,8 @@ import { computed } from '@ember/object'; import Service, { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; +// import { isProxy, withoutProxies } from 'ember-proxy-util'; + import { _internalModel_data } from '../../utils/ember-devel'; @@ -92,12 +94,19 @@ export default Service.extend({ }, pushBlockArgs(datasetId, name, scope, namespace) { + const fnName = 'pushBlockArgs'; let /** may need to pass search ID also; depends on whether distinct blocks should be used for each search result. */ /** prefix _id with datasetId to make it unique enough. May use UUID. */ data = {_id : datasetId + '-' + name, scope, name, namespace, datasetId}, store = this.get('store'), record = this.pushData(store, 'block', data); + if (scope === undefined) { + console.warn(fnName, 'scope', scope); + } else if (record.scope !== data.scope) { + dLog(fnName, record.scope, data.scope); + record.scope = data.scope; + } return record; }, /** Push into the (default) store blocks for the given datasetId and {name,scope,namespace} @@ -109,6 +118,31 @@ export default Service.extend({ return blocks; }, + /** Resolve Proxy references from blocks[] to dataset. + * Currently : + * models/block.js : datasetId : ... async: true + * models/dataset.js : blocks: ... async: false + * which causes pushBlockArgs() to assign a Proxy of the identified dataset to + * blocks[] .datasetId. This could be solved by combining pushBlockArgs() + * with pushDatasetArgs() and using a single pushPayload(), or by changing the + * above async settings, which is likely to have side effects so it can be + * done later. This function implements another solution which is to replace + * the Proxy references with references to the actual ember-data store object. + */ + datasetBlocksResolveProxies(dataset, blocks) { + const fnName = 'datasetBlocksResolveProxies'; + blocks.forEach(block => { + /** may be Proxy of dataset, i.e. Proxy { : {…}, : {…} } */ + const dp = block.get('datasetId'); + if (dp.content?.id) { // isProxy(dp) + const dc = dp.content; // withoutProxies(dp); // i.e. + dLog(fnName, dp, dc, dc.blocks.length, block); + block.set('datasetId', dc); + dc.blocks.addObject(block); + } + }); + }, + /** * @param active true if the tab containing these results is (becoming) active * Features are displayed only while the tab is active, to separate diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index 8280e335..6082ecd4 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1852,7 +1852,13 @@ div.ht_clone_top_left_corner span.colHeader.cornerHeader { #observational-table td.featureIsFilter { border: 2px dashed green; - border-left-width : 23px !important; + /* In Firefox this shows solid top and bottom borders approx 4px; + * Chrome shows the whole cell to be green. I think the motive for + * this was that cell click shows blue border, so it would be good + * if the .featureIsFilter border was visible over that. Increasing + * border width increases cell size, affecting table geometry. + * Original edit was .A3 63037 2023 Sep 22 17:25. */ + /* border-left-width : 23px !important; */ } // ----------------------------------------------------------------------------- diff --git a/frontend/app/templates/components/draw-map.hbs b/frontend/app/templates/components/draw-map.hbs index ff6a790c..e6ffb511 100644 --- a/frontend/app/templates/components/draw-map.hbs +++ b/frontend/app/templates/components/draw-map.hbs @@ -10,7 +10,7 @@
{{this.headsUp.tipText}}
-
+
{{yield}} diff --git a/frontend/app/utils/ember-simple-auth-mixin-replacements/application-route-mixin.js b/frontend/app/utils/ember-simple-auth-mixin-replacements/application-route-mixin.js index 43050c42..4536ff6e 100644 --- a/frontend/app/utils/ember-simple-auth-mixin-replacements/application-route-mixin.js +++ b/frontend/app/utils/ember-simple-auth-mixin-replacements/application-route-mixin.js @@ -11,9 +11,13 @@ export default Mixin.create({ init() { this._super(...arguments); - this.session.on('authenticationSucceeded', () => this.sessionAuthenticated()); - this.session.on('invalidationSucceeded', () => this.session.handleInvalidation()); - this.session.on('authenticationRequested', () => this.session.triggerAuthentication('login')); + /** Registrations for session.on authenticationSucceeded and + * invalidationSucceeded which were here until 83f6bda4 are already done by + * ember-simple-auth/addon/services/session.js : _setupHandlers(), which + * uses Configuration.{routeAfterAuthentication,rootURL}. + * That does not include authenticationRequested : triggerAuthentication(), + * which seems no longer applicable. + */ }, beforeModel() { diff --git a/frontend/config/environment.js b/frontend/config/environment.js index 3a4c1bf6..e2d4914d 100644 --- a/frontend/config/environment.js +++ b/frontend/config/environment.js @@ -66,6 +66,13 @@ module.exports = function (environment) { // keyDelimiter: '/' // will use / as a delimiter - the default is : }, + 'ember-simple-auth' : { + /** default is '', defined in ember-simple-auth/addon/configuration.js : DEFAULTS + * rootURL : '', + */ + routeAfterAuthentication : 'mapview', + }, + EmberENV: { FEATURES: { diff --git a/frontend/package.json b/frontend/package.json index 4a44d9ce..f8e410e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pretzel-frontend", - "version": "3.0.0", + "version": "3.1.0", "description": "Frontend code for Pretzel", "repository": "", "license": "MIT", diff --git a/lb4app/lb3app/common/models/block.js b/lb4app/lb3app/common/models/block.js index fc2599f8..fd27dab6 100644 --- a/lb4app/lb3app/common/models/block.js +++ b/lb4app/lb3app/common/models/block.js @@ -1011,6 +1011,7 @@ function blockAddFeatures(db, datasetId, blockId, features, cb) { } else { let db = this.dataSource.connector; + let cbCalled = false; blockFeatures.blockValues(db, fieldName) .then((cursor) => cursor.toArray()) .then(function(traits) { @@ -1018,9 +1019,25 @@ function blockAddFeatures(db, datasetId, blockId, features, cb) { console.log(fnName, cacheId, 'put', traits[0] || traits); } cache.put(cacheId, traits); + /** potentially cb() could throw, so set cbCalled to guard against calling + * cb() a second time in .catch(). + * This cb() is returning a response with the given value; perhaps this is + * also trying to set headers ? + */ + cbCalled = true; cb(null, traits); }).catch(function(err) { - cb(err); + if (cbCalled) { + console.log(fnName, cacheId, 'cb was already called, so not calling it again', err); + } else { + /** sometimes 'Callback was already called.' occurs here when client + * starts, so guard this with cbCalled. + * err was : 'Error: Cannot set headers after they are sent to the + * client', which has been seen new server is queried by existing + * client. + */ + cb(err); + } }); } }; diff --git a/lb4app/lb3app/common/models/dataset.js b/lb4app/lb3app/common/models/dataset.js index 0cac6cb4..15bfaed8 100644 --- a/lb4app/lb3app/common/models/dataset.js +++ b/lb4app/lb3app/common/models/dataset.js @@ -340,7 +340,8 @@ module.exports = function(Dataset) { } else { cb(null, status); } - }); + }) + .catch(err => { console.log(fnName, err.message); cb(err); }); }; /** Continuation of spreadsheetUploadInternal() - originally these 2 were a * single function, but split to enable additional (asynchronous) diff --git a/lb4app/lb3app/common/utilities/errorStatus.js b/lb4app/lb3app/common/utilities/errorStatus.js index 98cd586e..89bc5fe9 100644 --- a/lb4app/lb3app/common/utilities/errorStatus.js +++ b/lb4app/lb3app/common/utilities/errorStatus.js @@ -7,7 +7,7 @@ */ exports.ErrorStatus = function(statusCode, text) { - let e = Error(text); + let e = new Error(text); e.statusCode = statusCode; return e; }; diff --git a/lb4app/lb3app/server/boot/exception_handling.js b/lb4app/lb3app/server/boot/exception_handling.js new file mode 100644 index 00000000..4566598d --- /dev/null +++ b/lb4app/lb3app/server/boot/exception_handling.js @@ -0,0 +1,20 @@ +'use strict'; + +/* global module */ +/* global process */ + +//------------------------------------------------------------------------------ + +module.exports = function(app) { + + process.on('uncaughtException', (err) => { + console.error('Unhandled Exception:', err); + }); + + process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection:', reason); + }); + +}; + +//------------------------------------------------------------------------------ diff --git a/package.json b/package.json index f9d95215..5861a6dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pretzel", "private": true, - "version": "3.0.0", + "version": "3.1.0", "dependencies": { }, "repository" : diff --git a/resources/data_templates/datasets.ots b/resources/data_templates/datasets.ots index e4c7ea0d..8e117adf 100644 Binary files a/resources/data_templates/datasets.ots and b/resources/data_templates/datasets.ots differ diff --git a/resources/data_templates/datasets.xltx b/resources/data_templates/datasets.xltx index a2333d31..6cccb16c 100644 Binary files a/resources/data_templates/datasets.xltx and b/resources/data_templates/datasets.xltx differ