spiderly 19.13.0 → 19.14.0
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.
package/fesm2022/spiderly.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Input, Component, EventEmitter, Output, Injectable, ViewChild, NgModule, PLATFORM_ID, Inject, HostBinding, Directive, inject, ElementRef, NgZone,
|
|
2
|
+
import { Input, Component, EventEmitter, Output, Injectable, ViewChild, NgModule, PLATFORM_ID, Inject, HostBinding, Directive, inject, ElementRef, NgZone, signal, computed, input, output, LOCALE_ID, Injector, viewChild, untracked, effect, TemplateRef, ContentChildren, ContentChild } from '@angular/core';
|
|
3
3
|
import * as i1 from '@jsverse/transloco';
|
|
4
4
|
import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
|
|
5
5
|
import * as i2 from '@angular/common';
|
|
@@ -4024,157 +4024,612 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4024
4024
|
}]
|
|
4025
4025
|
}], ctorParameters: () => [] });
|
|
4026
4026
|
|
|
4027
|
+
var MatchModeCodes;
|
|
4028
|
+
(function (MatchModeCodes) {
|
|
4029
|
+
MatchModeCodes["StartsWith"] = "startsWith";
|
|
4030
|
+
MatchModeCodes["Contains"] = "contains";
|
|
4031
|
+
MatchModeCodes["Equals"] = "equals";
|
|
4032
|
+
MatchModeCodes["LessThan"] = "lessThan";
|
|
4033
|
+
MatchModeCodes["GreaterThan"] = "greaterThan";
|
|
4034
|
+
MatchModeCodes["In"] = "in";
|
|
4035
|
+
})(MatchModeCodes || (MatchModeCodes = {}));
|
|
4036
|
+
|
|
4027
4037
|
/**
|
|
4028
|
-
*
|
|
4029
|
-
*
|
|
4030
|
-
*
|
|
4038
|
+
* What each value type may be asked — the ONE operator table, mirroring what the generated
|
|
4039
|
+
* paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
|
|
4040
|
+
* with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
|
|
4031
4041
|
*
|
|
4032
|
-
*
|
|
4033
|
-
* a
|
|
4034
|
-
*
|
|
4042
|
+
* Deliberately a LEAF module (imports nothing but the codes) so every consumer can derive from
|
|
4043
|
+
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), the
|
|
4044
|
+
* compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`), and the data view's
|
|
4045
|
+
* kind-level lists (via `operatorOptionsForKind`). These used to be hand-kept copies; edit the
|
|
4046
|
+
* table here and every surface follows.
|
|
4047
|
+
*
|
|
4048
|
+
* Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
|
|
4049
|
+
* "more/after" — the order the header dropdowns always shipped).
|
|
4035
4050
|
*/
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
}]
|
|
4056
|
-
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { filterId: [{
|
|
4057
|
-
type: Input,
|
|
4058
|
-
args: ['spiderlyFilterTemplate']
|
|
4059
|
-
}] } });
|
|
4051
|
+
const ALLOWED_OPERATORS = {
|
|
4052
|
+
text: [
|
|
4053
|
+
MatchModeCodes.StartsWith,
|
|
4054
|
+
MatchModeCodes.Contains,
|
|
4055
|
+
MatchModeCodes.Equals,
|
|
4056
|
+
],
|
|
4057
|
+
number: [
|
|
4058
|
+
MatchModeCodes.Equals,
|
|
4059
|
+
MatchModeCodes.LessThan,
|
|
4060
|
+
MatchModeCodes.GreaterThan,
|
|
4061
|
+
MatchModeCodes.In,
|
|
4062
|
+
],
|
|
4063
|
+
boolean: [MatchModeCodes.Equals],
|
|
4064
|
+
date: [
|
|
4065
|
+
MatchModeCodes.Equals,
|
|
4066
|
+
MatchModeCodes.LessThan,
|
|
4067
|
+
MatchModeCodes.GreaterThan,
|
|
4068
|
+
],
|
|
4069
|
+
};
|
|
4060
4070
|
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4071
|
+
const OPERATOR_WORDS = {
|
|
4072
|
+
text: {
|
|
4073
|
+
[MatchModeCodes.StartsWith]: {
|
|
4074
|
+
pickerKey: 'StartsWith',
|
|
4075
|
+
phraseKey: 'FilterChipStartsWith',
|
|
4076
|
+
},
|
|
4077
|
+
[MatchModeCodes.Contains]: {
|
|
4078
|
+
pickerKey: 'Contains',
|
|
4079
|
+
phraseKey: 'FilterChipContains',
|
|
4080
|
+
},
|
|
4081
|
+
[MatchModeCodes.Equals]: {
|
|
4082
|
+
pickerKey: 'Equals',
|
|
4083
|
+
phraseKey: 'FilterChipEquals',
|
|
4084
|
+
},
|
|
4085
|
+
// No `In` for text: ALLOWED_OPERATORS.text does not carry it, so the entry was unreachable —
|
|
4086
|
+
// and it made `textFilter({ options })` look supported when the store throws on it.
|
|
4087
|
+
},
|
|
4088
|
+
number: {
|
|
4089
|
+
[MatchModeCodes.Equals]: {
|
|
4090
|
+
pickerKey: 'Equals',
|
|
4091
|
+
phraseKey: 'FilterChipEquals',
|
|
4092
|
+
},
|
|
4093
|
+
[MatchModeCodes.GreaterThan]: {
|
|
4094
|
+
pickerKey: 'MoreThan',
|
|
4095
|
+
phraseKey: 'FilterChipGreaterThan',
|
|
4096
|
+
},
|
|
4097
|
+
[MatchModeCodes.LessThan]: {
|
|
4098
|
+
pickerKey: 'LessThan',
|
|
4099
|
+
phraseKey: 'FilterChipLessThan',
|
|
4100
|
+
},
|
|
4101
|
+
// The picker never renders for a pick-list (one operator), but the CHIP always does.
|
|
4102
|
+
[MatchModeCodes.In]: { pickerKey: 'In', phraseKey: 'FilterChipIn' },
|
|
4103
|
+
},
|
|
4104
|
+
boolean: {
|
|
4105
|
+
[MatchModeCodes.Equals]: {
|
|
4106
|
+
pickerKey: 'Equals',
|
|
4107
|
+
phraseKey: 'FilterChipEquals',
|
|
4108
|
+
},
|
|
4109
|
+
},
|
|
4110
|
+
date: {
|
|
4111
|
+
[MatchModeCodes.Equals]: {
|
|
4112
|
+
pickerKey: 'OnDate',
|
|
4113
|
+
phraseKey: 'FilterChipEquals',
|
|
4114
|
+
},
|
|
4115
|
+
[MatchModeCodes.LessThan]: {
|
|
4116
|
+
pickerKey: 'DatesBefore',
|
|
4117
|
+
phraseKey: 'FilterChipBefore',
|
|
4118
|
+
},
|
|
4119
|
+
[MatchModeCodes.GreaterThan]: {
|
|
4120
|
+
pickerKey: 'DatesAfter',
|
|
4121
|
+
phraseKey: 'FilterChipAfter',
|
|
4122
|
+
},
|
|
4123
|
+
},
|
|
4124
|
+
};
|
|
4065
4125
|
/**
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
4068
|
-
*
|
|
4126
|
+
* `In` and options are the same fact seen from two sides: `In` needs a list of values, and the
|
|
4127
|
+
* editor only draws one when the filter declares options. So options mean `In` and nothing else,
|
|
4128
|
+
* and their absence rules `In` out — offering it on a plain number filter would hand the
|
|
4129
|
+
* operator a mode with no control behind it.
|
|
4069
4130
|
*
|
|
4070
|
-
*
|
|
4071
|
-
*
|
|
4072
|
-
* without a Serbian layout actually types. Mapping it to "d" — as this first shipped — folds
|
|
4073
|
-
* "Đorđe" to "dorde", so typing "djordje" finds nothing: the exact case the comment claimed to
|
|
4074
|
-
* fix, and one no spec covered because the test used "drzava", which has no đ in it.
|
|
4131
|
+
* Widened first: the per-kind tuples are literal, so `boolean`'s `[Equals]` makes the `In`
|
|
4132
|
+
* comparison a "no overlap" error rather than an empty result.
|
|
4075
4133
|
*/
|
|
4076
|
-
function
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4134
|
+
function baseOperators(kind, hasOptions) {
|
|
4135
|
+
const accepted = ALLOWED_OPERATORS[kind];
|
|
4136
|
+
return accepted.filter((value) => hasOptions ? value === MatchModeCodes.In : value !== MatchModeCodes.In);
|
|
4137
|
+
}
|
|
4138
|
+
function toOperatorOptions(kind, operators) {
|
|
4139
|
+
return operators.map((value) => ({
|
|
4140
|
+
value,
|
|
4141
|
+
labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
|
|
4142
|
+
}));
|
|
4084
4143
|
}
|
|
4085
4144
|
/**
|
|
4086
|
-
*
|
|
4087
|
-
*
|
|
4088
|
-
*
|
|
4089
|
-
*
|
|
4090
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
* while it still shows every row.
|
|
4145
|
+
* A KIND's operator picker options, for callers with no filter behind them — today that is
|
|
4146
|
+
* `spiderly-data-view`, whose own filter config derives its operator lists here instead of
|
|
4147
|
+
* hand-keeping copies (the drift `allowed-operators.ts` exists to end). Per-FILTER narrowing
|
|
4148
|
+
* lives in the store (`createFilterStore` resolves it once per declaration), so this cannot be
|
|
4149
|
+
* handed a fake definition to satisfy — that shape shipped once and interpolated an empty
|
|
4150
|
+
* filter name into its own error message.
|
|
4093
4151
|
*/
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4152
|
+
function operatorOptionsForKind(kind, hasOptions = false) {
|
|
4153
|
+
return toOperatorOptions(kind, baseOperators(kind, hasOptions));
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* The operator applied when nobody picked one, which has to be what a person MEANS by filling in
|
|
4157
|
+
* the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
|
|
4158
|
+
* `Equals` — a date equality against a TIMESTAMP matches only the row written in that exact
|
|
4159
|
+
* second, and answers with an empty grid rather than an error (PACMS ran into this on all three
|
|
4160
|
+
* of its date columns, Filip 2026-08-28).
|
|
4161
|
+
*/
|
|
4162
|
+
const DEFAULT_OPERATOR = {
|
|
4163
|
+
text: MatchModeCodes.Contains,
|
|
4164
|
+
number: MatchModeCodes.Equals,
|
|
4165
|
+
boolean: MatchModeCodes.Equals,
|
|
4166
|
+
date: MatchModeCodes.GreaterThan,
|
|
4167
|
+
};
|
|
4168
|
+
function textFilter(config) {
|
|
4169
|
+
return {
|
|
4170
|
+
kind: 'text',
|
|
4171
|
+
label: config.label,
|
|
4172
|
+
operators: config.operators,
|
|
4173
|
+
offered: config.offered,
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Only a NUMBER filter accepts options, and the asymmetry is forced rather than chosen: options
|
|
4178
|
+
* mean `In`, and `In` is the one operator ALLOWED_OPERATORS grants to numbers alone — because the
|
|
4179
|
+
* generated paginator answers it with InvalidMatchMode on a string column. A text pick-list used
|
|
4180
|
+
* to type-check and then throw at the first `set`.
|
|
4181
|
+
*/
|
|
4182
|
+
function numberFilter(config) {
|
|
4183
|
+
return {
|
|
4184
|
+
kind: 'number',
|
|
4185
|
+
label: config.label,
|
|
4186
|
+
options: config.options,
|
|
4187
|
+
operators: config.operators,
|
|
4188
|
+
offered: config.offered,
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function booleanFilter(config) {
|
|
4192
|
+
return {
|
|
4193
|
+
kind: 'boolean',
|
|
4194
|
+
label: config.label,
|
|
4195
|
+
operators: config.operators,
|
|
4196
|
+
offered: config.offered,
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
function dateFilter(config) {
|
|
4200
|
+
return {
|
|
4201
|
+
kind: 'date',
|
|
4202
|
+
label: config.label,
|
|
4203
|
+
operators: config.operators,
|
|
4204
|
+
offered: config.offered,
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
/** The shape `Date.toJSON` writes, which is the only value here JSON cannot round-trip itself. */
|
|
4208
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
4209
|
+
function reviveValue(value) {
|
|
4210
|
+
if (typeof value === 'string' && ISO_DATE.test(value))
|
|
4211
|
+
return new Date(value);
|
|
4212
|
+
if (Array.isArray(value))
|
|
4213
|
+
return value.map(reviveValue);
|
|
4214
|
+
return value;
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* A blanked value is NOT a constraint. `contains ''` matches every row, so emitting one would
|
|
4218
|
+
* narrow nothing while the bar claims the grid is filtered.
|
|
4219
|
+
*/
|
|
4220
|
+
function isBlank(value) {
|
|
4221
|
+
if (value === null || value === undefined)
|
|
4222
|
+
return true;
|
|
4223
|
+
if (typeof value === 'string')
|
|
4224
|
+
return value.trim() === '';
|
|
4225
|
+
if (Array.isArray(value))
|
|
4226
|
+
return value.length === 0;
|
|
4227
|
+
// A half-typed datepicker yields an Invalid Date, which JSON.stringify writes as `null`.
|
|
4228
|
+
if (value instanceof Date)
|
|
4229
|
+
return Number.isNaN(value.getTime());
|
|
4230
|
+
return false;
|
|
4231
|
+
}
|
|
4232
|
+
/**
|
|
4233
|
+
* Whether two constraint values are the same QUESTION. Identity is not it: every `set` builds a
|
|
4234
|
+
* fresh draft, a multiselect hands back a fresh array per change, and two Dates at one instant
|
|
4235
|
+
* are one constraint. This is what `commit` bails on, so a paste-over-itself or a double Apply
|
|
4236
|
+
* cannot spend a request — the rule PACMS's order list used to hand-roll as `lastCommittedTerm`.
|
|
4237
|
+
*/
|
|
4238
|
+
function valueEquals(a, b) {
|
|
4239
|
+
if (a === b)
|
|
4240
|
+
return true;
|
|
4241
|
+
if (a instanceof Date && b instanceof Date)
|
|
4242
|
+
return a.getTime() === b.getTime();
|
|
4243
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
4244
|
+
return a.length === b.length && a.every((item, i) => valueEquals(item, b[i]));
|
|
4245
|
+
}
|
|
4246
|
+
return false;
|
|
4247
|
+
}
|
|
4248
|
+
function constraintEquals(a, b) {
|
|
4249
|
+
return a != null && a.operator === b.operator && valueEquals(a.value, b.value);
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* The filter engine. It knows nothing about columns: a filter has an id, a value type and a
|
|
4253
|
+
* constraint, and that is the entire vocabulary.
|
|
4254
|
+
*/
|
|
4255
|
+
function createFilterStore(definitions) {
|
|
4256
|
+
// Per-filter offered operators, resolved ONCE per declaration — so an invalid narrowing is
|
|
4257
|
+
// said once, not on every editor open — and re-resolved only where the inputs can actually
|
|
4258
|
+
// change: `setOptions`, because options' PRESENCE flips a filter between pick-list (`In`) and
|
|
4259
|
+
// plain operators. A per-store Map rather than the module-level WeakMap this first shipped
|
|
4260
|
+
// as: that memo was keyed on a definition object consumers mutate, so late-filled options
|
|
4261
|
+
// could freeze a filter's operators at the wrong answer.
|
|
4262
|
+
const resolveOffered = (id) => {
|
|
4263
|
+
const definition = definitions[id];
|
|
4264
|
+
const base = baseOperators(definition.kind, definition.options != null);
|
|
4265
|
+
let codes = base;
|
|
4266
|
+
if (definition.operators?.length) {
|
|
4267
|
+
const survivors = definition.operators.filter((operator) => base.includes(operator));
|
|
4268
|
+
// Same polarity as the column path's computeMatchModes: a bad entry is dropped and said
|
|
4269
|
+
// out loud, and a narrowing that leaves nothing falls back to the full list — an editor
|
|
4270
|
+
// with no operators would be unusable, which is worse than an unwanted one. Mostly
|
|
4271
|
+
// unreachable now that FilterConfig types `operators` per kind; kept for dynamically
|
|
4272
|
+
// built arrays.
|
|
4273
|
+
if (survivors.length < definition.operators.length) {
|
|
4274
|
+
console.error(`Filter '${definition.label}' narrows to operators its ${definition.kind} kind does not ` +
|
|
4275
|
+
`offer (${definition.operators.join(', ')}; offered: ${base.join(', ')}). ` +
|
|
4276
|
+
`Unsupported entries are ignored${survivors.length ? '' : '; falling back to the full list'}.`);
|
|
4277
|
+
}
|
|
4278
|
+
codes = survivors.length ? survivors : base;
|
|
4279
|
+
}
|
|
4280
|
+
return { codes, options: toOperatorOptions(definition.kind, codes) };
|
|
4281
|
+
};
|
|
4282
|
+
const offered = new Map();
|
|
4283
|
+
for (const id of Object.keys(definitions)) {
|
|
4284
|
+
offered.set(id, resolveOffered(id));
|
|
4285
|
+
}
|
|
4286
|
+
/**
|
|
4287
|
+
* The operator applied when nobody picked one. Options mean `In`, whatever the kind's default;
|
|
4288
|
+
* a narrowed filter defaults to its FIRST declared operator.
|
|
4289
|
+
*/
|
|
4290
|
+
const defaultOperatorFor = (id) => {
|
|
4291
|
+
const definition = definitions[id];
|
|
4292
|
+
if (definition.options != null)
|
|
4293
|
+
return MatchModeCodes.In;
|
|
4294
|
+
return definition.operators?.length
|
|
4295
|
+
? offered.get(id).codes[0]
|
|
4296
|
+
: DEFAULT_OPERATOR[definition.kind];
|
|
4297
|
+
};
|
|
4298
|
+
// Two stores, and the split is the whole point: `set` writes a DRAFT, `commit` publishes it.
|
|
4299
|
+
// A chip drawn off a draft would repeat the mistake the header's filter icon already shipped —
|
|
4300
|
+
// claiming the grid is narrowed on the first keystroke. Controls that apply on change
|
|
4301
|
+
// (multiselect, boolean, date) call both at once; a text box commits on Enter or blur.
|
|
4302
|
+
const drafts = signal(new Map());
|
|
4303
|
+
const writeDraft = (id, constraint) => {
|
|
4304
|
+
const next = new Map(drafts());
|
|
4305
|
+
if (constraint === null)
|
|
4306
|
+
next.delete(id);
|
|
4307
|
+
else
|
|
4308
|
+
next.set(id, constraint);
|
|
4309
|
+
drafts.set(next);
|
|
4310
|
+
};
|
|
4311
|
+
const committed = signal(new Map());
|
|
4312
|
+
const applied = computed(() => [...committed()].map(([id, constraint]) => ({
|
|
4313
|
+
id: id,
|
|
4314
|
+
label: definitions[id].label,
|
|
4315
|
+
kind: definitions[id].kind,
|
|
4316
|
+
operator: constraint.operator,
|
|
4317
|
+
operatorPhraseKey: OPERATOR_WORDS[definitions[id].kind][constraint.operator]?.phraseKey ??
|
|
4318
|
+
constraint.operator,
|
|
4319
|
+
value: constraint.value,
|
|
4320
|
+
})));
|
|
4321
|
+
const store = {
|
|
4322
|
+
definitions,
|
|
4323
|
+
applied,
|
|
4324
|
+
/**
|
|
4325
|
+
* A handle bound to one filter. This is the PLACEMENT api: it needs nothing from the component
|
|
4326
|
+
* tree, so the same filter can be driven from the bar, from a header cell, or from a drawer
|
|
4327
|
+
* rendered at app root. `value` is the DRAFT — a control shows what was typed, not what was
|
|
4328
|
+
* applied; `applied()` is the other question and the bar asks that one.
|
|
4329
|
+
*/
|
|
4330
|
+
get(id) {
|
|
4331
|
+
return {
|
|
4332
|
+
id: id,
|
|
4333
|
+
label: definitions[id].label,
|
|
4334
|
+
kind: definitions[id].kind,
|
|
4335
|
+
value: computed(() => drafts().get(id)?.value),
|
|
4336
|
+
operator: computed(() => drafts().get(id)?.operator),
|
|
4337
|
+
// Getters, not snapshots: options can land AFTER a handle is taken — the bar's editor
|
|
4338
|
+
// holds one for the life of the popover, and PACMS fills its lookups asynchronously —
|
|
4339
|
+
// and a snapshot left an already-open pick-list empty until it was reopened. The
|
|
4340
|
+
// operator list rides along, since setOptions can flip it to `In`.
|
|
4341
|
+
get operators() {
|
|
4342
|
+
return offered.get(id).options;
|
|
4343
|
+
},
|
|
4344
|
+
get defaultOperator() {
|
|
4345
|
+
return defaultOperatorFor(id);
|
|
4346
|
+
},
|
|
4347
|
+
get options() {
|
|
4348
|
+
return definitions[id].options;
|
|
4349
|
+
},
|
|
4350
|
+
set: (constraint) => store.set(id, constraint),
|
|
4351
|
+
commit: () => store.commit(id),
|
|
4352
|
+
reset: () => store.reset(id),
|
|
4353
|
+
};
|
|
4354
|
+
},
|
|
4355
|
+
/**
|
|
4356
|
+
* The ONE seam for choices that arrive after the store is built (PACMS fills its pick-lists
|
|
4357
|
+
* from lookups that race a deploy). Never write `definitions[id].options` by hand: options'
|
|
4358
|
+
* PRESENCE is what makes a filter a pick-list, so the setter also re-resolves the offered
|
|
4359
|
+
* operators — a hand write would leave a late-filled filter offering `Equals` on a control
|
|
4360
|
+
* that emits a list.
|
|
4361
|
+
*/
|
|
4362
|
+
setOptions(id, options) {
|
|
4363
|
+
definitions[id].options = options;
|
|
4364
|
+
offered.set(id, resolveOffered(id));
|
|
4365
|
+
},
|
|
4366
|
+
set(id, constraint) {
|
|
4367
|
+
const kind = definitions[id].kind;
|
|
4368
|
+
// Throws rather than logging and ignoring, which is what `computeMatchModes` does for a
|
|
4369
|
+
// column's OFFERED match modes. Different stakes: that one narrows a dropdown, so a bad
|
|
4370
|
+
// entry merely fails to show, while this one assembles a QUERY — dropping it silently would
|
|
4371
|
+
// leave the operator looking at unfiltered rows believing they are filtered.
|
|
4372
|
+
if (!ALLOWED_OPERATORS[kind].includes(constraint.operator)) {
|
|
4373
|
+
throw new Error(`Filter '${String(id)}' is ${kind}, which does not accept the '${constraint.operator}' ` +
|
|
4374
|
+
`operator (allowed: ${ALLOWED_OPERATORS[kind].join(', ')}). The generated paginator ` +
|
|
4375
|
+
`answers an unsupported match mode with InvalidMatchMode.`);
|
|
4376
|
+
}
|
|
4377
|
+
writeDraft(id, constraint);
|
|
4378
|
+
},
|
|
4379
|
+
/**
|
|
4380
|
+
* `set` + `commit` in one breath — the shape every PROGRAMMATIC write takes (a view's
|
|
4381
|
+
* `apply`, the sheet flow). The split API stays for controls, where draft-on-type /
|
|
4382
|
+
* publish-on-Apply is the point; but a bare `set` whose `commit` is forgotten (or written
|
|
4383
|
+
* with the wrong id — the pair repeats the string twice) fails silently: the draft is
|
|
4384
|
+
* written, nothing is published, and a view narrows by nothing under a labelled tab.
|
|
4385
|
+
*/
|
|
4386
|
+
setAndCommit(id, constraint) {
|
|
4387
|
+
store.set(id, constraint);
|
|
4388
|
+
store.commit(id);
|
|
4389
|
+
},
|
|
4390
|
+
/**
|
|
4391
|
+
* Publishes the draft. A blanked draft REMOVES the constraint rather than committing an empty
|
|
4392
|
+
* one, so `applied()` and the payload agree: no chip, no key, no `contains ''`.
|
|
4393
|
+
*/
|
|
4394
|
+
commit(id) {
|
|
4395
|
+
const draft = drafts().get(id);
|
|
4396
|
+
const current = committed();
|
|
4397
|
+
const shouldRemove = draft === undefined || isBlank(draft.value);
|
|
4398
|
+
// A new Map is a new identity even with identical contents, and everything downstream
|
|
4399
|
+
// reacts to identity — so a no-op commit would spend a request. The bail compares the
|
|
4400
|
+
// CONSTRAINT, not the draft object: every set() builds a fresh draft, and the case that
|
|
4401
|
+
// matters is an identical value re-committed (see valueEquals).
|
|
4402
|
+
if (shouldRemove ? !current.has(id) : constraintEquals(current.get(id), draft))
|
|
4403
|
+
return;
|
|
4404
|
+
const next = new Map(current);
|
|
4405
|
+
if (shouldRemove)
|
|
4406
|
+
next.delete(id);
|
|
4407
|
+
else
|
|
4408
|
+
next.set(id, draft);
|
|
4409
|
+
committed.set(next);
|
|
4410
|
+
},
|
|
4411
|
+
/**
|
|
4412
|
+
* The chip's `x`. Drops the DRAFT as well as the committed constraint: a surviving draft
|
|
4413
|
+
* leaves the control still showing the cleared text, and the next commit would restore a
|
|
4414
|
+
* filter nobody re-typed.
|
|
4415
|
+
*/
|
|
4416
|
+
reset(id) {
|
|
4417
|
+
if (drafts().has(id))
|
|
4418
|
+
writeDraft(id, null);
|
|
4419
|
+
if (!committed().has(id))
|
|
4420
|
+
return;
|
|
4421
|
+
const next = new Map(committed());
|
|
4422
|
+
next.delete(id);
|
|
4423
|
+
committed.set(next);
|
|
4424
|
+
},
|
|
4425
|
+
/**
|
|
4426
|
+
* The applied set, plain enough to store. Drafts are deliberately NOT included: a half-typed
|
|
4427
|
+
* value is not a state anyone should come back to, and the bar's whole claim is that what it
|
|
4428
|
+
* shows is what narrows the grid.
|
|
4429
|
+
*/
|
|
4430
|
+
snapshot() {
|
|
4431
|
+
const snapshot = {};
|
|
4432
|
+
for (const [id, constraint] of committed()) {
|
|
4433
|
+
snapshot[id] = {
|
|
4434
|
+
operator: constraint.operator,
|
|
4435
|
+
value: constraint.value,
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
return snapshot;
|
|
4439
|
+
},
|
|
4440
|
+
/**
|
|
4441
|
+
* Replaces the applied set. The drafts are written too, so reopening a restored chip shows
|
|
4442
|
+
* the value it is filtering by rather than an empty control.
|
|
4443
|
+
*
|
|
4444
|
+
* Ids the store no longer declares are dropped rather than carried: a filter removed from the
|
|
4445
|
+
* declaration in a later release would otherwise ride every request from anyone whose storage
|
|
4446
|
+
* still held it, with no chip able to name it and no way to clear it from the UI.
|
|
4447
|
+
*/
|
|
4448
|
+
restore(snapshot) {
|
|
4449
|
+
const next = new Map();
|
|
4450
|
+
for (const [id, constraint] of Object.entries(snapshot ?? {})) {
|
|
4451
|
+
if (!(id in definitions))
|
|
4452
|
+
continue;
|
|
4453
|
+
next.set(id, {
|
|
4454
|
+
operator: constraint.operator,
|
|
4455
|
+
value: reviveValue(constraint.value),
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
drafts.set(new Map(next));
|
|
4459
|
+
committed.set(next);
|
|
4460
|
+
},
|
|
4461
|
+
/** The bar's "Clear all". `reset` for every filter, drafts included. */
|
|
4462
|
+
clear() {
|
|
4463
|
+
if (drafts().size > 0)
|
|
4464
|
+
drafts.set(new Map());
|
|
4465
|
+
if (committed().size > 0)
|
|
4466
|
+
committed.set(new Map());
|
|
4467
|
+
},
|
|
4468
|
+
/** The `filters` half of `Filter` — what the generated paginator reads. */
|
|
4469
|
+
toFilterPayload() {
|
|
4470
|
+
const payload = {};
|
|
4471
|
+
for (const [id, constraint] of committed()) {
|
|
4472
|
+
payload[id] = [
|
|
4473
|
+
{ matchMode: constraint.operator, value: constraint.value },
|
|
4474
|
+
];
|
|
4475
|
+
}
|
|
4476
|
+
return payload;
|
|
4477
|
+
},
|
|
4478
|
+
};
|
|
4479
|
+
return store;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
/**
|
|
4483
|
+
* Renders the editor for ONE filter of a `spiderly-data-table`'s bar, replacing the control the
|
|
4484
|
+
* bar would have drawn for that filter's kind. Bind it to the filter's id; filters with no
|
|
4485
|
+
* matching template keep the built-in control, so a table opts in one filter at a time.
|
|
4486
|
+
*
|
|
4487
|
+
* This is the narrow job the directive has. Placing a filter ANYWHERE else — a drawer, a modal,
|
|
4488
|
+
* a header cell — needs no directive at all: `store.get(id)` returns the same handle and depends
|
|
4489
|
+
* on nothing in the component tree, which is why the store is the consumer's to own.
|
|
4490
|
+
*/
|
|
4491
|
+
class SpiderlyFilterTemplateDirective {
|
|
4492
|
+
constructor(template) {
|
|
4493
|
+
this.template = template;
|
|
4494
|
+
}
|
|
4495
|
+
/**
|
|
4496
|
+
* Without this the `let-` variable is `any` at every consumer, however well the context is
|
|
4497
|
+
* declared — a directive's injected `TemplateRef<C>` never reaches the template that declares
|
|
4498
|
+
* it. Same mechanism as `SpiderlyCellTemplateDirective`.
|
|
4499
|
+
*/
|
|
4500
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
4501
|
+
return true;
|
|
4502
|
+
}
|
|
4503
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
4504
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyFilterTemplateDirective, isStandalone: true, selector: "ng-template[spiderlyFilterTemplate]", inputs: { filterId: ["spiderlyFilterTemplate", "filterId"] }, ngImport: i0 }); }
|
|
4505
|
+
}
|
|
4506
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFilterTemplateDirective, decorators: [{
|
|
4507
|
+
type: Directive,
|
|
4508
|
+
args: [{
|
|
4509
|
+
selector: 'ng-template[spiderlyFilterTemplate]',
|
|
4510
|
+
}]
|
|
4511
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { filterId: [{
|
|
4512
|
+
type: Input,
|
|
4513
|
+
args: ['spiderlyFilterTemplate']
|
|
4514
|
+
}] } });
|
|
4515
|
+
|
|
4516
|
+
/** One option's word for one value — the honest raw value when no option matches (failed lookup). */
|
|
4517
|
+
function spellOptionValue(options, value) {
|
|
4518
|
+
return String(options?.find((option) => option.value === value)?.label ?? value);
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Lowercased and stripped of diacritics for matching. NFD decomposition handles č/ć/š/ž, but NOT
|
|
4522
|
+
* đ — it is its own letter (U+0111), not a d carrying a mark, so it decomposes to nothing and has
|
|
4523
|
+
* to be mapped by hand.
|
|
4524
|
+
*
|
|
4525
|
+
* It maps to "dj", which is what the rest of this workspace already does (pa-cms's
|
|
4526
|
+
* `normalizePlaceQuery` and `toAscii`, the backend's `PlaceNameNormalizer`) and what someone
|
|
4527
|
+
* without a Serbian layout actually types. Mapping it to "d" — as this first shipped — folds
|
|
4528
|
+
* "Đorđe" to "dorde", so typing "djordje" finds nothing: the exact case the comment claimed to
|
|
4529
|
+
* fix, and one no spec covered because the test used "drzava", which has no đ in it.
|
|
4530
|
+
*/
|
|
4531
|
+
function foldForSearch(value) {
|
|
4532
|
+
return value
|
|
4533
|
+
.normalize('NFD')
|
|
4534
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
4535
|
+
.replace(/\u0111/g, 'dj')
|
|
4536
|
+
.replace(/\u0110/g, 'Dj')
|
|
4537
|
+
.toLowerCase()
|
|
4538
|
+
.trim();
|
|
4539
|
+
}
|
|
4540
|
+
/**
|
|
4541
|
+
* The visible home of every applied constraint. This is what licenses a hidden column keeping its
|
|
4542
|
+
* filter: the header is no longer the only place a filter can be seen, so hiding a column stops
|
|
4543
|
+
* meaning "silently drop what it was filtering by".
|
|
4544
|
+
*
|
|
4545
|
+
* Everything drawn here comes from `applied()`, never from a draft — a chip over a half-typed
|
|
4546
|
+
* value would repeat the mistake the header's filter icon shipped, claiming the grid is narrowed
|
|
4547
|
+
* while it still shows every row.
|
|
4548
|
+
*/
|
|
4549
|
+
class SpiderlyFilterBarComponent {
|
|
4550
|
+
constructor() {
|
|
4551
|
+
this.filters = input.required();
|
|
4552
|
+
/** The current query's row count, supplied by whoever owns the query. Null hides the counter. */
|
|
4553
|
+
this.totalRecords = input(null);
|
|
4554
|
+
/**
|
|
4555
|
+
* What the grid is ordered by, in priority order and already LABELLED — the bar knows nothing
|
|
4556
|
+
* about columns, so whoever owns the sort resolves the names.
|
|
4557
|
+
*/
|
|
4558
|
+
this.sort = input([]);
|
|
4559
|
+
/**
|
|
4560
|
+
* What the picker offers, labelled by the owner — every sortable column, HIDDEN ones
|
|
4561
|
+
* included (their ordering is otherwise unreachable, which is the trap the picker closes).
|
|
4562
|
+
* Empty keeps the chip read-only and the picker off.
|
|
4563
|
+
*/
|
|
4564
|
+
this.sortOptions = input([]);
|
|
4565
|
+
/** Whether the grid already sits on its default ordering — gates the reset row, nothing else. */
|
|
4566
|
+
this.sortIsDefault = input(true);
|
|
4567
|
+
/** Asked for, never done here — see the button's comment. */
|
|
4568
|
+
this.clearAll = output();
|
|
4569
|
+
/** A single-sort ask. Multi-sort stays a header gesture (ctrl-click); the picker replaces. */
|
|
4570
|
+
this.sortPick = output();
|
|
4571
|
+
/** Back to the owner's default ordering — the owner knows what that resolves to, not the bar. */
|
|
4572
|
+
this.sortReset = output();
|
|
4573
|
+
this.transloco = inject(TranslocoService);
|
|
4574
|
+
/** For the chip's date wording — the same locale mechanism the table's cells format with. */
|
|
4575
|
+
this.locale = inject(LOCALE_ID);
|
|
4576
|
+
/**
|
|
4577
|
+
* Controls a consumer projected for specific filters, collected by whoever hosts this bar. A
|
|
4578
|
+
* filter with no template keeps the control the bar draws for its kind, so a table opts in one
|
|
4579
|
+
* filter at a time.
|
|
4580
|
+
*/
|
|
4581
|
+
this.controlTemplates = input(undefined);
|
|
4582
|
+
this.isAddOpen = signal(false);
|
|
4583
|
+
this.isSortOpen = signal(false);
|
|
4584
|
+
/** Cleared when the popover closes, so reopening never starts inside someone's old query. */
|
|
4585
|
+
this.addSearch = signal('');
|
|
4586
|
+
/** The filter whose control is open. One at a time: the bar edits, it is not a form. */
|
|
4587
|
+
this.editing = signal(null);
|
|
4588
|
+
/**
|
|
4589
|
+
* `addable` narrowed by the search box. Matching is unaccented and case-folded because that is
|
|
4590
|
+
* how the label gets typed: "drzava" for "Država" on any keyboard without a Serbian layout. The
|
|
4591
|
+
* backend's own product search is unaccented for the same reason.
|
|
4592
|
+
*/
|
|
4593
|
+
this.offered = computed(() => {
|
|
4594
|
+
const needle = foldForSearch(this.addSearch());
|
|
4595
|
+
if (!needle)
|
|
4596
|
+
return this.addable();
|
|
4597
|
+
return this.addable().filter((option) => foldForSearch(option.label).includes(needle));
|
|
4598
|
+
});
|
|
4599
|
+
/**
|
|
4600
|
+
* What "+ Filter" offers: every declared filter that is not already on the bar. Sourced from the
|
|
4601
|
+
* DEFINITIONS, so a filter reaches this list whether or not it has a column, and whether or not
|
|
4602
|
+
* that column is visible. That is the whole reason the bar exists.
|
|
4603
|
+
*
|
|
4604
|
+
* `offered: false` filters stay out — they have a dedicated control of their own on the page
|
|
4605
|
+
* (PACMS's order search box), so the generic entry point would give one question two homes.
|
|
4606
|
+
* Their chips still render: the applied list above reads no such flag.
|
|
4607
|
+
*/
|
|
4608
|
+
this.addable = computed(() => {
|
|
4609
|
+
const onBar = new Set(this.filters().applied().map((chip) => chip.id));
|
|
4610
|
+
return Object.entries(this.filters().definitions)
|
|
4611
|
+
.filter(([id, definition]) => !onBar.has(id) && definition.offered !== false)
|
|
4612
|
+
.map(([id, definition]) => ({ id, ...definition }));
|
|
4613
|
+
});
|
|
4614
|
+
/**
|
|
4615
|
+
* The menu's rows: each option decorated with its ACTIVE direction's arrow (any key, not
|
|
4616
|
+
* just the primary), or null. A `computed`, not a per-row method call in the binding — the
|
|
4617
|
+
* popover's content is projected, so it change-detects even while closed, and a `find` per
|
|
4618
|
+
* option per pass is exactly what `operatorChoices` documents against.
|
|
4619
|
+
*/
|
|
4620
|
+
this.sortRows = computed(() => {
|
|
4621
|
+
const active = new Map(this.sort().map((key) => [key.field, key.descending]));
|
|
4622
|
+
return this.sortOptions().map((option) => {
|
|
4623
|
+
const descending = active.get(option.field);
|
|
4624
|
+
return {
|
|
4625
|
+
...option,
|
|
4626
|
+
arrow: descending == null ? null : descending ? '↓' : '↑',
|
|
4627
|
+
};
|
|
4628
|
+
});
|
|
4629
|
+
});
|
|
4630
|
+
/**
|
|
4631
|
+
* The open editor's operator options, already translated — p-select takes labels, not keys.
|
|
4632
|
+
*
|
|
4178
4633
|
* A `computed`, not a method in the binding: both inputs are fixed for the life of an open
|
|
4179
4634
|
* editor, and PrimeNG's `Select.options` setter deep-compares whatever it is handed — so a
|
|
4180
4635
|
* freshly mapped array per change-detection pass paid for a recursive structural compare to
|
|
@@ -5501,7 +5956,10 @@ class SpiderlyDataTableComponent {
|
|
|
5501
5956
|
* reading exactly the keys they already wrote.
|
|
5502
5957
|
*/
|
|
5503
5958
|
get viewScope() {
|
|
5504
|
-
return this.
|
|
5959
|
+
return this.viewScopeOf(this.activeViewId);
|
|
5960
|
+
}
|
|
5961
|
+
viewScopeOf(viewId) {
|
|
5962
|
+
return viewId ? `:${viewId}` : '';
|
|
5505
5963
|
}
|
|
5506
5964
|
/**
|
|
5507
5965
|
* Where the applied filters live. Follows `stateStorage` rather than the layout's always-local
|
|
@@ -5511,8 +5969,11 @@ class SpiderlyDataTableComponent {
|
|
|
5511
5969
|
* durable preference like a column's width.
|
|
5512
5970
|
*/
|
|
5513
5971
|
get filtersStateKey() {
|
|
5972
|
+
return this.filtersStateKeyOf(this.activeViewId);
|
|
5973
|
+
}
|
|
5974
|
+
filtersStateKeyOf(viewId) {
|
|
5514
5975
|
return this.resolvedStateKey
|
|
5515
|
-
? `${this.resolvedStateKey}${this.
|
|
5976
|
+
? `${this.resolvedStateKey}${this.viewScopeOf(viewId)}:filters`
|
|
5516
5977
|
: null;
|
|
5517
5978
|
}
|
|
5518
5979
|
get filterStorage() {
|
|
@@ -5605,17 +6066,18 @@ class SpiderlyDataTableComponent {
|
|
|
5605
6066
|
* the empty state the operator left — they cleared it, and F5 must give the cleared state
|
|
5606
6067
|
* back. On a table with no views, `view` is null and this is a plain restore of the
|
|
5607
6068
|
* `persistAppliedFilters` snapshot.
|
|
6069
|
+
*
|
|
6070
|
+
* `viewFilterPayload` passes a scratch store and another view's key, to read what entering
|
|
6071
|
+
* that view would send.
|
|
5608
6072
|
*/
|
|
5609
|
-
enterViewFilters(view, semantics) {
|
|
5610
|
-
if (!
|
|
6073
|
+
enterViewFilters(view, semantics, store = this.filters, key = this.filtersStateKey) {
|
|
6074
|
+
if (!store)
|
|
5611
6075
|
return;
|
|
5612
|
-
const stored = !view?.transient && this.
|
|
5613
|
-
? readStoredJson(this.filterStorage, this.filtersStateKey)
|
|
5614
|
-
: null;
|
|
6076
|
+
const stored = !view?.transient && key ? readStoredJson(this.filterStorage, key) : null;
|
|
5615
6077
|
if (stored)
|
|
5616
|
-
|
|
6078
|
+
store.restore(stored);
|
|
5617
6079
|
else if (view?.transient || semantics === 'click')
|
|
5618
|
-
view?.apply?.(
|
|
6080
|
+
view?.apply?.(store);
|
|
5619
6081
|
}
|
|
5620
6082
|
get layoutStateKey() {
|
|
5621
6083
|
return this.resolvedStateKey
|
|
@@ -6035,6 +6497,39 @@ class SpiderlyDataTableComponent {
|
|
|
6035
6497
|
this.refetchFromFirstPage();
|
|
6036
6498
|
}
|
|
6037
6499
|
}
|
|
6500
|
+
/**
|
|
6501
|
+
* The `filters` payload a view's own list request carries — the same shape the store's
|
|
6502
|
+
* `toFilterPayload()` emits — so a consumer's count query can narrow each view by the bar
|
|
6503
|
+
* filters the operator saved on it, and a `TableView.count` stays the number of rows that tab
|
|
6504
|
+
* lists: on screen for the active view, after a click for the others (spiderly#439).
|
|
6505
|
+
*
|
|
6506
|
+
* - The ACTIVE view answers with the live filters. Not with what a click would enter: clicking
|
|
6507
|
+
* the active tab re-asks its question when the operator has cleared the bar, but the list on
|
|
6508
|
+
* screen, and so its count, has not.
|
|
6509
|
+
* - Any other view answers with what a click would enter (`enterViewFilters`): the filters
|
|
6510
|
+
* saved on it, else its `apply` — always `apply` for a transient view. That runs on a scratch
|
|
6511
|
+
* `createFilterStore` of the same declarations, so nothing switches, persists or re-queries.
|
|
6512
|
+
* Untracked: the scratch store's signal writes would otherwise make a read inside a
|
|
6513
|
+
* `computed` throw NG0600.
|
|
6514
|
+
*
|
|
6515
|
+
* `{}` on a table without `[filters]`, whose requests carry none; `null` for an id that is not
|
|
6516
|
+
* in `[views]`.
|
|
6517
|
+
*/
|
|
6518
|
+
viewFilterPayload(viewId) {
|
|
6519
|
+
const view = this.viewById(viewId);
|
|
6520
|
+
if (!view)
|
|
6521
|
+
return null;
|
|
6522
|
+
if (!this.filters)
|
|
6523
|
+
return {};
|
|
6524
|
+
if (view.id === this.activeViewId)
|
|
6525
|
+
return this.filters.toFilterPayload();
|
|
6526
|
+
const definitions = this.filters.definitions;
|
|
6527
|
+
return untracked(() => {
|
|
6528
|
+
const scratch = createFilterStore(definitions);
|
|
6529
|
+
this.enterViewFilters(view, 'click', scratch, this.filtersStateKeyOf(view.id));
|
|
6530
|
+
return scratch.toFilterPayload();
|
|
6531
|
+
});
|
|
6532
|
+
}
|
|
6038
6533
|
/**
|
|
6039
6534
|
* The filter this column stands for. Defaults to the column's own `field`, because a store id
|
|
6040
6535
|
* IS a backend property name — `toFilterPayload` emits it straight into `Filter.filters`. So
|
|
@@ -6933,490 +7428,35 @@ class Action {
|
|
|
6933
7428
|
class Column {
|
|
6934
7429
|
constructor({ name, field, filterType, filterId, showMatchModes, filterField, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
6935
7430
|
this.name = name;
|
|
6936
|
-
this.field = field;
|
|
6937
|
-
this.filterType = filterType;
|
|
6938
|
-
this.filterId = filterId;
|
|
6939
|
-
this.showMatchModes = showMatchModes;
|
|
6940
|
-
this.filterField = filterField;
|
|
6941
|
-
this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
|
|
6942
|
-
this.actions = actions;
|
|
6943
|
-
this.editable = editable;
|
|
6944
|
-
this.showTime = showTime;
|
|
6945
|
-
this.decimalPlaces = decimalPlaces;
|
|
6946
|
-
this.sortable = sortable;
|
|
6947
|
-
this.visible = visible;
|
|
6948
|
-
this.lockVisible = lockVisible;
|
|
6949
|
-
this.width = width;
|
|
6950
|
-
this.onCellClick = onCellClick;
|
|
6951
|
-
}
|
|
6952
|
-
}
|
|
6953
|
-
class RowClickEvent {
|
|
6954
|
-
constructor({ index, id, additionalIndexes, } = {}) {
|
|
6955
|
-
this.index = index;
|
|
6956
|
-
this.id = id;
|
|
6957
|
-
this.additionalIndexes = additionalIndexes;
|
|
6958
|
-
}
|
|
6959
|
-
}
|
|
6960
|
-
class AllClickEvent {
|
|
6961
|
-
constructor({ checked, additionalIndexes, } = {}) {
|
|
6962
|
-
this.checked = checked;
|
|
6963
|
-
this.additionalIndexes = additionalIndexes;
|
|
6964
|
-
}
|
|
6965
|
-
}
|
|
6966
|
-
|
|
6967
|
-
var MatchModeCodes;
|
|
6968
|
-
(function (MatchModeCodes) {
|
|
6969
|
-
MatchModeCodes["StartsWith"] = "startsWith";
|
|
6970
|
-
MatchModeCodes["Contains"] = "contains";
|
|
6971
|
-
MatchModeCodes["Equals"] = "equals";
|
|
6972
|
-
MatchModeCodes["LessThan"] = "lessThan";
|
|
6973
|
-
MatchModeCodes["GreaterThan"] = "greaterThan";
|
|
6974
|
-
MatchModeCodes["In"] = "in";
|
|
6975
|
-
})(MatchModeCodes || (MatchModeCodes = {}));
|
|
6976
|
-
|
|
6977
|
-
/**
|
|
6978
|
-
* What each value type may be asked — the ONE operator table, mirroring what the generated
|
|
6979
|
-
* paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
|
|
6980
|
-
* with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
|
|
6981
|
-
*
|
|
6982
|
-
* Deliberately a LEAF module (imports nothing but the codes) so every consumer can derive from
|
|
6983
|
-
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), the
|
|
6984
|
-
* compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`), and the data view's
|
|
6985
|
-
* kind-level lists (via `operatorOptionsForKind`). These used to be hand-kept copies; edit the
|
|
6986
|
-
* table here and every surface follows.
|
|
6987
|
-
*
|
|
6988
|
-
* Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
|
|
6989
|
-
* "more/after" — the order the header dropdowns always shipped).
|
|
6990
|
-
*/
|
|
6991
|
-
const ALLOWED_OPERATORS = {
|
|
6992
|
-
text: [
|
|
6993
|
-
MatchModeCodes.StartsWith,
|
|
6994
|
-
MatchModeCodes.Contains,
|
|
6995
|
-
MatchModeCodes.Equals,
|
|
6996
|
-
],
|
|
6997
|
-
number: [
|
|
6998
|
-
MatchModeCodes.Equals,
|
|
6999
|
-
MatchModeCodes.LessThan,
|
|
7000
|
-
MatchModeCodes.GreaterThan,
|
|
7001
|
-
MatchModeCodes.In,
|
|
7002
|
-
],
|
|
7003
|
-
boolean: [MatchModeCodes.Equals],
|
|
7004
|
-
date: [
|
|
7005
|
-
MatchModeCodes.Equals,
|
|
7006
|
-
MatchModeCodes.LessThan,
|
|
7007
|
-
MatchModeCodes.GreaterThan,
|
|
7008
|
-
],
|
|
7009
|
-
};
|
|
7010
|
-
|
|
7011
|
-
const OPERATOR_WORDS = {
|
|
7012
|
-
text: {
|
|
7013
|
-
[MatchModeCodes.StartsWith]: {
|
|
7014
|
-
pickerKey: 'StartsWith',
|
|
7015
|
-
phraseKey: 'FilterChipStartsWith',
|
|
7016
|
-
},
|
|
7017
|
-
[MatchModeCodes.Contains]: {
|
|
7018
|
-
pickerKey: 'Contains',
|
|
7019
|
-
phraseKey: 'FilterChipContains',
|
|
7020
|
-
},
|
|
7021
|
-
[MatchModeCodes.Equals]: {
|
|
7022
|
-
pickerKey: 'Equals',
|
|
7023
|
-
phraseKey: 'FilterChipEquals',
|
|
7024
|
-
},
|
|
7025
|
-
// No `In` for text: ALLOWED_OPERATORS.text does not carry it, so the entry was unreachable —
|
|
7026
|
-
// and it made `textFilter({ options })` look supported when the store throws on it.
|
|
7027
|
-
},
|
|
7028
|
-
number: {
|
|
7029
|
-
[MatchModeCodes.Equals]: {
|
|
7030
|
-
pickerKey: 'Equals',
|
|
7031
|
-
phraseKey: 'FilterChipEquals',
|
|
7032
|
-
},
|
|
7033
|
-
[MatchModeCodes.GreaterThan]: {
|
|
7034
|
-
pickerKey: 'MoreThan',
|
|
7035
|
-
phraseKey: 'FilterChipGreaterThan',
|
|
7036
|
-
},
|
|
7037
|
-
[MatchModeCodes.LessThan]: {
|
|
7038
|
-
pickerKey: 'LessThan',
|
|
7039
|
-
phraseKey: 'FilterChipLessThan',
|
|
7040
|
-
},
|
|
7041
|
-
// The picker never renders for a pick-list (one operator), but the CHIP always does.
|
|
7042
|
-
[MatchModeCodes.In]: { pickerKey: 'In', phraseKey: 'FilterChipIn' },
|
|
7043
|
-
},
|
|
7044
|
-
boolean: {
|
|
7045
|
-
[MatchModeCodes.Equals]: {
|
|
7046
|
-
pickerKey: 'Equals',
|
|
7047
|
-
phraseKey: 'FilterChipEquals',
|
|
7048
|
-
},
|
|
7049
|
-
},
|
|
7050
|
-
date: {
|
|
7051
|
-
[MatchModeCodes.Equals]: {
|
|
7052
|
-
pickerKey: 'OnDate',
|
|
7053
|
-
phraseKey: 'FilterChipEquals',
|
|
7054
|
-
},
|
|
7055
|
-
[MatchModeCodes.LessThan]: {
|
|
7056
|
-
pickerKey: 'DatesBefore',
|
|
7057
|
-
phraseKey: 'FilterChipBefore',
|
|
7058
|
-
},
|
|
7059
|
-
[MatchModeCodes.GreaterThan]: {
|
|
7060
|
-
pickerKey: 'DatesAfter',
|
|
7061
|
-
phraseKey: 'FilterChipAfter',
|
|
7062
|
-
},
|
|
7063
|
-
},
|
|
7064
|
-
};
|
|
7065
|
-
/**
|
|
7066
|
-
* `In` and options are the same fact seen from two sides: `In` needs a list of values, and the
|
|
7067
|
-
* editor only draws one when the filter declares options. So options mean `In` and nothing else,
|
|
7068
|
-
* and their absence rules `In` out — offering it on a plain number filter would hand the
|
|
7069
|
-
* operator a mode with no control behind it.
|
|
7070
|
-
*
|
|
7071
|
-
* Widened first: the per-kind tuples are literal, so `boolean`'s `[Equals]` makes the `In`
|
|
7072
|
-
* comparison a "no overlap" error rather than an empty result.
|
|
7073
|
-
*/
|
|
7074
|
-
function baseOperators(kind, hasOptions) {
|
|
7075
|
-
const accepted = ALLOWED_OPERATORS[kind];
|
|
7076
|
-
return accepted.filter((value) => hasOptions ? value === MatchModeCodes.In : value !== MatchModeCodes.In);
|
|
7077
|
-
}
|
|
7078
|
-
function toOperatorOptions(kind, operators) {
|
|
7079
|
-
return operators.map((value) => ({
|
|
7080
|
-
value,
|
|
7081
|
-
labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
|
|
7082
|
-
}));
|
|
7083
|
-
}
|
|
7084
|
-
/**
|
|
7085
|
-
* A KIND's operator picker options, for callers with no filter behind them — today that is
|
|
7086
|
-
* `spiderly-data-view`, whose own filter config derives its operator lists here instead of
|
|
7087
|
-
* hand-keeping copies (the drift `allowed-operators.ts` exists to end). Per-FILTER narrowing
|
|
7088
|
-
* lives in the store (`createFilterStore` resolves it once per declaration), so this cannot be
|
|
7089
|
-
* handed a fake definition to satisfy — that shape shipped once and interpolated an empty
|
|
7090
|
-
* filter name into its own error message.
|
|
7091
|
-
*/
|
|
7092
|
-
function operatorOptionsForKind(kind, hasOptions = false) {
|
|
7093
|
-
return toOperatorOptions(kind, baseOperators(kind, hasOptions));
|
|
7094
|
-
}
|
|
7095
|
-
/**
|
|
7096
|
-
* The operator applied when nobody picked one, which has to be what a person MEANS by filling in
|
|
7097
|
-
* the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
|
|
7098
|
-
* `Equals` — a date equality against a TIMESTAMP matches only the row written in that exact
|
|
7099
|
-
* second, and answers with an empty grid rather than an error (PACMS ran into this on all three
|
|
7100
|
-
* of its date columns, Filip 2026-08-28).
|
|
7101
|
-
*/
|
|
7102
|
-
const DEFAULT_OPERATOR = {
|
|
7103
|
-
text: MatchModeCodes.Contains,
|
|
7104
|
-
number: MatchModeCodes.Equals,
|
|
7105
|
-
boolean: MatchModeCodes.Equals,
|
|
7106
|
-
date: MatchModeCodes.GreaterThan,
|
|
7107
|
-
};
|
|
7108
|
-
function textFilter(config) {
|
|
7109
|
-
return {
|
|
7110
|
-
kind: 'text',
|
|
7111
|
-
label: config.label,
|
|
7112
|
-
operators: config.operators,
|
|
7113
|
-
offered: config.offered,
|
|
7114
|
-
};
|
|
7115
|
-
}
|
|
7116
|
-
/**
|
|
7117
|
-
* Only a NUMBER filter accepts options, and the asymmetry is forced rather than chosen: options
|
|
7118
|
-
* mean `In`, and `In` is the one operator ALLOWED_OPERATORS grants to numbers alone — because the
|
|
7119
|
-
* generated paginator answers it with InvalidMatchMode on a string column. A text pick-list used
|
|
7120
|
-
* to type-check and then throw at the first `set`.
|
|
7121
|
-
*/
|
|
7122
|
-
function numberFilter(config) {
|
|
7123
|
-
return {
|
|
7124
|
-
kind: 'number',
|
|
7125
|
-
label: config.label,
|
|
7126
|
-
options: config.options,
|
|
7127
|
-
operators: config.operators,
|
|
7128
|
-
offered: config.offered,
|
|
7129
|
-
};
|
|
7130
|
-
}
|
|
7131
|
-
function booleanFilter(config) {
|
|
7132
|
-
return {
|
|
7133
|
-
kind: 'boolean',
|
|
7134
|
-
label: config.label,
|
|
7135
|
-
operators: config.operators,
|
|
7136
|
-
offered: config.offered,
|
|
7137
|
-
};
|
|
7138
|
-
}
|
|
7139
|
-
function dateFilter(config) {
|
|
7140
|
-
return {
|
|
7141
|
-
kind: 'date',
|
|
7142
|
-
label: config.label,
|
|
7143
|
-
operators: config.operators,
|
|
7144
|
-
offered: config.offered,
|
|
7145
|
-
};
|
|
7146
|
-
}
|
|
7147
|
-
/** The shape `Date.toJSON` writes, which is the only value here JSON cannot round-trip itself. */
|
|
7148
|
-
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
7149
|
-
function reviveValue(value) {
|
|
7150
|
-
if (typeof value === 'string' && ISO_DATE.test(value))
|
|
7151
|
-
return new Date(value);
|
|
7152
|
-
if (Array.isArray(value))
|
|
7153
|
-
return value.map(reviveValue);
|
|
7154
|
-
return value;
|
|
7155
|
-
}
|
|
7156
|
-
/**
|
|
7157
|
-
* A blanked value is NOT a constraint. `contains ''` matches every row, so emitting one would
|
|
7158
|
-
* narrow nothing while the bar claims the grid is filtered.
|
|
7159
|
-
*/
|
|
7160
|
-
function isBlank(value) {
|
|
7161
|
-
if (value === null || value === undefined)
|
|
7162
|
-
return true;
|
|
7163
|
-
if (typeof value === 'string')
|
|
7164
|
-
return value.trim() === '';
|
|
7165
|
-
if (Array.isArray(value))
|
|
7166
|
-
return value.length === 0;
|
|
7167
|
-
// A half-typed datepicker yields an Invalid Date, which JSON.stringify writes as `null`.
|
|
7168
|
-
if (value instanceof Date)
|
|
7169
|
-
return Number.isNaN(value.getTime());
|
|
7170
|
-
return false;
|
|
7171
|
-
}
|
|
7172
|
-
/**
|
|
7173
|
-
* Whether two constraint values are the same QUESTION. Identity is not it: every `set` builds a
|
|
7174
|
-
* fresh draft, a multiselect hands back a fresh array per change, and two Dates at one instant
|
|
7175
|
-
* are one constraint. This is what `commit` bails on, so a paste-over-itself or a double Apply
|
|
7176
|
-
* cannot spend a request — the rule PACMS's order list used to hand-roll as `lastCommittedTerm`.
|
|
7177
|
-
*/
|
|
7178
|
-
function valueEquals(a, b) {
|
|
7179
|
-
if (a === b)
|
|
7180
|
-
return true;
|
|
7181
|
-
if (a instanceof Date && b instanceof Date)
|
|
7182
|
-
return a.getTime() === b.getTime();
|
|
7183
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
7184
|
-
return a.length === b.length && a.every((item, i) => valueEquals(item, b[i]));
|
|
7431
|
+
this.field = field;
|
|
7432
|
+
this.filterType = filterType;
|
|
7433
|
+
this.filterId = filterId;
|
|
7434
|
+
this.showMatchModes = showMatchModes;
|
|
7435
|
+
this.filterField = filterField;
|
|
7436
|
+
this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
|
|
7437
|
+
this.actions = actions;
|
|
7438
|
+
this.editable = editable;
|
|
7439
|
+
this.showTime = showTime;
|
|
7440
|
+
this.decimalPlaces = decimalPlaces;
|
|
7441
|
+
this.sortable = sortable;
|
|
7442
|
+
this.visible = visible;
|
|
7443
|
+
this.lockVisible = lockVisible;
|
|
7444
|
+
this.width = width;
|
|
7445
|
+
this.onCellClick = onCellClick;
|
|
7185
7446
|
}
|
|
7186
|
-
return false;
|
|
7187
7447
|
}
|
|
7188
|
-
|
|
7189
|
-
|
|
7448
|
+
class RowClickEvent {
|
|
7449
|
+
constructor({ index, id, additionalIndexes, } = {}) {
|
|
7450
|
+
this.index = index;
|
|
7451
|
+
this.id = id;
|
|
7452
|
+
this.additionalIndexes = additionalIndexes;
|
|
7453
|
+
}
|
|
7190
7454
|
}
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
function createFilterStore(definitions) {
|
|
7196
|
-
// Per-filter offered operators, resolved ONCE per declaration — so an invalid narrowing is
|
|
7197
|
-
// said once, not on every editor open — and re-resolved only where the inputs can actually
|
|
7198
|
-
// change: `setOptions`, because options' PRESENCE flips a filter between pick-list (`In`) and
|
|
7199
|
-
// plain operators. A per-store Map rather than the module-level WeakMap this first shipped
|
|
7200
|
-
// as: that memo was keyed on a definition object consumers mutate, so late-filled options
|
|
7201
|
-
// could freeze a filter's operators at the wrong answer.
|
|
7202
|
-
const resolveOffered = (id) => {
|
|
7203
|
-
const definition = definitions[id];
|
|
7204
|
-
const base = baseOperators(definition.kind, definition.options != null);
|
|
7205
|
-
let codes = base;
|
|
7206
|
-
if (definition.operators?.length) {
|
|
7207
|
-
const survivors = definition.operators.filter((operator) => base.includes(operator));
|
|
7208
|
-
// Same polarity as the column path's computeMatchModes: a bad entry is dropped and said
|
|
7209
|
-
// out loud, and a narrowing that leaves nothing falls back to the full list — an editor
|
|
7210
|
-
// with no operators would be unusable, which is worse than an unwanted one. Mostly
|
|
7211
|
-
// unreachable now that FilterConfig types `operators` per kind; kept for dynamically
|
|
7212
|
-
// built arrays.
|
|
7213
|
-
if (survivors.length < definition.operators.length) {
|
|
7214
|
-
console.error(`Filter '${definition.label}' narrows to operators its ${definition.kind} kind does not ` +
|
|
7215
|
-
`offer (${definition.operators.join(', ')}; offered: ${base.join(', ')}). ` +
|
|
7216
|
-
`Unsupported entries are ignored${survivors.length ? '' : '; falling back to the full list'}.`);
|
|
7217
|
-
}
|
|
7218
|
-
codes = survivors.length ? survivors : base;
|
|
7219
|
-
}
|
|
7220
|
-
return { codes, options: toOperatorOptions(definition.kind, codes) };
|
|
7221
|
-
};
|
|
7222
|
-
const offered = new Map();
|
|
7223
|
-
for (const id of Object.keys(definitions)) {
|
|
7224
|
-
offered.set(id, resolveOffered(id));
|
|
7455
|
+
class AllClickEvent {
|
|
7456
|
+
constructor({ checked, additionalIndexes, } = {}) {
|
|
7457
|
+
this.checked = checked;
|
|
7458
|
+
this.additionalIndexes = additionalIndexes;
|
|
7225
7459
|
}
|
|
7226
|
-
/**
|
|
7227
|
-
* The operator applied when nobody picked one. Options mean `In`, whatever the kind's default;
|
|
7228
|
-
* a narrowed filter defaults to its FIRST declared operator.
|
|
7229
|
-
*/
|
|
7230
|
-
const defaultOperatorFor = (id) => {
|
|
7231
|
-
const definition = definitions[id];
|
|
7232
|
-
if (definition.options != null)
|
|
7233
|
-
return MatchModeCodes.In;
|
|
7234
|
-
return definition.operators?.length
|
|
7235
|
-
? offered.get(id).codes[0]
|
|
7236
|
-
: DEFAULT_OPERATOR[definition.kind];
|
|
7237
|
-
};
|
|
7238
|
-
// Two stores, and the split is the whole point: `set` writes a DRAFT, `commit` publishes it.
|
|
7239
|
-
// A chip drawn off a draft would repeat the mistake the header's filter icon already shipped —
|
|
7240
|
-
// claiming the grid is narrowed on the first keystroke. Controls that apply on change
|
|
7241
|
-
// (multiselect, boolean, date) call both at once; a text box commits on Enter or blur.
|
|
7242
|
-
const drafts = signal(new Map());
|
|
7243
|
-
const writeDraft = (id, constraint) => {
|
|
7244
|
-
const next = new Map(drafts());
|
|
7245
|
-
if (constraint === null)
|
|
7246
|
-
next.delete(id);
|
|
7247
|
-
else
|
|
7248
|
-
next.set(id, constraint);
|
|
7249
|
-
drafts.set(next);
|
|
7250
|
-
};
|
|
7251
|
-
const committed = signal(new Map());
|
|
7252
|
-
const applied = computed(() => [...committed()].map(([id, constraint]) => ({
|
|
7253
|
-
id: id,
|
|
7254
|
-
label: definitions[id].label,
|
|
7255
|
-
kind: definitions[id].kind,
|
|
7256
|
-
operator: constraint.operator,
|
|
7257
|
-
operatorPhraseKey: OPERATOR_WORDS[definitions[id].kind][constraint.operator]?.phraseKey ??
|
|
7258
|
-
constraint.operator,
|
|
7259
|
-
value: constraint.value,
|
|
7260
|
-
})));
|
|
7261
|
-
const store = {
|
|
7262
|
-
definitions,
|
|
7263
|
-
applied,
|
|
7264
|
-
/**
|
|
7265
|
-
* A handle bound to one filter. This is the PLACEMENT api: it needs nothing from the component
|
|
7266
|
-
* tree, so the same filter can be driven from the bar, from a header cell, or from a drawer
|
|
7267
|
-
* rendered at app root. `value` is the DRAFT — a control shows what was typed, not what was
|
|
7268
|
-
* applied; `applied()` is the other question and the bar asks that one.
|
|
7269
|
-
*/
|
|
7270
|
-
get(id) {
|
|
7271
|
-
return {
|
|
7272
|
-
id: id,
|
|
7273
|
-
label: definitions[id].label,
|
|
7274
|
-
kind: definitions[id].kind,
|
|
7275
|
-
value: computed(() => drafts().get(id)?.value),
|
|
7276
|
-
operator: computed(() => drafts().get(id)?.operator),
|
|
7277
|
-
// Getters, not snapshots: options can land AFTER a handle is taken — the bar's editor
|
|
7278
|
-
// holds one for the life of the popover, and PACMS fills its lookups asynchronously —
|
|
7279
|
-
// and a snapshot left an already-open pick-list empty until it was reopened. The
|
|
7280
|
-
// operator list rides along, since setOptions can flip it to `In`.
|
|
7281
|
-
get operators() {
|
|
7282
|
-
return offered.get(id).options;
|
|
7283
|
-
},
|
|
7284
|
-
get defaultOperator() {
|
|
7285
|
-
return defaultOperatorFor(id);
|
|
7286
|
-
},
|
|
7287
|
-
get options() {
|
|
7288
|
-
return definitions[id].options;
|
|
7289
|
-
},
|
|
7290
|
-
set: (constraint) => store.set(id, constraint),
|
|
7291
|
-
commit: () => store.commit(id),
|
|
7292
|
-
reset: () => store.reset(id),
|
|
7293
|
-
};
|
|
7294
|
-
},
|
|
7295
|
-
/**
|
|
7296
|
-
* The ONE seam for choices that arrive after the store is built (PACMS fills its pick-lists
|
|
7297
|
-
* from lookups that race a deploy). Never write `definitions[id].options` by hand: options'
|
|
7298
|
-
* PRESENCE is what makes a filter a pick-list, so the setter also re-resolves the offered
|
|
7299
|
-
* operators — a hand write would leave a late-filled filter offering `Equals` on a control
|
|
7300
|
-
* that emits a list.
|
|
7301
|
-
*/
|
|
7302
|
-
setOptions(id, options) {
|
|
7303
|
-
definitions[id].options = options;
|
|
7304
|
-
offered.set(id, resolveOffered(id));
|
|
7305
|
-
},
|
|
7306
|
-
set(id, constraint) {
|
|
7307
|
-
const kind = definitions[id].kind;
|
|
7308
|
-
// Throws rather than logging and ignoring, which is what `computeMatchModes` does for a
|
|
7309
|
-
// column's OFFERED match modes. Different stakes: that one narrows a dropdown, so a bad
|
|
7310
|
-
// entry merely fails to show, while this one assembles a QUERY — dropping it silently would
|
|
7311
|
-
// leave the operator looking at unfiltered rows believing they are filtered.
|
|
7312
|
-
if (!ALLOWED_OPERATORS[kind].includes(constraint.operator)) {
|
|
7313
|
-
throw new Error(`Filter '${String(id)}' is ${kind}, which does not accept the '${constraint.operator}' ` +
|
|
7314
|
-
`operator (allowed: ${ALLOWED_OPERATORS[kind].join(', ')}). The generated paginator ` +
|
|
7315
|
-
`answers an unsupported match mode with InvalidMatchMode.`);
|
|
7316
|
-
}
|
|
7317
|
-
writeDraft(id, constraint);
|
|
7318
|
-
},
|
|
7319
|
-
/**
|
|
7320
|
-
* `set` + `commit` in one breath — the shape every PROGRAMMATIC write takes (a view's
|
|
7321
|
-
* `apply`, the sheet flow). The split API stays for controls, where draft-on-type /
|
|
7322
|
-
* publish-on-Apply is the point; but a bare `set` whose `commit` is forgotten (or written
|
|
7323
|
-
* with the wrong id — the pair repeats the string twice) fails silently: the draft is
|
|
7324
|
-
* written, nothing is published, and a view narrows by nothing under a labelled tab.
|
|
7325
|
-
*/
|
|
7326
|
-
setAndCommit(id, constraint) {
|
|
7327
|
-
store.set(id, constraint);
|
|
7328
|
-
store.commit(id);
|
|
7329
|
-
},
|
|
7330
|
-
/**
|
|
7331
|
-
* Publishes the draft. A blanked draft REMOVES the constraint rather than committing an empty
|
|
7332
|
-
* one, so `applied()` and the payload agree: no chip, no key, no `contains ''`.
|
|
7333
|
-
*/
|
|
7334
|
-
commit(id) {
|
|
7335
|
-
const draft = drafts().get(id);
|
|
7336
|
-
const current = committed();
|
|
7337
|
-
const shouldRemove = draft === undefined || isBlank(draft.value);
|
|
7338
|
-
// A new Map is a new identity even with identical contents, and everything downstream
|
|
7339
|
-
// reacts to identity — so a no-op commit would spend a request. The bail compares the
|
|
7340
|
-
// CONSTRAINT, not the draft object: every set() builds a fresh draft, and the case that
|
|
7341
|
-
// matters is an identical value re-committed (see valueEquals).
|
|
7342
|
-
if (shouldRemove ? !current.has(id) : constraintEquals(current.get(id), draft))
|
|
7343
|
-
return;
|
|
7344
|
-
const next = new Map(current);
|
|
7345
|
-
if (shouldRemove)
|
|
7346
|
-
next.delete(id);
|
|
7347
|
-
else
|
|
7348
|
-
next.set(id, draft);
|
|
7349
|
-
committed.set(next);
|
|
7350
|
-
},
|
|
7351
|
-
/**
|
|
7352
|
-
* The chip's `x`. Drops the DRAFT as well as the committed constraint: a surviving draft
|
|
7353
|
-
* leaves the control still showing the cleared text, and the next commit would restore a
|
|
7354
|
-
* filter nobody re-typed.
|
|
7355
|
-
*/
|
|
7356
|
-
reset(id) {
|
|
7357
|
-
if (drafts().has(id))
|
|
7358
|
-
writeDraft(id, null);
|
|
7359
|
-
if (!committed().has(id))
|
|
7360
|
-
return;
|
|
7361
|
-
const next = new Map(committed());
|
|
7362
|
-
next.delete(id);
|
|
7363
|
-
committed.set(next);
|
|
7364
|
-
},
|
|
7365
|
-
/**
|
|
7366
|
-
* The applied set, plain enough to store. Drafts are deliberately NOT included: a half-typed
|
|
7367
|
-
* value is not a state anyone should come back to, and the bar's whole claim is that what it
|
|
7368
|
-
* shows is what narrows the grid.
|
|
7369
|
-
*/
|
|
7370
|
-
snapshot() {
|
|
7371
|
-
const snapshot = {};
|
|
7372
|
-
for (const [id, constraint] of committed()) {
|
|
7373
|
-
snapshot[id] = {
|
|
7374
|
-
operator: constraint.operator,
|
|
7375
|
-
value: constraint.value,
|
|
7376
|
-
};
|
|
7377
|
-
}
|
|
7378
|
-
return snapshot;
|
|
7379
|
-
},
|
|
7380
|
-
/**
|
|
7381
|
-
* Replaces the applied set. The drafts are written too, so reopening a restored chip shows
|
|
7382
|
-
* the value it is filtering by rather than an empty control.
|
|
7383
|
-
*
|
|
7384
|
-
* Ids the store no longer declares are dropped rather than carried: a filter removed from the
|
|
7385
|
-
* declaration in a later release would otherwise ride every request from anyone whose storage
|
|
7386
|
-
* still held it, with no chip able to name it and no way to clear it from the UI.
|
|
7387
|
-
*/
|
|
7388
|
-
restore(snapshot) {
|
|
7389
|
-
const next = new Map();
|
|
7390
|
-
for (const [id, constraint] of Object.entries(snapshot ?? {})) {
|
|
7391
|
-
if (!(id in definitions))
|
|
7392
|
-
continue;
|
|
7393
|
-
next.set(id, {
|
|
7394
|
-
operator: constraint.operator,
|
|
7395
|
-
value: reviveValue(constraint.value),
|
|
7396
|
-
});
|
|
7397
|
-
}
|
|
7398
|
-
drafts.set(new Map(next));
|
|
7399
|
-
committed.set(next);
|
|
7400
|
-
},
|
|
7401
|
-
/** The bar's "Clear all". `reset` for every filter, drafts included. */
|
|
7402
|
-
clear() {
|
|
7403
|
-
if (drafts().size > 0)
|
|
7404
|
-
drafts.set(new Map());
|
|
7405
|
-
if (committed().size > 0)
|
|
7406
|
-
committed.set(new Map());
|
|
7407
|
-
},
|
|
7408
|
-
/** The `filters` half of `Filter` — what the generated paginator reads. */
|
|
7409
|
-
toFilterPayload() {
|
|
7410
|
-
const payload = {};
|
|
7411
|
-
for (const [id, constraint] of committed()) {
|
|
7412
|
-
payload[id] = [
|
|
7413
|
-
{ matchMode: constraint.operator, value: constraint.value },
|
|
7414
|
-
];
|
|
7415
|
-
}
|
|
7416
|
-
return payload;
|
|
7417
|
-
},
|
|
7418
|
-
};
|
|
7419
|
-
return store;
|
|
7420
7460
|
}
|
|
7421
7461
|
|
|
7422
7462
|
class SpiderlyDataViewComponent {
|