diff --git a/src/components/Common/FilterBuilder.vue b/src/components/Common/FilterBuilder.vue
index 15e1484..5f344ab 100644
--- a/src/components/Common/FilterBuilder.vue
+++ b/src/components/Common/FilterBuilder.vue
@@ -3,7 +3,13 @@ import { computed, ref } from 'vue'
import { onClickOutside, onKeyStroke } from '@vueuse/core'
import { INQUIRY_TYPE_LABELS, STATUS_META } from '@/services/inquiryEnums'
-import { SORT_OPTIONS, sortOption, type ExplorerQuery, type SortField } from '@/services/explorer'
+import {
+ SORT_OPTIONS,
+ isDefaultSort,
+ sortOption,
+ type ExplorerQuery,
+ type SortField,
+} from '@/services/explorer'
/**
* The `+ Filter` popover.
@@ -62,14 +68,16 @@ function setMine(value: 'reviewer' | 'creator' | null) {
/**
* Picking a column sorts it descending — newest, highest, most recent first is
* what people mean by "sorteer op datum". Picking the column it already sorts
- * on turns sorting off again, so the same button is both the on and the off.
+ * on flips the direction, because there is no "off" to return to: the explorer
+ * is always sorted by something (see `DEFAULT_SORT`), so the only question a
+ * second click can answer is which end you want.
*/
function setSort(value: SortField) {
- const off = props.query.sort === value
+ const active = props.query.sort === value
emit('update', {
...props.query,
- sort: off ? null : value,
- order: off ? 'desc' : props.query.order,
+ sort: value,
+ order: active ? (props.query.order === 'desc' ? 'asc' : 'desc') : 'desc',
page: 1,
})
}
@@ -80,13 +88,11 @@ function setOrder(order: 'asc' | 'desc') {
/** The active column's own words for its two directions. */
const direction = computed(() => {
- const option = props.query.sort ? sortOption(props.query.sort) : null
- return option
- ? [
- { value: 'desc' as const, label: option.desc },
- { value: 'asc' as const, label: option.asc },
- ]
- : []
+ const option = sortOption(props.query.sort)
+ return [
+ { value: 'desc' as const, label: option.desc },
+ { value: 'asc' as const, label: option.asc },
+ ]
})
@@ -196,9 +202,9 @@ const direction = computed(() => {
-
-
+
+
-
- Klik dezelfde kolom nog eens om terug te gaan naar de standaardvolgorde.
-
-
- Zonder keuze houdt de API haar eigen volgorde aan (laatst gewijzigd eerst).
-
+ Klik dezelfde kolom nog eens om de richting om te draaien.
+ Dit is de standaardvolgorde.
Type en status volgen de volgorde van de database-enum: dat groepeert de rijen,
diff --git a/src/services/explorer.ts b/src/services/explorer.ts
index 4efb288..bfae5c3 100644
--- a/src/services/explorer.ts
+++ b/src/services/explorer.ts
@@ -28,12 +28,37 @@ export interface ExplorerQuery {
type: number[]
/** Restrict to rows where you are the reviewer or the creator. */
mine: 'reviewer' | 'creator' | null
- sort: SortField | null
+ /** Always set — see `DEFAULT_SORT`. There is no "unsorted". */
+ sort: SortField
order: 'asc' | 'desc'
/** 1-based. */
page: number
}
+/**
+ * What the explorer sorts by when nobody has said otherwise — including every
+ * built-in view.
+ *
+ * There used to be no answer to that: leaving `sort` off let `GET /inquiry`
+ * fall back to `coalesce(update_date, create_date) DESC`, which sounds like
+ * recency and is not. The #973 backfill stamped one identical `update_date`
+ * onto 20,950 of 26,671 inquiries, so four rows in five sat in a single tie
+ * group in an order no one chose and `LIMIT`/`OFFSET` does not promise to keep
+ * stable between two pages. "Alles" only looked sorted.
+ *
+ * `document_date` descending instead: newest first, on the one date the table
+ * actually shows, so the claim can be checked by reading down the Datum column
+ * rather than taken on faith. The API ends every ordering on the primary key
+ * (FunderMapsApi #101), which is what makes paging through it total.
+ */
+export const DEFAULT_SORT: SortField = 'document_date'
+export const DEFAULT_ORDER: 'asc' | 'desc' = 'desc'
+
+/** Whether the query still carries the ordering it started with. */
+export function isDefaultSort(query: ExplorerQuery): boolean {
+ return query.sort === DEFAULT_SORT && query.order === DEFAULT_ORDER
+}
+
/**
* How many rows a page of the explorer holds.
*
@@ -47,7 +72,15 @@ export interface ExplorerQuery {
export const PAGE_SIZE = 20
export function emptyQuery(): ExplorerQuery {
- return { q: '', status: [], type: [], mine: null, sort: null, order: 'desc', page: 1 }
+ return {
+ q: '',
+ status: [],
+ type: [],
+ mine: null,
+ sort: DEFAULT_SORT,
+ order: DEFAULT_ORDER,
+ page: 1,
+ }
}
export interface SavedView {
@@ -156,7 +189,9 @@ export function describeSort(sort: SortField, order: 'asc' | 'desc'): string {
export function parseQuery(raw: Record): ExplorerQuery {
const page = Number(raw.page)
- const sort = SORT_FIELDS.find((field) => field === raw.sort) ?? null
+ // An unknown or absent `?sort=` lands on the default rather than on nothing:
+ // a link someone hand-edited should show a sorted list, not an arbitrary one.
+ const sort = SORT_FIELDS.find((field) => field === raw.sort) ?? DEFAULT_SORT
return {
q: typeof raw.q === 'string' ? raw.q : '',
status: ints(raw.status, (n) => statusMeta(n).label !== 'Onbekend'),
@@ -179,7 +214,7 @@ export function toRouteQuery(query: ExplorerQuery, viewKey: string): LocationQue
if (query.status.length) out.status = query.status.join(',')
if (query.type.length) out.type = query.type.join(',')
if (query.mine) out.mine = query.mine
- if (query.sort) {
+ if (!isDefaultSort(query)) {
out.sort = query.sort
out.order = query.order
}
@@ -189,7 +224,12 @@ export function toRouteQuery(query: ExplorerQuery, viewKey: string): LocationQue
/** A saved view's stored shape, merged over the empty query. */
export function fromView(view: SavedView): ExplorerQuery {
- return { ...emptyQuery(), ...view.query }
+ const merged = { ...emptyQuery(), ...view.query }
+ // Views saved before the explorer had a default ordering stored `sort: null`,
+ // and they live in someone's `localStorage` rather than in a table we can
+ // migrate. Anything the sort no longer recognises falls back to the default.
+ const sort = SORT_FIELDS.find((field) => field === merged.sort)
+ return { ...merged, sort: sort ?? DEFAULT_SORT, order: merged.order === 'asc' ? 'asc' : 'desc' }
}
/* -------------------------------------------------------------- API mapping */
@@ -211,10 +251,11 @@ export function toListOpts(query: ExplorerQuery, userId: string | null): IInquir
if (query.status.length) opts.status = [...query.status]
if (query.mine === 'creator' && userId) opts.creator = userId
if (query.mine === 'reviewer' && userId) opts.reviewer = userId
- if (query.sort) {
- opts.sort = query.sort
- opts.order = query.order
- }
+ // Always sent, default included. Letting the parameter fall away would hand
+ // the ordering back to the endpoint's fallback, which is the one thing this
+ // module exists to stop happening — see `DEFAULT_SORT`.
+ opts.sort = query.sort
+ opts.order = query.order
return opts
}
@@ -275,13 +316,16 @@ export function chipsFor(query: ExplorerQuery): Chip[] {
// The sort is not a filter, but it belongs here for the same reason the
// filters do: it changes which rows you see first, it survives in a shared
// link, and half of what you can sort on (opsteller) has no column in the
- // table to carry an arrow. Clearing it returns to the API's own ordering.
- if (query.sort) {
+ // table to carry an arrow.
+ //
+ // Only once it differs from the default, though. A chip that is present on
+ // every screen is furniture, and its × would be a button that does nothing.
+ if (!isDefaultSort(query)) {
chips.push({
id: 'sort',
label: 'sortering',
value: describeSort(query.sort, query.order),
- clear: (q) => ({ ...q, sort: null, order: 'desc', page: 1 }),
+ clear: (q) => ({ ...q, sort: DEFAULT_SORT, order: DEFAULT_ORDER, page: 1 }),
})
}
diff --git a/src/services/worklist.ts b/src/services/worklist.ts
index 0d2b696..5365dc6 100644
--- a/src/services/worklist.ts
+++ b/src/services/worklist.ts
@@ -15,6 +15,7 @@
*/
import type { IInquiryListOpts } from '@/services/fundermaps/endpoints/inquiry'
+import { DEFAULT_ORDER, DEFAULT_SORT } from '@/services/explorer'
import { AUDIT_STATUS } from '@/services/inquiryEnums'
/** Which side of the dossier the lane puts you on. */
@@ -74,6 +75,12 @@ export function laneQuery(lane: Lane, userId: string): IInquiryListOpts {
status: lane.statuses,
...(lane.role === 'reviewer' ? { reviewer: userId } : { creator: userId }),
limit: LANE_FETCH,
+ // Sorted for the same reason the explorer is, and it shows more here: a
+ // lane displays the first eight of the fifty it fetches, so without an
+ // ordering those eight are eight arbitrary rows out of a tie group the
+ // #973 backfill made 20,950 rows wide. Newest first — see `DEFAULT_SORT`.
+ sort: DEFAULT_SORT,
+ order: DEFAULT_ORDER,
}
}
diff --git a/src/views/InquiryListView.vue b/src/views/InquiryListView.vue
index a08266f..b7b1153 100644
--- a/src/views/InquiryListView.vue
+++ b/src/views/InquiryListView.vue
@@ -182,16 +182,17 @@ const sortField = computed(() =>
Object.keys(SORT_KEYS).find((field) => SORT_KEYS[field] === query.value.sort) ?? null,
)
-// desc → asc → back to default recency ordering. Descending first, because
-// that is what "sorteer op datum" means, and because the filter popover's sort
-// section starts there too — two controls writing one piece of state should not
-// disagree about which way the first click points.
+// A new column sorts descending; the same column again flips the direction.
+// There is no third click that switches sorting off, because the explorer is
+// always sorted by something — see `DEFAULT_SORT`. Descending first, because
+// that is what "sorteer op datum" means, and because the filter popover starts
+// there too: two controls writing one piece of state should not disagree about
+// which way the first click points.
function onSort(field: string) {
const key = SORT_KEYS[field]
if (!key) return
if (query.value.sort !== key) push({ ...query.value, sort: key, order: 'desc', page: 1 })
- else if (query.value.order === 'desc') push({ ...query.value, order: 'asc', page: 1 })
- else push({ ...query.value, sort: null, order: 'desc', page: 1 })
+ else push({ ...query.value, order: query.value.order === 'desc' ? 'asc' : 'desc', page: 1 })
}
const selectedIds = ref>(new Set())