@meshmakers/octo-meshboard 3.5.710 → 3.5.730

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import * as i0 from '@angular/core';
4
4
  import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, input, Directive, model, computed, ElementRef, Injector, EnvironmentInjector, ApplicationRef, signal, effect, untracked, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ChangeDetectorRef, ViewChildren, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, output, viewChild } from '@angular/core';
5
5
  import * as i1$1 from '@angular/forms';
6
6
  import { NG_VALUE_ACCESSOR, FormsModule, NG_VALIDATORS } from '@angular/forms';
7
- import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, forkJoin, catchError, NEVER, interval, Subject, BehaviorSubject, filter as filter$1, combineLatest, defer, EMPTY } from 'rxjs';
7
+ import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, forkJoin, catchError, EMPTY, NEVER, interval, Subject, BehaviorSubject, filter as filter$1, combineLatest, defer } from 'rxjs';
8
8
  import * as i1$8 from '@meshmakers/shared-ui';
9
9
  import { EntitySelectInputComponent, MM_ACTION_ICONS, WindowStateService, TimeRangeUtils, MmDialogActionsDirective, ListViewComponent, FetchResultTyped, DataSourceBase, ConfirmationService, TimeRangePickerComponent, ImportStrategyDialogService, HAS_UNSAVED_CHANGES, UnsavedChangesDirective } from '@meshmakers/shared-ui';
10
10
  import * as i1 from 'apollo-angular';
@@ -10929,6 +10929,12 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10929
10929
  stateService = inject(MeshBoardStateService);
10930
10930
  variableService = inject(MeshBoardVariableService);
10931
10931
  _config = null;
10932
+ /**
10933
+ * Signature of the query inputs outside the grid state (data source, resolved
10934
+ * widget filters, time filter, entity-selector scope) of the last fetch.
10935
+ * `null` until the first fetch. See {@link refetch}.
10936
+ */
10937
+ _lastQueryInputs = null;
10932
10938
  /** Signal for dynamically derived columns from persistent query */
10933
10939
  _queryColumns = signal([], /* @ts-ignore */
10934
10940
  ...(ngDevMode ? [{ debugName: "_queryColumns" }] : /* istanbul ignore next */ []));
@@ -10955,7 +10961,43 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
10955
10961
  const listViewComponent = inject(ListViewComponent);
10956
10962
  super(listViewComponent);
10957
10963
  }
10964
+ /**
10965
+ * Refetches the widget data (board refresh, auto-refresh, config change).
10966
+ *
10967
+ * When a query input outside the grid state changed since the last fetch — a
10968
+ * MeshBoard variable or widget filter, the time filter, the entity-selector
10969
+ * scope or the data source itself — the result set is a different one, so the
10970
+ * list returns to page 1 before the query is sent (AB#8801). Otherwise the old
10971
+ * page offset could point past the end of the new result (asset repository:
10972
+ * ASSET1002 / INCOMPLETE_SLICE). A plain refresh with unchanged inputs keeps
10973
+ * the current page.
10974
+ */
10975
+ refetch() {
10976
+ const previous = this._lastQueryInputs;
10977
+ const resetSkip = previous !== null && previous !== this.queryInputsSignature();
10978
+ this.fetchAgain(resetSkip ? { resetSkip: true } : undefined);
10979
+ }
10980
+ /**
10981
+ * Page-out-of-range detection for both error shapes: the inherited check reads
10982
+ * `graphQLErrors` (Apollo 3 `ApolloError`); Apollo 4 rejects with
10983
+ * `CombinedGraphQLErrors`, which carries the same entries in `errors`.
10984
+ */
10985
+ isPageOutOfRangeError(error) {
10986
+ if (super.isPageOutOfRangeError(error)) {
10987
+ return true;
10988
+ }
10989
+ const errors = error?.errors;
10990
+ if (!Array.isArray(errors)) {
10991
+ return false;
10992
+ }
10993
+ return errors.some((e) => {
10994
+ const extensions = e?.extensions;
10995
+ const codes = Array.isArray(extensions?.['codes']) ? extensions['codes'] : [];
10996
+ return codes.includes('INCOMPLETE_SLICE') || extensions?.['code'] === 'INCOMPLETE_SLICE';
10997
+ });
10998
+ }
10958
10999
  fetchData(queryOptions) {
11000
+ this._lastQueryInputs = this.queryInputsSignature();
10959
11001
  if (!this._config) {
10960
11002
  return of(new FetchResultTyped([], 0));
10961
11003
  }
@@ -11018,10 +11060,52 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
11018
11060
  return record;
11019
11061
  });
11020
11062
  return new FetchResultTyped(data, totalCount);
11021
- }), catchError$1(err => {
11022
- console.error('Error fetching table data:', err);
11023
- return of(new FetchResultTyped([], 0));
11024
- }));
11063
+ }), catchError$1(err => this.handleFetchError(err, queryOptions, 'Error fetching table data:')));
11064
+ }
11065
+ /**
11066
+ * Error path of both fetch paths. A page offset beyond the result set
11067
+ * (ASSET1002 / INCOMPLETE_SLICE, e.g. the data shrank since the last load)
11068
+ * is not shown as an empty list: the list returns to page 1 and refetches
11069
+ * once (AB#8801). The refetch runs on a microtask so the list binding's
11070
+ * current subscription has ended first; skip 0 cannot be out of range, so
11071
+ * this cannot loop. Any other error keeps today's behaviour (logged, no rows).
11072
+ */
11073
+ handleFetchError(err, queryOptions, logMessage) {
11074
+ const skip = queryOptions.state.skip ?? 0;
11075
+ if (skip > 0 && this.isPageOutOfRangeError(err)) {
11076
+ console.warn(`octo-meshboard: table widget page offset ${skip} lies beyond the result; returning to page 1`);
11077
+ queueMicrotask(() => this.fetchAgain({ resetSkip: true }));
11078
+ return EMPTY;
11079
+ }
11080
+ console.error(logMessage, err);
11081
+ return of(new FetchResultTyped([], 0));
11082
+ }
11083
+ /**
11084
+ * Signature of every query input outside the grid state (paging, sort, grid
11085
+ * filter): the data source, the widget filters with MeshBoard variables
11086
+ * resolved, the time filter selection and the entity-selector scope. The time
11087
+ * filter enters as its selection, not as resolved instants, so a relative
11088
+ * range ("last 24 hours") does not look changed on every auto-refresh. The
11089
+ * board's time-zone mode enters with an active selection (no host period),
11090
+ * because the selection's window is resolved in that zone.
11091
+ */
11092
+ queryInputsSignature() {
11093
+ const config = this._config;
11094
+ if (!config) {
11095
+ return 'none';
11096
+ }
11097
+ const dataSource = config.dataSource;
11098
+ const filters = this.convertFiltersToDto(config.filters) ?? null;
11099
+ if (dataSource.type !== 'persistentQuery') {
11100
+ return JSON.stringify({ dataSource, filters });
11101
+ }
11102
+ const timeFilter = dataSource.ignoreTimeFilter ? null : (this.stateService.getConfig().timeFilter ?? null);
11103
+ const host = dataSource.ignoreTimeFilter ? null : this.stateService.hostTimeRange();
11104
+ // Host periods are absolute instants (no zone conversion), so the zone only
11105
+ // matters for the board's own selection.
11106
+ const timeZone = timeFilter?.enabled && timeFilter.selection && !host ? this.stateService.timeZoneMode() : null;
11107
+ const scope = this.stateService.resolveStreamDataScope(dataSource.entitySelectorId, dataSource.queryFamily, dataSource.ignoreTimeFilter);
11108
+ return JSON.stringify({ dataSource, filters, timeFilter, timeZone, host, scope });
11025
11109
  }
11026
11110
  /**
11027
11111
  * Recursively flattens attributes into the record using dot notation for nested attributes.
@@ -11103,7 +11187,11 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
11103
11187
  fieldFilter: fieldFilter ?? undefined,
11104
11188
  streamDataArgs,
11105
11189
  // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
11106
- scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
11190
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined,
11191
+ // A refetch (board refresh, changed inputs, page-1 recovery) bypasses the
11192
+ // Apollo cache like the runtime-entity path does, so a cached page with a
11193
+ // stale total count is never shown again (AB#8801).
11194
+ forceRefresh: queryOptions.forceRefresh ?? false
11107
11195
  }).pipe(map$1(result => {
11108
11196
  // Extract columns from query response and update signal.
11109
11197
  // Replace dots with underscores for grid compatibility (Kendo treats dots as nested paths).
@@ -11120,10 +11208,7 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
11120
11208
  .filter(row => TableWidgetDataSourceDirective.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
11121
11209
  .map((row, index) => this.queryRowToRecord(row, columns, hasRtIdColumn, hasCkTypeIdColumn, index));
11122
11210
  return new FetchResultTyped(data, result.totalCount);
11123
- }), catchError$1(err => {
11124
- console.error('Error fetching query data:', err);
11125
- return of(new FetchResultTyped([], 0));
11126
- }));
11211
+ }), catchError$1(err => this.handleFetchError(err, queryOptions, 'Error fetching query data:')));
11127
11212
  }
11128
11213
  /**
11129
11214
  * Flattens a unified `QueryResultRow` into a Kendo-grid-friendly record.
@@ -12019,7 +12104,8 @@ class TableWidgetComponent {
12019
12104
  if (this.dataSource) {
12020
12105
  // Clear query columns in directive before fetching new data
12021
12106
  this.dataSource.clearQueryColumns();
12022
- this.dataSource.fetchAgain();
12107
+ // Returns to page 1 when a variable, filter, time filter or scope changed (AB#8801).
12108
+ this.dataSource.refetch();
12023
12109
  }
12024
12110
  }
12025
12111
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: TableWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });