Compare commits

...

2 Commits

Author SHA1 Message Date
Maxime Beauchemin
36cf9337a8 chore(list-view): make SelectOption.title an explicit typed field
Follow-up from code review on sc-104554. `SelectOption.title` was
previously accessed via the `[key: string]: unknown` index signature,
making the "plain-text fallback" contract implicit. Make it a typed
optional string so callers can rely on it and TypeScript can enforce
the shape. Simplify the label-resolution expression in `Select.tsx`
accordingly.
2026-04-21 05:09:52 +00:00
Maxime Beauchemin
d952109cbc fix(list-view): preserve user name in filter pill after navigation
In the Chart list (and other CRUD lists), filtering by a user, navigating
into an asset, and returning to the list replaced the user's name in the
filter pill with a numeric user id.

Root cause: Owner filter options supply a React element as `label` (via
`OwnerSelectLabel`, which renders name + email). When a selection was
serialized to URL / filter state, `SelectFilter.onChange` only accepted
string labels and otherwise fell back to `String(selected.value)` (the
numeric id). On return to the list, the rehydrated `{label, value}` held
the numeric id as the label and that is what the pill rendered.

Prefer the option's plain-text `title` (already set to the human-readable
name by `createFetchOwners`) before falling back to the stringified value.
Add a unit test covering both the ReactNode-with-title path and the
no-title fallback.
2026-04-21 04:51:48 +00:00
3 changed files with 133 additions and 1 deletions

View File

@@ -0,0 +1,123 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, selectOption, waitFor } from 'spec/helpers/testing-library';
import { ListViewFilterOperator } from '../types';
import UIFilters from './index';
const mockUpdateFilterValue = jest.fn();
beforeEach(() => {
mockUpdateFilterValue.mockClear();
});
test('select filter with ReactNode label uses option title when serializing selection', async () => {
// Regression for sc-104554: the chart-list Owner filter renders options
// with ReactNode labels (name + email). The value passed to
// updateFilterValue is serialized into URL / filter state and re-used to
// render the filter pill on return. It must carry the plain-text name
// (from `title`) and not fall back to the numeric user id.
const ReactNodeLabel = (
<div>
<span>John Doe</span>
<span>john@example.com</span>
</div>
);
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: ReactNodeLabel,
value: 42,
title: 'John Doe',
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
fetchSelects,
paginate: true,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('John Doe', 'Owner');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith({
label: 'John Doe',
value: 42,
});
});
});
test('select filter falls back to stringified value when no string label or title is available', async () => {
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: <span>123</span>,
value: 123,
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Something',
key: 'something',
id: 'something',
input: 'select' as const,
operator: ListViewFilterOperator.RelationOneMany,
unfilteredLabel: 'All',
fetchSelects,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectOption('123', 'Something');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith({
label: '123',
value: 123,
});
});
});

View File

@@ -62,10 +62,15 @@ function SelectFilter(
onSelect(
selected
? {
// Options may supply a ReactNode label (e.g. OwnerSelectLabel for
// the chart list Owner filter). Since this object is serialized
// into the URL and rehydrated as the filter pill on return, we
// need a plain string. Prefer `title` (set by callers to the
// human-readable name) before falling back to the value.
label:
typeof selected.label === 'string'
? selected.label
: String(selected.value),
: (selected.title ?? String(selected.value)),
value: selected.value,
}
: undefined,

View File

@@ -26,6 +26,10 @@ export interface SortColumn {
export interface SelectOption {
label: ReactNode;
value: any;
// Plain-text representation of the option. Callers should set this when
// `label` is a ReactNode so that the option can be serialized (e.g. into
// URL filter state) without losing the human-readable name.
title?: string;
[key: string]: unknown;
}