You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
Commit d0c0c7f
Browse filesBrowse the repository at this point in the historyBrowse files
feat(fields,app-shell,plugin-list): 「is empty」 / 「is not empty」 builders write the spec's one operator $empty; fields and app-shell floor at @objectstack/spec ^17.6.0 (objectui#10813) (#11555)
Fixes#10813
Clause-②: yes
## What this PR is
The builders that offer 「is empty」 / 「is not empty」 now write the spec's
one operator, `$empty`, and keep no per-type table. The spec's
`expandEmptyOperator` does the expansion by the column's declared type.
| Builder | Before | After |
|:--|:--|:--|
| `@object-ui/fields` `FilterConditionField` | objectui#10790's `$or` of
`$in: ['']` and `$null` | `{ F: { $empty: true \| false } }` |
| `@object-ui/app-shell` dataset bridge | `$exists` | `{ F: { $empty:
true \| false } }` |
| `@object-ui/plugin-list` `ListView.convertFilterGroupToAST` | a null
test | the value-less `isempty` / `isnotempty` node, so the live query
and the saved view agree |
**How stored rules read:**
- Reading writes nothing, so an un-edited stored rule keeps its bytes.
- `FilterConditionField` opens both earlier shapes as the same row, and
writes `$empty` on the next edit. The re-scope per column class is
stated in the changeset.
- The dataset bridge opens a stored `$exists` in the Source tab, bytes
untouched (Q1 = A, by objectui#10257's invariant and objectui#9559
ruling B).
**Floors:** `@object-ui/fields` and `@object-ui/app-shell` raise their
`@objectstack/spec` floor to `^17.6.0`, because they write `$empty`,
which 17.5.0 declares staged and refuses. plugin-list's peer `^17.0.0`
stays: it writes `isempty`, which every spec in that range accepts.
**Drill:** the dataset inspector's `{ $empty: false }` drills through
objectui#11547's `[empty]` arm, which is already on `main`. This PR
changes prose only in `drillUrlFilters.ts`.
## Reviews
- Seat ACCEPT `5969017543`, with Q1 = A ruled in the seat.
- Contract review FAIL `5969173746` (② floors), then REWORK
`5969287928`, then the delta contract review **PASS** `5969468192` on
`20770e4e9`.
- **This head** merges `main` (objectui#11531, objectui#11547). It drops
one stale clause from the dataset changeset: `driver-memory` 17.6.0
compiles `$null` on the cube face. It also reconciles the two drill
sentences with objectui#11547's `[empty]` arm, prose only. No contract
surface moved beyond the reviewed head.
## Tests (dev reports `5968977150`, the rework report, and the landing
report)
- 16 files and 592 tests green, covering the slice's 10 files and 6
drill files;
- type-check passes for fields and app-shell;
- `check:spec-floors` is green, with no `types` finding now;
- the lockfile integrity check is clean;
- the changeset gates and citations (0 new) pass;
- the ablations from the build round were 25, 13 and 7 red.
## Changeset
Three `minor` changesets: `10813-filter-condition-empty-operator`,
`10813-dataset-filter-empty-operator` and
`10813-list-view-empty-operator`. There are dated corrections on the
pending `10790` and `9359` changesets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Studio dataset filter builder writes "Is empty" / "Is not empty" as the spec's `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` instead of `$exists` (objectui#10813).
6
+
7
+
`dataset.filter` and `measure.filter` stored the pair as `{ FIELD: { $exists: false } }` / `{ FIELD: { $exists: true } }`. `$exists` is the spec's has-a-value test (`!= null`), so a text value of `''` or a multi-value `[]` was never "empty" there, while the same operator in the sharing-rule widget and in a saved view meant something else. `@objectstack/spec` 17.6.0 admits `$empty` (objectstack#20446), whose meaning is the column's DECLARED row of the spec's per-type table (ruling B on objectstack#20311): null or `''` on a text-like column, null or `[]` on a multi-value one, null alone on every other type. The analytics service answers it from the field's declared type and `multiple` on its SQL and ObjectQL strategies. The spec's own face table declares one face that refuses it: the `driver-memory` analytics (cube) face, the lowest-priority fallback strategy (`MemoryAnalyticsService`), refuses a newly authored "Is empty" / "Is not empty" filter with `INVALID_FILTER` / 400, where it answered `$exists`. The bridge keeps no copy of the table.
8
+
9
+
The `@objectstack/spec` dependency floor rises from `^17.5.0` to `^17.6.0`, because this package now writes `$empty`, which `@objectstack/spec` 17.5.0 declares staged and refuses.
10
+
11
+
The read half reads `$empty` back as the pair, with a boolean flag only.
12
+
13
+
**What moves for a stored filter.** A stored `$exists` no longer opens as "Is empty" / "Is not empty": it opens in the Source tab, with its bytes untouched, and keeps matching what it matched before. Opening it as the pair would let an edit to ANOTHER row rewrite it to `$empty` and move `''` / `[]` across the line, which the read-half invariant (objectui#10257) forbids, and this inspector offers no `exists` row. To move such a filter to the new meaning, remove the row and add "Is empty" again. Readable stores (this repository, objectstack, hotcrm and cloud) hold no `dataset.filter` with `$exists`.
`FilterConditionField` writes "Is empty" / "Is not empty" as the spec's one 「is empty」 operator, `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` (objectui#10813).
6
+
7
+
The widget behind sharing-rule `criteria_json`, `relatedListFilter` and `summaryOperations.filter` used to write its own meaning of 「is empty」: `{ $or: [{ FIELD: { $in: [''] } }, { FIELD: { $null: true } }] }` and its complement `{ FIELD: { $nin: [''], $null: false } }`, i.e. "no value OR `''`" on every field type. The `''` member reached a number or date column as `IN ('')`, and a multi-value column (stored as JSON by the SQL driver) as an `$in` that driver refuses. `@objectstack/spec` 17.6.0 admits `$empty` to `FILTER_OPERATORS` (objectstack#20446), and its meaning is the field's DECLARED row of the spec's per-type table (ruling B on objectstack#20311): null or `''` on a text-like field, null or `[]` on a multi-value field, null alone on every other type. Every evaluator expands it itself (`expandEmptyOperator`), so the widget now writes the same token on every column type and keeps no copy of the table.
8
+
9
+
The `@objectstack/spec` dependency floor rises from `^17.5.0` to `^17.6.0`, because this package now writes `$empty`, which `@objectstack/spec` 17.5.0 declares staged and refuses.
10
+
11
+
`kvToCondition` reads `$empty` back as the pair, with a boolean flag only: any other flag stays the raw criteria it is, as every evaluator refuses it.
12
+
13
+
**What moves for a stored rule.** Reading alone rewrites nothing: a criteria saved in either earlier shape (the objectui#10790`$or` entry and `$nin` + `$null` pair, or the older `$in: [null, '']` / `$nin: [null, '']`) still opens as the same "Is empty" / "Is not empty" row, and an un-edited rule keeps its stored bytes and keeps matching as before. The next time the criteria is edited, those rows are written as `$empty`, which re-scopes the rule:
14
+
15
+
- text-like column: no change (both mean null or `''`);
16
+
- every other single-valued column (number, boolean, date, datetime, time, select, a single lookup): a row holding `''` is no longer "empty" and becomes "not empty". Since objectstack#20308 the write door stores a cleared number, boolean, date, datetime or time as null, so on those types only a value written before it can hold `''`; a select or lookup can still hold one, and the spec's ruled table does not count it as empty;
17
+
- multi-value column (multiselect, checkboxes, tags, or a `multiple: true` select, lookup or user): a row holding `[]` becomes "empty" and leaves "is not empty". On the SQL driver the earlier shape was refused outright, so a rule there starts running;
18
+
- the `$in: [null, '']` shapes were refused by every objectstack filter face, so a rule still in them starts running.
19
+
20
+
Measured on the installed 17.6.0 by-value readers (`@objectstack/formula`'s `matchesFilterCondition` and `ValueDataSource`) over null, an absent key, `''`, `[]` and set values: the `$or` entry and `$empty: true` disagree only on `[]`. Readable stores (this repository, objectstack, hotcrm and cloud) hold no criteria in either earlier shape.
The list view's live query sends "Is empty" / "Is not empty" as the spec's `isempty` / `isnotempty` instead of an equality to `null` (objectui#10813). `@objectstack/spec` 17.6.0 and later lower that pair to `$empty`; an earlier reader lowers it to `$null`, as it does the saved view's `is_empty`.
6
+
7
+
`convertFilterGroupToAST` resolved the pair to `[FIELD, '=', null]` / `[FIELD, '!=', null]`, a null-only test, before `mapOperator` was consulted. The same rule saved into the view is persisted as `is_empty`, which `@objectstack/spec` 17.6.0 lowers to `$empty` (objectstack#20446): so one filter panel answered two record sets, depending on whether the view had been saved. The pair now takes the value-less path like `is_null`, and `mapOperator` gains `isempty` / `isnotempty` arms. The node is `[FIELD, 'isempty', null]`; the spec discards the third slot.
8
+
9
+
**What moves.** Nothing is stored by this path. Against a 17.6.0 or later server, on a text-like column a row holding `''` is now "empty", and on a multi-value column a row holding `[]` is, matching the server's per-type answer for the saved view. On a `provider: 'value'` list a row with no key at all is now "empty" too: the equality to `null` did not select it there.
Copy file name to clipboardExpand all lines: packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.readHalfHolds-10257.test.tsx
+13-4Lines changed: 13 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -208,7 +208,7 @@ describe('class 1 — an incomplete stored value is not opened as a row the next
208
208
});
209
209
210
210
it('CONTROL: the value-less tokens carry no value to be incomplete, and stay opened',()=>{
0 commit comments