Skip to content

feat(table): query-bar filter mode and a redesigned settings modal - #124

Merged
sdcorejs merged 12 commits into
mainfrom
claude/sd-table-query-bar-0720e1
Oct 30, 2026
Merged

sdcorejs merged 12 commits into
mainfrom
claude/sd-table-query-bar-0720e1

Conversation

@sdcorejs

@sdcorejs sdcorejs commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Two design handoffs for sd-table, one commit per step, plus two default-mode bug fixes found on the way. Everything is written in versions/v19 and rolled out to v20/v21/v22 with npm run sync (check:sync green). Unreleased; CHANGELOG [Unreleased] is updated.

1. Query-bar filter mode (.sdcorejs/handoff/table-query-bar/)

filter.mode: 'query-bar' (also SD_TABLE_CONFIGURATION.filter.mode) replaces quick search, the external filter section and the column filter row with one sd-query-bar above the table. The bar is a display layer only: TableFilterValue, the filter service, SdConvertToPagingReq, items(), filterReq, pagingReq and the cache format are unchanged, so the same edit sends the same request in both modes (the showcase demo prints pagingReq to compare).

  • eaa249cb0 sd-query-bar: pinned / required / group fields, lazy option.views, applyOn, appearance: 'flush', onClearAll, clearDisabled, openField(), [sdQueryBarEnd], field-picker find box, date formatting on chips.
  • 3fe970aa9 table option filter.mode / filter.queryBar, adapter table-query-bar.util.ts (68 parity cases), TableFilterRegister.value.default().
  • 3256d0839 the bar in the table: required, manualFilter, clear, cache, loading, mobile (default controls kept), dev warning for filters the bar cannot edit.
  • 3d9476da9 filter icon in each filterable column header, opening that column's chip.
  • 9206a580f docs (sd-table.md "Query bar mode"), CHANGELOG, showcase "Lọc bằng query bar".

Default mode is unchanged (DOM, classes, autoIds, emit order); the host gets data-filter-mode in both modes.

2. Default-mode fixes (687343b99)

  • Grouped headers: filters of columns under a type: 'children' header were never sent, matched locally, sorted, defaulted or restored from cache; leafColumns() now feeds SdConvertToPagingReq, local filter/sort, the filter service and the configuration operator merge. Cache key unchanged.
  • Split filters: clearFilter() with a split-number / split-date column threw in the column-filter template; it now reads ?.from / ?.to and writes through setRangeEnd().

3. Settings modal (.sdcorejs/handoff/table-settings/)

  • 103c54a9b Columns tab becomes a list: handle, Hiện checkbox, summary (title, fixed / shortened flags, width); a row opens to edit title (with “Gốc: … · Khôi phục”), width (number → ${n}px, empty → undefined, other units verbatim, clamped to px minWidth/maxWidth on blur and Apply) and the two checkboxes. Find box above 8 columns, “Hiện n/N cột”, Hiện tất cả. Footer: Khôi phục mặc định left, Bỏ qua outline, Áp dụng filled, all sm.
  • 79db5f374 Keyboard reorder on the handle (Space/Enter, ↑/↓, Escape) with a live region inside the modal.
  • a8402f3f7 Primary dot on the footer settings button (sd-button indicator) while the table uses custom settings: stored columns that differ from the option (modal or a resize on the table), or inline external filters that differ from defaultShowing. Restore defaults turns it off; no dot on mobile.
  • f4c49f320 docs (sd-table.md "Settings modal"), CHANGELOG, showcase "Thiết lập cột" (14 columns); forced-colours title for hidden columns.

Storage key and format, ConfiguredColumn / ConfiguredTable / ConfiguredTableResult, ConfigService, loadConfigurationResult, open() / close() / onSave() / onReset() and the footer autoIds are unchanged.

4. Independent review fixes

Two independent reviewers (query bar + fix; settings modal) read the branch on 2026-10-30. Every finding was verified against the code, then fixed with a failing spec first:

  • 7696ebd54 query bar: with applyOn: 'commit' a chip change applies the search text applied last (the text still being typed is applied by Enter, as in default mode — mode parity); Enter waits for required fields; inline mode keeps disabled fields read-only; a hidden column filtering with NULL / NOT_NULL keeps its chip and a covered column's NULL / NOT_NULL operator is reset; required external filters the bar cannot show are named in the hint; one-ended ranges keep their chip identity; header trigger aria-haspopup="menu".
  • e3b971f88 settings: pressing a handle closes only its own open row (dragging below an open row jumped); the toolbar wraps in a narrow modal; only edited widths are rounded / clamped (no dot after an Apply without edits; -50 / 0 → undefined, 1e3 → 1000px); a click on the resize grip without a change stores nothing; handle and flag names, aria-controls only while open; mouse-path specs; docs and CHANGELOG corrections.

Behaviour changes to note

  • Restore defaults also puts the inline external filters back to defaultShowing (the confirm text already says “all settings”).
  • Column resize: a click on the grip that does not change the width no longer stores a width or calls config.onResize (released feature; listed under Changed).
  • Reset label changed from Mặc định to Khôi phục mặc định; core.component.table.fixed, char-limit, core.component.form-builder.default stay in i18n but the modal no longer uses them.

API consistency review

  • Compared with: sd-query-bar option callbacks (onApply / onQueryChange → new onClearAll), SdTableExternalFilter (required / disabled mirrored on query fields), SdTableColumnLazyValues.option.views, sd-button indicator inputs, sd-input / sd-checkbox size="sm" / hideInlineError / label, sd-modal footer slots, existing table autoIds (-config-*, <B>-query-bar).
  • Additions: filter.mode, filter.queryBar (option + configuration, option wins), TableFilterRegister.value.default() (optional), query-bar field/option knobs (option-only, per the “option is the main API” rule; showOperatorOnChip input widened to boolean | 'auto', compatible), SdQueryBar.applySearch(), autoIds <B>-filter-trigger-<field>, -config-find (sd-input → forms-input-…), -config-show-all. SdTable.hasCustomSetup and SdTable.onConfigDefaultsRestored are additive public members of the exported class (they appear in its .d.ts), named like the existing mobileCards computed and onColumnResize template handler. Internal: ConfigComponent.defaultsRestored (not exported), config-custom.util.ts, column-leaf.util.ts.
  • Renamed / removed public API: none.
  • Versions: v19 → v20/v21/v22 through npm run sync; no hand edits in derived workspaces.
  • Independent review: done 2026-10-30 by two reviewers. API consistency confirmed (compared with the items above, isMobile / mobileCards, onColumnResize, internal outputs such as SdColumnResize.resizeEnd, existing -tree-toggle-<id> / -config-* autoIds). One naming note kept as is: SdQueryBarOption.appearance: 'default' | 'flush' reuses a word that means MatFormFieldAppearance on form controls, without conflict. Findings fixed in section 4.

Deviations from the handoffs and their reasons are listed in each PLAN.md.

Verification

  • ng test sdcorejs-angular --code-coverage: 8669/8669 after the review fixes (Statements 87.48%, Branches 78.4%, Functions 86.49%, Lines 89.92%). Specs written first and seen failing for every step and every review finding.
  • npm run lint, npm run test:theme (53/53), npm run check:scss-hex (0), i18n parity (1466 keys × 5 locales), npm run check:sync, test:showcase-changelog, test:showcase-examples, showcase docs/core specs (111/111).
  • Showcase, both handoff checklists: light, dark, graphite tokens, forced colours, reduced motion, 1280 / 1920px, mobile (query bar), mouse and keyboard reorder, dot on Apply / resize / reload, off after Restore defaults. Not checked with a real screen reader (live region content only).

Screenshots

query bar header icon opens chip
settings, 14 columns settings, open row
keyboard reorder settings button dot

All screenshots: .sdcorejs/handoff/screenshots/table-query-bar-*.png and table-settings-*.png.

🤖 Generated with Claude Code

nghiatt15 and others added 12 commits October 29, 2026 15:41
…enField and flush appearance

Host options for a bar that owns its chip list, the first step of the
sd-table query-bar filter mode (.sdcorejs/handoff/table-query-bar). A bar
that sets none of them behaves as before, except the two fixes below.

- Fields: `pinned` (always on the bar, no remove button, out of the field
  picker, empty chip reads "All"), `required` (empty chip gets the error look,
  `aria-invalid` and `aria-describedby` = `option.invalidHintId`; Search is
  disabled; `invalidFields()` lists them), `group` (field-picker headings),
  and `disabled` is now honoured on the chip (no popover). The picker gets a
  find box above eight fields, matching labels without accents.
- Option: `applyOn: 'commit'` applies a changed popover / inline commit, the
  removal of a chip with a value and Enter in the search box, drops an added
  chip that closes empty, and hides the Search button. `appearance: 'flush'`,
  `onClearAll`, `clearDisabled`, `searchPlaceholder`, `invalidHintId`, and
  `showOperatorOnChip: 'auto'` (input too).
- `openField(key, returnFocusTo?)` opens a field's editor from outside the bar
  and can return focus to the caller. Slot `[sdQueryBarEnd]` after the toolbar.
- Chip text: dates as dd/MM/yyyy (dd/MM/yyyy HH:mm), a range inside one year
  as `01/10 – 31/10/2026`, an open-ended range as "Từ …" / "Đến …". Values of a
  lazy-values chip (VALUE search or `option.views`) and of a values field with
  an items function are looked up once, so restored chips show labels.
- Fixes: the clear-all label counts chips with a value, and reads without a
  count when nothing has one; a BETWEEN with both ends empty is an empty chip.
- i18n (vi/en/ja/ko/zh): query-bar.all, .required, .find-field, .range.from,
  .range.to, .clear.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base of the sd-table query-bar filter mode (.sdcorejs/handoff/table-query-bar,
sections 3–5). Nothing renders differently yet.

- `SdTableOptionFilter.mode` ('default' | 'query-bar') and `queryBar`
  (`SdTableOptionFilterQueryBar`: `editor`, `pinned`, `exclude`), and the same
  two keys in `ISdTableConfiguration.filter`.
- `TableFilterRegister.value.default()` (optional member): the value `remove()`
  stores, read without writing it, for the "clear filters" state.
- `table-query-bar.util.ts` (not exported): `sdTableQueryFields` lists the bar
  fields of a table (keys `quick:` / `external:` / `column:`, groups, pinned
  chips, operators the raw record can hold, filters left out with a reason,
  columns covered by an external filter of the same field);
  `tableFilterToQuery` and `queryToTableFilter` translate between
  `TableFilterValue` and `SdQuery`, keep chip identity, keep the stored form of
  an equivalent value, and report changed fields for the callbacks.
- Spec: for 34 edits, `SdConvertToPagingReq` of the raw record written through
  the bar equals the one of the default-mode controls, in the machine zone and
  in Asia/Ho_Chi_Minh.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `filter.mode: 'query-bar'` (or SD_TABLE_CONFIGURATION.filter.mode) the
table shows one sd-query-bar at the top of its surface instead of quick
search, the external filter section and the column filter row
(.sdcorejs/handoff/table-query-bar, sections 1–2, 6–7, 9–11). The bar is a
view over the same TableFilterValue: reads, cache, `pagingReq`, `onFilter`
and the filter callbacks are those of the default mode.

- Mode: option > configuration > 'default'; mobile always renders the default
  controls. Host attribute `data-filter-mode` reflects what is rendered.
  `externalFilterPerRow`, `collapsible`, `externalFilterCollapsed` and
  `hideExternalFilterToolbar` are ignored in this mode.
- Bar: flush appearance, role="search" labelled "Bộ lọc", data-autoid
  `<autoId>-query-bar`, quick-search placeholder, `sdTableQuickSearchRightDef`
  after its toolbar. Pinned: visible quick filters and inline / required
  external filters (`filter.queryBar.pinned` replaces the list); the rest is
  under "+", grouped "Bộ lọc" / "Cột". Excluded filters keep their values and
  are listed once per table in a dev-mode console warning.
- Applying: every committed chip edit, chip removal and Enter applies in one
  `value.set` (an operator change on an empty column is stored without a read,
  as in the column filter); with `manualFilter` edits wait for the Search
  button. Column `filter.onChange`, `filter.operatorChange`, external and
  quick-filter `onChange` run after the write, only for user changes.
- Raw → bar: chips keep their identity; outside changes (setFilter, a signal
  default, the cache) wait while a chip popover is open and do not overwrite
  unapplied edits of other chips.
- Required: an empty required external filter blocks a server read through
  its value (no ExternalFilterComponent form in this mode) and is reported to
  `onFilter` as `externalFilterValid: false`; empty required chips are marked
  and a hint line under the bar names them.
- Clear: resets to the defaults, calls `filter.onClearFilter`, reads once;
  disabled while the applied state equals the defaults.
- A column hidden through the table settings keeps its chip while it still
  filters.
- i18n (vi/en/ja/ko/zh): table.query-bar.filters, table.query-bar.columns.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In `filter.mode: 'query-bar'` every leaf column whose field is on the query
bar, or is covered by an external filter of the same field, gets a 20 × 20
filter icon next to its title (.sdcorejs/handoff/table-query-bar, section 8).

- Clicking it opens that field's chip on the bar, adding the chip when the
  field has none; it does not sort or start a column resize. When the popover
  closes, focus returns to the icon.
- Hidden (opacity 0, still in the tab order) until the header cell is hovered
  or focused, or on a coarse pointer; always shown in the primary colour while
  the field filters by an applied value. `aria-label` "Lọc theo …" /
  "Đang lọc theo …", `aria-haspopup="dialog"`, data-autoid
  `<autoId>-filter-trigger-<field>`.
- The icon sits beside `.c-header-title`, not inside it: a sortable title is
  role="button" and handles Enter, so a nested button would be nested
  interactive content and Enter on it would sort. Right-aligned columns put it
  before the title so the title edge does not move.
- Grouped headers: the icon is in the leaf cell.
- Reduced motion drops the fade; forced colors use ButtonText / Highlight.
- i18n (vi/en/ja/ko/zh): table.filter-by, table.filtering-by.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 5 of the sd-table query-bar filter mode (.sdcorejs/handoff/table-query-bar,
sections 17–18).

- sd-table.md: "Query bar mode" under the filter option (options, layout,
  chips and the field mapping table, filters left off the bar, applying,
  required, clear, header icons, options ignored in this mode), the
  `data-filter-mode` host attribute, the query-bar and header-icon autoIds,
  and the configuration example.
- CHANGELOG [Unreleased]: query bar host options, table query-bar mode,
  `TableFilterRegister.value.default()`, the clear-all count and the empty
  BETWEEN fix.
- Showcase, Table → "Lọc bằng query bar": switches for `filter.mode` and
  `manualFilter`, a cached option with quick search (term, a required and a
  multiple dropdown), four external filters, operator / split-number / values
  / date columns, a column covered by an external filter and a grouped header,
  and the `pagingReq` the demo backend receives. Registry count 35 → 36.
- The quick-search slot at the end of the bar no longer shrinks under its
  content and stays on the first chip row.
- Screenshots of both modes, dark, graphite, forced colors, required, manual
  filter, refresh and mobile in .sdcorejs/handoff/screenshots.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r a reset

Two default-mode bugs found while building the query-bar filter mode.

Grouped headers: the filter pipeline only walked the top level of `columns`,
so a column under a `type: 'children'` header was invisible to it — its value
was left out of `pagingReq`, ignored by local filtering and sorting, and its
`filter.default`, cached value and SD_TABLE_CONFIGURATION operators were never
applied. A grouped header with a key in `rawColumnFilter` even sent its own
field. `leafColumns()` (services/column-leaf.util.ts) now feeds
`SdConvertToPagingReq`, `filterLocalItems`, the filter service defaults and
cache restore, and the operator merge of `#initConfiguration`. The filter
cache key is unchanged.

Split filters: clearing the filters (clearFilter, the external filter's clear
button, setFilter without the field) can drop the `{ from, to }` of a
split-number / split-date column from the shared columnFilter object, and the
split template threw "Cannot read properties of undefined (reading 'from')".
The template now reads `?.from` / `?.to` and writes through `setRangeEnd()`,
which recreates `{ from, to }` when it is gone and still writes in place
otherwise.

Specs, written first and seen failing (21 red): grouped-header-filter.spec.ts
(converter, local filter and sort, filter service), table.component
.grouped-filter.spec.ts (first request, setFilter, configuration operators,
clear and retype with split filters), and column-filter "split filter whose
range object disappears in place".

Docs: sd-table.md gains "Grouped headers and split filters", CHANGELOG
[Unreleased] gets a Fixed › Table entry, and the query-bar showcase demo
puts its money columns back under a grouped header.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The columns tab of the table settings modal drops its five-column mat-table
for a list: one 44px row per column with the drag handle, the Show checkbox
and a button that opens the row. The summary shows the title, push_pin /
short_text flags and the width ("120 px", "Tự động", "Đang ẩn", or a non-px
value verbatim). The open row edits the title (with "Gốc: … · Khôi phục"),
the width and the fixed / truncate checkboxes; one row is open at a time and
a hidden column locks its details while its Show checkbox stays usable.

- Find box past eight columns, matching the current and the original title
  without accents; reordering is off while searching and the hint says so.
  A row the search filters out closes.
- "Hiện {shown}/{total} cột" and Show all.
- Width: a number is stored as `${n}px`, empty as undefined, a non-px width
  is edited as text. Rounded and clamped to the column's px minWidth /
  maxWidth on blur and on Apply (config-width.util.ts).
- Footer: Restore defaults moves to sdFooterLeft (text, restart_alt);
  Skip is outline, Apply filled; all sm. AutoIds and the reset confirm are
  unchanged; the find box and Show all get -config-find / -config-show-all.
- Edits replace the column object instead of mutating it, so Skip leaves the
  stored value alone.
- i18n: 12 new core.component.table.config.* keys in five locales, new
  drag-hint text. core.component.table.fixed / char-limit and
  core.component.form-builder.default stay but the modal no longer uses them.

Handoff package and plan in .sdcorejs/handoff/table-settings/.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drag handle of each settings row now works from the keyboard. Space or
Enter picks the row up, the arrow keys move it, Space or Enter drops it and
Escape puts the order back. While a row is held, the keys do not reach the
dialog, so Escape does not close it and the arrows do not scroll the list.
Focus follows the moved handle after each step, and leaving the handle drops
the row where it is. Reordering stays off while a search narrows the list.

Each step is read out by a live region inside the dialog
(aria-live="assertive"): picked, moved, dropped, cancelled, with the column
title and its position. The handle is described by the drag hint. A held row
is outlined in primary like the mouse drag preview.

i18n: core.component.table.config.reorder-picked / -moved / -dropped /
-cancelled in five locales.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings

The footer settings button (-config) now carries a primary dot while the
table uses a setup of its own, through the existing sd-button indicator:
[indicator], indicatorColor="primary" and an indicatorLabel ("Đang dùng
thiết lập riêng") read through aria-describedby. Mobile shows no dot.

hasCustomSetup() is true when:
- the stored columns differ from the option in order, or from their stored
  origin in visibility, title, width, fixed or truncate. A width dragged on
  the table counts: persistColumnWidth writes the same storage entry, so the
  table re-reads it on widthChange$. Columns added to or removed from the
  option since the setup was stored are ignored, and comparing with the
  stored origin keeps a title changed in code (or by language) from counting;
- the inline external filters differ from defaultShowing.
Without option.key it follows the session storage, as the table does.

Restore defaults turns the dot off at once: the settings modal now emits an
internal defaultsRestored output and the table puts the inline external
filters back to defaultShowing (the confirm text already says "all
settings").

services/config-custom.util.ts holds defaultConfiguredTable(), extracted from
ConfigService.#default (which now calls it), and sdTableHasCustomSetup().
ConfiguredColumn, ConfiguredTable, ConfigService and the storage format are
unchanged.

i18n: core.component.table.config.customized in five locales.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sd-table.md: new "Settings modal" section (list, open row, width rules,
  find box, keyboard reorder, footer) and "Settings button indicator";
  config.visible row updated; -config-find / -config-show-all autoIds;
  an Accessibility note on the modal.
- CHANGELOG [Unreleased]: Added › Table (settings button dot, keyboard
  reorder) and Changed › Table (columns tab list, footer, Restore defaults
  also restores the inline external filters, Skip no longer touches stored
  settings, i18n keys).
- Showcase: "Thiết lập cột" demo, a 14-column table with key, resizable
  columns, a minWidth/maxWidth column and a 20% column; table registry
  count 37 (total 496), keyword "column settings"; example sources
  regenerated.
- Settings modal: in forced colours a hidden column's title uses GrayText,
  found while checking the showcase.
- Handoff plan: commits, deviations, API consistency notes for the PR and
  the showcase checks; screenshots 13-15 (1920px, graphite, forced colours).

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- applyOn 'commit': a chip change applies the search text applied last;
  the text still being typed is applied by Enter, as the default-mode quick
  search does, so the same edits send the same pagingReq in both modes. The
  table keeps its applied term and leaves the typed text in the box when
  the bar leaves it out. Enter in the search box no longer applies while a
  required field is empty (applySearch()).
- Inline mode renders a disabled field as the read-only chip; it could be
  edited before.
- A hidden column filtering with NULL / NOT_NULL keeps its chip (and the
  clear button); a column covered by an external filter gets its NULL /
  NOT_NULL operator reset, so no invisible condition is sent.
- A required external filter the bar cannot show (custom, filterDef,
  exclude, …) is named in the hint instead of blocking reads silently.
- Chip identity compares chip data in its normalized shape, so a one-ended
  range committed by the popover keeps its chip (and focus).
- Header filter trigger: aria-haspopup="menu" (the popover is a mat-menu).

Specs written first and seen failing: query-bar (3), table query-bar (4),
util (2). sd-table.md and CHANGELOG changes land with the next commit.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pressing a handle closes only its own open row. Closing an open row above
  moved the pressed row away from the pointer before CDK measured, so the
  row landed in the wrong place.
- The toolbar wraps in a narrow modal (count and Hiện tất cả form one
  group) instead of making the modal body scroll sideways.
- Apply and blur round and clamp only edited widths, so an untouched
  `120.5px` or a width below the column's own minWidth no longer lights the
  settings dot. `-50` / `0` store undefined, `1e3` stores `1000px`.
- Column resize: a click on the grip that does not change the width stores
  nothing and does not call config.onResize (it wrote the rendered width of
  auto / % columns before, which now also lit the dot).
- Accessibility: each handle is named after its column, the fixed /
  shortened flags are named, aria-controls only while the row is open.
- Specs for the mouse path: drop index mapping, drop while searching,
  pressing a handle.

Docs: sd-table.md (settings modal, onResize, and the query-bar hint and
trigger from the previous commit), sd-query-bar.md, CHANGELOG [Unreleased]
(also drops the "Skip" bullet: SdStorageService.get() already returned a
clone), both handoff PLAN.md files (review fixes, `defaultsRestored`).

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs
sdcorejs merged commit 8e1bdab into main Oct 30, 2026
7 checks passed
@sdcorejs
sdcorejs deleted the claude/sd-table-query-bar-0720e1 branch October 30, 2026 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants