Compare commits

..

2 Commits

Author SHA1 Message Date
Claude Code
8966a1d17e fix(filters): sort numeric filter values numerically, not lexicographically
Closes #36775

sortComparator compared options via propertyComparator('label'), but
getDataRecordFormatter (filters/utils.ts) always returns a formatted
string label regardless of the column's underlying datatype. Numeric
filter values therefore always hit propertyComparator's string branch
(localeCompare) instead of its numeric branch, producing "1, 10, 100,
2" instead of "1, 2, 10, 100".

propertyComparator already has a numeric branch; it just needs a
property that actually holds a number. `value` (the raw, unformatted
option value) does for numeric columns, while `label` never does. Fix
compares by `value` when the column's GenericDataType is Numeric, and
keeps comparing by `label` for every other type -- unaffected here
since #34858 already fixed the analogous SelectControl.jsx bug; this
native-filter value dropdown just has its own independent sort path
that #34858 never touched, and #36775 is the recurrence.

Disclosure: type-checking-frontend was skipped locally (SKIP set) --
this worktree's `tsc --build` currently fails on unrelated TS2742
"inferred type... not portable" errors in Tabs.tsx/SafeMarkdown.tsx/
spec/index.tsx (pre-existing composite-build fragility, not touched
by this change; the PR's own prior commit disclosed the same
build-environment limitation). oxlint, prettier, and the full
SelectFilterPlugin.test.tsx suite (39/39, including the new
regression test and both pre-existing alphabetical-sort tests) are
green locally. CI's Type-Checking (Frontend) job is the real gate.
2026-07-30 12:38:41 -07:00
Claude Code
418abb4de0 test(filters): pin numeric filter values sort numerically, not lexicographically (#36775)
Closes #36775

Adds a regression test on PluginFilterSelect confirming that when no
sortMetric is configured, numeric filter-value options are ordered by
their numeric value (2, 10, 100) rather than by lexicographic
comparison of their formatted string labels (10, 100, 2).

Root cause: SelectFilterPlugin's sortComparator always compares the
formatted `label` via propertyComparator('label'). getDataRecordFormatter
(superset-frontend/src/filters/utils.ts) always returns a string, so
numeric columns hit propertyComparator's string branch (localeCompare)
instead of its numeric branch, even though the values themselves are
numeric. This is the second recurrence of the #35008/#34858 fix, which
patched SelectControl.jsx (chart-builder numeric selects like Row limit)
but never touched this native-filter value dropdown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 17:54:05 -07:00
5 changed files with 98 additions and 89 deletions

View File

@@ -1,9 +1,6 @@
# This workflow automates the release process for Helm charts.
# Each run force-recreates a single 'helm-publish' branch from the tip of 'gh-pages' and
# opens (or reuses) one pull request against 'gh-pages', allowing the changes to be
# reviewed and merged manually. Because chart-releaser rebuilds index.yaml from all
# published GitHub releases, the branch always contains every chart released since the
# last merge, and the PR can never go stale or conflict with gh-pages.
# The workflow creates a new branch for the release and opens a pull request against the 'gh-pages' branch,
# allowing the changes to be reviewed and merged manually.
name: "Helm: release charts"
@@ -21,12 +18,6 @@ on:
required: false
default: "master"
# Serialize runs: concurrent runs would race on force-pushing the shared
# helm-publish branch while chart-releaser is mid-release.
concurrency:
group: helm-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-26.04
@@ -66,9 +57,9 @@ jobs:
echo "DEBUG TAGS"
git show-ref --tags
- name: Set pages branch name
- name: Create unique pages branch name
id: vars
run: echo "branch_name=helm-publish" >> $GITHUB_ENV
run: echo "branch_name=helm-publish-${GITHUB_SHA:0:7}" >> $GITHUB_ENV
- name: Force recreate branch from gh-pages
env:
@@ -106,17 +97,12 @@ jobs:
version: v1.6.0
charts_dir: helm
mark_as_latest: false
# A helm/** change without a Chart.yaml version bump repackages the
# already-released version; without this, cr aborts on the existing
# release tag (422 already_exists) instead of proceeding to rebuild
# the index, and the whole run fails.
skip_existing: true
pages_branch: ${{ env.branch_name }}
env:
CR_TOKEN: "${{ github.token }}"
CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}"
- name: Open or reuse Pull Request
- name: Open Pull Request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
@@ -127,72 +113,15 @@ jobs:
throw new Error("Branch name is not defined.");
}
// The branch is force-recreated from gh-pages on every run, so an
// already-open PR for it now reflects this run's charts; opening
// another would both fail (422) and recreate the stale-PR pileup
// this fixed branch exists to avoid.
const { data: existing } = await github.rest.pulls.list({
const pr = await github.rest.pulls.create({
owner,
repo,
state: "open",
head: `${owner}:${branchName}`,
base: "gh-pages",
title: `Helm chart release for ${branchName}`,
head: branchName,
base: "gh-pages", // Adjust if the target branch is different
body: `This PR releases Helm charts to the gh-pages branch.`,
});
let current;
if (existing.length > 0) {
current = existing[0];
core.info(`Reusing existing pull request: ${current.html_url}`);
} else {
const { data: pr } = await github.rest.pulls.create({
owner,
repo,
title: "Helm chart release",
head: branchName,
base: "gh-pages", // Adjust if the target branch is different
body: [
"This PR releases Helm charts to the gh-pages branch.",
"",
"It is force-updated from the tip of `gh-pages` by every release run,",
"so it always contains every chart released since the last merge and",
"never needs to be closed as superseded.",
].join("\n"),
});
current = pr;
core.info(`Pull request created: ${current.html_url}`);
}
// Sweep release PRs left open by older runs (per-SHA helm-publish-*
// branches from the previous scheme). Their content is a subset of
// the evergreen PR, so close them with a pointer to it.
const { data: openPrs } = await github.rest.pulls.list({
owner,
repo,
state: "open",
base: "gh-pages",
per_page: 100,
});
for (const stale of openPrs) {
if (
stale.number !== current.number &&
stale.head.repo?.full_name === process.env.GITHUB_REPOSITORY &&
stale.head.ref.startsWith("helm-publish")
) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: stale.number,
body: `Superseded by #${current.number}, which now carries all unreleased charts. Closing.`,
});
await github.rest.pulls.update({
owner,
repo,
pull_number: stale.number,
state: "closed",
});
core.info(`Closed superseded release PR #${stale.number}`);
}
}
core.info(`Pull request created: ${pr.data.html_url}`);
env:
BRANCH_NAME: ${{ env.branch_name }}

View File

@@ -297,7 +297,7 @@ pre-commit run eslint # Frontend linting
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor

View File

@@ -14,7 +14,7 @@
"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
under the License.
-->
# Change Log

View File

@@ -720,6 +720,80 @@ describe('SelectFilterPlugin', () => {
expect(options[2]).toHaveTextContent('beta');
});
test('sorts numeric filter values numerically, not lexicographically, when no sortMetric is specified', () => {
// Regression for #36775: numeric filter values were sorted as strings
// (localeCompare on the formatted label), producing "1, 10, 100, 2"
// instead of the expected "1, 2, 10, 100".
const testData = [{ age: 10 }, { age: 2 }, { age: 100 }];
const testProps = {
...selectMultipleProps,
formData: {
...selectMultipleProps.formData,
groupby: ['age'],
sortMetric: undefined,
sortAscending: true,
},
queriesData: [
{
rowcount: 3,
colnames: ['age'],
coltypes: [0],
data: testData,
applied_filters: [{ column: 'age' }],
rejected_filters: [],
},
],
filterState: {
value: [],
label: '',
excludeFilterValues: true,
},
};
render(
// @ts-expect-error
<SelectFilterPlugin
// @ts-expect-error
{...transformProps(testProps)}
setDataMask={jest.fn()}
showOverflow={false}
/>,
{
useRedux: true,
initialState: {
nativeFilters: {
filters: {
'test-filter': {
name: 'Test Filter',
},
},
},
dataMask: {
'test-filter': {
extraFormData: {},
filterState: {
value: [],
label: '',
excludeFilterValues: true,
},
},
},
},
},
);
const filterSelect = screen.getAllByRole('combobox')[0];
userEvent.click(filterSelect);
// Options should appear in ascending numeric order (2, 10, 100), not
// ascending lexicographic order of their formatted labels (10, 100, 2).
const options = screen.getAllByRole('option');
expect(options[0]).toHaveTextContent('2');
expect(options[1]).toHaveTextContent('10');
expect(options[2]).toHaveTextContent('100');
});
test('shows create option for multi-select creatable filter when typing', async () => {
getWrapper({ creatable: true, multiSelect: true });
userEvent.type(screen.getByRole('combobox'), 'brand-new');

View File

@@ -354,14 +354,20 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
return 0; // Preserve the original order from the backend
}
// Only apply alphabetical sorting when no sortMetric is specified
const labelComparator = propertyComparator('label');
// Only apply sorting when no sortMetric is specified. `label` is always
// a formatted string (see getDataRecordFormatter), so comparing by it
// never reaches propertyComparator's numeric branch; numeric columns
// sort by the raw `value` instead so "2, 10, 100" doesn't collapse
// into lexicographic "10, 100, 2".
const comparator = propertyComparator(
datatype === GenericDataType.Numeric ? 'value' : 'label',
);
if (formData.sortAscending) {
return labelComparator(a, b);
return comparator(a, b);
}
return labelComparator(b, a);
return comparator(b, a);
},
[formData.sortAscending, formData.sortMetric],
[formData.sortAscending, formData.sortMetric, datatype],
);
// Use effect for initialisation for filter plugin