@ai-matrx/records-ui 0.69.0 → 0.70.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/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ComponentType, RefObject } from 'react';
3
3
  import * as _ai_matrx_records from '@ai-matrx/records';
4
- import { PermissionLevel, Table, RecordScopeContext, EnrichCell, RecordsError, Uuid as Uuid$1, AggregateFilter, Field, RecordDocument, ReadRow, RelationDisplay, WriteConflict, ParityFieldType, NewFieldDeclaration, RuleExpression, ValueEnvelope, ChecklistRequirementKind, ContextPolicy, WorkDueState, FieldSensitivity, StageRuleOnFail, SubscriptionCadence, WorkInboxKind, FieldKind, FormSummary, PortalCard, PortalPrincipal, PortalPreviewRow, DashboardBlockKind, AggregateBucket, AggregateMeasure, DashboardBlock, DashboardSummary, DocTemplateRow, DocRenderRow, QuietHours, DashboardBlockResult, ChecklistRunStep, BookingSummary, CaptureSheetFace, CaptureField, AnonTokenBinding, RecordsConfig, RecordsActor, RecordsDataSource, HistoryActor, FieldHistoryEntry, FieldTypeWord } from '@ai-matrx/records';
5
- import { Uuid, Field as Field$1, ReadRow as ReadRow$1 } from '@ai-matrx/records/react';
4
+ import { PermissionLevel, Table, RecordScopeContext, EnrichCell, RecordsError, Uuid as Uuid$1, AggregateFilter, Field, RecordDocument, ReadRow, RelationDisplay, WriteConflict, ParityFieldType, NewFieldDeclaration, RuleExpression, ValueEnvelope, ChecklistRequirementKind, ContextPolicy, WorkDueState, FieldSensitivity, StageRuleOnFail, SubscriptionCadence, WorkInboxKind, FieldKind, RecordFilter as RecordFilter$1, FormSummary, PortalCard, PortalPrincipal, PortalPreviewRow, DashboardBlockKind, AggregateBucket, AggregateMeasure, DashboardBlock, DashboardSummary, DocTemplateRow, DocRenderRow, QuietHours, DashboardBlockResult, ChecklistRunStep, BookingSummary, CaptureSheetFace, CaptureField, AnonTokenBinding, RecordsConfig, RecordsActor, RecordsDataSource, HistoryActor, FieldHistoryEntry, FieldTypeWord } from '@ai-matrx/records';
5
+ import { Uuid, RecordFilter, Field as Field$1, ReadRow as ReadRow$1 } from '@ai-matrx/records/react';
6
6
  import { RecordsClient } from '@ai-matrx/records/core';
7
7
  import { FieldFormatConfig } from '@ai-matrx/design-system/field-formats';
8
8
  import { MatrxColumnDef } from '@ai-matrx/design-system/data-table/types';
@@ -1040,6 +1040,16 @@ interface GridProps {
1040
1040
  tableId: Uuid;
1041
1041
  /** Rows per page. The table's own pagination handles the rest. */
1042
1042
  pageSize?: number | undefined;
1043
+ /**
1044
+ * THE QUESTION THIS GRID IS ANSWERING, in the store's one filter shape.
1045
+ *
1046
+ * Left out, it is the whole table. Supplied — a dashboard number was clicked,
1047
+ * or a saved view carries `filters` — the STORE narrows the page inside its
1048
+ * own query. Never filter `rows` here: this grid only ever holds one page,
1049
+ * so filtering in the browser would drop every matching record on page two
1050
+ * and call the result the answer.
1051
+ */
1052
+ filter?: RecordFilter | undefined;
1043
1053
  /** Opened when a row is clicked. A host that has a record page passes its own. */
1044
1054
  onOpenRecord?: ((recordId: Uuid) => void) | undefined;
1045
1055
  /** SCR-2. Called when an admin clicks the "+" at the end of the headers. */
@@ -1093,7 +1103,7 @@ interface GridProps {
1093
1103
  onPresentationChange?: ((next: GridPresentation) => void) | undefined;
1094
1104
  className?: string | undefined;
1095
1105
  }
1096
- declare function Grid({ tableId, pageSize, onOpenRecord, onAddField, onNewRecordForm, toolbarActions, editable, presentation, onPresentationChange, className, onAskWhoChanged, }: GridProps): react.JSX.Element;
1106
+ declare function Grid({ tableId, pageSize, filter, onOpenRecord, onAddField, onNewRecordForm, toolbarActions, editable, presentation, onPresentationChange, className, onAskWhoChanged, }: GridProps): react.JSX.Element;
1097
1107
  /**
1098
1108
  * One Field becomes one column. The header is the Field's own NAME — never its
1099
1109
  * key (`names.ts`) — and the cell is the value, rendered by its parity type and
@@ -1817,17 +1827,24 @@ interface ViewSwitcherProps {
1817
1827
  /** "Who changed this?" from a grid cell — the host opens that column's timeline. */
1818
1828
  onAskWhoChanged?: ((fieldKey: string, recordId: Uuid) => void) | undefined;
1819
1829
  pageSize?: number | undefined;
1830
+ /**
1831
+ * THE QUESTION THE ADDRESS ASKED, in the store's one filter shape — the same
1832
+ * object the chart counted with. It narrows the grid AND the board through
1833
+ * the store, so switching layout after a drill-through keeps the same rows
1834
+ * instead of quietly widening back to the whole table.
1835
+ */
1836
+ filter?: RecordFilter$1 | undefined;
1820
1837
  className?: string | undefined;
1821
1838
  }
1822
1839
  /** The records this view holds: its Rule's members when it names one, the table otherwise. */
1823
- declare function useViewRecords(view: SavedViewSpec, pageSize?: number): {
1840
+ declare function useViewRecords(view: SavedViewSpec, pageSize?: number, filter?: RecordFilter$1): {
1824
1841
  rows: ReadRow$1[];
1825
1842
  loading: boolean;
1826
1843
  error: RecordsError | null;
1827
1844
  /** Where membership came from, in a sentence. A screen that hides this is hiding the view's meaning. */
1828
1845
  membership: string;
1829
1846
  };
1830
- declare function ViewSwitcher({ view, onLayoutChange, onViewChange, onOpenRecord, onMoved, onNewRecordForm, onAskWhoChanged, pageSize, className, }: ViewSwitcherProps): react.JSX.Element;
1847
+ declare function ViewSwitcher({ view, onLayoutChange, onViewChange, onOpenRecord, onMoved, onNewRecordForm, onAskWhoChanged, pageSize, filter, className, }: ViewSwitcherProps): react.JSX.Element;
1831
1848
 
1832
1849
  interface ViewBarProps {
1833
1850
  /** The Table whose views these are. */
@@ -2169,8 +2186,16 @@ interface DigestSchedulerProps {
2169
2186
  * view's name, so nothing in the list is called "Saved view".
2170
2187
  */
2171
2188
  subjectName?: string | null | undefined;
2172
- /** The filters of the thing being scheduled, written straight into the saved view. */
2173
- filters?: Record<string, unknown> | null | undefined;
2189
+ /**
2190
+ * The filters of the thing being scheduled, written straight into the saved
2191
+ * view — in the store's ONE filter shape (`RecordFilter`), which is what
2192
+ * `custom.view_declare` stores, what `custom.agg_view_admits` reads to decide
2193
+ * whether a change belongs to this view, and what the read and aggregate
2194
+ * doors both take. It was `Record<string, unknown>` until DRILL named the
2195
+ * shape: an unknown here is a subscription that silently notifies about the
2196
+ * wrong records.
2197
+ */
2198
+ filters?: RecordFilter$1 | null | undefined;
2174
2199
  onScheduled?: (() => void) | undefined;
2175
2200
  onClose?: (() => void) | undefined;
2176
2201
  className?: string | undefined;
@@ -2892,14 +2917,12 @@ interface TablePageProps {
2892
2917
  /**
2893
2918
  * WHICH FIELD THE ADDRESS WANTS THE BOARD'S COLUMNS TO BE, for this visit.
2894
2919
  *
2895
- * 🚨 THIS IS HOW A DASHBOARD NUMBER CLICKS THROUGH (lane TAILS-6, 2026-09-22).
2896
- * Until today the canvas told the owner of the business to "bind `openRecords`
2897
- * on <RecordsUiProvider>" a developer's sentence on a customer's screen
2898
- * because nothing in this app routed a bar or a slice anywhere. The store has
2899
- * no door that returns the ROWS behind an aggregate filter, so a click cannot
2900
- * honestly promise a narrowed list; what it CAN do is open this table's board
2901
- * with its columns set to the very field the chart grouped by, where the
2902
- * column the person clicked is right there and every card in it is real.
2920
+ * 🚨 THIS IS HALF OF HOW A DASHBOARD NUMBER CLICKS THROUGH (lane TAILS-6,
2921
+ * 2026-09-22): the board opens with its columns set to the very field the
2922
+ * chart grouped by, so the column the person clicked is right there. The
2923
+ * other half is `filter`, which lane DRILL added hours later once the store
2924
+ * grew `custom.read_records_matching` until then a click could only widen,
2925
+ * and the sentence under the board said so out loud.
2903
2926
  *
2904
2927
  * It overrides the saved view's own `groupField` for this visit only, exactly
2905
2928
  * as `activeView` overrides its layout, and it is never written back.
@@ -2908,40 +2931,48 @@ interface TablePageProps {
2908
2931
  /**
2909
2932
  * WHAT THE PERSON CLICKED TO GET HERE — "Jobs by stage · Awaiting parts".
2910
2933
  *
2911
- * Present, the board says where they came from AND says plainly that it is
2912
- * showing every record grouped by that field rather than only the ones the
2913
- * number counted. A screen that quietly showed more rows than the number
2914
- * promised would be the number lying twice.
2934
+ * Present, the board says where they came from AND, when `filter` came with
2935
+ * it, which question is holding the rest of the table back. A screen showing
2936
+ * a subset without saying so is a table lying about how much of itself it is
2937
+ * and one showing MORE rows than the number promised is the number lying
2938
+ * twice.
2915
2939
  */
2916
2940
  cameFrom?: string | null | undefined;
2941
+ /**
2942
+ * WHICH RECORDS THE ADDRESS ASKED FOR, in the store's one filter shape.
2943
+ *
2944
+ * 🚨 THIS IS WHAT MAKES A DASHBOARD NUMBER WALKABLE (lane DRILL, 2026-09-22).
2945
+ * TAILS-6 could route a bar click here but could not narrow anything: the
2946
+ * store had no door that took a filter, so the click opened the WHOLE table
2947
+ * and a sentence underneath said so. `custom.read_records_matching` takes the
2948
+ * same object the chart counted with, and this prop is how the address hands
2949
+ * it to the grid and the board — both of them, so switching layout does not
2950
+ * quietly widen back to every record.
2951
+ *
2952
+ * It is a QUESTION for this visit, exactly like `activeView` and
2953
+ * `activeGroupField`, and is never written onto the saved view.
2954
+ */
2955
+ filter?: RecordFilter$1 | null | undefined;
2917
2956
  className?: string | undefined;
2918
2957
  }
2919
- /**
2920
- * THE SENTENCE A TABLE YOU CANNOT SEE SAYS.
2921
- *
2922
- * Measured on the real screen (independent verdict, 19 September): opening a
2923
- * table's page while another organization was active left the main area's text
2924
- * "literally the empty string" after eighteen seconds — no sentence, no
2925
- * spinner, no console error. A made-up id did the same. The cause is exactly
2926
- * one line of this file: `custom.table_list` correctly answers the tables this
2927
- * person can see in THIS organization, the table is not among them, so
2928
- * `useTable` answers `{ data: null, error: null }` — not a refusal, just
2929
- * absence — and the page rendered a skeleton for ever.
2930
- *
2931
- * Absence is an ANSWER and it gets a sentence. It deliberately does not say
2932
- * whether the table exists: this person cannot see it, and which of the two it
2933
- * is is not theirs to learn.
2934
- */
2935
2958
  /**
2936
2959
  * WHAT THE BOARD SAYS TO SOMEBODY A DASHBOARD NUMBER SENT HERE.
2937
2960
  *
2938
- * It says two things and neither is optional: which number they clicked, and
2939
- * that this is every record grouped by that field rather than only the ones the
2940
- * number counted. The store has no door that returns the rows behind an
2941
- * aggregate filter, so claiming a narrowed list would be a lie — and showing a
2942
- * wider one silently would be the same lie with nobody told.
2943
- */
2944
- declare function cameFromLine(label: string, groupField: string | null): string;
2961
+ * 🚨 IT USED TO SAY THE OPPOSITE, AND IT WAS TELLING THE TRUTH WHEN IT DID.
2962
+ * Until 2026-09-22 this sentence read "This is every record grouped into the
2963
+ * same columns the chart used not only the ones that number counted",
2964
+ * because `custom.read_records` took no filter and `custom.record_aggregate`
2965
+ * answers groups and counts, never ids: the store had no door that could open
2966
+ * the rows behind a number, so claiming a narrowed list would have been a lie.
2967
+ * `custom.read_records_matching` is that door, and it evaluates the question
2968
+ * through the very same `custom.record_filter_sql` the number was counted
2969
+ * with — so this now says the narrow thing, and means it.
2970
+ *
2971
+ * It still says both halves out loud: which number they clicked, and WHICH
2972
+ * QUESTION is holding rows back. A screen showing a subset without saying so
2973
+ * is a table lying about how much of itself it is.
2974
+ */
2975
+ declare function cameFromLine(label: string, groupField: string | null, question?: string | null): string;
2945
2976
  declare const TABLE_NOT_REACHABLE: string;
2946
2977
  /**
2947
2978
  * THE VIEW EVERY TABLE HAS, AND WHY IT IS NOT OPTIONAL.
@@ -3050,7 +3081,7 @@ declare function openingView(activeView: string | null | undefined, activeDashbo
3050
3081
  layout: ViewLayout | null;
3051
3082
  unknown: string | null;
3052
3083
  };
3053
- declare function TablePage({ tableId, pageSize, seedViews, onLeave, leaveLabel, activeDashboardId, activeRecordId, activeView, onViewChanged, activeGroupField, cameFrom, className, }: TablePageProps): react.JSX.Element;
3084
+ declare function TablePage({ tableId, pageSize, seedViews, onLeave, leaveLabel, activeDashboardId, activeRecordId, activeView, onViewChanged, activeGroupField, cameFrom, filter, className, }: TablePageProps): react.JSX.Element;
3054
3085
 
3055
3086
  /** What the roster can tell us about a user id. `null` when it cannot. */
3056
3087
  type ResolveName = (userId: Uuid$1) => string | null;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ComponentType, RefObject } from 'react';
3
3
  import * as _ai_matrx_records from '@ai-matrx/records';
4
- import { PermissionLevel, Table, RecordScopeContext, EnrichCell, RecordsError, Uuid as Uuid$1, AggregateFilter, Field, RecordDocument, ReadRow, RelationDisplay, WriteConflict, ParityFieldType, NewFieldDeclaration, RuleExpression, ValueEnvelope, ChecklistRequirementKind, ContextPolicy, WorkDueState, FieldSensitivity, StageRuleOnFail, SubscriptionCadence, WorkInboxKind, FieldKind, FormSummary, PortalCard, PortalPrincipal, PortalPreviewRow, DashboardBlockKind, AggregateBucket, AggregateMeasure, DashboardBlock, DashboardSummary, DocTemplateRow, DocRenderRow, QuietHours, DashboardBlockResult, ChecklistRunStep, BookingSummary, CaptureSheetFace, CaptureField, AnonTokenBinding, RecordsConfig, RecordsActor, RecordsDataSource, HistoryActor, FieldHistoryEntry, FieldTypeWord } from '@ai-matrx/records';
5
- import { Uuid, Field as Field$1, ReadRow as ReadRow$1 } from '@ai-matrx/records/react';
4
+ import { PermissionLevel, Table, RecordScopeContext, EnrichCell, RecordsError, Uuid as Uuid$1, AggregateFilter, Field, RecordDocument, ReadRow, RelationDisplay, WriteConflict, ParityFieldType, NewFieldDeclaration, RuleExpression, ValueEnvelope, ChecklistRequirementKind, ContextPolicy, WorkDueState, FieldSensitivity, StageRuleOnFail, SubscriptionCadence, WorkInboxKind, FieldKind, RecordFilter as RecordFilter$1, FormSummary, PortalCard, PortalPrincipal, PortalPreviewRow, DashboardBlockKind, AggregateBucket, AggregateMeasure, DashboardBlock, DashboardSummary, DocTemplateRow, DocRenderRow, QuietHours, DashboardBlockResult, ChecklistRunStep, BookingSummary, CaptureSheetFace, CaptureField, AnonTokenBinding, RecordsConfig, RecordsActor, RecordsDataSource, HistoryActor, FieldHistoryEntry, FieldTypeWord } from '@ai-matrx/records';
5
+ import { Uuid, RecordFilter, Field as Field$1, ReadRow as ReadRow$1 } from '@ai-matrx/records/react';
6
6
  import { RecordsClient } from '@ai-matrx/records/core';
7
7
  import { FieldFormatConfig } from '@ai-matrx/design-system/field-formats';
8
8
  import { MatrxColumnDef } from '@ai-matrx/design-system/data-table/types';
@@ -1040,6 +1040,16 @@ interface GridProps {
1040
1040
  tableId: Uuid;
1041
1041
  /** Rows per page. The table's own pagination handles the rest. */
1042
1042
  pageSize?: number | undefined;
1043
+ /**
1044
+ * THE QUESTION THIS GRID IS ANSWERING, in the store's one filter shape.
1045
+ *
1046
+ * Left out, it is the whole table. Supplied — a dashboard number was clicked,
1047
+ * or a saved view carries `filters` — the STORE narrows the page inside its
1048
+ * own query. Never filter `rows` here: this grid only ever holds one page,
1049
+ * so filtering in the browser would drop every matching record on page two
1050
+ * and call the result the answer.
1051
+ */
1052
+ filter?: RecordFilter | undefined;
1043
1053
  /** Opened when a row is clicked. A host that has a record page passes its own. */
1044
1054
  onOpenRecord?: ((recordId: Uuid) => void) | undefined;
1045
1055
  /** SCR-2. Called when an admin clicks the "+" at the end of the headers. */
@@ -1093,7 +1103,7 @@ interface GridProps {
1093
1103
  onPresentationChange?: ((next: GridPresentation) => void) | undefined;
1094
1104
  className?: string | undefined;
1095
1105
  }
1096
- declare function Grid({ tableId, pageSize, onOpenRecord, onAddField, onNewRecordForm, toolbarActions, editable, presentation, onPresentationChange, className, onAskWhoChanged, }: GridProps): react.JSX.Element;
1106
+ declare function Grid({ tableId, pageSize, filter, onOpenRecord, onAddField, onNewRecordForm, toolbarActions, editable, presentation, onPresentationChange, className, onAskWhoChanged, }: GridProps): react.JSX.Element;
1097
1107
  /**
1098
1108
  * One Field becomes one column. The header is the Field's own NAME — never its
1099
1109
  * key (`names.ts`) — and the cell is the value, rendered by its parity type and
@@ -1817,17 +1827,24 @@ interface ViewSwitcherProps {
1817
1827
  /** "Who changed this?" from a grid cell — the host opens that column's timeline. */
1818
1828
  onAskWhoChanged?: ((fieldKey: string, recordId: Uuid) => void) | undefined;
1819
1829
  pageSize?: number | undefined;
1830
+ /**
1831
+ * THE QUESTION THE ADDRESS ASKED, in the store's one filter shape — the same
1832
+ * object the chart counted with. It narrows the grid AND the board through
1833
+ * the store, so switching layout after a drill-through keeps the same rows
1834
+ * instead of quietly widening back to the whole table.
1835
+ */
1836
+ filter?: RecordFilter$1 | undefined;
1820
1837
  className?: string | undefined;
1821
1838
  }
1822
1839
  /** The records this view holds: its Rule's members when it names one, the table otherwise. */
1823
- declare function useViewRecords(view: SavedViewSpec, pageSize?: number): {
1840
+ declare function useViewRecords(view: SavedViewSpec, pageSize?: number, filter?: RecordFilter$1): {
1824
1841
  rows: ReadRow$1[];
1825
1842
  loading: boolean;
1826
1843
  error: RecordsError | null;
1827
1844
  /** Where membership came from, in a sentence. A screen that hides this is hiding the view's meaning. */
1828
1845
  membership: string;
1829
1846
  };
1830
- declare function ViewSwitcher({ view, onLayoutChange, onViewChange, onOpenRecord, onMoved, onNewRecordForm, onAskWhoChanged, pageSize, className, }: ViewSwitcherProps): react.JSX.Element;
1847
+ declare function ViewSwitcher({ view, onLayoutChange, onViewChange, onOpenRecord, onMoved, onNewRecordForm, onAskWhoChanged, pageSize, filter, className, }: ViewSwitcherProps): react.JSX.Element;
1831
1848
 
1832
1849
  interface ViewBarProps {
1833
1850
  /** The Table whose views these are. */
@@ -2169,8 +2186,16 @@ interface DigestSchedulerProps {
2169
2186
  * view's name, so nothing in the list is called "Saved view".
2170
2187
  */
2171
2188
  subjectName?: string | null | undefined;
2172
- /** The filters of the thing being scheduled, written straight into the saved view. */
2173
- filters?: Record<string, unknown> | null | undefined;
2189
+ /**
2190
+ * The filters of the thing being scheduled, written straight into the saved
2191
+ * view — in the store's ONE filter shape (`RecordFilter`), which is what
2192
+ * `custom.view_declare` stores, what `custom.agg_view_admits` reads to decide
2193
+ * whether a change belongs to this view, and what the read and aggregate
2194
+ * doors both take. It was `Record<string, unknown>` until DRILL named the
2195
+ * shape: an unknown here is a subscription that silently notifies about the
2196
+ * wrong records.
2197
+ */
2198
+ filters?: RecordFilter$1 | null | undefined;
2174
2199
  onScheduled?: (() => void) | undefined;
2175
2200
  onClose?: (() => void) | undefined;
2176
2201
  className?: string | undefined;
@@ -2892,14 +2917,12 @@ interface TablePageProps {
2892
2917
  /**
2893
2918
  * WHICH FIELD THE ADDRESS WANTS THE BOARD'S COLUMNS TO BE, for this visit.
2894
2919
  *
2895
- * 🚨 THIS IS HOW A DASHBOARD NUMBER CLICKS THROUGH (lane TAILS-6, 2026-09-22).
2896
- * Until today the canvas told the owner of the business to "bind `openRecords`
2897
- * on <RecordsUiProvider>" a developer's sentence on a customer's screen
2898
- * because nothing in this app routed a bar or a slice anywhere. The store has
2899
- * no door that returns the ROWS behind an aggregate filter, so a click cannot
2900
- * honestly promise a narrowed list; what it CAN do is open this table's board
2901
- * with its columns set to the very field the chart grouped by, where the
2902
- * column the person clicked is right there and every card in it is real.
2920
+ * 🚨 THIS IS HALF OF HOW A DASHBOARD NUMBER CLICKS THROUGH (lane TAILS-6,
2921
+ * 2026-09-22): the board opens with its columns set to the very field the
2922
+ * chart grouped by, so the column the person clicked is right there. The
2923
+ * other half is `filter`, which lane DRILL added hours later once the store
2924
+ * grew `custom.read_records_matching` until then a click could only widen,
2925
+ * and the sentence under the board said so out loud.
2903
2926
  *
2904
2927
  * It overrides the saved view's own `groupField` for this visit only, exactly
2905
2928
  * as `activeView` overrides its layout, and it is never written back.
@@ -2908,40 +2931,48 @@ interface TablePageProps {
2908
2931
  /**
2909
2932
  * WHAT THE PERSON CLICKED TO GET HERE — "Jobs by stage · Awaiting parts".
2910
2933
  *
2911
- * Present, the board says where they came from AND says plainly that it is
2912
- * showing every record grouped by that field rather than only the ones the
2913
- * number counted. A screen that quietly showed more rows than the number
2914
- * promised would be the number lying twice.
2934
+ * Present, the board says where they came from AND, when `filter` came with
2935
+ * it, which question is holding the rest of the table back. A screen showing
2936
+ * a subset without saying so is a table lying about how much of itself it is
2937
+ * and one showing MORE rows than the number promised is the number lying
2938
+ * twice.
2915
2939
  */
2916
2940
  cameFrom?: string | null | undefined;
2941
+ /**
2942
+ * WHICH RECORDS THE ADDRESS ASKED FOR, in the store's one filter shape.
2943
+ *
2944
+ * 🚨 THIS IS WHAT MAKES A DASHBOARD NUMBER WALKABLE (lane DRILL, 2026-09-22).
2945
+ * TAILS-6 could route a bar click here but could not narrow anything: the
2946
+ * store had no door that took a filter, so the click opened the WHOLE table
2947
+ * and a sentence underneath said so. `custom.read_records_matching` takes the
2948
+ * same object the chart counted with, and this prop is how the address hands
2949
+ * it to the grid and the board — both of them, so switching layout does not
2950
+ * quietly widen back to every record.
2951
+ *
2952
+ * It is a QUESTION for this visit, exactly like `activeView` and
2953
+ * `activeGroupField`, and is never written onto the saved view.
2954
+ */
2955
+ filter?: RecordFilter$1 | null | undefined;
2917
2956
  className?: string | undefined;
2918
2957
  }
2919
- /**
2920
- * THE SENTENCE A TABLE YOU CANNOT SEE SAYS.
2921
- *
2922
- * Measured on the real screen (independent verdict, 19 September): opening a
2923
- * table's page while another organization was active left the main area's text
2924
- * "literally the empty string" after eighteen seconds — no sentence, no
2925
- * spinner, no console error. A made-up id did the same. The cause is exactly
2926
- * one line of this file: `custom.table_list` correctly answers the tables this
2927
- * person can see in THIS organization, the table is not among them, so
2928
- * `useTable` answers `{ data: null, error: null }` — not a refusal, just
2929
- * absence — and the page rendered a skeleton for ever.
2930
- *
2931
- * Absence is an ANSWER and it gets a sentence. It deliberately does not say
2932
- * whether the table exists: this person cannot see it, and which of the two it
2933
- * is is not theirs to learn.
2934
- */
2935
2958
  /**
2936
2959
  * WHAT THE BOARD SAYS TO SOMEBODY A DASHBOARD NUMBER SENT HERE.
2937
2960
  *
2938
- * It says two things and neither is optional: which number they clicked, and
2939
- * that this is every record grouped by that field rather than only the ones the
2940
- * number counted. The store has no door that returns the rows behind an
2941
- * aggregate filter, so claiming a narrowed list would be a lie — and showing a
2942
- * wider one silently would be the same lie with nobody told.
2943
- */
2944
- declare function cameFromLine(label: string, groupField: string | null): string;
2961
+ * 🚨 IT USED TO SAY THE OPPOSITE, AND IT WAS TELLING THE TRUTH WHEN IT DID.
2962
+ * Until 2026-09-22 this sentence read "This is every record grouped into the
2963
+ * same columns the chart used not only the ones that number counted",
2964
+ * because `custom.read_records` took no filter and `custom.record_aggregate`
2965
+ * answers groups and counts, never ids: the store had no door that could open
2966
+ * the rows behind a number, so claiming a narrowed list would have been a lie.
2967
+ * `custom.read_records_matching` is that door, and it evaluates the question
2968
+ * through the very same `custom.record_filter_sql` the number was counted
2969
+ * with — so this now says the narrow thing, and means it.
2970
+ *
2971
+ * It still says both halves out loud: which number they clicked, and WHICH
2972
+ * QUESTION is holding rows back. A screen showing a subset without saying so
2973
+ * is a table lying about how much of itself it is.
2974
+ */
2975
+ declare function cameFromLine(label: string, groupField: string | null, question?: string | null): string;
2945
2976
  declare const TABLE_NOT_REACHABLE: string;
2946
2977
  /**
2947
2978
  * THE VIEW EVERY TABLE HAS, AND WHY IT IS NOT OPTIONAL.
@@ -3050,7 +3081,7 @@ declare function openingView(activeView: string | null | undefined, activeDashbo
3050
3081
  layout: ViewLayout | null;
3051
3082
  unknown: string | null;
3052
3083
  };
3053
- declare function TablePage({ tableId, pageSize, seedViews, onLeave, leaveLabel, activeDashboardId, activeRecordId, activeView, onViewChanged, activeGroupField, cameFrom, className, }: TablePageProps): react.JSX.Element;
3084
+ declare function TablePage({ tableId, pageSize, seedViews, onLeave, leaveLabel, activeDashboardId, activeRecordId, activeView, onViewChanged, activeGroupField, cameFrom, filter, className, }: TablePageProps): react.JSX.Element;
3054
3085
 
3055
3086
  /** What the roster can tell us about a user id. `null` when it cannot. */
3056
3087
  type ResolveName = (userId: Uuid$1) => string | null;
package/dist/index.js CHANGED
@@ -3332,6 +3332,7 @@ var NO_COLUMNS_WHY = "A column is what a record holds \u2014 a name, a date, an
3332
3332
  function Grid({
3333
3333
  tableId,
3334
3334
  pageSize = 50,
3335
+ filter,
3335
3336
  onOpenRecord,
3336
3337
  onAddField,
3337
3338
  onNewRecordForm,
@@ -3347,7 +3348,7 @@ function Grid({
3347
3348
  const table = useTable2(tableId);
3348
3349
  const fields = useFields3(tableId);
3349
3350
  const [page, setPage] = useState10(0);
3350
- const records = useRecords2(tableId, { pageSize, page });
3351
+ const records = useRecords2(tableId, { pageSize, page, ...filter ? { filter } : {} });
3351
3352
  const rights = useTableRights(table.data);
3352
3353
  const rows = useMemo9(() => records.data?.rows ?? [], [records.data]);
3353
3354
  const rowIds = useMemo9(() => rows.map((row) => row.id), [rows]);
@@ -7131,9 +7132,12 @@ function unplacedName(record, field, labels) {
7131
7132
  // src/ViewSwitcher.tsx
7132
7133
  import { jsx as jsx26, jsxs as jsxs22 } from "react/jsx-runtime";
7133
7134
  var NO_FIELDS = [];
7134
- function useViewRecords(view, pageSize = 200) {
7135
+ function useViewRecords(view, pageSize = 200, filter) {
7135
7136
  const client = useRecordsClient16();
7136
- const table = useRecords5(view.ruleId ? null : view.subject, { pageSize });
7137
+ const table = useRecords5(view.ruleId ? null : view.subject, {
7138
+ pageSize,
7139
+ ...filter ? { filter } : {}
7140
+ });
7137
7141
  const [ruled, setRuled] = useState24({
7138
7142
  rows: [],
7139
7143
  loading: Boolean(view.ruleId),
@@ -7188,6 +7192,7 @@ function ViewSwitcher({
7188
7192
  onNewRecordForm,
7189
7193
  onAskWhoChanged,
7190
7194
  pageSize = 200,
7195
+ filter,
7191
7196
  className
7192
7197
  }) {
7193
7198
  const [layout, setLayout] = useState24(view.layout);
@@ -7225,6 +7230,7 @@ function ViewSwitcher({
7225
7230
  {
7226
7231
  tableId: view.subject,
7227
7232
  pageSize,
7233
+ ...filter ? { filter } : {},
7228
7234
  ...local.presentation ?? view.presentation ? { presentation: local.presentation ?? view.presentation } : {},
7229
7235
  ...onViewChange ? { onPresentationChange: (next) => change({ presentation: next }) } : {},
7230
7236
  ...onOpenRecord ? { onOpenRecord } : {},
@@ -7236,6 +7242,7 @@ function ViewSwitcher({
7236
7242
  {
7237
7243
  view: { ...view, ...local, layout },
7238
7244
  pageSize,
7245
+ ...filter ? { filter } : {},
7239
7246
  onChange: change,
7240
7247
  remembered: Boolean(onViewChange),
7241
7248
  ...onOpenRecord ? { onOpenRecord } : {},
@@ -7283,13 +7290,14 @@ function boardColumnChoice(fields, chosenKey, stage) {
7283
7290
  function Board({
7284
7291
  view,
7285
7292
  pageSize,
7293
+ filter,
7286
7294
  onChange,
7287
7295
  remembered,
7288
7296
  onOpenRecord,
7289
7297
  onMoved
7290
7298
  }) {
7291
7299
  const fields = useFields12(view.subject);
7292
- const records = useViewRecords(view, pageSize);
7300
+ const records = useViewRecords(view, pageSize, filter);
7293
7301
  const stage = useStageField(view.subject);
7294
7302
  if (fields.error) return /* @__PURE__ */ jsx26(RefusalNotice, { error: fields.error });
7295
7303
  if (records.error) return /* @__PURE__ */ jsx26(RefusalNotice, { error: records.error });
@@ -12852,14 +12860,20 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
12852
12860
  rights.admin ? /* @__PURE__ */ jsx47(Button39, { size: "sm", variant: "ghost", onClick: () => void remove(board), children: "Delete" }) : null
12853
12861
  ] }) : null
12854
12862
  ] }),
12855
- board && scheduling ? /* @__PURE__ */ jsx47(
12856
- DigestScheduler,
12857
- {
12858
- tableId,
12859
- subjectName: board.name,
12860
- ...filter ? { filters: filter } : {},
12861
- onClose: () => setScheduling(false)
12862
- }
12863
+ board && scheduling ? (
12864
+ // ONE SHAPE, SO NO CAST. The dashboard's own filter, the saved view it
12865
+ // schedules over and the drill-through it opens are all the same
12866
+ // `RecordFilter`; the `as Record<string, unknown>` that used to be here
12867
+ // was the seam where they could have drifted.
12868
+ /* @__PURE__ */ jsx47(
12869
+ DigestScheduler,
12870
+ {
12871
+ tableId,
12872
+ subjectName: board.name,
12873
+ ...filter ? { filters: filter } : {},
12874
+ onClose: () => setScheduling(false)
12875
+ }
12876
+ )
12863
12877
  ) : null,
12864
12878
  board ? /* @__PURE__ */ jsxs43("div", { className: "flex flex-col gap-1", children: [
12865
12879
  /* @__PURE__ */ jsxs43("div", { className: "flex items-center gap-1", children: [
@@ -14862,12 +14876,38 @@ function TablesHome({ onOpenTable, className }) {
14862
14876
 
14863
14877
  // src/TablePage.tsx
14864
14878
  import { useCallback as useCallback37, useEffect as useEffect41, useState as useState53 } from "react";
14865
- import { useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14879
+ import { useFields as useFields23, useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14866
14880
  import { Button as Button48, Separator as Separator12, Skeleton as Skeleton34, cn as cn51 } from "@ai-matrx/design-system";
14867
14881
  import { Fragment as Fragment28, jsx as jsx58, jsxs as jsxs53 } from "react/jsx-runtime";
14868
- function cameFromLine(label, groupField) {
14869
- const grouped = groupField ? `grouped into the same columns the chart used` : `for this table`;
14870
- return `You came here from \u201C${label}\u201D. This is every record ${grouped} \u2014 not only the ones that number counted \u2014 so the column you clicked is here with the rest beside it.`;
14882
+ function filterInWords(filter, fields) {
14883
+ const nameOf = (key) => {
14884
+ const field = fields.find((f) => f.key === key);
14885
+ return field ? fieldName(field) : humanize(key);
14886
+ };
14887
+ const parts = Object.keys(filter).map((key) => {
14888
+ const value = filter[key];
14889
+ if (value === null || value === void 0) return `${nameOf(key)} is not filled in`;
14890
+ if (typeof value === "object") {
14891
+ const from = (value.from ?? "").toString().trim();
14892
+ const to = (value.to ?? "").toString().trim();
14893
+ if (from !== "" && to !== "") return `${nameOf(key)} from ${from} up to ${to}`;
14894
+ if (from !== "") return `${nameOf(key)} from ${from} onwards`;
14895
+ if (to !== "") return `${nameOf(key)} up to ${to}`;
14896
+ return `${nameOf(key)} in a period nobody named`;
14897
+ }
14898
+ return `${nameOf(key)} is ${String(value)}`;
14899
+ });
14900
+ if (parts.length === 0) return "";
14901
+ if (parts.length === 1) return parts[0];
14902
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
14903
+ }
14904
+ function cameFromLine(label, groupField, question) {
14905
+ const asked = (question ?? "").trim();
14906
+ if (asked === "") {
14907
+ const grouped = groupField ? " grouped into the same columns the chart used" : "";
14908
+ return `You came here from \u201C${label}\u201D. This is every record in this table${grouped} \u2014 that number was counted over all of them.`;
14909
+ }
14910
+ return `You came here from \u201C${label}\u201D. These are only the records that number counted: ${asked}. Everything else in this table is being held back \u2014 clear the filter to see all of it.`;
14871
14911
  }
14872
14912
  var TABLE_NOT_REACHABLE = "This table is not in the organization you are working in, so there is nothing here to show. Switch to the organization that owns it and open it again \u2014 or it may have been deleted.";
14873
14913
  var DEFAULT_VIEW_NAME = "All records";
@@ -14914,11 +14954,14 @@ function TablePage({
14914
14954
  onViewChanged,
14915
14955
  activeGroupField,
14916
14956
  cameFrom,
14957
+ filter,
14917
14958
  className
14918
14959
  }) {
14919
14960
  const client = useRecordsClient43();
14920
14961
  const table = useTable22(tableId);
14921
14962
  const rights = useTableRights(table.data);
14963
+ const pageFields = useFields23(filter ? tableId : null);
14964
+ const askedInWords = filter ? filterInWords(filter, pageFields.data ?? []) : null;
14922
14965
  const organizationId = useRecordsClient43().config.organizationId;
14923
14966
  const [view, setView] = useState53(null);
14924
14967
  const opening = openingRail(activeRecordId);
@@ -15112,7 +15155,7 @@ function TablePage({
15112
15155
  {
15113
15156
  "data-testid": "came-from-a-number",
15114
15157
  className: "rounded-md border px-3 py-2 text-xs leading-relaxed text-muted-foreground",
15115
- children: cameFromLine(cameFrom, activeGroupField ?? null)
15158
+ children: cameFromLine(cameFrom, activeGroupField ?? null, askedInWords)
15116
15159
  }
15117
15160
  ) : null,
15118
15161
  /* @__PURE__ */ jsx58(
@@ -15126,6 +15169,7 @@ function TablePage({
15126
15169
  ...activeGroupField ? { groupField: activeGroupField } : {}
15127
15170
  },
15128
15171
  pageSize,
15172
+ ...filter ? { filter } : {},
15129
15173
  onLayoutChange: (layout) => {
15130
15174
  setLayoutFromLink(layout);
15131
15175
  onViewChanged?.(layout);