@navette-js/tables 1.0.0-beta.1 → 1.0.0-beta.2

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.
@@ -1,11 +1,7 @@
1
- // Filtre « constructeur de requêtes » — port de Filament\Tables\Filters\QueryBuilder et de filament/query-builder.
2
- // L'utilisateur compose des règles (contrainte + opérateur + réglages), éventuellement regroupées par des blocs « OR ».
3
- // Tout l'état vient du navigateur : il est nettoyé contre les contraintes déclarées avant de servir (requête, indicateurs, rendu).
4
- // Portés : TextConstraint, NumberConstraint, BooleanConstraint, DateConstraint (avec ou sans heure), SelectConstraint,
5
- // RelationshipConstraint (nombre d'enregistrements liés, vide, `selectable(IsRelatedToOperator)`), contraintes sur une relation
6
- // (`relationship(nom, attribut)`), opérateur « Is filled » (`nullable()`), bloc OR, indicateurs.
7
- // Non portés : agrégats de relation (somme, moyenne… d'une colonne liée), groupes de contraintes, recherche serveur dans les listes.
8
- // Le SQL de date et de recherche est écrit pour SQLite, MySQL / MariaDB, PostgreSQL et SQL Server, selon le client knex de la requête.
1
+ // Query builder filter: users compose rules (constraint + operator + settings), optionally grouped in "OR" blocks.
2
+ // All state comes from the browser: it is sanitized against the declared constraints before any use (query, indicators, render).
3
+ // Not supported yet: relationship aggregates (sum, average… of a related column), constraint groups, server-side option search.
4
+ // Date and search SQL is generated for SQLite, MySQL / MariaDB, PostgreSQL and SQL Server, depending on the query's knex client.
9
5
  import { Action } from '@navette-js/actions';
10
6
  import { DatePicker, DateTimePicker, Field, Select, TextInput } from '@navette-js/forms';
11
7
  import { Block, Builder, colorsOf, iconActionButton, isItemKey, shortKey, writeState } from '@navette-js/forms/builder';
@@ -16,16 +12,16 @@ import { createHash } from 'node:crypto';
16
12
  const tr = (key, replace = {}) => t(`filament-query-builder::query-builder.${key}`, replace);
17
13
  const headline = (s) => s.replace(/[_.-]+/g, ' ').replace(/^./, (c) => c.toUpperCase());
18
14
  const scalar = (value) => typeof value === 'string' ? value.slice(0, 1000) : typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)) ? value : null;
19
- /** Réglage reçu du navigateur : un scalaire, ou une liste de scalaires (sélection multiple), bornée. */
15
+ /** Setting received from the browser: a scalar, or a bounded list of scalars (multiple select). */
20
16
  const setting = (value) => Array.isArray(value) ? value.slice(0, MAX_LIST_VALUES).map(scalar).filter((item) => typeof item === 'string' || typeof item === 'number') : scalar(value);
21
- /** Plafonds appliqués à l'arbre de règles reçu du navigateur. */
17
+ /** Limits applied to the rule tree received from the browser. */
22
18
  export const MAX_RULES = 100;
23
19
  export const MAX_NESTING_DEPTH = 3;
24
- /** Nombre maximal de valeurs d'une sélection multiple. */
20
+ /** Maximum number of values in a multiple select. */
25
21
  export const MAX_LIST_VALUES = 100;
26
22
  const OR = 'or';
27
23
  const label = (group, name) => (inverse) => tr(`operators.${group}.${name}.label.${inverse ? 'inverse' : 'direct'}`);
28
- /** Liste de libellés à la manière de `Arr::join($labels, ', ', ' or ')`. */
24
+ /** Joins labels as "a, b or c" using the translated glue strings. */
29
25
  const joinLabels = (labels, glueKey) => (labels.length <= 1 ? labels.join('') : `${labels.slice(0, -1).join(tr(`${glueKey}.0`))}${tr(`${glueKey}.final`)}${labels[labels.length - 1]}`);
30
26
  const summary = (group, name, inverse, replace) => tr(`operators.${group}.${name}.summary.${inverse ? 'inverse' : 'direct'}`, replace);
31
27
  const likeEscape = (text) => text.replace(/[\\%_]/g, '\\$&');
@@ -48,7 +44,7 @@ function textOperator(name, key, pattern) {
48
44
  function numberOperator(name, key, direct, inverted) {
49
45
  return {
50
46
  name, label: label('number', key), defaults: () => ({ number: null, aggregate: null }),
51
- // `aggregate` ne sert qu'aux relations : le champ existe, masqué, comme dans Filament.
47
+ // `aggregate` is only used for relationships; the field exists but stays hidden.
52
48
  fields: () => [TextInput.make('number').label(tr('operators.number.form.number.label')).numeric().required(), Select.make('aggregate').label(tr('operators.number.form.aggregate.label')).hidden()],
53
49
  resolve: (settings, inverse, constraint) => {
54
50
  const raw = settings.number;
@@ -59,11 +55,10 @@ function numberOperator(name, key, direct, inverted) {
59
55
  },
60
56
  };
61
57
  }
62
- // --- dates
63
58
  const pad = (n) => String(n).padStart(2, '0');
64
59
  const toDateString = (date) => `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
65
60
  const toDateTimeString = (date) => `${toDateString(date)} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`;
66
- /** Date `AAAA-MM-JJ` réelle, sinon null. */
61
+ /** A valid `YYYY-MM-DD` date, otherwise null. */
67
62
  function parseDate(value) {
68
63
  const m = typeof value === 'string' ? /^(\d{4})-(\d{2})-(\d{2})/.exec(value) : null;
69
64
  if (!m)
@@ -71,7 +66,7 @@ function parseDate(value) {
71
66
  const date = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
72
67
  return toDateString(date) === `${m[1]}-${m[2]}-${m[3]}` ? toDateString(date) : null;
73
68
  }
74
- /** Date et heure `AAAA-MM-JJ HH:MM[:SS]` (ou avec `T`) réelles → `AAAA-MM-JJ HH:MM:SS` ; une date seule vaut minuit. */
69
+ /** A valid `YYYY-MM-DD HH:MM[:SS]` (or `T`-separated) date-time → `YYYY-MM-DD HH:MM:SS`; a bare date means midnight. */
75
70
  function parseDateTime(value) {
76
71
  const date = parseDate(value);
77
72
  if (date === null)
@@ -83,7 +78,7 @@ function parseDateTime(value) {
83
78
  return Number(hours) < 24 && Number(minutes) < 60 && Number(seconds) < 60 ? `${date} ${hours}:${minutes}:${seconds}` : null;
84
79
  }
85
80
  const formatDate = (date) => new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${date.slice(0, 10)}T00:00:00Z`));
86
- /** Date et heure comme Carbon `toFormattedDayDateString() . ' ' . format('H:i:s')` : « Mon, Jun 1, 2026 14:30:00 ». */
81
+ /** Formats a date-time as "Mon, Jun 1, 2026 14:30:00". */
87
82
  const formatDayDateTime = (dateTime) => `${new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' }).format(new Date(`${dateTime.slice(0, 10)}T00:00:00Z`))}, ${formatDate(dateTime)} ${dateTime.length > 10 ? dateTime.slice(11, 19) : '00:00:00'}`;
88
83
  const MONTHS = Array.from({ length: 12 }, (_, i) => new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format(new Date(Date.UTC(2024, i, 1))));
89
84
  const UNITS = ['day', 'week', 'month', 'quarter', 'year'];
@@ -108,7 +103,7 @@ const shift = (now, unit, amount) => {
108
103
  date.setUTCFullYear(date.getUTCFullYear() + amount);
109
104
  return date;
110
105
  };
111
- /** Préréglages de fenêtre glissante (port de resolveRelativeDate). */
106
+ /** Relative date presets. */
112
107
  const PRESETS = {
113
108
  past_decade: (n) => shift(n, 'year', -10), past_5_years: (n) => shift(n, 'year', -5), past_2_years: (n) => shift(n, 'year', -2), past_year: (n) => shift(n, 'year', -1),
114
109
  past_6_months: (n) => shift(n, 'month', -6), past_quarter: (n) => shift(n, 'quarter', -1), past_month: (n) => shift(n, 'month', -1), past_2_weeks: (n) => shift(n, 'week', -2), past_week: (n) => shift(n, 'week', -1),
@@ -117,21 +112,21 @@ const PRESETS = {
117
112
  next_week: (n) => shift(n, 'week', 1), next_2_weeks: (n) => shift(n, 'week', 2), next_month: (n) => shift(n, 'month', 1), next_quarter: (n) => shift(n, 'quarter', 1), next_6_months: (n) => shift(n, 'month', 6),
118
113
  next_year: (n) => shift(n, 'year', 1), next_2_years: (n) => shift(n, 'year', 2), next_5_years: (n) => shift(n, 'year', 5), next_decade: (n) => shift(n, 'year', 10),
119
114
  };
120
- /** Préréglages à la minute ou à l'heure, proposés seulement pour une contrainte `time()` : ils comparent la date et l'heure. */
115
+ /** Minute and hour presets, only offered for `time()` constraints: they compare date and time. */
121
116
  const TIME_PRESETS = {
122
117
  past_hour: (n) => shift(n, 'hour', -1), past_minute: (n) => shift(n, 'minute', -1),
123
118
  this_hour: (n) => new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate(), n.getUTCHours())), this_minute: (n) => new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate(), n.getUTCHours(), n.getUTCMinutes())),
124
119
  next_minute: (n) => shift(n, 'minute', 1), next_hour: (n) => shift(n, 'hour', 1),
125
120
  };
126
- /** Ordre des préréglages dans la liste, comme Filament (les horaires s'intercalent seulement avec `time()`). */
121
+ /** Preset order in the select (time presets are interleaved only with `time()`). */
127
122
  const presetNames = (hasTime) => [
128
123
  'past_decade', 'past_5_years', 'past_2_years', 'past_year', 'past_6_months', 'past_quarter', 'past_month', 'past_2_weeks', 'past_week', ...(hasTime ? ['past_hour', 'past_minute'] : []),
129
124
  'this_decade', 'this_year', 'this_quarter', 'this_month', 'today', ...(hasTime ? ['this_hour', 'this_minute', 'next_minute', 'next_hour'] : []),
130
125
  'next_week', 'next_2_weeks', 'next_month', 'next_quarter', 'next_6_months', 'next_year', 'next_2_years', 'next_5_years', 'next_decade', 'custom',
131
126
  ];
132
127
  /**
133
- * Moment visé par les réglages d'un opérateur « après / avant » : date fixe, préréglage, ou décalage personnalisé. `time` est vrai quand la
134
- * comparaison porte sur la date et l'heure (contrainte `time()` avec une date fixe, un préréglage horaire ou une unité de moins d'un jour).
128
+ * Moment targeted by an "after / before" operator's settings: fixed date, preset, or custom offset. `time` is true when the comparison
129
+ * uses date and time (`time()` constraint with a fixed date, a time preset or a sub-day unit).
135
130
  */
136
131
  function resolveRuleMoment(settings, now, hasTime) {
137
132
  if (settings.mode !== 'relative') {
@@ -144,7 +139,7 @@ function resolveRuleMoment(settings, now, hasTime) {
144
139
  if (preset !== 'custom')
145
140
  return { value: toDateString((Object.hasOwn(PRESETS, preset) ? PRESETS[preset] : PRESETS.today)(now)), time: false };
146
141
  const value = Math.floor(Number(settings.relative_value));
147
- // Comme le formulaire de Filament : un entier d'au moins 1 (borné ici pour rester dans les dates représentables).
142
+ // An integer of at least 1, capped to stay within representable dates.
148
143
  if (!Number.isFinite(value) || value < 1 || value > 10000)
149
144
  return null;
150
145
  const units = hasTime ? [...TIME_UNITS, ...UNITS] : UNITS;
@@ -152,7 +147,7 @@ function resolveRuleMoment(settings, now, hasTime) {
152
147
  const moment = shift(now, unit, settings.tense === 'future' ? value : -value);
153
148
  return TIME_UNITS.includes(unit) ? { value: toDateTimeString(moment), time: true } : { value: toDateString(moment), time: false };
154
149
  }
155
- /** Date (ou date et heure, avec `hasTime`) visée par les réglages d'un opérateur « après / avant ». */
150
+ /** Date (or date-time, with `hasTime`) targeted by an "after / before" operator's settings. */
156
151
  export function resolveRuleDate(settings, now = new Date(), hasTime = false) { return resolveRuleMoment(settings, now, hasTime)?.value ?? null; }
157
152
  function windowOperator(name, key, direct, inverted, hasTime) {
158
153
  const option = (path, names) => names.map((value) => [value, tr(`${path}.${value}`)]);
@@ -164,7 +159,6 @@ function windowOperator(name, key, direct, inverted, hasTime) {
164
159
  (hasTime ? DateTimePicker.make('date') : DatePicker.make('date')).label(tr('operators.date.form.date.label')).hidden(({ get }) => get('mode') === 'relative').required(({ get }) => get('mode') !== 'relative'),
165
160
  Select.make('preset').label(tr('operators.date.form.preset.label')).live().options(option('operators.date.presets', presetNames(hasTime))).default('past_month')
166
161
  .hidden(({ get }) => get('mode') !== 'relative').required(({ get }) => get('mode') === 'relative'),
167
- // Dans Filament ces trois champs sont fusionnés (FusedGroup) ; ici ils sont côte à côte.
168
162
  Group.make([
169
163
  Select.make('tense').label(tr('operators.date.form.tense.label')).options(option('operators.date.form.tense.options', ['past', 'future'])).default('past').required(),
170
164
  TextInput.make('relative_value').label(tr('operators.date.form.relative_value.label')).numeric().minValue(1).default(1).required(),
@@ -175,7 +169,7 @@ function windowOperator(name, key, direct, inverted, hasTime) {
175
169
  const moment = resolveRuleMoment(settings, new Date(), hasTime);
176
170
  if (moment === null)
177
171
  return null;
178
- // Avec l'heure, la colonne est comparée telle quelle (`where`) ; sinon, sa seule date (`whereDate`), comme Filament.
172
+ // With time, the column is compared as is; otherwise only its date part is compared.
179
173
  const condition = moment.time
180
174
  ? { kind: 'compare', column: constraint.getColumn(), operator: inverse ? inverted : direct, value: moment.value }
181
175
  : { kind: 'date', column: constraint.getColumn(), operator: inverse ? inverted : direct, value: moment.value };
@@ -183,7 +177,7 @@ function windowOperator(name, key, direct, inverted, hasTime) {
183
177
  },
184
178
  };
185
179
  }
186
- /** Opérateur « Is filled » (port de IsFilledOperator), proposé par les contraintes `nullable()`. */
180
+ /** "Is filled" operator, offered by `nullable()` constraints. */
187
181
  const isFilledOperator = {
188
182
  name: 'isFilled', label: (inverse) => tr(`operators.is_filled.label.${inverse ? 'inverse' : 'direct'}`), defaults: () => null, fields: () => [],
189
183
  resolve: (_settings, inverse, constraint) => ({ condition: { kind: 'filled', column: constraint.getColumn(), negate: inverse }, summary: tr(`operators.is_filled.summary.${inverse ? 'inverse' : 'direct'}`, { attribute: constraint.getAttributeLabel() }) }),
@@ -226,8 +220,8 @@ const OPERATORS = {
226
220
  ],
227
221
  };
228
222
  /**
229
- * Attribut filtrable par le constructeur de requêtes (port de Constraint). La colonne et la relation interrogées sont celles déclarées
230
- * ici, jamais des valeurs du navigateur : celui-ci ne choisit qu'une contrainte (par son nom) et un opérateur de sa liste.
223
+ * Attribute the query builder can filter on. The queried column and relationship are the ones declared here, never browser values:
224
+ * the browser only picks a constraint (by name) and one of its operators.
231
225
  */
232
226
  export class Constraint {
233
227
  name;
@@ -239,24 +233,24 @@ export class Constraint {
239
233
  _relationship = null;
240
234
  _modifyRelationshipQuery = null;
241
235
  _nullable = false;
242
- /** Modèle Objection interrogé (enregistrements de la table), pour les listes d'enregistrements liés. */
236
+ /** Objection model of the table's records, used to list related records. */
243
237
  _model = null;
244
238
  constructor(name, type, icon) {
245
239
  if (!isItemKey(name) || name === OR)
246
- throw new Error(`Nom de contrainte invalide : ${name}`);
240
+ throw new Error(`Invalid constraint name: ${name}`);
247
241
  this.name = name;
248
242
  this.type = type;
249
243
  this._icon = icon;
250
244
  }
251
245
  label(label) { this._label = label; return this; }
252
- /** Nom de l'attribut dans les résumés des règles (par défaut, le libellé). */
246
+ /** Attribute name used in rule summaries (defaults to the label). */
253
247
  attributeLabel(label) { this._attributeLabel = label; return this; }
254
248
  icon(name) { this._icon = name; return this; }
255
- /** Colonne (ou alias de la requête) interrogée quand elle diffère du nom. */
249
+ /** Queried column (or query alias) when it differs from the name. */
256
250
  attribute(column) { this._column = column; return this; }
257
251
  /**
258
- * La contrainte porte sur un attribut d'enregistrements liés (`whereHas`) : `relationship('author', 'name')` filtre sur le nom de
259
- * l'auteur. La relation doit être déclarée par le modèle Objection (`relationMappings`).
252
+ * Filters on an attribute of related records: `relationship('author', 'name')` filters on the author's name.
253
+ * The relationship must be declared by the Objection model (`relationMappings`).
260
254
  */
261
255
  relationship(name, titleAttribute, modifyQueryUsing = null) {
262
256
  this._relationship = name;
@@ -265,7 +259,7 @@ export class Constraint {
265
259
  return this;
266
260
  }
267
261
  modifyRelationshipQueryUsing(callback) { this._modifyRelationshipQuery = callback; return this; }
268
- /** Ajoute l'opérateur « Is filled » / « Is blank » (port de `nullable()`). */
262
+ /** Adds the "Is filled" / "Is blank" operator. */
269
263
  nullable(condition = true) { this._nullable = condition; return this; }
270
264
  isNullable() { return this._nullable; }
271
265
  model(model) { this._model = model; return this; }
@@ -274,11 +268,11 @@ export class Constraint {
274
268
  getAttributeLabel() { return this._attributeLabel ?? this.getLabel(); }
275
269
  getIcon() { return this._icon; }
276
270
  getColumn() { return this._column ?? this.name; }
277
- /** Relation dont les enregistrements portent l'attribut, ou `null` pour une colonne de la table. */
271
+ /** Relationship whose records hold the attribute, or `null` for a column of the table. */
278
272
  getRelationshipName() { return this._relationship; }
279
273
  getModifyRelationshipQueryUsing() { return this._modifyRelationshipQuery; }
280
274
  getOperators() { return [...OPERATORS[this.type], ...(this._nullable ? [isFilledOperator] : [])]; }
281
- /** `contains` ou `contains.inverse` → opérateur et sens ; null si la contrainte ne le déclare pas. */
275
+ /** `contains` or `contains.inverse` → operator and direction; null if the constraint does not declare it. */
282
276
  parseOperator(value) {
283
277
  if (typeof value !== 'string')
284
278
  return null;
@@ -287,9 +281,9 @@ export class Constraint {
287
281
  return operator ? { operator, inverse } : null;
288
282
  }
289
283
  getOperatorOptions() { return this.getOperators().flatMap((operator) => [[operator.name, operator.label(false)], [`${operator.name}.inverse`, operator.label(true)]]); }
290
- /** Noms de réglages connus de la contrainte, tous opérateurs confondus (un changement d'opérateur conserve les anciens, comme Filament). */
284
+ /** Setting names known to the constraint across all operators (switching operator keeps the previous settings). */
291
285
  getSettingNames() { return [...new Set(this.getOperators().flatMap((operator) => Object.keys(operator.defaults() ?? {})))]; }
292
- /** Réglages dont la valeur est une liste (sélection multiple). */
286
+ /** Settings whose value is a list (multiple select). */
293
287
  getListSettingNames() { return [...new Set(this.getOperators().flatMap((operator) => Object.entries(operator.defaults() ?? {}).filter(([, value]) => Array.isArray(value)).map(([name]) => name)))]; }
294
288
  getDefaultData() { const [first] = this.getOperators(); return { operator: first.name, settings: first.defaults() }; }
295
289
  }
@@ -302,7 +296,7 @@ export class NumberConstraint extends Constraint {
302
296
  export class BooleanConstraint extends Constraint {
303
297
  static make(name) { return configured(new BooleanConstraint(name, 'boolean', 'm-check-circle')); }
304
298
  }
305
- /** Date ; avec `time()`, la date et l'heure (sélecteur date-heure, préréglages et unités à la minute ou à l'heure). */
299
+ /** Date constraint; with `time()`, date and time (date-time picker, minute and hour presets and units). */
306
300
  export class DateConstraint extends Constraint {
307
301
  _time = false;
308
302
  static make(name) { return configured(new DateConstraint(name, 'date', 'm-calendar')); }
@@ -312,13 +306,13 @@ export class DateConstraint extends Constraint {
312
306
  return [windowOperator('isAfter', 'is_after', '>=', '<', this._time), windowOperator('isBefore', 'is_before', '<=', '>', this._time), ...super.getOperators()];
313
307
  }
314
308
  }
315
- /** Valeur choisie dans une liste fixe (port de SelectConstraint) ; `multiple()` : l'une des valeurs choisies. */
309
+ /** Value picked from a fixed list; with `multiple()`, any of the picked values. */
316
310
  export class SelectConstraint extends Constraint {
317
311
  _options = [];
318
312
  _multiple = false;
319
313
  _searchable = false;
320
314
  static make(name) { return configured(new SelectConstraint(name, 'select', 'm-chevron-up-down')); }
321
- /** Options `[valeur, libellé]` ou `{ valeur: libellé }` ; seules ces valeurs filtrent. */
315
+ /** Options as `[value, label]` pairs or `{ value: label }`; only these values can filter. */
322
316
  options(options) {
323
317
  this._options = (Array.isArray(options) ? options : Object.entries(options)).map(([value, label]) => [String(value), String(label)]);
324
318
  return this;
@@ -335,7 +329,7 @@ export class SelectConstraint extends Constraint {
335
329
  fields: () => [Select.make(field).label(tr(`operators.select.is.form.${field}.label`)).options(this._options).multiple(this._multiple).searchable(this._searchable).required().columnSpanFull()],
336
330
  resolve: (settings, inverse, constraint) => {
337
331
  const raw = settings[field];
338
- // Valeurs forgées ou inconnues écartées : il ne reste que des options déclarées (sinon la règle est sans effet).
332
+ // Forged or unknown values are dropped: only declared options remain (otherwise the rule has no effect).
339
333
  const values = (Array.isArray(raw) ? raw : this._multiple ? [] : [raw]).filter(known).map(String);
340
334
  if (!values.length)
341
335
  return null;
@@ -349,7 +343,7 @@ export class SelectConstraint extends Constraint {
349
343
  return [is, ...(this._nullable ? [isFilledOperator] : [])];
350
344
  }
351
345
  }
352
- /** Nombre entier d'enregistrements liés, d'au moins 1 (formulaire `count` des opérateurs de RelationshipConstraint). */
346
+ /** Integer related-record count of at least 1 (the `count` setting of RelationshipConstraint operators). */
353
347
  const countSetting = (value) => (isNumericValue(value) && Number(value) >= 1 && Number(value) <= 1e9 ? Math.floor(Number(value)) : null);
354
348
  const isNumericValue = (value) => typeof value === 'number' ? Number.isFinite(value) : typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value));
355
349
  function relationshipCountOperator(name, key, direct, inverted) {
@@ -365,8 +359,8 @@ function relationshipCountOperator(name, key, direct, inverted) {
365
359
  };
366
360
  }
367
361
  /**
368
- * Opérateur « Is » / « Contains » d'une RelationshipConstraint (port de IsRelatedToOperator) : les enregistrements liés sont choisis dans
369
- * une liste tirée du modèle lié, par leur clé ; `titleAttribute` est la colonne affichée.
362
+ * "Is" / "Contains" operator of a RelationshipConstraint: related records are picked by key from a list loaded from the related model;
363
+ * `titleAttribute` is the displayed column.
370
364
  */
371
365
  export class IsRelatedToOperator {
372
366
  _titleAttribute = null;
@@ -385,7 +379,7 @@ export class IsRelatedToOperator {
385
379
  optionsLimit(limit) { this._optionsLimit = Math.max(1, Math.floor(limit)); return this; }
386
380
  modifyRelationshipQueryUsing(callback) { this._modifyQuery = callback; return this; }
387
381
  getModifyRelationshipQueryUsing() { return this._modifyQuery; }
388
- /** Opérateur du constructeur pour la contrainte donnée. */
382
+ /** Builds the query builder operator for the given constraint. */
389
383
  toOperator(constraint) {
390
384
  const field = constraint.isMultiple() ? 'values' : 'value';
391
385
  const variant = constraint.isMultiple() ? 'multiple' : 'single';
@@ -411,14 +405,14 @@ export class IsRelatedToOperator {
411
405
  getOptionsLimit() { return this._optionsLimit; }
412
406
  }
413
407
  /**
414
- * Enregistrements liés (port de RelationshipConstraint) : nombre minimal, maximal ou exact (`multiple()`), relation vide (`emptyable()`,
415
- * par défaut avec `multiple()`), ou liés à des enregistrements choisis (`selectable(IsRelatedToOperator.make()…)`).
408
+ * Related records: minimum, maximum or exact count (`multiple()`), empty relationship (`emptyable()`, on by default with `multiple()`),
409
+ * or related to picked records (`selectable(IsRelatedToOperator.make()…)`).
416
410
  */
417
411
  export class RelationshipConstraint extends Constraint {
418
412
  _multiple = false;
419
413
  _emptyable = null;
420
414
  _selectable = null;
421
- /** Libellés des enregistrements liés cités par les règles, chargés avant le calcul des indicateurs. */
415
+ /** Labels of the related records cited by rules, loaded before indicators are computed. */
422
416
  _relatedLabels = new Map();
423
417
  static make(name) { return configured(new RelationshipConstraint(name, 'relationship', 'm-arrows-pointing-out')); }
424
418
  multiple(condition = true) { this._multiple = condition; return this; }
@@ -427,7 +421,7 @@ export class RelationshipConstraint extends Constraint {
427
421
  canBeEmpty() { return this._emptyable ?? this._multiple; }
428
422
  selectable(operator) { this._selectable = operator; return this; }
429
423
  getSelectableOperator() { return this._selectable; }
430
- /** Relation interrogée : celle de `relationship()`, sinon `attribute()`, sinon le nom de la contrainte. */
424
+ /** Queried relationship: from `relationship()`, else `attribute()`, else the constraint name. */
431
425
  getRelationshipName() { return this._relationship ?? this.getColumn(); }
432
426
  getOperators() {
433
427
  return [
@@ -443,11 +437,11 @@ export class RelationshipConstraint extends Constraint {
443
437
  ];
444
438
  }
445
439
  getRelatedLabel(key) { return this._relatedLabels.get(String(key)) ?? null; }
446
- /** Modèle lié, retrouvé par la relation du modèle de la table (déclaré, ou celui de l'adaptateur Objection de l'hôte). */
440
+ /** Related model, found through the table model's relationship (declared, or the host's Objection adapter model). */
447
441
  relatedModel(host) {
448
442
  const model = this._model ?? modelOfHost(host);
449
443
  if (!model?.getRelation)
450
- throw new Error(`RelationshipConstraint ${this.name} : modèle de la table introuvable ; déclarez-le avec model() sur la contrainte ou le QueryBuilder.`);
444
+ throw new Error(`RelationshipConstraint ${this.name}: table model not found; declare it with model() on the constraint or the QueryBuilder.`);
451
445
  return model.getRelation(this.getRelationshipName()).relatedModelClass;
452
446
  }
453
447
  relatedQuery(operator, host) {
@@ -457,14 +451,14 @@ export class RelationshipConstraint extends Constraint {
457
451
  operator.getModifyRelationshipQueryUsing()?.(query);
458
452
  return { query, id: `${related.tableName}.${related.idColumn}`, title: `${related.tableName}.${title}` };
459
453
  }
460
- /** Options de la liste : enregistrements du modèle lié, triés par leur titre. */
454
+ /** Select options: records of the related model, sorted by title. */
461
455
  async loadRelatedOptions(operator, host) {
462
456
  const { query, id, title } = this.relatedQuery(operator, host);
463
- // Comme une liste non cherchable de Filament : toutes les options, ici plafonnées.
457
+ // All options are loaded (no server-side search), capped.
464
458
  const rows = await query.clearSelect().select({ __key: id, __title: title }).orderBy(title).limit(1000);
465
459
  return rows.map((row) => [String(row.__key), String(row.__title ?? row.__key)]);
466
460
  }
467
- /** Charge les libellés des clés citées par des règles (indicateurs « Author is Ada or Linus »). */
461
+ /** Loads labels of the keys cited by rules (for indicators such as "Author is Ada or Linus"). */
468
462
  async loadRelatedLabels(keys, host) {
469
463
  if (!this._selectable || !keys.length)
470
464
  return;
@@ -474,7 +468,7 @@ export class RelationshipConstraint extends Constraint {
474
468
  this._relatedLabels.set(String(row.__key), String(row.__title ?? row.__key));
475
469
  }
476
470
  }
477
- /** Modèle Objection de la table de l'hôte (adaptateur `ObjectionAdapter`), quand l'application ne l'a pas déclaré. */
471
+ /** Objection model of the host's table (`ObjectionAdapter`), when the application did not declare one. */
478
472
  function modelOfHost(host) {
479
473
  const adapter = host?.getDataAdapterForTable?.() ?? host?.resource?.adapter ?? null;
480
474
  const model = adapter?.model;
@@ -482,8 +476,8 @@ function modelOfHost(host) {
482
476
  }
483
477
  const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
484
478
  /**
485
- * Arbre de règles reçu du navigateur → arbre ne contenant que ce que le filtre déclare : types de blocs connus, opérateurs de la contrainte,
486
- * réglages scalaires aux noms connus, profondeur et nombre de règles bornés. Un opérateur inconnu est remis à vide (règle sans effet).
479
+ * Reduces the browser's rule tree to what the filter declares: known block types, the constraint's operators, scalar settings with
480
+ * known names, bounded depth and rule count. An unknown operator is reset to null (the rule has no effect).
487
481
  */
488
482
  export function sanitizeRules(constraints, raw, depth = 1, budget = { rules: MAX_RULES }) {
489
483
  const rules = {};
@@ -510,21 +504,21 @@ export function sanitizeRules(constraints, raw, depth = 1, budget = { rules: MAX
510
504
  if (!constraint)
511
505
  continue;
512
506
  budget.rules--;
513
- // Une liste n'est acceptée que pour un réglage qui en est une par défaut (sélection multiple) ; ailleurs, seulement un scalaire.
507
+ // A list is only accepted for settings whose default is a list (multiple select); elsewhere only a scalar.
514
508
  const lists = constraint.getListSettingNames();
515
509
  const settings = isObject(data.settings) ? Object.fromEntries(constraint.getSettingNames().filter((name) => Object.hasOwn(data.settings, name)).map((name) => [name, lists.includes(name) ? setting(data.settings[name]) : scalar(data.settings[name])])) : null;
516
510
  rules[key] = { type: constraint.name, data: { operator: constraint.parseOperator(data.operator) ? data.operator : null, settings } };
517
511
  }
518
512
  return rules;
519
513
  }
520
- /** Règles nettoyées → conditions et libellés d'indicateurs (une entrée par règle de premier niveau qui a un effet). */
514
+ /** Sanitized rules → conditions and indicator labels (one entry per effective top-level rule). */
521
515
  export function compileRules(constraints, rules, iteration = 1) {
522
516
  const nodes = [];
523
517
  const summaries = {};
524
518
  for (const [key, rule] of Object.entries(rules)) {
525
519
  if (rule.type === OR) {
526
520
  const groups = Object.values((rule.data.groups ?? {})).map((group) => compileRules(constraints, group.rules, iteration + 1));
527
- // Un groupe sans règle effective n'ajoute rien (dans Filament, `orWhere` avec une clôture vide est ignoré).
521
+ // A group without effective rules adds nothing.
528
522
  const effective = groups.filter((group) => group.nodes.length);
529
523
  if (!effective.length)
530
524
  continue;
@@ -540,7 +534,7 @@ export function compileRules(constraints, rules, iteration = 1) {
540
534
  const resolved = parsed.operator.resolve(isObject(rule.data.settings) ? rule.data.settings : {}, parsed.inverse, constraint);
541
535
  if (!resolved)
542
536
  continue;
543
- // Attribut d'une relation : la condition porte sur les enregistrements liés (`whereHas`), comme Operator::applyToBaseQuery.
537
+ // Relationship attribute: the condition applies to the related records, unless the operator targets the base query.
544
538
  const relationship = parsed.operator.onBaseQuery ? null : constraint.getRelationshipName();
545
539
  const modify = constraint.getModifyRelationshipQueryUsing();
546
540
  nodes.push(relationship === null ? resolved.condition : { relationship, nodes: [resolved.condition], ...(modify ? { modify } : {}) });
@@ -548,7 +542,7 @@ export function compileRules(constraints, rules, iteration = 1) {
548
542
  }
549
543
  return { nodes, summaries };
550
544
  }
551
- /** Dialecte lu dans le client knex (`query.client.config.client`, ou celui de la connexion du modèle Objection) ; SQLite par défaut. */
545
+ /** Dialect read from the knex client (`query.client.config.client`, or the Objection model's connection); SQLite by default. */
552
546
  export function sqlDialectOf(query) {
553
547
  const client = query?.client ?? query?.knex?.()?.client ?? query?.modelClass?.()?.knex?.()?.client;
554
548
  const configured = client?.config?.client;
@@ -561,28 +555,28 @@ export function sqlDialectOf(query) {
561
555
  return 'mssql';
562
556
  return 'sqlite';
563
557
  }
564
- /** Fragments SQL qui diffèrent d'une base à l'autre. `??` est la colonne (identifiant lié), jamais une valeur du navigateur. */
558
+ /** Dialect-specific SQL fragments. `??` is the column (bound identifier), never a browser value. */
565
559
  const SQL = {
566
560
  sqlite: { date: 'date(??)', month: "cast(strftime('%m', ??) as integer)", year: "cast(strftime('%Y', ??) as integer)", text: '??', like: "?? like ? escape '\\'" },
567
- // MySQL : la barre oblique inverse est déjà le caractère d'échappement de LIKE (et doit être doublée dans un littéral).
561
+ // MySQL: backslash is already LIKE's escape character (and would need doubling inside a literal).
568
562
  mysql: { date: 'date(??)', month: 'month(??)', year: 'year(??)', text: '??', like: '?? like ?' },
569
- // PostgreSQL : LIKE sensible à la casse, d'où la comparaison en minuscules (comme Filament) ; `cast` pour les colonnes non textuelles.
563
+ // PostgreSQL: LIKE is case-sensitive, hence the lowercase comparison; `cast` handles non-text columns.
570
564
  pg: { date: 'cast(?? as date)', month: 'extract(month from ??)', year: 'extract(year from ??)', text: 'cast(?? as text)', like: "lower(cast(?? as text)) like ? escape '\\'" },
571
565
  mssql: { date: 'cast(?? as date)', month: 'month(??)', year: 'year(??)', text: '??', like: "?? like ? escape '\\'" },
572
566
  };
573
- /** Requête des enregistrements liés à ceux de la requête (sous-requête corrélée d'Objection, `relatedQuery`). */
567
+ /** Correlated subquery of the records related to the query's records (Objection `relatedQuery`). */
574
568
  function relatedQuery(query, relationship, modify) {
575
569
  const model = query?.modelClass?.();
576
570
  if (typeof model?.relatedQuery !== 'function')
577
- throw new Error(`Constructeur de requêtes : la relation ${relationship} demande une requête Objection (modèle avec relationMappings).`);
578
- // Alias fixé ici (sinon Objection en pose un seulement pour certaines relations) : les colonnes liées sont qualifiées par lui.
571
+ throw new Error(`Query builder: relationship ${relationship} requires an Objection query (model with relationMappings).`);
572
+ // Fixed alias (Objection only sets one for some relationships): related columns are qualified with it.
579
573
  const sub = model.relatedQuery(relationship).alias(`${relationship}_related`);
580
574
  modify?.(sub);
581
575
  return sub;
582
576
  }
583
577
  /**
584
- * Applique des conditions à une requête knex / objection. Colonnes passées en identifiants, valeurs en paramètres liés ; les opérateurs
585
- * de comparaison viennent d'une liste fermée. Le SQL de date et de recherche suit le dialecte de la connexion.
578
+ * Applies conditions to a knex / objection query. Columns are bound as identifiers and values as parameters; comparison operators
579
+ * come from a closed list. Date and search SQL follows the connection's dialect.
586
580
  */
587
581
  export function applyConditions(query, nodes, dialect = sqlDialectOf(query)) {
588
582
  const sql = SQL[dialect];
@@ -593,7 +587,7 @@ export function applyConditions(query, nodes, dialect = sqlDialectOf(query)) {
593
587
  }
594
588
  else if ('nodes' in node) {
595
589
  const sub = relatedQuery(query, node.relationship, node.modify);
596
- // Colonne qualifiée par la table liée : la sous-requête peut joindre une table pivot.
590
+ // Qualify columns with the related table: the subquery may join a pivot table.
597
591
  const table = sub.tableRefFor(sub.modelClass());
598
592
  sub.where((inner) => applyConditions(inner, node.nodes.map((child) => qualify(child, table)), dialect));
599
593
  query.whereExists(sub);
@@ -611,7 +605,7 @@ export function applyConditions(query, nodes, dialect = sqlDialectOf(query)) {
611
605
  else if (node.kind === 'year')
612
606
  query.whereRaw(`${sql.year} ${node.operator} ?`, [node.column, node.value]);
613
607
  else if (node.kind === 'filled') {
614
- // Port de IsFilledOperator : ni NULL ni chaîne vide (la colonne est lue en texte sous PostgreSQL).
608
+ // Neither NULL nor an empty string (the column is cast to text on PostgreSQL).
615
609
  const column = node.column;
616
610
  query.where((inner) => (node.negate ? inner.whereNull(column).orWhereRaw(`${sql.text} = ?`, [column, '']) : inner.whereNotNull(column).whereRaw(`not (${sql.text} = ?)`, [column, ''])));
617
611
  }
@@ -622,7 +616,7 @@ export function applyConditions(query, nodes, dialect = sqlDialectOf(query)) {
622
616
  query.whereIn(node.column, node.values);
623
617
  }
624
618
  else if (node.kind === 'relationCount') {
625
- // Sous-requête de comptage en paramètre lié (Objection ne l'accepte pas comme colonne de `where`).
619
+ // Count subquery passed as a bound parameter (Objection does not accept it as a `where` column).
626
620
  query.whereRaw(`(?) ${node.operator} ?`, [relatedQuery(query, node.relationship, node.modify).count(), node.value]);
627
621
  }
628
622
  else if (node.kind === 'related') {
@@ -638,7 +632,7 @@ export function applyConditions(query, nodes, dialect = sqlDialectOf(query)) {
638
632
  }
639
633
  }
640
634
  }
641
- /** Colonne d'une condition qualifiée par la table des enregistrements liés (sauf si elle l'est déjà). */
635
+ /** Qualifies a condition's column with the related table (unless already qualified). */
642
636
  function qualify(node, table) {
643
637
  if ('or' in node)
644
638
  return { or: node.or.map((group) => group.map((child) => qualify(child, table))) };
@@ -646,11 +640,11 @@ function qualify(node, table) {
646
640
  return node;
647
641
  return { ...node, column: `${table}.${node.column}` };
648
642
  }
649
- // ------------------------------------------------------------------ formulaire
643
+ // ------------------------------------------------------------------ form
650
644
  const CONTAINER_GRID = { '@md': 2, '@xl': 3, '!@sm': 2, '!@md': 3 };
651
645
  const SETTINGS_GRID = { '@xl': 2, '!@md': 2 };
652
646
  const anonymousScope = (path) => `x-data="filamentSchemaComponent(${e(js({ key: null, exposedMethods: [], path, containerPath: path }))})"`;
653
- /** Réglages d'un opérateur : groupe à état `settings`, masqué quand l'opérateur n'a pas de formulaire. */
647
+ /** Operator settings: a stateful `settings` group, hidden when the operator has no form. */
654
648
  class SettingsGroup extends Component {
655
649
  fields;
656
650
  constructor(fields) {
@@ -667,13 +661,13 @@ class SettingsGroup extends Component {
667
661
  }
668
662
  async toHtml() { return `<div id="${e(this.getKey())}">${await this.getChildSchemas()[0].toHtml()}</div>`; }
669
663
  }
670
- /** Boutons « dupliquer » et « supprimer » d'un élément, portés par le Builder parent. */
664
+ /** "Clone" and "delete" buttons of an item; the actions belong to the parent Builder. */
671
665
  function itemActions(builder, itemKey, path, cloneDisabled) {
672
666
  const label = (name) => t(`filament-forms::components.builder.actions.${name}.label`);
673
667
  const danger = colorsOf(builder.livewire)?.getComponentClasses('icon-button', 'danger') ?? [];
674
668
  return `<div wire:key="" ${anonymousScope(path)}><div class="fi-sc-actions"><div class="fi-ac">${iconActionButton({ label: label('clone'), icon: 'm-square-2-stack', expression: builder.actionExpression('clone', { item: itemKey }), disabled: cloneDisabled })}${iconActionButton({ label: label('delete'), icon: 'm-trash', expression: builder.actionExpression('delete', { item: itemKey }), colorClasses: danger })}</div></div></div>`;
675
669
  }
676
- /** Ligne d'une règle : sélecteur d'opérateur, réglages, actions (port du Flex de Constraint::getBuilderBlock). */
670
+ /** Rule row: operator select, settings, actions. */
677
671
  class RuleRow extends Component {
678
672
  builder;
679
673
  itemKey;
@@ -685,7 +679,7 @@ class RuleRow extends Component {
685
679
  const constraint = this.constraint;
686
680
  const parsed = constraint.parseOperator(this.data.operator);
687
681
  const operator = Select.make('operator').label(constraint.getLabel()).placeholder(t('filament-forms::components.select.placeholder')).options(constraint.getOperatorOptions()).live()
688
- // Changement d'opérateur : ses réglages par défaut complètent les réglages existants (comme `fill()` dans Filament).
682
+ // On operator change, its default settings fill in the gaps without overwriting existing settings.
689
683
  .afterStateUpdated(({ state, get, set }) => {
690
684
  const next = constraint.parseOperator(state)?.operator.defaults() ?? null;
691
685
  const current = get('settings');
@@ -694,14 +688,14 @@ class RuleRow extends Component {
694
688
  return [new Schema(c.livewire).key(c.getKey()).statePath(c.getStatePath() || null).operation(c.getOperation()).record(c.getRecord()).root(false).columns(CONTAINER_GRID).dense()
695
689
  .components([operator, new SettingsGroup(parsed ? parsed.operator.fields() : [])])];
696
690
  }
697
- /** Le conteneur de la ligne est un conteneur de requêtes CSS (`gridContainer()`), dont dépendent les colonnes `@md` / `@xl`. */
691
+ /** The row is a CSS container (`fi-grid-ctn`), which the `@md` / `@xl` columns depend on. */
698
692
  async toSchemaHtml(isVisible) { return (await super.toSchemaHtml(isVisible)).replace('class="fi-sc-component"', 'class="fi-sc-component fi-grid-ctn"'); }
699
693
  async toHtml() {
700
694
  const path = this.container.getStatePath();
701
695
  return `<div role="group" aria-label="${e(this.constraint.getLabel())}" class="fi-sc-flex fi-from-default"><div wire:key="" ${anonymousScope(path)} class="fi-growable"><div>${await this.getChildSchemas()[0].toHtml()}</div></div>${itemActions(this.builder, this.itemKey, path, this.builder.isAtRuleLimit())}</div>`;
702
696
  }
703
697
  }
704
- /** Groupes d'un bloc « OR » : chaque groupe contient son propre constructeur de règles (port du Repeater `groups`). */
698
+ /** Groups of an "OR" block: each group holds its own rule builder (rendered as a repeater). */
705
699
  class OrGroups extends Field {
706
700
  builder;
707
701
  itemKey;
@@ -759,7 +753,7 @@ class OrGroups extends Field {
759
753
  return `<div role="group" aria-label="${e(tr('form.or_groups.block.label'))}" class="fi-sc-flex fi-from-default"><div wire:key="${e(livewireKey)}" ${anonymousScope(path)} class="fi-growable">${await this.wrap(repeater, { labelAsDiv: true })}</div>${itemActions(this.builder, this.itemKey, path, this.builder.isAtRuleLimit())}</div>`;
760
754
  }
761
755
  }
762
- /** Constructeur de règles (port de RuleBuilder) : un Builder dont les blocs sont les contraintes du filtre, plus le bloc « OR ». */
756
+ /** Rule builder: a Builder whose blocks are the filter's constraints, plus the "OR" block. */
763
757
  export class RuleBuilder extends Builder {
764
758
  filter;
765
759
  depth;
@@ -782,7 +776,7 @@ export class RuleBuilder extends Builder {
782
776
  .blockPickerColumns(columns).blockPickerWidth(columns === 2 ? 'md' : columns === 3 ? '2xl' : null)
783
777
  .extraClass('fi-fo-builder-not-contained').maxItems(MAX_RULES);
784
778
  }
785
- /** Le bloc « OR » n'est plus proposé (ni accepté) quand il dépasserait la profondeur autorisée ; rien ne l'est une fois le plafond de règles atteint. */
779
+ /** The "OR" block is neither offered nor accepted beyond the maximum depth; nothing is offered once the rule limit is reached. */
786
780
  getBlockPickerBlocks() {
787
781
  if (this.isAtRuleLimit())
788
782
  return [];
@@ -790,13 +784,13 @@ export class RuleBuilder extends Builder {
790
784
  }
791
785
  getBlock(name) { return name === OR && this.depth >= MAX_NESTING_DEPTH ? undefined : super.getBlock(name); }
792
786
  canAdd() { return !this.isAtRuleLimit(); }
793
- /** Nombre de règles de l'arbre entier (les blocs « OR » ne comptent pas), lu dans l'état différé de l'hôte. */
787
+ /** Counts rules across the whole tree ("OR" blocks excluded), read from the host's deferred filter state. */
794
788
  isAtRuleLimit() { return this.filter.countRules(this.livewire.tableDeferredFilters?.[this.filter.name]) >= MAX_RULES; }
795
- /** Action de composant sans modale (utilisée aussi par les groupes « OR »). */
789
+ /** Component action without modal (also used by "OR" groups). */
796
790
  makeAction(name, handler) { return this.getActions()[0].constructor.make(name).action(({ arguments: args }) => handler(args)); }
797
791
  }
798
- // ------------------------------------------------------------------ filtre
799
- /** Filtre « constructeur de requêtes » d'une table. */
792
+ // ------------------------------------------------------------------ filter
793
+ /** Query builder table filter. */
800
794
  export class QueryBuilder {
801
795
  name = 'queryBuilder';
802
796
  _constraints = [];
@@ -805,7 +799,7 @@ export class QueryBuilder {
805
799
  static make() { return configured(new QueryBuilder()); }
806
800
  constraints(constraints) {
807
801
  if (new Set(constraints.map((c) => c.name)).size !== constraints.length)
808
- throw new Error('QueryBuilder : deux contraintes portent le même nom.');
802
+ throw new Error('QueryBuilder: two constraints have the same name.');
809
803
  this._constraints = constraints;
810
804
  if (this._model)
811
805
  for (const constraint of constraints)
@@ -813,7 +807,7 @@ export class QueryBuilder {
813
807
  constraint.model(this._model);
814
808
  return this;
815
809
  }
816
- /** Modèle Objection de la table, transmis aux contraintes de relation (listes d'enregistrements liés). */
810
+ /** Objection model of the table, passed to relationship constraints (related record lists). */
817
811
  model(model) {
818
812
  this._model = model;
819
813
  for (const constraint of this._constraints)
@@ -821,7 +815,7 @@ export class QueryBuilder {
821
815
  constraint.model(model);
822
816
  return this;
823
817
  }
824
- /** Avant les indicateurs : charge les libellés des enregistrements liés que citent les règles (`RelationshipConstraint.selectable`). */
818
+ /** Before indicators: loads labels of the related records cited by rules (`RelationshipConstraint.selectable`). */
825
819
  async loadIndicatorData(state, host) {
826
820
  const keys = new Map();
827
821
  const walk = (rules) => {
@@ -849,7 +843,7 @@ export class QueryBuilder {
849
843
  getLabel() { return tr('label'); }
850
844
  getDefaultState() { return { rules: {} }; }
851
845
  getFormDefinition() { return { placeholder: '', options: [] }; }
852
- /** Le filtre occupe toute la largeur du formulaire des filtres. */
846
+ /** The filter spans the full width of the filters form. */
853
847
  getColumnSpan() { return { default: 'full' }; }
854
848
  getFormComponents() { return [new RuleBuilder('rules', this).label(this.getLabel()).hiddenLabel()]; }
855
849
  sanitizeState(state) { return { rules: sanitizeRules(this._constraints, isObject(state) ? state.rules : null) }; }
@@ -860,7 +854,7 @@ export class QueryBuilder {
860
854
  compile(state) { return compileRules(this._constraints, this.sanitizeState(state).rules); }
861
855
  isActive(state) { return this.compile(state).nodes.length > 0; }
862
856
  getIndicators(state) { return Object.entries(this.compile(state).summaries).map(([field, label]) => ({ label, field })); }
863
- /** Retrait d'un indicateur : la règle de premier niveau correspondante disparaît. */
857
+ /** Removing an indicator removes the matching top-level rule. */
864
858
  removeIndicator(state, field) {
865
859
  const rules = { ...this.sanitizeState(state).rules };
866
860
  if (Object.hasOwn(rules, field))