Tables: sort by column, choose and order columns, filter by typing in the header #20

Closed
opened 2026-09-05 12:34:56 +00:00 by tiagoagueda · 0 comments
Owner

Observation

ability to alter the order and sorting of columns in companies and applications. i like the option of filtering out the entries by typing on the column header

What exists today

  • Applications (applications/application_list.html): five fixed columns — Role, Company, Location, Status, Applied — in a fixed order, with no sort control. Rows come newest first (Application.Meta.ordering = ("-created_at",)). Filtering is a form above the table: one search box across role, company and location, plus status, tag and open/closed (ApplicationFilterMixin, applications/views.py, line 38). Pages of 50.
  • Companies (jobs/company_list.html): five fixed columns — Name, Location, Industry, Postings, Applications — always by name, one search box (CompanyListView, jobs/views.py, line 29). Pages of 50.
  • htmx is loaded on every page (base.html, line 9) and no template uses it yet. Typing in a header is the first thing in the interface that genuinely wants it.

Shape

Three capabilities, one implementation shared by both tables, and later by the reminders and captures lists.

1. Sort by column. Every header is a link that toggles ascending and descending, with an arrow (#8) on the active one: ?sort=applied, ?sort=-applied. Sort keys are an allow-list on the view, never a field name taken from the query string. Sortable on applications: role, company, location, status, applied, deadline, priority, last activity. On companies: name, location, industry, postings, applications, last activity. Server-side, because pagination is server-side: sorting in the browser would sort the page, not the list.

2. Filter by typing in the header. A small input beneath each header that narrows that column: ?role=python&company=aperture&status=interviewing. Text columns match case-insensitively; status is a select; dates are a from/to pair. Header filters combine with the search box and the existing status and tag filters, which become the same mechanism. Implementation:

  • the table is a Django 6 template partial ({% partialdef %}), and the view returns just the partial to an htmx request (django-htmx is already installed);
  • each input carries hx-get with hx-trigger="input changed delay:300ms", hx-target on the table and hx-push-url="true", so the address bar stays bookmarkable and the back button works;
  • no JavaScript of Postulo's own, and a plain Apply button so the page works with scripts off;
  • a live region announces the count ("23 applications") after each change.

3. Choose and order columns. A Columns control listing every available column with a checkbox and up/down buttons, including columns not shown today — on applications: deadline, priority, channel, salary, tags, last activity, reminders due, next interview (#14); on companies: website, careers page, contacts, a notes excerpt. The choice is per person and per table, stored on the Profile as a JSON field keyed by table name, so it follows the account to every device. The default reproduces today's tables. Page size (25 / 50 / 100) lives in the same setting.

Where state lives. Sort and filters live in the URL: shareable, bookmarkable, back-button-safe. Column layout and page size live on the profile: they are a preference, not a query.

Details

  • Filters only ever narrow the owner-scoped queryset (for_user()), so no new exposure.
  • Headers are real links; filter inputs carry aria-labels ("Filter by company"); everything works from the keyboard.
  • "Nothing matches these filters", with a Clear filters link, is a different empty state from "no applications yet".
  • The board (application_board.html) shares the filter mixin: the search, status and tag filters keep working there; header filters are table-only.
  • Tests: the sort allow-list rejects unknown keys; filters compose; column settings round-trip; an htmx request receives the partial and a plain request the whole page.

Classification

Enhancement. Not breaking: new query parameters, one nullable JSON field on the profile, and defaults that reproduce today's tables exactly.

Open questions

  1. Filter inputs always visible as a second header row, or revealed by a funnel icon per column? Proposal: visible on wide screens, behind the icon on narrow ones.
  2. Drag-to-reorder needs a few dozen lines of Postulo's own JavaScript (CSP-safe, in app.js); up/down buttons need none. Proposal: buttons first, drag if the buttons grate.
  3. Should the same column setting drive what a board card shows? Probably its own issue.
## Observation > ability to alter the order and sorting of columns in companies and applications. i like the option of filtering out the entries by typing on the column header ## What exists today - **Applications** (`applications/application_list.html`): five fixed columns — Role, Company, Location, Status, Applied — in a fixed order, with no sort control. Rows come newest first (`Application.Meta.ordering = ("-created_at",)`). Filtering is a form above the table: one search box across role, company and location, plus status, tag and open/closed (`ApplicationFilterMixin`, `applications/views.py`, line 38). Pages of 50. - **Companies** (`jobs/company_list.html`): five fixed columns — Name, Location, Industry, Postings, Applications — always by name, one search box (`CompanyListView`, `jobs/views.py`, line 29). Pages of 50. - **htmx is loaded on every page** (`base.html`, line 9) **and no template uses it yet.** Typing in a header is the first thing in the interface that genuinely wants it. ## Shape Three capabilities, one implementation shared by both tables, and later by the reminders and captures lists. **1. Sort by column.** Every header is a link that toggles ascending and descending, with an arrow (#8) on the active one: `?sort=applied`, `?sort=-applied`. Sort keys are an allow-list on the view, never a field name taken from the query string. Sortable on applications: role, company, location, status, applied, deadline, priority, last activity. On companies: name, location, industry, postings, applications, last activity. Server-side, because pagination is server-side: sorting in the browser would sort the page, not the list. **2. Filter by typing in the header.** A small input beneath each header that narrows that column: `?role=python&company=aperture&status=interviewing`. Text columns match case-insensitively; status is a select; dates are a from/to pair. Header filters combine with the search box and the existing status and tag filters, which become the same mechanism. Implementation: - the table is a Django 6 template partial (`{% partialdef %}`), and the view returns just the partial to an htmx request (`django-htmx` is already installed); - each input carries `hx-get` with `hx-trigger="input changed delay:300ms"`, `hx-target` on the table and `hx-push-url="true"`, so the address bar stays bookmarkable and the back button works; - no JavaScript of Postulo's own, and a plain *Apply* button so the page works with scripts off; - a live region announces the count ("23 applications") after each change. **3. Choose and order columns.** A *Columns* control listing every available column with a checkbox and up/down buttons, including columns not shown today — on applications: deadline, priority, channel, salary, tags, last activity, reminders due, next interview (#14); on companies: website, careers page, contacts, a notes excerpt. The choice is per person and per table, stored on the `Profile` as a JSON field keyed by table name, so it follows the account to every device. The default reproduces today's tables. Page size (25 / 50 / 100) lives in the same setting. **Where state lives.** Sort and filters live in the URL: shareable, bookmarkable, back-button-safe. Column layout and page size live on the profile: they are a preference, not a query. ## Details - Filters only ever narrow the owner-scoped queryset (`for_user()`), so no new exposure. - Headers are real links; filter inputs carry `aria-label`s ("Filter by company"); everything works from the keyboard. - "Nothing matches these filters", with a *Clear filters* link, is a different empty state from "no applications yet". - The board (`application_board.html`) shares the filter mixin: the search, status and tag filters keep working there; header filters are table-only. - Tests: the sort allow-list rejects unknown keys; filters compose; column settings round-trip; an htmx request receives the partial and a plain request the whole page. ## Classification Enhancement. Not breaking: new query parameters, one nullable JSON field on the profile, and defaults that reproduce today's tables exactly. ## Open questions 1. Filter inputs always visible as a second header row, or revealed by a funnel icon per column? Proposal: visible on wide screens, behind the icon on narrow ones. 2. Drag-to-reorder needs a few dozen lines of Postulo's own JavaScript (CSP-safe, in `app.js`); up/down buttons need none. Proposal: buttons first, drag if the buttons grate. 3. Should the same column setting drive what a board card shows? Probably its own issue.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 12:34:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#20
No description provided.