Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
* Added a `lossless` option to `fmtQuantity` to compact values to millions / billions units only
when doing so loses no precision, rendering the full value otherwise (e.g. `7,100,100` stays
`7,100,100` rather than collapsing to `7.10m`).
* `GridCountLabel` and `StoreCountLabel` support a new `includeMode` prop
(`'roots'` | `'all'` | `'leaves'`) controlling which records are counted. The `'leaves'` mode
excludes parent records, useful for tree grids. Deprecates the `includeChildren` prop.
* Added `Store.leafCount` and `StoreRecord.isLeaf`, reflecting records with no (filtered) children.

### 🐞 Bug Fixes

Expand Down
15 changes: 13 additions & 2 deletions cmp/grid/helpers/GridCountLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ import {box} from '@xh/hoist/cmp/layout';
import {BoxProps, hoistCmp, HoistProps, useContextModel} from '@xh/hoist/core';
import {fmtNumber} from '@xh/hoist/format';
import {logError, pluralize, singularize, withDefault} from '@xh/hoist/utils/js';
import {CountMode, resolveCountLabelValue} from '@xh/hoist/cmp/store/impl/CountLabelSupport';
import {GridModel} from '../GridModel';

export interface GridCountLabelProps extends HoistProps, BoxProps {
/** GridModel to which this component should bind. */
gridModel?: GridModel;

/** Which records to count: 'roots' (default), 'all', or 'leaves' (exclude parents). */
includeMode?: CountMode;

/**
* True to count nested child records.
* If false (default) only root records will be included in count.
* @deprecated use `includeMode` instead ('all' for true, 'roots' for false).
*/
includeChildren?: boolean;

Expand All @@ -41,7 +46,8 @@ export const [GridCountLabel, gridCountLabel] = hoistCmp.withFactory<GridCountLa

render({
gridModel,
includeChildren = false,
includeMode,
includeChildren,
showSelectionCount = 'auto',
unit = 'record',
...props
Expand All @@ -60,7 +66,12 @@ export const [GridCountLabel, gridCountLabel] = hoistCmp.withFactory<GridCountLa

const fmtCount = count => fmtNumber(count, {precision: 0, asHtml: true}),
recCountString = () => {
const count = includeChildren ? store.count : store.rootCount,
const count = resolveCountLabelValue(
store,
includeMode,
includeChildren,
GridCountLabel
),
unitLabel = count === 1 ? singularize(unit) : pluralize(unit);

return `${fmtCount(count)} ${unitLabel}`;
Expand Down
9 changes: 7 additions & 2 deletions cmp/store/StoreCountLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core';
import {Store} from '@xh/hoist/data';
import {fmtNumber} from '@xh/hoist/format';
import {pluralize, singularize} from '@xh/hoist/utils/js';
import {CountMode, resolveCountLabelValue} from './impl/CountLabelSupport';

export interface StoreCountLabelProps extends HoistProps, BoxProps {
/** Store to which this component should bind. */
store: Store;

/** Which records to count: 'roots' (default), 'all', or 'leaves' (exclude parents). */
includeMode?: CountMode;

/**
* True to count nested child records.
* If false (default) only root records will be included in count.
* @deprecated use `includeMode` instead ('all' for true, 'roots' for false).
*/
includeChildren?: boolean;

Expand All @@ -30,8 +35,8 @@ export interface StoreCountLabelProps extends HoistProps, BoxProps {
export const [StoreCountLabel, storeCountLabel] = hoistCmp.withFactory<StoreCountLabelProps>({
className: 'xh-store-count-label',

render({store, unit = 'record', includeChildren = false, ...props}, ref) {
const count = includeChildren ? store.count : store.rootCount,
render({store, unit = 'record', includeMode, includeChildren, ...props}, ref) {
const count = resolveCountLabelValue(store, includeMode, includeChildren, StoreCountLabel),
unitLabel = count === 1 ? singularize(unit) : pluralize(unit),
item = `${fmtNumber(count, {precision: 0, asHtml: true})} ${unitLabel}`;

Expand Down
48 changes: 48 additions & 0 deletions cmp/store/impl/CountLabelSupport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/
import {Store} from '@xh/hoist/data';
import {apiDeprecated, NameSource} from '@xh/hoist/utils/js';
import {isNil} from 'lodash';

/**
* Specifies which records a count label (e.g. {@link GridCountLabel}, {@link StoreCountLabel})
* should include in its count:
* - `roots` (default): top-level records only.
* - `all`: all records, including nested children.
* - `leaves`: only records without children (i.e. excluding parents) - useful for tree grids.
*/
export type CountMode = 'roots' | 'all' | 'leaves';

/**
* Resolve the record count for a count-label component, honoring the deprecated `includeChildren`
* boolean when `includeMode` is not specified.
* @internal
*/
export function resolveCountLabelValue(
store: Store,
includeMode: CountMode,
includeChildren: boolean,
source: NameSource
): number {
if (!isNil(includeChildren)) {
apiDeprecated('includeChildren', {
v: 'v88',
msg: "Use 'includeMode' instead ('all' for true, 'roots' for false).",
source
});
includeMode = includeMode ?? (includeChildren ? 'all' : 'roots');
}

switch (includeMode ?? 'roots') {
case 'all':
return store.count;
case 'leaves':
return store.leafCount;
default:
return store.rootCount;
}
}
6 changes: 6 additions & 0 deletions data/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,12 @@ export class Store
return this._current.rootCount;
}

/** The count of the filtered leaf records (those with no children) in the store. */
@computed
get leafCount(): number {
return this._filtered.list.reduce((count, rec) => count + (rec.isLeaf ? 1 : 0), 0);
}

/** True if the store is empty after filters have been applied */
@computed
get empty(): boolean {
Expand Down
5 changes: 5 additions & 0 deletions data/StoreRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export class StoreRecord {
return this.store.getChildrenById(this.id, false);
}

/** True if this record has no children, respecting any active filter. */
get isLeaf(): boolean {
return !this.children.length;
}

/** Descendants of this record, respecting any filter (if applied). */
get descendants(): StoreRecord[] {
return this.store.getDescendantsById(this.id, true);
Expand Down
Loading