Open a Filter and Work Inside It — Reworking Table Saved Filters
I added search, sharing, and editing to saved filters, and the screen turned into a mess. The culprit wasn't the feature count — it was deciding 'which filter is active' by value comparison. Notes from rebuilding it around a focus model.
Open a Filter and Work Inside It — Reworking Table Saved Filters
fecit’s desktop Table view has saved filters: combine conditions like state, keyword, priority, and assignee, give the set a name, and recall it with a single chip. This round of work started with four additions — searching saved filters by name, a share link to send a filter to someone, editing a saved filter’s name and conditions, and showing which conditions are currently applied.
I built all four. Then I looked at the screen and knew immediately: it was a mess.
Judging by Value Erases Identity
The problem wasn’t the number of features. In the first implementation, “which saved filter is active” was decided like this:
A filter is active if the serialized JSON of the current conditions exactly matches its stored JSON.
Value comparison is the easiest thing to implement. But the moment you apply a filter and change a single condition, the match breaks and the connection is severed. The chip you were just using quietly turns off, and your current conditions become an orphan that belongs to nothing. There’s no room for the natural gesture of “save my edits back into that filter,” so a separate “overwrite with current filter” item ended up buried in a menu. Each condition chip grew its own clear button and click-to-edit. The active highlight depended on a coincidence of values. Every feature made sense in isolation; together they didn’t form a sentence.
When the UI is a mess, the model underneath it usually is too.
The Focus Model
So I changed the judgment from value to identity. Clicking a filter chip opens that filter. Its conditions apply, and “I am inside this filter” is recorded as an ID. However much you then change conditions through the column header menus, the association holds — you’re editing a working copy of the open filter. When you want to keep the changes, hit Update in the chip’s ⋯ menu. Click the chip again and the filter closes, returning to the default view.
Open it, work inside it, save or close. Now it reads as one sentence.
The interesting part: once the model changed, the UI shrank on its own.
- The “overwrite” menu item disappeared. With a filter open, “Update” becomes the obvious first action.
- The modified indicator (●) and inline save/revert buttons went away. At first, edit buttons popped out next to the chip whenever conditions diverged, and the bar kept jumping around. With a clear edit-then-save flow, a single Update inside ⋯ was enough — no dirty-state UI at all.
- Condition chips disappeared entirely in the end. First they were a row of chips with per-condition clear buttons, then display-only, then gone. An open filter’s chip represents the condition set, and applied conditions already show as filter icons on the column headers. The original requirement — “show what’s applied” — survived; it just didn’t need its own row of chips.
All actions converged into one ⋯ popover: Update, Rename, Archive, Copy link, Delete. Whether you open it from a bar chip or a list row, it’s the same menu.
From Pin to Archive
The first implementation was favorites-style: only pinned filters showed on the bar. I flipped it. Saving a filter puts its chip on the bar immediately; you archive only what you want hidden. You should never have to dig through a list wondering where the filter you just made went. It’s the same ACTIVE/ARCHIVED grammar fecit’s labels already use, so there’s nothing new to learn. The save entry point also moved out of the list — a “+ Save current filter” button lives right on the bar.
Sharing Is a URL
Filter sharing creates no server-side object. The entire condition set is serialized as base64url and carried in a URL parameter:
fecit.vaudium.net/table?scope=<project>&filter=<serialized conditions>
Opening the link switches the recipient’s workspace scope and applies the same conditions. No server state means no expiry and no cleanup.
Then one step further. fecit’s web app already syncs the open document and workspace scope to the URL under the principle of “paste the address bar = see my screen.” Now the applied filter stays in sync too. Change a condition and the address bar follows; return to the default and the parameter disappears. You don’t even need to find the share button — copying the address bar is sharing the filter.
The Server Side — Parting with the Embedded Array
Saved filters used to live as an embedded array inside the project document — a structure where the document grows without bound as members keep saving filters. They now have their own collection, following the precedent set when milestones made the same move. Each embedded entry’s key was promoted to the new document’s _id, so every identifier clients hold stays valid and the response shape is unchanged — the client didn’t change a single line. The backfill script is idempotent and has already run in production.
There was a byproduct. The “50 per project” hard cap on milestones existed to keep the embedded array bounded; with milestones in their own collection, the rule had nothing left to protect. I removed it. Taking a constraint out forces you to ask what it was originally guarding.
What I Learned
Deciding “what am I using right now” by value comparison erases identity. Without identity, actions like edit, save, and share have nowhere to go, and auxiliary UI accumulates to fill the gap until the screen is a mess. The few lines that switched the judgment to an ID-based focus cleaned up more of the screen than the four new features had cluttered.
The same model shipped to the iPad Table view with the same grammar — chip tap, ⋯ popover, archive, link copy, all symmetric. The only touch-specific difference: an ↑ button in the list instead of chip dragging.