`GambitManager.from()` breaks negated filters: `-is:` gambits lose their `-` and negated key-value gambits throw
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript, php
Research direction
Start in framework/core/js/src/common/GambitManager.ts and trace GambitManager.from() alongside apply() to see how filter keys and values are converted. Add unit coverage for negated yes/no gambits and key-value gambits, then run the relevant GambitManager tests. Done means the filters round-trip correctly and the reported search link restores test -author:admin without an error.
Written by the indexing model from the issue text.
Description
Summary
GambitManager.from() turns API filters back into search text. It mishandles negated filter keys (-author, -unread, …). This affects every page that rebuilds its search box from the URL (GlobalSearchState.searchToQuery()), so it breaks the search modal's "Search all discussions" link whenever the query has a negated gambit.
Steps to reproduce
As a logged-in member, open the search modal and search for:
test -author:admin, then choose Search all discussions. The link is/?q=test&filter[-author]=admin. The index page throwsTypeError: Cannot read properties of undefined (reading 'toString')(fromfilterValueToGambitValue), shows no discussions, and the header search falls back to "Search Forum".test -is:unread, then choose Search all discussions. The list is filtered correctly, but the header search showstest is:unread. Searching again from there returns the opposite results.
Results of from('discussions', 'foo', filter):
| filter | result |
|---|---|
{ author: 'admin' } |
foo author:admin |
{ '-author': 'admin' } |
throws |
{ '-created': '2026-01-01' } |
throws |
{ '-hidden': true } |
foo is:hidden |
{ '-unread': true } |
foo is:unread |
Extensions' gambits behave the same way. For example, fof/best-answer's -is:solved comes back as is:solved.
Cause
framework/core/js/src/common/GambitManager.ts, from():
Object.keys(filter).forEach((key) => {
for (const gambit of gambits) {
const negate = key[0] === '-';
if (negate) key = key.substring(1);
if (gambit.filterKey() !== key) continue;
q += ` ${gambit.fromFilter(filter[key], negate)}`;
}
});
There are two bugs:
keyis reassigned inside the per-gambit loop. The first gambit,AuthorGambitfor discussions, strips the-. Every later gambit then seesnegate === false, so negated yes/no gambits come back un-negated.- The value is read with the stripped key.
filter['author']isundefinedwhen the filter holds'-author'. Key-value gambits then callundefined.toString()infilterValueToGambitValue(). That's why-authorthrows even thoughAuthorGambitis first and still seesnegate === true.
Fix
Work out the negation and the bare key once per filter key, outside the gambit loop, and read the value with the original key:
Object.keys(filter).forEach((filterKey) => {
const negate = filterKey[0] === '-';
const key = negate ? filterKey.substring(1) : filterKey;
for (const gambit of gambits) {
if (gambit.filterKey() !== key) continue;
q += ` ${gambit.fromFilter(filter[filterKey], negate)}`;
}
});
Done when
from()round-tripsapply()for negated yes/no gambits that aren't first in the list (-is:unread,-is:hidden) and for negated key-value gambits (-author:x,-created:x).- The search modal's Search all discussions link for
test -author:adminopens the index without an error, withtest -author:adminin the search box. - A unit test covers
GambitManager.from()with negated filters.
- Dominant language
- PHP
- Stars
- 6.8k
- Forks
- 887
- Avg merge
- 12h 56m
- Merged PRs (30d)
- 117
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from flarum/framework
-
type/bug
Difficulty 1/5 Under an hour Newbie friendliness 75/100
flarum/framework#5231 · 1 comment ·
Maintainers usually reply within 1 day
-
[2.x] Extension Manager: queued tasks fail on PostgreSQL (output NOT NULL, command varchar(50))Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Minimized composer should show title onlyMay be free again @subikshajegadish claimed this 14 days ago, and no pull request is open. Opentype/bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
flarum/framework#5076 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
flarum/framework#4298 · 1 comment ·
Maintainers usually reply within 1 day
All issues in flarum/framework
Similar issues
-
PHP workflow streaming requests buffer the full SSE responsePossibly taken @Winoren claimed this today. Openreview: medium
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
langgenius/dify#44270 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
bug customer-reported
Difficulty 1/5 Under an hour Newbie friendliness 90/100
MagnaCapax/PMSS#1085 ·
Maintainers usually reply within 5 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
nunomaduro/collision#375 ·
-
enhancement good first issue hacktoberfest help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
thetheago/laravel-seeder-versioning#19 ·
Maintainers usually reply within 1 day