@theseam/ui-common 1.0.0-beta.9 → 1.0.1-beta.7

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.
Files changed (152) hide show
  1. package/ai/package.json +3 -0
  2. package/asset-reader/package.json +3 -0
  3. package/breadcrumbs/package.json +3 -0
  4. package/buttons/index.d.ts +4 -1
  5. package/buttons/package.json +3 -0
  6. package/card/package.json +3 -0
  7. package/carousel/package.json +3 -0
  8. package/checkbox/package.json +3 -0
  9. package/confirm-dialog/package.json +3 -0
  10. package/core/package.json +3 -0
  11. package/data-exporter/package.json +3 -0
  12. package/data-filters/package.json +3 -0
  13. package/datatable/index.d.ts +44 -24
  14. package/datatable/package.json +3 -0
  15. package/datatable-alterations-display/package.json +3 -0
  16. package/datatable-dynamic/package.json +3 -0
  17. package/dynamic/package.json +3 -0
  18. package/dynamic-component-loader/package.json +3 -0
  19. package/fesm2022/theseam-ui-common-asset-reader.mjs +10 -10
  20. package/fesm2022/theseam-ui-common-asset-reader.mjs.map +1 -1
  21. package/fesm2022/theseam-ui-common-breadcrumbs.mjs +6 -6
  22. package/fesm2022/theseam-ui-common-breadcrumbs.mjs.map +1 -1
  23. package/fesm2022/theseam-ui-common-buttons.mjs +35 -28
  24. package/fesm2022/theseam-ui-common-buttons.mjs.map +1 -1
  25. package/fesm2022/theseam-ui-common-card.mjs +16 -16
  26. package/fesm2022/theseam-ui-common-card.mjs.map +1 -1
  27. package/fesm2022/theseam-ui-common-carousel.mjs +10 -10
  28. package/fesm2022/theseam-ui-common-carousel.mjs.map +1 -1
  29. package/fesm2022/theseam-ui-common-checkbox.mjs +7 -7
  30. package/fesm2022/theseam-ui-common-checkbox.mjs.map +1 -1
  31. package/fesm2022/theseam-ui-common-confirm-dialog.mjs +13 -13
  32. package/fesm2022/theseam-ui-common-confirm-dialog.mjs.map +1 -1
  33. package/fesm2022/theseam-ui-common-data-exporter.mjs +31 -14
  34. package/fesm2022/theseam-ui-common-data-exporter.mjs.map +1 -1
  35. package/fesm2022/theseam-ui-common-data-filters.mjs +13 -13
  36. package/fesm2022/theseam-ui-common-data-filters.mjs.map +1 -1
  37. package/fesm2022/theseam-ui-common-datatable-alterations-display.mjs +12 -12
  38. package/fesm2022/theseam-ui-common-datatable-alterations-display.mjs.map +1 -1
  39. package/fesm2022/theseam-ui-common-datatable-dynamic.mjs +25 -25
  40. package/fesm2022/theseam-ui-common-datatable-dynamic.mjs.map +1 -1
  41. package/fesm2022/theseam-ui-common-datatable.mjs +177 -145
  42. package/fesm2022/theseam-ui-common-datatable.mjs.map +1 -1
  43. package/fesm2022/theseam-ui-common-dynamic-component-loader.mjs +7 -7
  44. package/fesm2022/theseam-ui-common-dynamic-component-loader.mjs.map +1 -1
  45. package/fesm2022/theseam-ui-common-dynamic.mjs +21 -21
  46. package/fesm2022/theseam-ui-common-dynamic.mjs.map +1 -1
  47. package/fesm2022/theseam-ui-common-footer-bar.mjs +7 -7
  48. package/fesm2022/theseam-ui-common-footer-bar.mjs.map +1 -1
  49. package/fesm2022/theseam-ui-common-form-field-error.mjs +16 -16
  50. package/fesm2022/theseam-ui-common-form-field-error.mjs.map +1 -1
  51. package/fesm2022/theseam-ui-common-form-field.mjs +22 -22
  52. package/fesm2022/theseam-ui-common-form-field.mjs.map +1 -1
  53. package/fesm2022/theseam-ui-common-framework.mjs +160 -158
  54. package/fesm2022/theseam-ui-common-framework.mjs.map +1 -1
  55. package/fesm2022/theseam-ui-common-google-maps.mjs +40 -40
  56. package/fesm2022/theseam-ui-common-google-maps.mjs.map +1 -1
  57. package/fesm2022/theseam-ui-common-graphql.mjs +852 -479
  58. package/fesm2022/theseam-ui-common-graphql.mjs.map +1 -1
  59. package/fesm2022/theseam-ui-common-icon.mjs +13 -13
  60. package/fesm2022/theseam-ui-common-icon.mjs.map +1 -1
  61. package/fesm2022/theseam-ui-common-layout.mjs +7 -7
  62. package/fesm2022/theseam-ui-common-layout.mjs.map +1 -1
  63. package/fesm2022/theseam-ui-common-loading.mjs +10 -10
  64. package/fesm2022/theseam-ui-common-loading.mjs.map +1 -1
  65. package/fesm2022/theseam-ui-common-menu.mjs +25 -25
  66. package/fesm2022/theseam-ui-common-menu.mjs.map +1 -1
  67. package/fesm2022/theseam-ui-common-modal.mjs +43 -43
  68. package/fesm2022/theseam-ui-common-modal.mjs.map +1 -1
  69. package/fesm2022/theseam-ui-common-navigation-reload.mjs +3 -3
  70. package/fesm2022/theseam-ui-common-navigation-reload.mjs.map +1 -1
  71. package/fesm2022/theseam-ui-common-popover.mjs +10 -10
  72. package/fesm2022/theseam-ui-common-popover.mjs.map +1 -1
  73. package/fesm2022/theseam-ui-common-progress.mjs +7 -7
  74. package/fesm2022/theseam-ui-common-progress.mjs.map +1 -1
  75. package/fesm2022/theseam-ui-common-rich-text.mjs +7 -7
  76. package/fesm2022/theseam-ui-common-rich-text.mjs.map +1 -1
  77. package/fesm2022/theseam-ui-common-scrollbar.mjs +6 -6
  78. package/fesm2022/theseam-ui-common-scrollbar.mjs.map +1 -1
  79. package/fesm2022/theseam-ui-common-services.mjs +12 -12
  80. package/fesm2022/theseam-ui-common-services.mjs.map +1 -1
  81. package/fesm2022/theseam-ui-common-shared.mjs +37 -37
  82. package/fesm2022/theseam-ui-common-shared.mjs.map +1 -1
  83. package/fesm2022/theseam-ui-common-storage.mjs +3 -3
  84. package/fesm2022/theseam-ui-common-storage.mjs.map +1 -1
  85. package/fesm2022/theseam-ui-common-story-helpers.mjs +26 -26
  86. package/fesm2022/theseam-ui-common-story-helpers.mjs.map +1 -1
  87. package/fesm2022/theseam-ui-common-tabbed.mjs +22 -22
  88. package/fesm2022/theseam-ui-common-tabbed.mjs.map +1 -1
  89. package/fesm2022/theseam-ui-common-table-cell-type.mjs +10 -10
  90. package/fesm2022/theseam-ui-common-table-cell-type.mjs.map +1 -1
  91. package/fesm2022/theseam-ui-common-table-cell-types.mjs +31 -31
  92. package/fesm2022/theseam-ui-common-table-cell-types.mjs.map +1 -1
  93. package/fesm2022/theseam-ui-common-table.mjs +20 -28
  94. package/fesm2022/theseam-ui-common-table.mjs.map +1 -1
  95. package/fesm2022/theseam-ui-common-tel-input.mjs +13 -13
  96. package/fesm2022/theseam-ui-common-tel-input.mjs.map +1 -1
  97. package/fesm2022/theseam-ui-common-tiled-select.mjs +22 -22
  98. package/fesm2022/theseam-ui-common-tiled-select.mjs.map +1 -1
  99. package/fesm2022/theseam-ui-common-toggle-edit.mjs +16 -16
  100. package/fesm2022/theseam-ui-common-toggle-edit.mjs.map +1 -1
  101. package/fesm2022/theseam-ui-common-toggle-group.mjs +10 -10
  102. package/fesm2022/theseam-ui-common-toggle-group.mjs.map +1 -1
  103. package/fesm2022/theseam-ui-common-tooltip.mjs +10 -10
  104. package/fesm2022/theseam-ui-common-tooltip.mjs.map +1 -1
  105. package/fesm2022/theseam-ui-common-unsaved-changes-dialog.mjs +10 -10
  106. package/fesm2022/theseam-ui-common-unsaved-changes-dialog.mjs.map +1 -1
  107. package/fesm2022/theseam-ui-common-vertical-list-filter.mjs +3 -3
  108. package/fesm2022/theseam-ui-common-vertical-list-filter.mjs.map +1 -1
  109. package/fesm2022/theseam-ui-common-viewers.mjs +12 -12
  110. package/fesm2022/theseam-ui-common-viewers.mjs.map +1 -1
  111. package/fesm2022/theseam-ui-common-widget.mjs +135 -135
  112. package/fesm2022/theseam-ui-common-widget.mjs.map +1 -1
  113. package/footer-bar/package.json +3 -0
  114. package/form-field/package.json +3 -0
  115. package/form-field-error/package.json +3 -0
  116. package/framework/package.json +3 -0
  117. package/google-maps/package.json +3 -0
  118. package/graphql/index.d.ts +265 -54
  119. package/graphql/package.json +3 -0
  120. package/icon/package.json +3 -0
  121. package/layout/package.json +3 -0
  122. package/loading/package.json +3 -0
  123. package/menu/package.json +3 -0
  124. package/modal/package.json +3 -0
  125. package/models/package.json +3 -0
  126. package/navigation-reload/package.json +3 -0
  127. package/package.json +55 -55
  128. package/popover/package.json +3 -0
  129. package/progress/package.json +3 -0
  130. package/rich-text/package.json +3 -0
  131. package/scrollbar/package.json +3 -0
  132. package/services/package.json +3 -0
  133. package/shared/package.json +3 -0
  134. package/storage/package.json +3 -0
  135. package/story-helpers/package.json +3 -0
  136. package/tabbed/package.json +3 -0
  137. package/table/index.d.ts +1 -5
  138. package/table/package.json +3 -0
  139. package/table-cell-type/package.json +3 -0
  140. package/table-cell-types/package.json +3 -0
  141. package/tel-input/package.json +3 -0
  142. package/testing/package.json +3 -0
  143. package/tiled-select/package.json +3 -0
  144. package/toggle-edit/package.json +3 -0
  145. package/toggle-group/package.json +3 -0
  146. package/tooltip/package.json +3 -0
  147. package/unsaved-changes-dialog/package.json +3 -0
  148. package/utils/package.json +3 -0
  149. package/validators/package.json +3 -0
  150. package/vertical-list-filter/package.json +3 -0
  151. package/viewers/package.json +3 -0
  152. package/widget/package.json +3 -0
@@ -1,16 +1,78 @@
1
+ import { ApolloLink, NetworkStatus, Observable as Observable$1 } from '@apollo/client/core';
2
+ import { print, visit as visit$1, parse, BREAK as BREAK$1, parseValue, graphqlSync, buildSchema } from 'graphql';
3
+ import { hasProperty, notNullOrUndefined, isNullOrUndefined, withoutProperty, wrapIntoObservable, withoutProperties } from '@theseam/ui-common/utils';
1
4
  import { visit, BREAK } from 'graphql/language';
2
5
  import * as i0 from '@angular/core';
3
6
  import { isDevMode, InjectionToken, Optional, Inject, Injectable, EventEmitter } from '@angular/core';
4
- import { hasProperty, notNullOrUndefined, isNullOrUndefined, wrapIntoObservable, subscriberCount, withoutProperty } from '@theseam/ui-common/utils';
5
- import { visit as visit$1, parse, BREAK as BREAK$1, graphqlSync, print, parseValue, buildSchema } from 'graphql';
6
- import { Observable, EMPTY, from, of, ReplaySubject, combineLatest, BehaviorSubject, defer, Subject, isObservable } from 'rxjs';
7
- import { switchMap, tap, take, concatMap, filter, toArray, map, shareReplay, startWith, distinctUntilChanged, skip, auditTime, finalize } from 'rxjs/operators';
8
- import { NetworkStatus, ApolloLink, Observable as Observable$1 } from '@apollo/client/core';
7
+ import { Observable, EMPTY, merge, from, defer, of, combineLatest, Subscription, Subject, BehaviorSubject, isObservable } from 'rxjs';
8
+ import { switchMap, auditTime, map, tap, take, concatMap, filter, toArray, distinctUntilChanged, finalize, catchError, shareReplay, startWith, skip } from 'rxjs/operators';
9
9
  import * as i1 from 'apollo-angular';
10
- import { gql, APOLLO_OPTIONS } from 'apollo-angular';
10
+ import { gql, provideApollo } from 'apollo-angular';
11
11
  import { THESEAM_COLUMNS_DATA_FILTER_DATE_TEXT_SEARCH_TYPES, getFormattedDateForComparison, THESEAM_COLUMNS_DATA_FILTER_DATE_RANGE_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_NUMERIC_TEXT_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_NUMERIC_RANGE_SEARCH_TYPES, THESEAM_COLUMNS_DATA_FILTER_TEXT_TEXT_SEARCH_TYPES } from '@theseam/ui-common/datatable';
12
12
  import { concat, InMemoryCache } from '@apollo/client';
13
13
 
14
+ /**
15
+ * Wraps an Apollo link and logs the operation state immediately before and
16
+ * after the wrapped link processes it.
17
+ *
18
+ * Useful for inspecting what `queryProcessingLink` does to a query:
19
+ *
20
+ * ```ts
21
+ * link: concat(
22
+ * logQueryLink(queryProcessingLink),
23
+ * httpLink,
24
+ * )
25
+ * ```
26
+ *
27
+ * The output uses `console.log` with optional CSS styles so each snapshot
28
+ * is easy to distinguish in the browser console. Tree-shaking will remove
29
+ * this from production builds when it is not imported.
30
+ */
31
+ function logQueryLink(inner, options) {
32
+ const beforeStyles = options?.beforeStyles ?? 'color: cyan';
33
+ const afterStyles = options?.afterStyles ?? 'color: limegreen';
34
+ const beforeLink = new ApolloLink((operation, forward) => {
35
+ // eslint-disable-next-line no-console
36
+ console.log(`%c~~~BEFORE\n${print(operation.query)}\n${JSON.stringify(operation.variables, null, 2)}`, beforeStyles);
37
+ return forward(operation);
38
+ });
39
+ const afterLink = new ApolloLink((operation, forward) => {
40
+ // eslint-disable-next-line no-console
41
+ console.log(`%c~~~AFTER\n${print(operation.query)}\n${JSON.stringify(operation.variables, null, 2)}`, afterStyles);
42
+ return forward(operation);
43
+ });
44
+ return ApolloLink.from([beforeLink, inner, afterLink]);
45
+ }
46
+
47
+ class GQLDirection {
48
+ direction;
49
+ constructor(direction) {
50
+ this.direction = direction;
51
+ }
52
+ static ASC = new GQLDirection('ASC');
53
+ static DESC = new GQLDirection('DESC');
54
+ }
55
+
56
+ var HintsKind;
57
+ (function (HintsKind) {
58
+ HintsKind["OperationDefinition"] = "OperationDefinition";
59
+ HintsKind["Field"] = "Field";
60
+ HintsKind["VariableDefinition"] = "VariableDefinition";
61
+ HintsKind["Variable"] = "Variable";
62
+ HintsKind["Argument"] = "Argument";
63
+ })(HintsKind || (HintsKind = {}));
64
+
65
+ const DEFAULT_TO_REMOVE_ON_UNDEFINED = ['where', 'order'];
66
+
67
+ class GQLVariable {
68
+ name;
69
+ type;
70
+ constructor(name, type) {
71
+ this.name = name;
72
+ this.type = type;
73
+ }
74
+ }
75
+
14
76
  function containsVariable(node, variableName) {
15
77
  let found = false;
16
78
  visit(node, {
@@ -101,35 +163,6 @@ function parseComments(ast) {
101
163
  return comments;
102
164
  }
103
165
 
104
- class GQLDirection {
105
- direction;
106
- constructor(direction) {
107
- this.direction = direction;
108
- }
109
- static ASC = new GQLDirection('ASC');
110
- static DESC = new GQLDirection('DESC');
111
- }
112
-
113
- var HintsKind;
114
- (function (HintsKind) {
115
- HintsKind["OperationDefinition"] = "OperationDefinition";
116
- HintsKind["Field"] = "Field";
117
- HintsKind["VariableDefinition"] = "VariableDefinition";
118
- HintsKind["Variable"] = "Variable";
119
- HintsKind["Argument"] = "Argument";
120
- })(HintsKind || (HintsKind = {}));
121
-
122
- const DEFAULT_TO_REMOVE_ON_UNDEFINED = ['where', 'order'];
123
-
124
- class GQLVariable {
125
- name;
126
- type;
127
- constructor(name, type) {
128
- this.name = name;
129
- this.type = type;
130
- }
131
- }
132
-
133
166
  const HINT_PREFIX_REGEX = /^\s*@gql-hint:.+/;
134
167
  const HINT_NAMES_CAPTURE_REGEX = /^\s*@gql-hint:([a-zA-z-\d\s]+)$/;
135
168
  /**
@@ -294,6 +327,23 @@ function removeVariable(query, variableName) {
294
327
  // type-safe one, but that is becoming surprisingly harder to find than I
295
328
  // expected for GraphQL.
296
329
  function toGQL(json) {
330
+ // Handle primitive top-level values so callers can pass non-objects (e.g.
331
+ // when inlining a String or Int variable directly into a query argument).
332
+ if (json === null || json === undefined) {
333
+ return 'null';
334
+ }
335
+ if (typeof json === 'string') {
336
+ return `"${json}"`;
337
+ }
338
+ if (typeof json === 'number' || typeof json === 'boolean') {
339
+ return `${json}`;
340
+ }
341
+ if (json instanceof GQLDirection) {
342
+ return `${json.direction}`;
343
+ }
344
+ if (Array.isArray(json)) {
345
+ return `[${json.map((v) => toGQL(v)).join(',')}]`;
346
+ }
297
347
  const props = Object.keys(json).map((prop) => {
298
348
  const value = json[prop];
299
349
  let resultValue;
@@ -329,6 +379,215 @@ function hintsTokensContainingHint(hintsTokens, hint) {
329
379
  return hintsTokens.filter((r) => r.hints.indexOf(hint) !== -1);
330
380
  }
331
381
 
382
+ /**
383
+ * Inlines the variable's current value directly into the query AST and removes
384
+ * the variable definition from the parameter list. The variable is also
385
+ * removed from the operation's variables map so it is not sent to the server.
386
+ *
387
+ * Applies to VariableDefinition (comment above the `$var` in the parameter
388
+ * list) or Variable (inline comment beside a `$var` usage in the query body).
389
+ */
390
+ const inlineVariableTransformer = (operation, hintsToken) => {
391
+ let varName = null;
392
+ if (hintsToken.kind === HintsKind.VariableDefinition) {
393
+ varName = hintsToken.node.variable.name.value;
394
+ }
395
+ else if (hintsToken.kind === HintsKind.Variable) {
396
+ varName = hintsToken.node.name.value;
397
+ }
398
+ if (varName === null) {
399
+ return operation;
400
+ }
401
+ const varValue = operation.variables[varName];
402
+ const newVariables = withoutProperty(operation.variables, varName);
403
+ let query = removeVariableDefinition(operation.query, varName);
404
+ query = inlineVariable(query, varName, parseValue(toGQL(varValue)));
405
+ return { query, variables: newVariables };
406
+ };
407
+
408
+ /**
409
+ * Removes every variable from the query that is null or undefined in the
410
+ * operation's variables map. Both the variable definition (from the parameter
411
+ * list) and any argument usages in the query body are removed so the resulting
412
+ * document remains valid.
413
+ */
414
+ const removeNotDefinedTransformer = (operation, hintsToken) => {
415
+ const operationNode = hintsToken.node;
416
+ const undefinedVarNames = (operationNode.variableDefinitions ?? [])
417
+ .filter((varDef) => isNullOrUndefined(operation.variables[varDef.variable.name.value]))
418
+ .map((varDef) => varDef.variable.name.value);
419
+ let query = operation.query;
420
+ for (const varName of undefinedVarNames) {
421
+ query = removeVariable(query, varName);
422
+ }
423
+ return { query, variables: operation.variables };
424
+ };
425
+
426
+ /**
427
+ * Maps variable value to gql and inlines it into the query.
428
+ */
429
+ const inlineVariableHintDef = {
430
+ name: 'inline-variable',
431
+ appliesTo: [HintsKind.Variable, HintsKind.VariableDefinition],
432
+ transformer: inlineVariableTransformer,
433
+ };
434
+
435
+ /**
436
+ * Remove the variable from the query if it is not defined in the operation
437
+ * variables.
438
+ */
439
+ const removeNotDefinedHintDef = {
440
+ name: 'remove-not-defined',
441
+ appliesTo: [HintsKind.OperationDefinition],
442
+ transformer: removeNotDefinedTransformer,
443
+ };
444
+
445
+ // /**
446
+ // * Remove the variable from the query if it is not defined in the operation
447
+ // * variables.
448
+ // *
449
+ // * Applies to: OperationDefinition
450
+ // */
451
+ // export const GQL_HINT_REMOVE_NOT_DEFINED = 'remove-not-defined'
452
+ // /**
453
+ // * Maps variable value to gql and inlines it into the query.
454
+ // *
455
+ // * Applies to: Variable, VariableDefinition
456
+ // */
457
+ // export const GQL_HINT_INLINE_VARIABLE = 'inline-variable'
458
+ // /**
459
+ // * Remove variable definition from the query if it is not used by the operation.
460
+ // *
461
+ // * Applies to: VariableDefinition
462
+ // */
463
+ // // export const GQL_HINT_REMOVE_IF_NOT_USED = 'remove-if-not-used'
464
+ // export const GQL_HINTS = [
465
+ // // GQL_HINT_REMOVE_IF_NOT_USED,
466
+ // GQL_HINT_REMOVE_NOT_DEFINED,
467
+ // GQL_HINT_INLINE_VARIABLE
468
+ // ]
469
+
470
+ /**
471
+ * Apollo link that transforms GraphQL operations before they are sent.
472
+ *
473
+ * Two mechanisms are supported and can be combined freely:
474
+ *
475
+ * **Hint-based** — place `# @gql-hint: <name>` comments directly in the query.
476
+ * Supported hints:
477
+ * - `remove-not-defined` on the operation definition: removes every variable
478
+ * whose value is null/undefined (definition + argument usage).
479
+ * - `inline-variable` on a variable definition or usage: substitutes the
480
+ * variable's current value directly into the query AST and removes it from
481
+ * the variables map.
482
+ *
483
+ * **Config-based** — pass a `QueryProcessingConfig` via Apollo context under
484
+ * the key `queryProcessingConfig`. Supported options:
485
+ * - `variables.removeIfNotDefined`: remove named variables when null/undefined.
486
+ * - `variables.removeIfNotUsed`: remove named variable definitions when the
487
+ * variable is not referenced anywhere in the (possibly already-transformed)
488
+ * query body.
489
+ * - `variables.inline`: inline named variables into the query AST.
490
+ *
491
+ * Hints are applied first, then config-based processing.
492
+ */
493
+ const queryProcessingLink = new ApolloLink((operation, forward) => {
494
+ const context = operation.getContext();
495
+ const queryProcessingConfig = context.queryProcessingConfig ?? {};
496
+ // Reparse to ensure token/comment info is present for hint parsing.
497
+ let _ast = parseAst(operation.query);
498
+ const rules = parseHints(_ast);
499
+ // ---- Hint: remove-not-defined ----------------------------------------
500
+ for (const hintsToken of hintsTokensContainingHint(rules, removeNotDefinedHintDef.name)) {
501
+ const result = removeNotDefinedHintDef.transformer({ query: _ast, variables: operation.variables }, hintsToken);
502
+ _ast = result.query;
503
+ operation.variables = result.variables;
504
+ }
505
+ // ---- Hint: inline-variable --------------------------------------------
506
+ for (const hintsToken of hintsTokensContainingHint(rules, inlineVariableHintDef.name)) {
507
+ const result = inlineVariableHintDef.transformer({ query: _ast, variables: operation.variables }, hintsToken);
508
+ _ast = result.query;
509
+ operation.variables = result.variables;
510
+ }
511
+ // ---- Config: removeIfNotDefined ---------------------------------------
512
+ for (const varName of queryProcessingConfig?.variables?.removeIfNotDefined ??
513
+ []) {
514
+ if (isNullOrUndefined(operation.variables[varName])) {
515
+ _ast = removeVariable(_ast, varName);
516
+ }
517
+ }
518
+ // ---- Config: removeIfNotUsed -----------------------------------------
519
+ // Intentionally runs after removeIfNotDefined so that variables which were
520
+ // only referenced inside another (now-removed) variable can be cleaned up.
521
+ for (const varName of queryProcessingConfig?.variables?.removeIfNotUsed ??
522
+ []) {
523
+ if (!containsVariable(_ast, varName)) {
524
+ _ast = removeVariable(_ast, varName);
525
+ }
526
+ }
527
+ // ---- Config: inline --------------------------------------------------
528
+ for (const varName of queryProcessingConfig?.variables?.inline ?? []) {
529
+ const varValue = operation.variables[varName];
530
+ operation.variables = withoutProperty(operation.variables, varName);
531
+ _ast = removeVariableDefinition(_ast, varName);
532
+ _ast = inlineVariable(_ast, varName, parseValue(toGQL(varValue)));
533
+ }
534
+ operation.query = _ast;
535
+ return forward(operation);
536
+ });
537
+
538
+ /**
539
+ * Creates a {@link SortsMapper} from a declarative field-name map.
540
+ *
541
+ * Each key must correspond to a datatable column `prop` value. The value
542
+ * controls how that column's sort is translated to a GQL sort object:
543
+ *
544
+ * - `string` – emits `{ [gqlField]: 'ASC' | 'DESC' }`
545
+ * - `null` – column is not sortable; the sort item is dropped
546
+ * - `function` – called with `(prop, context)` and may return a field
547
+ * name or `null` to drop the item dynamically
548
+ *
549
+ * In dev mode an error is thrown when a sort item's `prop` is not present
550
+ * in the map. In production the item is silently dropped.
551
+ *
552
+ * @example
553
+ * // Simple static mapping
554
+ * const mapSorts = createSortsMapper<'id' | 'name'>({
555
+ * id: 'id',
556
+ * name: 'name',
557
+ * })
558
+ *
559
+ * @example
560
+ * // Dynamic mapping with context access
561
+ * const mapSorts = createSortsMapper<'id' | 'name' | 'computed'>({
562
+ * id: 'id',
563
+ * name: 'name',
564
+ * computed: (prop, context) =>
565
+ * context.extraVariables.useAlt ? 'altField' : prop,
566
+ * })
567
+ */
568
+ function createSortsMapper(fieldMap) {
569
+ return (sorts, context) => {
570
+ const result = [];
571
+ for (const s of sorts) {
572
+ const prop = s?.prop;
573
+ if (!(prop in fieldMap)) {
574
+ if (isDevMode()) {
575
+ throw new Error(`createSortsMapper: no mapping found for column prop "${prop}". ` +
576
+ `Add an entry to the field map or set the value to null to ignore it.`);
577
+ }
578
+ continue;
579
+ }
580
+ const entry = fieldMap[prop];
581
+ const dir = s?.dir?.toUpperCase();
582
+ const gqlField = typeof entry === 'function' ? entry(prop, context) : entry;
583
+ if (gqlField === null)
584
+ continue;
585
+ result.push({ [gqlField]: dir });
586
+ }
587
+ return result;
588
+ };
589
+ }
590
+
332
591
  const DEFAULT_PAGE_SIZE = 20;
333
592
  function getPageInfo(datatable, defaultPageSize = DEFAULT_PAGE_SIZE) {
334
593
  return {
@@ -357,7 +616,10 @@ function createPageInfoObservable(datatable$, defaultPageSize = 20) {
357
616
  return EMPTY;
358
617
  }
359
618
  handlePageInfo(getPageInfo(dt, defaultPageSize));
360
- return dt.page.pipe(tap((p) => handlePageInfo(p)));
619
+ // `page` does not emit when the page size changes (e.g. on
620
+ // window resize). Merging the `resize` event is a workaround.
621
+ const resize$ = dt.resize.pipe(auditTime(100), map(() => dt.pageInfo));
622
+ return merge(dt.page, resize$).pipe(tap((p) => handlePageInfo(p)));
361
623
  }))
362
624
  .subscribe();
363
625
  return () => {
@@ -387,8 +649,12 @@ function resolveMapper(filterState, filterStateMappers, context) {
387
649
  function resolveMappers(filterStates, filterStateMappers, context) {
388
650
  return from(filterStates).pipe(concatMap((filterState) => resolveMapper(filterState, filterStateMappers, context)), filter(notNullOrUndefined), toArray());
389
651
  }
652
+ /**
653
+ * Combines multiple active filter results with AND so that all conditions must
654
+ * be satisfied simultaneously (e.g. a search filter AND a status filter).
655
+ */
390
656
  function mergeFilters(filters) {
391
- return { or: filters };
657
+ return { and: filters };
392
658
  }
393
659
  /**
394
660
  * Merges variable objects.
@@ -452,180 +718,123 @@ function mapPageInfo(pageInfo) {
452
718
  }
453
719
 
454
720
  function observeRowsWithGqlInputsHandling(queryRef, rows, datatable, extraVariables, sortsMapper, filterStateMappers) {
455
- return new Observable((subscriber) => {
456
- const datatable$ = wrapIntoObservable(datatable);
457
- const extraVariables$ = wrapIntoObservable(extraVariables);
458
- const context$ = extraVariables$.pipe(map((_extraVariables) => {
459
- const context = {
460
- extraVariables: _extraVariables,
461
- };
462
- return context;
463
- }), shareReplay({ bufferSize: 1, refCount: true }));
464
- const datatableMappers = {
465
- pageInfo: mapPageInfo,
466
- sorts: sortsMapper,
467
- filters: filterStateMappers,
468
- };
469
- const datatableResults$ = _createDatatableResultsObservable(datatable$, datatableMappers, context$);
470
- const queryVariablesChanged$ = datatableResults$.pipe(tap((results) => {
471
- queryRef.setVariables({
472
- ...(results.context || {}),
473
- ...results.pageInfo,
474
- ...(results.sorts.length > 0 ? { order: results.sorts } : {}),
475
- ...(results.filter?.variables || {}),
476
- ...(results.filter?.filter ? { where: results.filter.filter } : {}),
477
- });
478
- }));
479
- // const _emitSubject = new Subject<void>()
480
- // const queryVarsChangedSub = queryVariablesChanged$.pipe(
481
- // switchMap(() => subscriberCount(rows, 'rows'))
482
- // ).subscribe(subscriber)
483
- const queryVarsChangedSub = queryVariablesChanged$.subscribe();
484
- const _sub = subscriberCount(rows, 'rows').subscribe(subscriber);
485
- return () => {
486
- queryVarsChangedSub.unsubscribe();
487
- _sub.unsubscribe();
488
- };
721
+ const datatable$ = wrapIntoObservable(datatable);
722
+ const extraVariables$ = wrapIntoObservable(extraVariables);
723
+ // Only emit page changes past the first when paging is enabled. When paging
724
+ // is disabled, all data is already in the buffer so there is no need to
725
+ // re-query on page changes.
726
+ const pageInfo$ = defer(() => {
727
+ let firstEmit = true;
728
+ return createPageInfoObservable(datatable$).pipe(switchMap((pageInfo) => {
729
+ if (!firstEmit && _isPagingDisabled(queryRef)) {
730
+ return EMPTY;
731
+ }
732
+ firstEmit = false;
733
+ return of(pageInfo);
734
+ }), map(mapPageInfo));
489
735
  });
490
- // const context$ = extraVariables$.pipe(
491
- // map(_extraVariables => {
492
- // const context: MapperContext = {
493
- // extraVariables: _extraVariables
494
- // }
495
- // return context
496
- // })
497
- // )
498
- // const datatableMappers: DatatableMappers = {
499
- // pageInfo: mapPageInfo,
500
- // sorts: sortsMapper,
501
- // filters: filterStateMappers,
502
- // }
503
- // const datatableResults$ = _createDatatableResultsObservable(
504
- // datatable$,
505
- // datatableMappers,
506
- // context$,
507
- // )
508
- // const queryVariablesChanged$ = datatableResults$.pipe(
509
- // tap(results => {
510
- // queryRef.setVariables({
511
- // ...(results.extraVariables || {}),
512
- // ...results.pageInfo,
513
- // ...(results.sorts.length > 0 ? { order: results.sorts } : {}),
514
- // ...(results.filterInfo?.variables || {}),
515
- // ...(results.filterInfo?.filter ? { where: results.filterInfo.filter } : {})
516
- // } as any)
517
- // })
518
- // )
519
- // const handleQueryInputs = combineLatest([ extraVariables$, pageInfo$ ]).pipe(
520
- // switchMap(([ _extraVariables, pageInfo ]) => {
521
- // // console.log('_extraVariables, pageInfo', _extraVariables, pageInfo)
522
- // const context: MapperContext = {
523
- // extraVariables: _extraVariables
524
- // }
525
- // return combineLatest([ sorts$, filterInfo$ ]).pipe(
526
- // // map(([ sorts, filterInfo ]) => ({ extraVariables: _extraVariables, pageInfo, sorts, filterInfo }))
527
- // map(([ sorts, filterInfo ]) => {
528
- // // console.log('sorts, filterInfo', sorts, filterInfo)
529
- // return { extraVariables: _extraVariables, pageInfo, sorts, filterInfo }
530
- // })
531
- // )
532
- // }),
533
- // tap(v => {
534
- // // console.log('~~~', v)
535
- // queryRef.setVariables({
536
- // ...(v.extraVariables || {}),
537
- // ...v.pageInfo,
538
- // ...(v.sorts.length > 0 ? { order: v.sorts } : {}),
539
- // ...(v.filterInfo?.variables || {}),
540
- // ...(v.filterInfo?.filter ? { where: v.filterInfo.filter } : {})
541
- // } as any)
542
- // })
543
- // )
544
- // return defer(() => {
545
- // const _emitted = new Subject<void>()
546
- // const handlerSub = handleQueryInputs.pipe(
547
- // // skip(1)
548
- // // ).subscribe(() => _emitted.next())
549
- // ).subscribe(() => {
550
- // _emitted.next()
551
- // })
552
- // return _emitted.pipe(
553
- // // tap(v => {
554
- // // console.log('emitted', v)
555
- // // }),
556
- // distinctUntilChanged(),
557
- // switchMap(() => subscriberCount(rows, 'rows')),
558
- // // tap(v => {
559
- // // console.log('emitting rows', v)
560
- // // }),
561
- // finalize(() => handlerSub.unsubscribe())
562
- // )
563
- // }).pipe(
564
- // // tap(v => {
565
- // // console.log('rows', v)
566
- // // }),
567
- // catchError(err => {
568
- // console.error(err)
569
- // return of([] as TRow[])
570
- // }),
571
- // shareReplay({ bufferSize: 1, refCount: true })
572
- // )
736
+ // Combines extraVariables and pageInfo, then derives sorts and filter state.
737
+ // auditTime(0) debounces rapid synchronous emissions (e.g. at startup) so
738
+ // only one setVariables call is made per event-loop turn.
739
+ const handleQueryInputs = combineLatest([extraVariables$, pageInfo$]).pipe(auditTime(0), switchMap(([_extraVariables, pageInfo]) => {
740
+ const context = { extraVariables: _extraVariables };
741
+ const sorts$ = _createSortsObservable(datatable$).pipe(switchMap((m) => wrapIntoObservable(sortsMapper(m, context))));
742
+ const filterInfo$ = _createFilterStatesObservable(datatable$).pipe(switchMap((x) => mapFilterStates(x, filterStateMappers, context)),
743
+ // TODO: Remove when the datatable fixes the bug causing it to emit more than it should.
744
+ distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)));
745
+ return combineLatest([sorts$, filterInfo$]).pipe(map(([sorts, filterInfo]) => ({
746
+ extraVariables: _extraVariables,
747
+ pageInfo,
748
+ sorts,
749
+ filterInfo,
750
+ })));
751
+ }), tap((v) => {
752
+ queryRef.setVariables({
753
+ ...(v.extraVariables || {}),
754
+ ...v.pageInfo,
755
+ ...(v.sorts.length > 0 ? { order: v.sorts } : {}),
756
+ ...(v.filterInfo?.variables || {}),
757
+ ...(v.filterInfo?.filter ? { where: v.filterInfo.filter } : {}),
758
+ });
759
+ }));
760
+ return defer(() => {
761
+ // Observe the optional refresh-button patch attached externally to the
762
+ // datatable instance. When the user triggers a refresh, refetch the data.
763
+ let refreshBtnSub = Subscription.EMPTY;
764
+ refreshBtnSub = datatable$
765
+ .pipe(switchMap((dt) => {
766
+ if (!dt || !dt.__refreshPatch) {
767
+ return EMPTY;
768
+ }
769
+ return dt.__refreshPatch.refreshTriggered.pipe(tap(() => queryRef.refetch(undefined, true)));
770
+ }))
771
+ .subscribe();
772
+ // Bridge query-input changes to row emissions via a Subject so that the
773
+ // rows observable is only subscribed once (on the first input change) and
774
+ // then continues to receive updates as the live query produces new data.
775
+ const _emitted = new Subject();
776
+ const handlerSub = handleQueryInputs.subscribe(() => _emitted.next(true));
777
+ return _emitted.pipe(distinctUntilChanged(), switchMap(() => rows), finalize(() => {
778
+ handlerSub.unsubscribe();
779
+ refreshBtnSub.unsubscribe();
780
+ }));
781
+ }).pipe(catchError((err) => {
782
+ // eslint-disable-next-line no-console
783
+ console.error(err);
784
+ return of([]);
785
+ }), shareReplay({ bufferSize: 1, refCount: true }));
573
786
  }
574
787
  function _createSortsObservable(datatable$) {
575
- return datatable$.pipe(
576
- // tap(v => console.log('sorts got dt', v)),
577
- switchMap((dt) => dt
578
- ? dt.sort.pipe(map((v) => v.sorts), startWith(dt.sorts)) // .pipe(tap(v => console.log('sorts 1', v)))
579
- : of([])), shareReplay({ bufferSize: 1, refCount: true }));
788
+ // NOTE: There is a bug in our datatable wrapper that isn't propagating
789
+ // external sorting changes to the wrapped datatable component, which we observe
790
+ // sort events from. This workaround observes our wrapper's internal column
791
+ // change events, which emit all changes to columns that our datatable tracks,
792
+ // and reads sorts from our wrapper when externalSorting is enabled.
793
+ const _observeSortsWorkaround = (dt) => {
794
+ if (!dt._columnsAlterationsManager) {
795
+ // Fallback for environments (e.g. tests) where the internal manager
796
+ // is not present.
797
+ return dt.sort.pipe(map((v) => v.sorts), startWith(dt.sorts));
798
+ }
799
+ return dt._columnsAlterationsManager.changes.pipe(map(() => (dt.externalSorting ? dt._sorts : dt.sorts)), startWith(dt.externalSorting ? dt._sorts : dt.sorts));
800
+ };
801
+ return datatable$.pipe(switchMap((dt) => (dt ? _observeSortsWorkaround(dt) : of([]))), shareReplay({ bufferSize: 1, refCount: true }));
580
802
  }
581
803
  function _createFilterStatesObservable(datatable$) {
582
- return datatable$.pipe(
583
- // tap(v => console.log('filters got dt', v)),
584
- switchMap((dt) => dt
585
- ? dt.filterStates // .pipe(tap(v => console.log('filterStates 1', v)))
586
- : of([])),
587
- // TODO: Remove when the datatable fixes the bug causing it to emit more than it should.
588
- distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)));
804
+ return datatable$.pipe(switchMap((dt) => (dt ? dt.filterStates : of([]))));
589
805
  }
590
- function _createDatatableResultsObservable(datatable$, mappers, context$) {
591
- return new Observable((subscriber) => {
592
- const datatableSubject = new ReplaySubject();
593
- const dtSub = datatable$.subscribe((dt) => datatableSubject.next(dt), (err) => datatableSubject.error(err), () => datatableSubject.complete());
594
- const ctxSub = context$
595
- .pipe(switchMap((context) => {
596
- // TODO: Decide if the disabled paging feature will be reimplemented in a way
597
- // that it should be considered here. `_isPagingDisabled(queryRef)`
598
- const pageInfo$ = createPageInfoObservable(datatable$).pipe(map((info) => mappers.pageInfo(info)));
599
- const sorts$ = _createSortsObservable(datatable$).pipe(switchMap((m) => wrapIntoObservable(mappers.sorts(m, context))));
600
- const filterInfo$ = _createFilterStatesObservable(datatable$).pipe(switchMap((x) => mapFilterStates(x, mappers.filters, context)));
601
- return combineLatest([pageInfo$, sorts$, filterInfo$]).pipe(map(([pageInfo, sorts, filterInfo]) => ({
602
- pageInfo,
603
- sorts,
604
- filter: filterInfo,
605
- context,
606
- })));
607
- }))
608
- .subscribe(subscriber);
609
- return () => {
610
- dtSub.unsubscribe();
611
- ctxSub.unsubscribe();
612
- };
613
- });
806
+ function _isPagingDisabled(queryRef) {
807
+ return queryRef.getQueryProcessingConfig()?.disablePaging ?? false;
614
808
  }
615
- // function _isPagingDisabled<TData, TVariables, TRow>(queryRef: DatatableGraphQLQueryRef<TData, TVariables, TRow>): boolean {
616
- // return queryRef.getQueryProcessingConfig()?.disablePaging ?? false
617
- // }
618
809
 
810
+ // `isNetworkRequestInFlight` is only exported from the ES module subpath
811
+ // `@apollo/client/core/networkStatus`, which can't be loaded by Jest's CJS
812
+ // transform. Reimplementing it here keeps the logic accessible in tests.
813
+ function isNetworkRequestInFlight(networkStatus) {
814
+ return networkStatus ? networkStatus < NetworkStatus.ready : false;
815
+ }
619
816
  /**
620
- * Partially wraps ApolloClient's QueryRef with some of our datatable boilerplate.
621
- *
622
- * TODO: Decide how to handle/display errors.
817
+ * Maximum number of error recovery attempts before throwing.
818
+ */
819
+ const MAX_ERROR_RECOVERY_ATTEMPTS = 10;
820
+ /**
821
+ * Partially wraps ApolloClient's QueryRef with datatable paging, loading
822
+ * state, error handling, and debounced variable updates.
623
823
  */
624
824
  class DatatableGraphQLQueryRef {
625
825
  _queryRef;
626
826
  _updatesPollDelay;
827
+ _defaultErrorHandler;
627
828
  _variablesSubject = new BehaviorSubject({});
628
829
  _observingChangesSubject = new BehaviorSubject(false);
830
+ _errorSubject = new Subject();
831
+ /**
832
+ * Used to manually trigger a pending/loading state when we know a request is
833
+ * coming but Apollo hasn't started it yet (e.g. after setVariables resolves to
834
+ * a non-paging variable change while a request is already in-flight).
835
+ */
836
+ _manualPendingSubject = new BehaviorSubject(false);
837
+ _needToRequerySubject = new Subject();
629
838
  /**
630
839
  * Temporary way of tracking total count when paging is disabled.
631
840
  */
@@ -639,6 +848,18 @@ class DatatableGraphQLQueryRef {
639
848
  _variablesUpdatePending = false;
640
849
  _valueChanges;
641
850
  loading$;
851
+ /**
852
+ * Emits whenever the query variables change.
853
+ */
854
+ variables$ = this._variablesSubject.asObservable();
855
+ /**
856
+ * Emits when a GraphQL error occurs.
857
+ *
858
+ * If nothing is subscribed to this, the `_defaultErrorHandler` is used
859
+ * (if provided). Once subscribed, the subscriber is responsible for
860
+ * handling the error.
861
+ */
862
+ error$ = this._errorSubject.asObservable();
642
863
  get updatesPollDelay() {
643
864
  return this._updatesPollDelay;
644
865
  }
@@ -649,107 +870,167 @@ class DatatableGraphQLQueryRef {
649
870
  /** Original ApolloClient's QueryRef. */
650
871
  _queryRef,
651
872
  /**
652
- * How long to wait before refetching from an update to the query or variables.
873
+ * How long to wait (ms) before applying variable changes and refetching.
874
+ */
875
+ _updatesPollDelay = 500,
876
+ /**
877
+ * Default error handler used when no subscriber is listening to `error$`.
653
878
  */
654
- _updatesPollDelay = 500) {
879
+ _defaultErrorHandler) {
655
880
  this._queryRef = _queryRef;
656
881
  this._updatesPollDelay = _updatesPollDelay;
882
+ this._defaultErrorHandler = _defaultErrorHandler;
657
883
  this._variablesSubject.next(this._queryRef.obsQuery.options.variables || {});
658
- // this._getValueChanges().subscribe(v => this._logNetworkStatus(v.networkStatus))
884
+ // Tracks whether the loading overlay should be held open until we know
885
+ // whether the first response will trigger a follow-up requery (e.g. when
886
+ // disablePaging is on and we first probe for totalCount). This prevents the
887
+ // loading overlay from flickering off and back on.
888
+ let hasEmittedTriggered = false;
889
+ const hasEmittedSubject = new BehaviorSubject(false);
890
+ const setHasEmitted = () => {
891
+ if (hasEmittedTriggered) {
892
+ return;
893
+ }
894
+ hasEmittedTriggered = true;
895
+ const sub = this._needToRequerySubject.pipe(take(1)).subscribe(() => {
896
+ hasEmittedSubject.next(true);
897
+ sub.unsubscribe();
898
+ });
899
+ };
659
900
  this._valueChanges = defer(() => {
660
- // console.log('Observing value changes')
901
+ let prev;
661
902
  const varChangesSub = this._variablesSubject
662
903
  .pipe(skip(1), tap(() => {
663
904
  this._variablesUpdatePending = true;
664
905
  }), auditTime(this._updatesPollDelay), finalize(() => {
665
- // If the query stopped being observed before setting the pending
666
- // variables, set them now.
906
+ // If the query stopped being observed before the debounce fired,
907
+ // apply the pending variables now.
667
908
  if (this._variablesUpdatePending) {
668
909
  this.refetch();
669
910
  this._variablesUpdatePending = false;
670
911
  }
671
912
  }))
672
913
  .subscribe((variables) => {
673
- // console.log('set vars', variables)
674
914
  this._setVariablesImmediate(variables);
675
- // this.refetch()
915
+ const current = withoutProperties(this.getVariables(), [
916
+ 'skip',
917
+ 'take',
918
+ ]);
919
+ const isVarsChanged = prev === undefined ||
920
+ JSON.stringify(prev) !== JSON.stringify(current);
921
+ prev = current;
676
922
  this._variablesUpdatePending = false;
923
+ // When the non-paging variables change while Apollo is already
924
+ // in setVariables state, manually signal a pending load so the
925
+ // loading overlay doesn't disappear until the response arrives.
926
+ if (isVarsChanged &&
927
+ this._queryRef.getCurrentResult().networkStatus ===
928
+ NetworkStatus.setVariables) {
929
+ this._manualPendingSubject.next(true);
930
+ }
677
931
  });
932
+ let repeatedErrors = 0;
678
933
  this._observingChanges = true;
679
934
  return this._queryRef.valueChanges.pipe(
680
- // tap(v => {
681
- // console.log('v', v)
682
- // }),
683
- filter((v) => v.networkStatus === NetworkStatus.ready), finalize(() => {
684
- // console.log('Done observing value changes')
935
+ // Once a response lands (not in-flight), begin tracking whether the
936
+ // initial load has truly completed (accounting for disablePaging requery).
937
+ tap((v) => !isNetworkRequestInFlight(v.networkStatus)
938
+ ? setHasEmitted()
939
+ : undefined),
940
+ // Guard against infinite error loops when polling / retrying.
941
+ tap((v) => {
942
+ if (v.networkStatus === NetworkStatus.error) {
943
+ repeatedErrors++;
944
+ if (repeatedErrors >= MAX_ERROR_RECOVERY_ATTEMPTS) {
945
+ throw Error('Max error recovery attempts reached.');
946
+ }
947
+ }
948
+ else if (v.networkStatus === NetworkStatus.ready) {
949
+ repeatedErrors = 0;
950
+ }
951
+ }), finalize(() => {
685
952
  varChangesSub.unsubscribe();
686
953
  this._observingChanges = false;
687
954
  }));
688
- }).pipe(
689
- // share()
690
- shareReplay({ bufferSize: 1, refCount: true }));
955
+ }).pipe(shareReplay({ bufferSize: 1, refCount: true }));
691
956
  this.loading$ = this._observingChangesSubject.pipe(switchMap((observingChanges) => {
692
957
  if (!observingChanges) {
693
958
  return of(false);
694
959
  }
695
- return this._valueChanges.pipe(map((result) => result.loading), startWith(this._queryRef.getCurrentResult().loading), auditTime(0), shareReplay({ bufferSize: 1, refCount: true }));
696
- }));
960
+ return this._valueChanges.pipe(map((result) => result.loading), startWith(this._queryRef.getCurrentResult().loading), tap((loading) => {
961
+ if (!loading) {
962
+ this._manualPendingSubject.next(false);
963
+ }
964
+ }),
965
+ // Swap the actual loading flag for the manual pending flag so that
966
+ // we can hold the loading state open when needed.
967
+ switchMap(() => this._manualPendingSubject), auditTime(0), shareReplay({ bufferSize: 1, refCount: true }));
968
+ }),
969
+ // Keep loading=true until we've confirmed the first full load cycle
970
+ // (including any disablePaging requery).
971
+ switchMap((v) => hasEmittedSubject.pipe(map((hasEmitted) => (hasEmitted ? v : true)))));
697
972
  }
698
973
  rows(mapper) {
699
974
  return this._rowsObservable(mapper);
700
975
  }
701
976
  _rowsObservable(mapper) {
702
977
  return new Observable((subscriber) => {
703
- // const rowsBufferSubject = new BehaviorSubject<TRow[]>([])
704
- // console.log('obs _rowsObservable')
705
- let rowsBuffer = [];
706
- // const rowsBufferSubject = new ReplaySubject<TRow[]>()
707
- const rowsBufferSubject = new Subject();
978
+ const rowsBufferSubject = new BehaviorSubject([]);
708
979
  const querySub = this._valueChanges
709
- .pipe(switchMap((result) => {
980
+ .pipe(filter((result) => !isNetworkRequestInFlight(result.networkStatus)), switchMap((result) => {
710
981
  if (result.data === undefined) {
711
- return of([]);
982
+ return of();
712
983
  }
713
- return this._resolveRowMapper(mapper(result.data)).pipe(tap((mapperResult) => {
714
- // console.log('mapperResult', mapperResult)
715
- if (this._needsToRequeryWithAllRecords(mapperResult)) {
984
+ // Capture the skip offset *before* any variable changes that the
985
+ // mapper might trigger (e.g. patchVariables for disablePaging).
986
+ const querySkip = this._getVariablesFromQueryRef().skip;
987
+ const _result = this._handleResult(result);
988
+ return this._resolveRowMapper(mapper(_result.data)).pipe(tap((mapperResult) => {
989
+ const needsToRequery = this._needsToRequeryWithAllRecords(mapperResult);
990
+ if (needsToRequery) {
716
991
  this.patchVariables({ take: mapperResult.totalCount });
717
992
  }
993
+ this._needToRequerySubject.next(needsToRequery);
718
994
  if (hasProperty(mapperResult, 'totalCount')) {
719
995
  this._totalCount = mapperResult.totalCount;
720
996
  }
721
- // let rows = rowsBufferSubject.value || []
722
- let rows = rowsBuffer || [];
997
+ let rows = rowsBufferSubject.value || [];
723
998
  const hasTotalCount = mapperResult.totalCount !== undefined &&
724
999
  mapperResult.totalCount !== null;
725
- // If the rows buffer is not the same size as the totalCount, create a
726
- // new buffer.
727
- //
728
- // TODO: Find out if this is resetting the buffer too eagerly.
729
- // ApolloClient may have a better solution.
1000
+ // If the rows buffer is not the same size as totalCount, create
1001
+ // a fresh buffer (sparse array — unfilled slots are undefined,
1002
+ // which is what checkRecordsHaveValue expects).
730
1003
  if (hasTotalCount) {
731
1004
  if (mapperResult.totalCount !== rows.length) {
732
1005
  rows = new Array(mapperResult.totalCount || 0);
733
1006
  }
734
- let startIndex = this.getVariables().skip ?? 0;
1007
+ let startIndex = querySkip ?? 0;
735
1008
  if (this.getQueryProcessingConfig()?.disablePaging) {
736
1009
  startIndex = 0;
737
1010
  }
738
- // Insert rows into buffer location.
739
- rows.splice(startIndex, mapperResult.rows.length, ...mapperResult.rows);
1011
+ // Insert rows into the correct buffer positions.
1012
+ for (let i = 0; i < mapperResult.rows.length; i++) {
1013
+ rows[startIndex + i] = mapperResult.rows[i];
1014
+ }
740
1015
  rows = [...rows];
741
1016
  }
742
1017
  else {
743
1018
  rows = [...mapperResult.rows];
744
1019
  }
745
- rowsBuffer = rows;
1020
+ // ngx-datatable does row lookups in a WeakMap and my assumption
1021
+ // is that the pre-allocated empty objects seem to be getting
1022
+ // recognized as the same object, so to avoid that, we add a
1023
+ // unique property to each row.
1024
+ rows = rows.map((v, i) => ({
1025
+ ...v,
1026
+ __dt_id: `row-${i}`,
1027
+ }));
746
1028
  rowsBufferSubject.next(rows);
747
1029
  }));
748
1030
  }))
749
1031
  .subscribe();
750
1032
  const rowsSub = rowsBufferSubject.subscribe(subscriber);
751
1033
  return () => {
752
- // console.log('unsub')
753
1034
  querySub.unsubscribe();
754
1035
  rowsSub.unsubscribe();
755
1036
  rowsBufferSubject.next([]);
@@ -757,6 +1038,28 @@ class DatatableGraphQLQueryRef {
757
1038
  };
758
1039
  });
759
1040
  }
1041
+ /**
1042
+ * Reads the result and returns it unchanged if no errors.
1043
+ *
1044
+ * On errors: emits them via `error$` and replaces any `null` data properties
1045
+ * with an empty collection shape `{ items: [], totalCount: 0 }` so mapper
1046
+ * functions can run safely without special-casing the error path.
1047
+ */
1048
+ _handleResult(result) {
1049
+ if (!result.errors || result.errors.length === 0)
1050
+ return result;
1051
+ this._emitError(result.errors);
1052
+ const defaultDataPropValue = { items: [], totalCount: 0 };
1053
+ const data = Object.keys(result.data).reduce((acc, key) => {
1054
+ ;
1055
+ acc[key] =
1056
+ result.data[key] === null
1057
+ ? defaultDataPropValue
1058
+ : result.data[key];
1059
+ return acc;
1060
+ }, {});
1061
+ return { ...result, data };
1062
+ }
760
1063
  _needsToRequeryWithAllRecords(data) {
761
1064
  if (!this.getQueryProcessingConfig()?.disablePaging) {
762
1065
  return false;
@@ -774,9 +1077,6 @@ class DatatableGraphQLQueryRef {
774
1077
  return from(Promise.resolve(mapperReturn));
775
1078
  }
776
1079
  getVariables() {
777
- // The types aren't accurate or Apollo has a bug, so I had to use `any`.
778
- // return (this._queryRef as any).obsQuery.options.variables || {}
779
- // TODO: Look into debouncing our variable setting, while still depending on Apollo for managing them.
780
1080
  return this._variablesSubject.value;
781
1081
  }
782
1082
  _setVariablesImmediate(variables) {
@@ -807,18 +1107,20 @@ class DatatableGraphQLQueryRef {
807
1107
  this._setVariablesImmediate(_variables);
808
1108
  }
809
1109
  }
810
- refetch(variables) {
1110
+ refetch(variables, showLoading = false) {
811
1111
  const _vars = this._withVariableOverrides(variables);
1112
+ if (showLoading) {
1113
+ this._manualPendingSubject.next(true);
1114
+ }
1115
+ // NOTE: There seems to be a bug causing Apollo to not emit changes unless
1116
+ // getCurrentResult() is called. This setTimeout is a workaround.
1117
+ setTimeout(() => this._queryRef.getCurrentResult());
812
1118
  return this._queryRef.refetch(_vars);
813
1119
  }
814
1120
  setQuery(query, triggerRefetch = false) {
815
1121
  this._queryRef.setOptions({ query });
816
1122
  if (triggerRefetch) {
817
- // TODO: Consider refactoring the refetch process. I can't call Apollo's
818
- // refetch directly, because I want it to share the update triggered
819
- // polling delay with the setting of variables.
820
- // this._queryRef.refetch(this.getVariables())
821
- // Fake variables update to trigger a refetch that shares it's refetch delay.
1123
+ // Fake a variables update to share the debounce delay with setVariables.
822
1124
  this.setVariables(this.getVariables());
823
1125
  }
824
1126
  }
@@ -828,6 +1130,15 @@ class DatatableGraphQLQueryRef {
828
1130
  getQueryProcessingConfig() {
829
1131
  return this.getOptions()?.context?.queryProcessingConfig;
830
1132
  }
1133
+ /**
1134
+ * Returns the variables that were actually sent with the last request, read
1135
+ * directly from Apollo's observable query. This is more accurate than
1136
+ * `_variablesSubject.value` when variables can change between the debounce
1137
+ * firing and the response arriving.
1138
+ */
1139
+ _getVariablesFromQueryRef() {
1140
+ return this._queryRef.obsQuery.variables;
1141
+ }
831
1142
  _withVariableOverrides(variables) {
832
1143
  if (!notNullOrUndefined(variables) &&
833
1144
  !this.getQueryProcessingConfig()?.disablePaging) {
@@ -839,6 +1150,12 @@ class DatatableGraphQLQueryRef {
839
1150
  }
840
1151
  return _vars;
841
1152
  }
1153
+ _emitError(error) {
1154
+ this._errorSubject.next(error);
1155
+ if (this._defaultErrorHandler && !this._errorSubject.observed) {
1156
+ this._defaultErrorHandler(error);
1157
+ }
1158
+ }
842
1159
  }
843
1160
 
844
1161
  const DATATABLE_GRAPHQL_SERVICE_CONFIG = new InjectionToken('DATATABLE_GRAPHQL_SERVICE_CONFIG');
@@ -874,10 +1191,10 @@ class DatatableGraphqlService {
874
1191
  const queryRef = this._apollo.watchQuery(_options);
875
1192
  return new DatatableGraphQLQueryRef(queryRef);
876
1193
  }
877
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DatatableGraphqlService, deps: [{ token: i1.Apollo }, { token: DATATABLE_GRAPHQL_SERVICE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
878
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DatatableGraphqlService, providedIn: 'root' });
1194
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatatableGraphqlService, deps: [{ token: i1.Apollo }, { token: DATATABLE_GRAPHQL_SERVICE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1195
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatatableGraphqlService, providedIn: 'root' });
879
1196
  }
880
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DatatableGraphqlService, decorators: [{
1197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatatableGraphqlService, decorators: [{
881
1198
  type: Injectable,
882
1199
  args: [{
883
1200
  providedIn: 'root',
@@ -1176,68 +1493,6 @@ const mapSearchTextColumnsDataFilterStateToGql = (filterState, context) => {
1176
1493
  return filter;
1177
1494
  };
1178
1495
 
1179
- const inlineVariableTransformer = (operation, hintsToken) => {
1180
- const query = operation.query;
1181
- const variables = operation.variables;
1182
- return {
1183
- query,
1184
- variables,
1185
- };
1186
- };
1187
-
1188
- const removeNotDefinedTransformer = (operation, hintsToken) => {
1189
- const query = operation.query;
1190
- const variables = operation.variables;
1191
- return {
1192
- query,
1193
- variables,
1194
- };
1195
- };
1196
-
1197
- /**
1198
- * Maps variable value to gql and inlines it into the query.
1199
- */
1200
- const inlineVariableHintDef = {
1201
- name: 'inline-variable',
1202
- appliesTo: [HintsKind.Variable, HintsKind.VariableDefinition],
1203
- transformer: inlineVariableTransformer,
1204
- };
1205
-
1206
- /**
1207
- * Remove the variable from the query if it is not defined in the operation
1208
- * variables.
1209
- */
1210
- const removeNotDefinedHintDef = {
1211
- name: 'remove-not-defined',
1212
- appliesTo: [HintsKind.OperationDefinition],
1213
- transformer: removeNotDefinedTransformer,
1214
- };
1215
-
1216
- // /**
1217
- // * Remove the variable from the query if it is not defined in the operation
1218
- // * variables.
1219
- // *
1220
- // * Applies to: OperationDefinition
1221
- // */
1222
- // export const GQL_HINT_REMOVE_NOT_DEFINED = 'remove-not-defined'
1223
- // /**
1224
- // * Maps variable value to gql and inlines it into the query.
1225
- // *
1226
- // * Applies to: Variable, VariableDefinition
1227
- // */
1228
- // export const GQL_HINT_INLINE_VARIABLE = 'inline-variable'
1229
- // /**
1230
- // * Remove variable definition from the query if it is not used by the operation.
1231
- // *
1232
- // * Applies to: VariableDefinition
1233
- // */
1234
- // // export const GQL_HINT_REMOVE_IF_NOT_USED = 'remove-if-not-used'
1235
- // export const GQL_HINTS = [
1236
- // // GQL_HINT_REMOVE_IF_NOT_USED,
1237
- // GQL_HINT_REMOVE_NOT_DEFINED,
1238
- // GQL_HINT_INLINE_VARIABLE
1239
- // ]
1240
-
1241
1496
  const baseSchemaFragment = gql `
1242
1497
  input ComparableInt32OperationFilterInput {
1243
1498
  eq: Int
@@ -1347,65 +1602,150 @@ const baseSchemaFragment = gql `
1347
1602
  }
1348
1603
  `;
1349
1604
 
1350
- const _whereItemCollectionKinds = ['and', 'or'];
1351
- function _createWhereFieldFn(fieldName) {
1352
- return (obj) => obj[fieldName];
1353
- }
1354
- function _resolveWhereField(obj, f) {
1355
- if (f instanceof Function) {
1356
- return f(obj);
1605
+ /**
1606
+ * The set of leaf-level operator keys used by HotChocolate filter inputs
1607
+ * (e.g. StringOperationFilterInput, ComparableInt32OperationFilterInput).
1608
+ * Any key not in this set is treated as a field name, causing the evaluator
1609
+ * to recurse into `value[key]`.
1610
+ */
1611
+ const OPERATOR_KEYS = new Set([
1612
+ 'eq',
1613
+ 'neq',
1614
+ 'gt',
1615
+ 'gte',
1616
+ 'lt',
1617
+ 'lte',
1618
+ 'ngt',
1619
+ 'ngte',
1620
+ 'nlt',
1621
+ 'nlte',
1622
+ 'in',
1623
+ 'nin',
1624
+ 'contains',
1625
+ 'ncontains',
1626
+ 'startsWith',
1627
+ 'nstartsWith',
1628
+ 'endsWith',
1629
+ 'nendsWith',
1630
+ 'objectContains',
1631
+ ]);
1632
+ function applyOperator(operator, value, operand) {
1633
+ switch (operator) {
1634
+ case 'eq':
1635
+ return value === operand;
1636
+ case 'neq':
1637
+ return value !== operand;
1638
+ case 'gt':
1639
+ return value > operand;
1640
+ case 'gte':
1641
+ return value >= operand;
1642
+ case 'lt':
1643
+ return value < operand;
1644
+ case 'lte':
1645
+ return value <= operand;
1646
+ // Negated comparisons — logical complements of the above
1647
+ case 'ngt':
1648
+ return !(value > operand); // equivalent to lte
1649
+ case 'ngte':
1650
+ return !(value >= operand); // equivalent to lt
1651
+ case 'nlt':
1652
+ return !(value < operand); // equivalent to gte
1653
+ case 'nlte':
1654
+ return !(value <= operand); // equivalent to gt
1655
+ case 'in':
1656
+ return Array.isArray(operand) && operand.includes(value);
1657
+ case 'nin':
1658
+ return Array.isArray(operand) && !operand.includes(value);
1659
+ case 'contains':
1660
+ return (typeof value === 'string' &&
1661
+ typeof operand === 'string' &&
1662
+ value.includes(operand));
1663
+ case 'ncontains':
1664
+ return (typeof value === 'string' &&
1665
+ typeof operand === 'string' &&
1666
+ !value.includes(operand));
1667
+ case 'startsWith':
1668
+ return (typeof value === 'string' &&
1669
+ typeof operand === 'string' &&
1670
+ value.startsWith(operand));
1671
+ case 'nstartsWith':
1672
+ return (typeof value === 'string' &&
1673
+ typeof operand === 'string' &&
1674
+ !value.startsWith(operand));
1675
+ case 'endsWith':
1676
+ return (typeof value === 'string' &&
1677
+ typeof operand === 'string' &&
1678
+ value.endsWith(operand));
1679
+ case 'nendsWith':
1680
+ return (typeof value === 'string' &&
1681
+ typeof operand === 'string' &&
1682
+ !value.endsWith(operand));
1683
+ case 'objectContains': {
1684
+ // Custom Seam/HotChocolate operator: convert the field value to a string
1685
+ // and check if it contains the operand (case-insensitive, matching search UX).
1686
+ const strValue = value == null ? '' : String(value);
1687
+ const strOperand = operand == null ? '' : String(operand);
1688
+ return strValue.toLowerCase().includes(strOperand.toLowerCase());
1689
+ }
1690
+ default:
1691
+ throw new Error(`Unknown filter operator: "${operator}"`);
1357
1692
  }
1358
- return f;
1359
1693
  }
1360
- const _eqConditionOp = (field1, field2) => (obj) => _resolveWhereField(obj, field1) === _resolveWhereField(obj, field2);
1361
- const _neqConditionOp = (field1, field2) => (obj) => _resolveWhereField(obj, field1) !== _resolveWhereField(obj, field2);
1362
- const _conditions = {
1363
- eq: _eqConditionOp,
1364
- neq: _neqConditionOp,
1365
- };
1366
- const _conditionKinds = ['eq', 'neq'];
1367
- function _getWhereCondition(x) {
1368
- for (const k of _conditionKinds) {
1369
- // if (hasProperty(x, k)) {
1370
- if (x[k] !== undefined) {
1371
- return { condition: _conditions[k], value: x[k] };
1694
+ /**
1695
+ * Recursively evaluates a HotChocolate-style filter against a value.
1696
+ *
1697
+ * Rules:
1698
+ * - `and` array: all sub-filters must pass (AND logic)
1699
+ * - `or` array: at least one sub-filter must pass (OR logic)
1700
+ * - Known operator keys (`eq`, `contains`, `gt`, ...): apply the operator to
1701
+ * the current `value`
1702
+ * - Any other key: treat as a field name; recurse with `value[key]` and the
1703
+ * nested filter
1704
+ * - All non-`and`/`or` conditions are implicitly ANDed together
1705
+ */
1706
+ function evaluateCondition(value, filter) {
1707
+ if (filter === null || filter === undefined) {
1708
+ return true;
1709
+ }
1710
+ if (Array.isArray(filter.and)) {
1711
+ if (!filter.and.every((f) => evaluateCondition(value, f))) {
1712
+ return false;
1372
1713
  }
1373
1714
  }
1374
- return null;
1375
- }
1376
- function _parseWhereItems(where) {
1377
- const conditions = [];
1378
- const keys = Object.keys(where);
1379
- if (keys.find((k) => _whereItemCollectionKinds.find((k2) => k === k2) !== undefined) !== undefined) {
1380
- // TODO: Implement
1381
- }
1382
- else {
1383
- for (const k of keys) {
1384
- const c = _getWhereCondition(where[k]);
1385
- if (c === null) {
1386
- throw Error(`Unexpected where item.`);
1715
+ if (Array.isArray(filter.or)) {
1716
+ if (!filter.or.some((f) => evaluateCondition(value, f))) {
1717
+ return false;
1718
+ }
1719
+ }
1720
+ for (const key of Object.keys(filter)) {
1721
+ if (key === 'and' || key === 'or') {
1722
+ continue;
1723
+ }
1724
+ if (OPERATOR_KEYS.has(key)) {
1725
+ if (!applyOperator(key, value, filter[key])) {
1726
+ return false;
1727
+ }
1728
+ }
1729
+ else {
1730
+ if (!evaluateCondition(value?.[key], filter[key])) {
1731
+ return false;
1387
1732
  }
1388
- conditions.push(c.condition(_createWhereFieldFn(`${k}`), c.value));
1389
1733
  }
1390
1734
  }
1391
- return conditions;
1735
+ return true;
1392
1736
  }
1737
+ /**
1738
+ * Filters an array using a HotChocolate-style where clause.
1739
+ *
1740
+ * Top-level field conditions are implicitly ANDed. Use `and`/`or` arrays for
1741
+ * explicit logical grouping.
1742
+ *
1743
+ * @example
1744
+ * filterWhere(records, { name: { contains: 'foo' }, id: { gt: 5 } })
1745
+ * filterWhere(records, { or: [{ name: { eq: 'a' } }, { name: { eq: 'b' } }] })
1746
+ */
1393
1747
  function filterWhere(data, where) {
1394
- const items = _parseWhereItems(where);
1395
- const filteredClaims = data.filter((c) => {
1396
- // const idx = items.indexOf(itm => itm(c))
1397
- // return idx !== -1
1398
- let found = false;
1399
- for (const itm of items) {
1400
- const b = itm(c);
1401
- if (b) {
1402
- found = b;
1403
- break;
1404
- }
1405
- }
1406
- return found;
1407
- });
1408
- return filteredClaims;
1748
+ return data.filter((item) => evaluateCondition(item, where));
1409
1749
  }
1410
1750
 
1411
1751
  function skipAndTake(data, skip, take) {
@@ -1416,18 +1756,76 @@ function skipAndTake(data, skip, take) {
1416
1756
  // throw Error(`Invalid 'take' value: ${take}`)
1417
1757
  // }
1418
1758
  const tmp = [];
1419
- for (let i = skip; i < take + skip && i < data.length - 1; i++) {
1759
+ for (let i = skip; i < take + skip && i < data.length; i++) {
1420
1760
  tmp.push(data[i]);
1421
1761
  }
1422
1762
  return tmp;
1423
1763
  }
1424
1764
 
1765
+ /**
1766
+ * Sorts an array by a HotChocolate-style `order` argument.
1767
+ *
1768
+ * Earlier entries in the array take higher sort precedence. When two records
1769
+ * are equal on all sort clauses they retain their original relative order
1770
+ * (stable sort).
1771
+ *
1772
+ * Handles strings (locale-aware), numbers/dates, booleans, and null/undefined
1773
+ * values (nulls sort last for both ASC and DESC).
1774
+ *
1775
+ * @example
1776
+ * sortItems(records, [{ name: 'ASC' }, { id: 'DESC' }])
1777
+ */
1778
+ function sortItems(items, order) {
1779
+ if (!order || order.length === 0) {
1780
+ return items;
1781
+ }
1782
+ return [...items].sort((a, b) => {
1783
+ for (const clause of order) {
1784
+ const entries = Object.entries(clause);
1785
+ if (entries.length === 0) {
1786
+ continue;
1787
+ }
1788
+ const [field, direction] = entries[0];
1789
+ const aVal = a[field];
1790
+ const bVal = b[field];
1791
+ // Nulls always sort last regardless of direction — check before inverting.
1792
+ const aNull = aVal == null;
1793
+ const bNull = bVal == null;
1794
+ if (aNull && bNull)
1795
+ continue;
1796
+ if (aNull)
1797
+ return 1;
1798
+ if (bNull)
1799
+ return -1;
1800
+ const comparison = _compareNonNullValues(aVal, bVal);
1801
+ if (comparison !== 0) {
1802
+ return direction === 'DESC' ? -comparison : comparison;
1803
+ }
1804
+ }
1805
+ return 0;
1806
+ });
1807
+ }
1808
+ function _compareNonNullValues(a, b) {
1809
+ if (typeof a === 'string' && typeof b === 'string') {
1810
+ return a.localeCompare(b);
1811
+ }
1812
+ if (a < b)
1813
+ return -1;
1814
+ if (a > b)
1815
+ return 1;
1816
+ return 0;
1817
+ }
1818
+
1425
1819
  function filteredResults(items, args) {
1426
1820
  let _items = items;
1427
1821
  const where = args?.where;
1428
1822
  if (where !== undefined) {
1429
1823
  _items = filterWhere(_items, where);
1430
1824
  }
1825
+ const order = args?.order;
1826
+ if (order !== undefined) {
1827
+ _items = sortItems(_items, order);
1828
+ }
1431
1829
  const totalCount = _items.length;
1432
1830
  const skip = args?.skip ?? 0;
1433
1831
  const take = args?.take ?? _items.length;
@@ -1454,6 +1852,8 @@ class MockDatatable {
1454
1852
  _bodyHeight = 500;
1455
1853
  _scrolledPosV = 0;
1456
1854
  page = new EventEmitter();
1855
+ resize = new EventEmitter();
1856
+ externalSorting = false;
1457
1857
  sort = new EventEmitter();
1458
1858
  get sorts() {
1459
1859
  return this._sorts;
@@ -1461,6 +1861,9 @@ class MockDatatable {
1461
1861
  set sorts(value) {
1462
1862
  this._sorts = value;
1463
1863
  }
1864
+ get pageInfo() {
1865
+ return this.ngxDatatable;
1866
+ }
1464
1867
  filterStates = this._filterStatesSubject.asObservable();
1465
1868
  static pageDefaults(dt, defaultPageSize = 20) {
1466
1869
  return {
@@ -1573,7 +1976,10 @@ function checkRecordsHaveValue(arr, indices, testValueCheckProp = 'name', onlyCh
1573
1976
  }
1574
1977
  else {
1575
1978
  if (!onlyCheckProvidedIndices) {
1576
- if (notNullOrUndefined(arr[i])) {
1979
+ const isItemDefined = notNullOrUndefined(testValueCheckProp)
1980
+ ? notNullOrUndefined(item?.[testValueCheckProp])
1981
+ : notNullOrUndefined(item);
1982
+ if (isItemDefined) {
1577
1983
  throw Error(`Record at index '${i}' should not be defined.`);
1578
1984
  }
1579
1985
  }
@@ -1603,133 +2009,61 @@ function graphQLLink(options) {
1603
2009
  return new ApolloLink((operation, forward) => {
1604
2010
  return new Observable$1((subscriber) => {
1605
2011
  // console.log('graphQLLink', operation.variables)
1606
- const response = graphqlSync({
1607
- schema: options.schema,
1608
- source: print(operation.query),
1609
- rootValue: options.rootValue,
1610
- contextValue: operation.getContext(),
1611
- variableValues: operation.variables,
1612
- operationName: operation.operationName,
1613
- // fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
1614
- // typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
1615
- });
1616
- // console.log('graphQLLink response', response)
1617
- operation.setContext({ response });
1618
- subscriber.next(response);
1619
- subscriber.complete();
2012
+ const execute = () => {
2013
+ const response = graphqlSync({
2014
+ schema: options.schema,
2015
+ source: print(operation.query),
2016
+ rootValue: options.rootValue,
2017
+ contextValue: operation.getContext(),
2018
+ variableValues: operation.variables,
2019
+ operationName: operation.operationName,
2020
+ // fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
2021
+ // typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
2022
+ });
2023
+ // console.log('graphQLLink response', response)
2024
+ operation.setContext({ response });
2025
+ subscriber.next(response);
2026
+ subscriber.complete();
2027
+ };
2028
+ if (options.delay && options.delay > 0) {
2029
+ const timeoutId = setTimeout(execute, options.delay);
2030
+ return () => clearTimeout(timeoutId);
2031
+ }
2032
+ execute();
1620
2033
  return () => { };
1621
2034
  });
1622
2035
  });
1623
2036
  }
1624
2037
 
1625
- const queryProcessingLink = new ApolloLink((operation, forward) => {
1626
- // console.log('~link operation', operation)
1627
- const context = operation.getContext();
1628
- const queryProcessingConfig = context.queryProcessingConfig || {};
1629
- // console.log(operation.query)
1630
- // const rules = parseHints(operation.query)
1631
- let _ast = parseAst(operation.query);
1632
- const rules = parseHints(_ast);
1633
- // console.log('rules', rules)
1634
- operation.query = _ast;
1635
- const removeNotDefined = hintsTokensContainingHint(rules, removeNotDefinedHintDef.name);
1636
- // console.log('removeNotDefined', removeNotDefined)
1637
- for (const rulesToken of removeNotDefined) {
1638
- // _ast = removeVariableDefinitionsNotDefined(_ast, rulesToken.node as OperationDefinitionNode, operation.variables)
1639
- if (!removeNotDefinedHintDef.transformer) {
1640
- continue;
1641
- }
1642
- const result = removeNotDefinedHintDef.transformer({
1643
- query: operation.query,
1644
- variables: operation.variables,
1645
- }, rulesToken);
1646
- operation.query = result.query;
1647
- operation.variables = result.variables;
1648
- }
1649
- const inlineVariableRulesTokens = hintsTokensContainingHint(rules, inlineVariableHintDef.name);
1650
- // console.log('inlineVariableRulesTokens', inlineVariableRulesTokens)
1651
- for (const rulesToken of inlineVariableRulesTokens) {
1652
- let varName = null;
1653
- let varDefaultValue;
1654
- if (rulesToken.kind === HintsKind.VariableDefinition) {
1655
- varName = rulesToken.node.variable.name.value;
1656
- varDefaultValue = rulesToken.node.defaultValue;
1657
- }
1658
- else if (rulesToken.kind === HintsKind.Variable) {
1659
- varName = rulesToken.node.name.value;
1660
- }
1661
- if (varName === null) {
1662
- // TODO: Throw error here?
1663
- continue;
1664
- }
1665
- const varValue = operation.variables[varName];
1666
- operation.variables = withoutProperty(operation.variables, varName);
1667
- _ast = removeVariableDefinition(_ast, varName);
1668
- const varValueNode = (_ast = inlineVariable(_ast, varName, parseValue(toGQL(varValue))));
1669
- }
1670
- // const removeIfNotDefined = hintsTokensContainingHint(rules, GQL_HINT_REMOVE_IF_NOT_USED)
1671
- // console.log('removeIfNotDefined', removeIfNotDefined)
1672
- // const _operation = operation
1673
- // const removeIdNotDefined = (queryProcessingConfig?.variables?.removeIfNotDefined || [])
1674
- // for (const varName of removeIdNotDefined) {
1675
- // if (isNullOrUndefined(operation.variables[varName])) {
1676
- // _operation.query = removeVariable(_operation.query, varName)
1677
- // }
1678
- // }
1679
- // const removeIfNotUsed = (queryProcessingConfig?.variables?.removeIfNotUsed || [])
1680
- // for (const varName of removeIfNotUsed) {
1681
- // if (!containsVariable(_operation.query, varName)) {
1682
- // _operation.query = removeVariable(_operation.query, varName)
1683
- // }
1684
- // }
1685
- // const inlineVariables = (queryProcessingConfig?.variables?.inline || [])
1686
- // for (const varName of inlineVariables) {
1687
- // const varValue = _operation.variables[varName]
1688
- // _operation.variables = withoutProperty(_operation.variables, varName)
1689
- // _operation.query = removeVariableDefinition(_operation.query, varName)
1690
- // _operation.query = inlineVariable(_operation.query, varName, parseValue(toGQL(varValue)))
1691
- // }
1692
- operation.query = _ast;
1693
- return forward(operation);
1694
- });
1695
-
1696
2038
  /**
1697
- * Creates an `APOLLO_OPTIONS` provider configured like our apps, except with
1698
- * the Apollo HttpLink replaced with a custom GraphQL link that queries from the
1699
- * schema and root provided. The responses to GQL queries should be the same as
1700
- * responses from our api, but the query features are limited to common features
1701
- * necessary for tests. Since we use a C# GraphQL api, it would be a lot of
1702
- * overhead to use that implementation in this project's tests, but you can
1703
- * check our `graphQLLink` to find what features have been implemented to
1704
- * hopefully match the result our api would return.
2039
+ * Creates Apollo providers configured like our apps, except with the Apollo
2040
+ * HttpLink replaced with a custom GraphQL link that queries from the schema
2041
+ * and root value provided. Responses match what our real API returns, but
2042
+ * query features are limited to what `graphQLLink` implements.
1705
2043
  *
1706
2044
  * NOTE: This was created because `ApolloTestingModule` is very limited. We
1707
2045
  * mostly use queries intended to emit more than once, but `ApolloTestingModule`
1708
2046
  * can only emit a query response once.
1709
2047
  */
1710
- function createApolloTestingProvider(schema, rootValue) {
1711
- return {
1712
- provide: APOLLO_OPTIONS,
1713
- useFactory: () => {
1714
- return {
1715
- cache: new InMemoryCache(),
1716
- link: concat(queryProcessingLink, graphQLLink({
1717
- schema,
1718
- rootValue,
1719
- })),
1720
- defaultOptions: {
1721
- watchQuery: {
1722
- fetchPolicy: 'cache-and-network',
1723
- errorPolicy: 'ignore',
1724
- },
1725
- query: {
1726
- fetchPolicy: 'network-only',
1727
- errorPolicy: 'all',
1728
- },
1729
- },
1730
- };
2048
+ function createApolloTestingProvider(schema, rootValue, delay) {
2049
+ return provideApollo(() => ({
2050
+ cache: new InMemoryCache(),
2051
+ link: concat(queryProcessingLink, graphQLLink({
2052
+ schema,
2053
+ rootValue,
2054
+ delay,
2055
+ })),
2056
+ defaultOptions: {
2057
+ watchQuery: {
2058
+ fetchPolicy: 'cache-and-network',
2059
+ errorPolicy: 'ignore',
2060
+ },
2061
+ query: {
2062
+ fetchPolicy: 'network-only',
2063
+ errorPolicy: 'all',
2064
+ },
1731
2065
  },
1732
- };
2066
+ }));
1733
2067
  }
1734
2068
 
1735
2069
  function createSimpleGqlTestRecord(num) {
@@ -1756,6 +2090,11 @@ const simpleGqlTestSchema = buildSchema(print(gql `
1756
2090
  name: StringOperationFilterInput
1757
2091
  }
1758
2092
 
2093
+ input SimpleGqlTestRecordSortInput {
2094
+ id: SortEnumType
2095
+ name: SortEnumType
2096
+ }
2097
+
1759
2098
  type SimpleGqlTestRecord {
1760
2099
  id: Int
1761
2100
  name: String
@@ -1765,6 +2104,7 @@ const simpleGqlTestSchema = buildSchema(print(gql `
1765
2104
  simpleGqlTestRecords(
1766
2105
  skip: Int
1767
2106
  take: Int
2107
+ order: [SimpleGqlTestRecordSortInput!]
1768
2108
  where: SimpleGqlTestRecordFilterInput
1769
2109
  ): SimpleGqlTestRecordCollectionSegment
1770
2110
  }
@@ -1782,9 +2122,42 @@ const SIMPLE_GQL_TEST_QUERY = gql `
1782
2122
  query ExampleQuery(
1783
2123
  $skip: Int
1784
2124
  $take: Int
2125
+ $order: [SimpleGqlTestRecordSortInput!]
2126
+ $where: SimpleGqlTestRecordFilterInput
2127
+ ) {
2128
+ simpleGqlTestRecords(
2129
+ skip: $skip
2130
+ take: $take
2131
+ order: $order
2132
+ where: $where
2133
+ ) {
2134
+ items {
2135
+ id
2136
+ name
2137
+ }
2138
+ totalCount
2139
+ }
2140
+ }
2141
+ `;
2142
+ /**
2143
+ * Like {@link SIMPLE_GQL_TEST_QUERY} but includes `$search: String` for use
2144
+ * with search filters that inline a `gqlVar('search')` reference via
2145
+ * `queryProcessingConfig.variables.inline`.
2146
+ */
2147
+ const SIMPLE_GQL_TEST_SEARCH_QUERY = gql `
2148
+ query ExampleQuery(
2149
+ $skip: Int
2150
+ $take: Int
2151
+ $order: [SimpleGqlTestRecordSortInput!]
1785
2152
  $where: SimpleGqlTestRecordFilterInput
2153
+ $search: String
1786
2154
  ) {
1787
- simpleGqlTestRecords(skip: $skip, take: $take, where: $where) {
2155
+ simpleGqlTestRecords(
2156
+ skip: $skip
2157
+ take: $take
2158
+ order: $order
2159
+ where: $where
2160
+ ) {
1788
2161
  items {
1789
2162
  id
1790
2163
  name
@@ -1798,5 +2171,5 @@ const SIMPLE_GQL_TEST_QUERY = gql `
1798
2171
  * Generated bundle index. Do not edit.
1799
2172
  */
1800
2173
 
1801
- export { DATATABLE_GRAPHQL_SERVICE_CONFIG, DEFAULT_PAGE_SIZE, DEFAULT_TO_REMOVE_ON_UNDEFINED, DatatableGraphQLQueryRef, DatatableGraphqlService, GQLDirection, GQLVariable, HINT_NAMES_CAPTURE_REGEX, HINT_PREFIX_REGEX, HintsKind, MockDatatable, SIMPLE_GQL_TEST_QUERY, baseSchemaFragment, checkRecordsHaveValue, containsVariable, createApolloTestingProvider, createHintsToken, createSimpleGqlTestRecord, createSimpleGqlTestRoot, filterWhere, filteredResults, getHintsToken, getPageInfo, getTokenAppliesTo, gqlVar, hintNamesFromHintToken, hintsTokensContainingHint, inlineVariable, inlineVariableHintDef, inlineVariableTransformer, isCommentToken, isHintToken, isInlineComment, mapFilterStates, mapSearchDateColumnsDataFilterStateToGql, mapSearchNumericColumnsDataFilterStateToGql, mapSearchTextColumnsDataFilterStateToGql, observeRowsWithGqlInputsHandling, parseAst, parseComments, parseHints, removeNotDefinedHintDef, removeNotDefinedTransformer, removeVariable, removeVariableDefinition, removeVariableDefinitionsNotDefined, simpleGqlTestSchema, skipAndTake, toGQL };
2174
+ export { DATATABLE_GRAPHQL_SERVICE_CONFIG, DEFAULT_PAGE_SIZE, DEFAULT_TO_REMOVE_ON_UNDEFINED, DatatableGraphQLQueryRef, DatatableGraphqlService, GQLDirection, GQLVariable, HINT_NAMES_CAPTURE_REGEX, HINT_PREFIX_REGEX, HintsKind, MAX_ERROR_RECOVERY_ATTEMPTS, MockDatatable, SIMPLE_GQL_TEST_QUERY, SIMPLE_GQL_TEST_SEARCH_QUERY, baseSchemaFragment, checkRecordsHaveValue, containsVariable, createApolloTestingProvider, createHintsToken, createSimpleGqlTestRecord, createSimpleGqlTestRoot, createSortsMapper, filterWhere, filteredResults, getHintsToken, getPageInfo, getTokenAppliesTo, gqlVar, hintNamesFromHintToken, hintsTokensContainingHint, inlineVariable, inlineVariableHintDef, inlineVariableTransformer, isCommentToken, isHintToken, isInlineComment, logQueryLink, mapFilterStates, mapSearchDateColumnsDataFilterStateToGql, mapSearchNumericColumnsDataFilterStateToGql, mapSearchTextColumnsDataFilterStateToGql, observeRowsWithGqlInputsHandling, parseAst, parseComments, parseHints, queryProcessingLink, removeNotDefinedHintDef, removeNotDefinedTransformer, removeVariable, removeVariableDefinition, removeVariableDefinitionsNotDefined, simpleGqlTestSchema, skipAndTake, sortItems, toGQL };
1802
2175
  //# sourceMappingURL=theseam-ui-common-graphql.mjs.map