Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
CopyPropertyTextContextMenuItem,
IModelAppUserPreferencesStorage,
RemoveFavoritePropertyContextMenuItem,
ShowHideNullValuesSettingsMenuItem,
ShowHideEmptyValuesSettingsMenuItem,
} from "@itwin/property-grid-react";
import type { IModelConnection } from "@itwin/core-frontend";
// __PUBLISH_EXTRACT_END__
Expand Down Expand Up @@ -78,7 +78,7 @@ describe("Property grid", () => {
// the list populates the settings menu
settingsMenuItems: [
// allows hiding properties without values
(props) => <ShowHideNullValuesSettingsMenuItem {...props} persist={true} />,
(props) => <ShowHideEmptyValuesSettingsMenuItem {...props} persist={true} />,
],

// supply an optional custom storage for user preferences, e.g. the show/hide null values used above
Expand Down
4 changes: 2 additions & 2 deletions apps/test-viewer/src/UiProvidersConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
createPropertyGrid,
PropertyGridManager,
RemoveFavoritePropertyContextMenuItem,
ShowHideNullValuesSettingsMenuItem,
ShowHideEmptyValuesSettingsMenuItem,
} from "@itwin/property-grid-react";
import { REPORTS_CONFIG_BASE_URL, ReportsConfigProvider, ReportsConfigWidget } from "@itwin/reports-config-widget-react";
import {
Expand Down Expand Up @@ -225,7 +225,7 @@ const configuredUiItems = new Map<string, UiItem>([
(props) => <CopyPropertyTextContextMenuItem {...props} />,
(props) => <CustomizeFormatPropertyContextMenuItem {...props} />,
],
settingsMenuItems: [(props) => <ShowHideNullValuesSettingsMenuItem {...props} persist={true} />],
settingsMenuItems: [(props) => <ShowHideEmptyValuesSettingsMenuItem {...props} persist={true} />],
onPerformanceMeasured: (feature, elapsedTime) => {
console.log(`PropertyGrid [${feature}] took ${elapsedTime} ms`);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added `ShowHideEmptyValuesSettingsMenuItem` whose name matches the rendered `Show/Hide Empty Values` setting and deprecated `ShowHideNullValuesSettingsMenuItem` in favor of it. Also fixed the setting incorrectly hiding boolean and merged properties.",
"packageName": "@itwin/property-grid-react",
"email": "AzureDevOps@users.noreply.github.com",
"dependentChangeType": "patch"
}
6 changes: 3 additions & 3 deletions packages/itwin/property-grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
CopyPropertyTextContextMenuItem,
IModelAppUserPreferencesStorage,
RemoveFavoritePropertyContextMenuItem,
ShowHideNullValuesSettingsMenuItem,
ShowHideEmptyValuesSettingsMenuItem,
} from "@itwin/property-grid-react";
import type { IModelConnection } from "@itwin/core-frontend";

Expand Down Expand Up @@ -81,7 +81,7 @@ UiItemsManager.register({
// the list populates the settings menu
settingsMenuItems: [
// allows hiding properties without values
(props) => <ShowHideNullValuesSettingsMenuItem {...props} persist={true} />,
(props) => <ShowHideEmptyValuesSettingsMenuItem {...props} persist={true} />,
],

// supply an optional custom storage for user preferences, e.g. the show/hide null values used above
Expand Down Expand Up @@ -225,7 +225,7 @@ The entry point is only rendered if there's at least one settings menu item prov

### Hiding empty values

The package delivers `ShowHideNullValuesSettingsMenuItem` that allows users to hide / show properties that don't have values:
The package delivers `ShowHideEmptyValuesSettingsMenuItem` that allows users to hide / show properties that don't have values:

| Empty values displayed | Empty values hidden |
| ----------------------------------------------------------------- | ------------------------------------------------------------- |
Expand Down
10 changes: 8 additions & 2 deletions packages/itwin/property-grid/api/property-grid-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,19 @@ export interface SettingsMenuProps {
}

// @public
export function ShowHideNullValuesSettingsMenuItem(input: ShowHideNullValuesSettingsMenuItemProps): JSX_2.Element;
export function ShowHideEmptyValuesSettingsMenuItem(input: ShowHideEmptyValuesSettingsMenuItemProps): JSX_2.Element;

// @public
export interface ShowHideNullValuesSettingsMenuItemProps extends SettingsMenuItemProps {
export interface ShowHideEmptyValuesSettingsMenuItemProps extends SettingsMenuItemProps {
persist?: boolean;
}

// @public @deprecated
export const ShowHideNullValuesSettingsMenuItem: typeof ShowHideEmptyValuesSettingsMenuItem;

// @public @deprecated
export type ShowHideNullValuesSettingsMenuItemProps = ShowHideEmptyValuesSettingsMenuItemProps;

// @public
export interface SingleElementDataProviderProps extends DataProviderProps {
instanceKey: InstanceKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ public;type;PropertyGridWidgetProps
public;function;RemoveFavoritePropertyContextMenuItem
public;interface;SettingsMenuItemProps
public;interface;SettingsMenuProps
public;function;ShowHideNullValuesSettingsMenuItem
public;interface;ShowHideNullValuesSettingsMenuItemProps
public;function;ShowHideEmptyValuesSettingsMenuItem
public;interface;ShowHideEmptyValuesSettingsMenuItemProps
public;const;ShowHideNullValuesSettingsMenuItem
deprecated;const;ShowHideNullValuesSettingsMenuItem
public;type;ShowHideNullValuesSettingsMenuItemProps
deprecated;type;ShowHideNullValuesSettingsMenuItemProps
public;interface;SingleElementDataProviderProps
public;function;SingleElementPropertyGrid
public;type;SingleElementPropertyGridProps
Expand Down
4 changes: 4 additions & 0 deletions packages/itwin/property-grid/src/property-grid-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export {
SettingsMenuProps,
PropertyGridSettingsMenuItemProps,
PropertyGridSettingsMenuItem,
ShowHideEmptyValuesSettingsMenuItemProps,
ShowHideEmptyValuesSettingsMenuItem,
// eslint-disable-next-line @typescript-eslint/no-deprecated
ShowHideNullValuesSettingsMenuItemProps,
// eslint-disable-next-line @typescript-eslint/no-deprecated
ShowHideNullValuesSettingsMenuItem,
} from "./property-grid-react/components/SettingsDropdownMenu.js";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ export class NonEmptyValuesPropertyDataFilterer extends PropertyRecordDataFilter
};
}

// merged values are not empty (the value differs between instances), so they should always match the filter.
// empty string values are considered empty, but `false`/`0` are valid non-empty values.
const hasValue = node.isMerged || (node.value.value !== undefined && node.value.value !== "");
Comment thread
Copilot marked this conversation as resolved.
Outdated
return {
filteredTypes: [FilteredType.Value],
matchesFilter: !!node.value.displayValue,
matchesCount: node.value.displayValue ? 1 : 0,
matchesFilter: hasValue,
matchesCount: hasValue ? 1 : 0,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export function PropertyGridSettingsMenuItem({ id, onClick, title, children }: P
}

/**
* Props for `ShowHideNullValuesSettingsMenuItem`.
* Props for `ShowHideEmptyValuesSettingsMenuItem`.
* @public
*/
export interface ShowHideNullValuesSettingsMenuItemProps extends SettingsMenuItemProps {
export interface ShowHideEmptyValuesSettingsMenuItemProps extends SettingsMenuItemProps {
/** Specifies whether setting value should be persisted on change. */
persist?: boolean;
}
Expand All @@ -70,7 +70,7 @@ export interface ShowHideNullValuesSettingsMenuItemProps extends SettingsMenuIte
* Renders `Show/Hide Empty Values` setting.
* @public
*/
export function ShowHideNullValuesSettingsMenuItem({ close, persist }: ShowHideNullValuesSettingsMenuItemProps) {
export function ShowHideEmptyValuesSettingsMenuItem({ close, persist }: ShowHideEmptyValuesSettingsMenuItemProps) {
const { showNullValues, setShowNullValues } = useNullValueSettingContext();

const label = showNullValues ? PropertyGridManager.translate("settings.hide-null.label") : PropertyGridManager.translate("settings.show-null.label");
Expand All @@ -92,6 +92,20 @@ export function ShowHideNullValuesSettingsMenuItem({ close, persist }: ShowHideN
);
}

/**
* Props for `ShowHideNullValuesSettingsMenuItem`.
* @deprecated in 1.20. Use `ShowHideEmptyValuesSettingsMenuItemProps` instead.
* @public
*/
export type ShowHideNullValuesSettingsMenuItemProps = ShowHideEmptyValuesSettingsMenuItemProps;

/**
* Renders `Show/Hide Empty Values` setting.
* @deprecated in 1.20. Use `ShowHideEmptyValuesSettingsMenuItem` instead.
* @public
*/
export const ShowHideNullValuesSettingsMenuItem = ShowHideEmptyValuesSettingsMenuItem;

/**
* Props for `SettingsDropdownMenu`.
* @internal
Expand Down
12 changes: 10 additions & 2 deletions packages/itwin/property-grid/src/test/TestUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ import type { SelectionStorage, StorageSelectionChangesListener } from "@itwin/u
import type { RenderHookOptions, RenderHookResult, RenderOptions, RenderResult } from "@testing-library/react";
import type { UserEvent } from "@testing-library/user-event";

export function createPropertyRecord(value: PropertyValue, description: Partial<PropertyDescription>) {
export function createPropertyRecord(
value: PropertyValue,
description: Partial<PropertyDescription>,
props?: Pick<PropertyRecord, "isMerged">,
): PropertyRecord {
const propertyDescription: PropertyDescription = {
displayLabel: "Test Property",
name: "test-prop",
typename: "string",
...description,
};
return new PropertyRecord(value, propertyDescription);
const record = new PropertyRecord(value, propertyDescription);
if (props?.isMerged !== undefined) {
record.isMerged = props.isMerged;
}
return record;
}

export function stubSelectionManager(presentationSingleton?: typeof Presentation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,98 @@ describe("<FilteringPropertyGrid />", () => {
expect(queryByText("Null Prop")).toBeNull();
});

it("filters out empty string primitive properties", async () => {
const filterer = new NonEmptyValuesPropertyDataFilterer();
const dataProvider = {
onDataChanged: new PropertyDataChangeEvent(),
getData: async () => ({
categories: [
{
expand: true,
label: "Test Category",
name: "test-category",
},
],
label: PropertyRecord.fromString("Test Instance"),
records: {
["test-category"]: [
createPropertyRecord(
{ valueFormat: PropertyValueFormat.Primitive, value: "Non-empty Value", displayValue: "Non-empty Value" },
{ name: "test-prop1", displayLabel: "Non-empty String Prop" },
),
createPropertyRecord(
{ valueFormat: PropertyValueFormat.Primitive, value: "", displayValue: "" },
{ name: "test-prop2", displayLabel: "Empty String Prop" },
),
],
},
}),
} as IPropertyDataProvider;

const { getByText, queryByText } = render(<FilteringPropertyGrid height={100} width={100} filterer={filterer} dataProvider={dataProvider} />);

await waitFor(() => getByText("Non-empty String Prop"));
expect(queryByText("Empty String Prop")).toBeNull();
});

it("does not filter out `false` boolean primitive properties", async () => {
const filterer = new NonEmptyValuesPropertyDataFilterer();
const dataProvider = {
onDataChanged: new PropertyDataChangeEvent(),
getData: async () => ({
categories: [
{
expand: true,
label: "Test Category",
name: "test-category",
},
],
label: PropertyRecord.fromString("Test Instance"),
records: {
["test-category"]: [
createPropertyRecord(
{ valueFormat: PropertyValueFormat.Primitive, value: false, displayValue: "" },
{ name: "test-prop1", displayLabel: "False Boolean Prop" },
),
],
},
}),
} as IPropertyDataProvider;

const { getByText } = render(<FilteringPropertyGrid height={100} width={100} filterer={filterer} dataProvider={dataProvider} />);

await waitFor(() => getByText("False Boolean Prop"));
});

it("does not filter out merged primitive properties", async () => {
const filterer = new NonEmptyValuesPropertyDataFilterer();
const mergedRecord = createPropertyRecord(
{ valueFormat: PropertyValueFormat.Primitive, value: undefined, displayValue: undefined },
{ name: "test-prop1", displayLabel: "Merged Prop" },
{ isMerged: true },
);
const dataProvider = {
onDataChanged: new PropertyDataChangeEvent(),
getData: async () => ({
categories: [
{
expand: true,
label: "Test Category",
name: "test-category",
},
],
label: PropertyRecord.fromString("Test Instance"),
records: {
["test-category"]: [mergedRecord],
},
}),
} as IPropertyDataProvider;

const { getByText } = render(<FilteringPropertyGrid height={100} width={100} filterer={filterer} dataProvider={dataProvider} />);

await waitFor(() => getByText("Merged Prop"));
});

it("filters out empty array properties", async () => {
const filterer = new NonEmptyValuesPropertyDataFilterer();
const dataProvider = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { PropertyDataChangeEvent } from "@itwin/components-react";
import { KeySet } from "@itwin/presentation-common";
import { PropertyGridContent } from "../../property-grid-react/components/PropertyGridContent.js";
import { PropertyGridSettingsMenuItem, ShowHideNullValuesSettingsMenuItem } from "../../property-grid-react/components/SettingsDropdownMenu.js";
import { PropertyGridSettingsMenuItem, ShowHideEmptyValuesSettingsMenuItem } from "../../property-grid-react/components/SettingsDropdownMenu.js";
import { NullValueSettingContext } from "../../property-grid-react/hooks/UseNullValuesSetting.js";
import { TelemetryContextProvider } from "../../property-grid-react/hooks/UseTelemetryContext.js";
import { PropertyGridManager } from "../../property-grid-react/PropertyGridManager.js";
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("<PropertyGridContent />", () => {
const provider = createProvider();

const { getByText, getByRole, queryByText, user } = renderWithContext(
<PropertyGridContent dataProvider={provider} imodel={imodel} settingsMenuItems={[(props) => <ShowHideNullValuesSettingsMenuItem {...props} />]} />,
<PropertyGridContent dataProvider={provider} imodel={imodel} settingsMenuItems={[(props) => <ShowHideEmptyValuesSettingsMenuItem {...props} />]} />,
);

await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { beforeEach, describe, expect, it, vi } from "vitest";
import { SettingsDropdownMenu, ShowHideNullValuesSettingsMenuItem } from "../../property-grid-react/components/SettingsDropdownMenu.js";
import { SettingsDropdownMenu, ShowHideEmptyValuesSettingsMenuItem } from "../../property-grid-react/components/SettingsDropdownMenu.js";
import { NullValueSettingContext, SHOWNULL_KEY } from "../../property-grid-react/hooks/UseNullValuesSetting.js";
import { PropertyGridManager } from "../../property-grid-react/PropertyGridManager.js";
import { PreferencesContextProvider } from "../../property-grid-react/PropertyGridPreferencesContext.js";
Expand Down Expand Up @@ -113,12 +113,12 @@ describe("Default settings", () => {
});

it("renders", async () => {
const { queryByText } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} />);
const { queryByText } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} />);
await waitFor(() => expect(queryByText("settings.hide-null.label")).not.toBeNull());
});

it("does not persist new value by default", async () => {
const { getByText, queryByText, user } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} />);
const { getByText, queryByText, user } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} />);
const item = await waitFor(() => getByText("settings.hide-null.label"));
await user.click(item);

Expand All @@ -128,7 +128,7 @@ describe("Default settings", () => {
});

it("persist new value", async () => {
const { getByText, queryByText, user } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} persist={true} />);
const { getByText, queryByText, user } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} persist={true} />);
const item = await waitFor(() => getByText("settings.hide-null.label"));
await user.click(item);

Expand All @@ -144,12 +144,12 @@ describe("Default settings", () => {
});

it("renders", async () => {
const { queryByText } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} />);
const { queryByText } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} />);
await waitFor(() => expect(queryByText("settings.show-null.label")).not.toBeNull());
});

it("does not persist new value by default", async () => {
const { getByText, queryByText, user } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} />);
const { getByText, queryByText, user } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} />);
const item = await waitFor(() => getByText("settings.show-null.label"));
await user.click(item);

Expand All @@ -159,7 +159,7 @@ describe("Default settings", () => {
});

it("persist new value", async () => {
const { getByText, queryByText, user } = renderWithContext(<ShowHideNullValuesSettingsMenuItem {...settingProps} persist={true} />);
const { getByText, queryByText, user } = renderWithContext(<ShowHideEmptyValuesSettingsMenuItem {...settingProps} persist={true} />);
const item = await waitFor(() => getByText("settings.show-null.label"));
await user.click(item);

Expand Down
Loading