things-api 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/cli/commands/area.d.ts +23 -0
- package/dist/cli/commands/area.js +164 -33
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.d.ts +19 -0
- package/dist/cli/commands/project.js +142 -29
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +10 -51
- package/dist/cli/commands/reads.js +454 -268
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.d.ts +9 -0
- package/dist/cli/commands/show.js +169 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/snapshot.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +9 -0
- package/dist/cli/commands/todo.js +101 -16
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +55 -1
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +29 -0
- package/dist/cli/did-you-mean.js +53 -0
- package/dist/cli/did-you-mean.js.map +1 -0
- package/dist/cli/glyphs.d.ts +95 -0
- package/dist/cli/glyphs.js +282 -0
- package/dist/cli/glyphs.js.map +1 -0
- package/dist/cli/main.js +11 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +32 -0
- package/dist/cli/period.js +129 -0
- package/dist/cli/period.js.map +1 -0
- package/dist/cli/read-driver.d.ts +76 -0
- package/dist/cli/read-driver.js +181 -0
- package/dist/cli/read-driver.js.map +1 -0
- package/dist/cli/render.d.ts +182 -0
- package/dist/cli/render.js +585 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-invocation.d.ts +78 -0
- package/dist/cli/resolve-invocation.js +178 -0
- package/dist/cli/resolve-invocation.js.map +1 -0
- package/dist/cli/shell-quote.d.ts +9 -0
- package/dist/cli/shell-quote.js +12 -0
- package/dist/cli/shell-quote.js.map +1 -0
- package/dist/cli/style.d.ts +5 -0
- package/dist/cli/style.js +5 -0
- package/dist/cli/style.js.map +1 -1
- package/dist/client.d.ts +45 -39
- package/dist/client.js +11 -74
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +66 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +7 -5
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +7 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/mcp/server.js +227 -44
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +21 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +17 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/occurrences.d.ts +6 -1
- package/dist/model/occurrences.js +9 -2
- package/dist/model/occurrences.js.map +1 -1
- package/dist/model/recurrence.d.ts +30 -2
- package/dist/model/recurrence.js +20 -0
- package/dist/model/recurrence.js.map +1 -1
- package/dist/read/area-view.js +21 -5
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/caps.d.ts +14 -0
- package/dist/read/caps.js +21 -0
- package/dist/read/caps.js.map +1 -0
- package/dist/read/detail.js +12 -2
- package/dist/read/detail.js.map +1 -1
- package/dist/read/log-boundary.d.ts +29 -0
- package/dist/read/log-boundary.js +38 -0
- package/dist/read/log-boundary.js.map +1 -0
- package/dist/read/pagination.d.ts +104 -0
- package/dist/read/pagination.js +208 -0
- package/dist/read/pagination.js.map +1 -0
- package/dist/read/predicates.d.ts +31 -0
- package/dist/read/predicates.js +48 -0
- package/dist/read/predicates.js.map +1 -0
- package/dist/read/project-view.js +28 -3
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +21 -3
- package/dist/read/queries.js +35 -8
- package/dist/read/queries.js.map +1 -1
- package/dist/read/search-rank.d.ts +36 -0
- package/dist/read/search-rank.js +31 -0
- package/dist/read/search-rank.js.map +1 -0
- package/dist/read/show-target.d.ts +25 -0
- package/dist/read/show-target.js +41 -0
- package/dist/read/show-target.js.map +1 -0
- package/dist/read/sidebar-order.d.ts +19 -0
- package/dist/read/sidebar-order.js +92 -0
- package/dist/read/sidebar-order.js.map +1 -0
- package/dist/read/snapshot.js +2 -0
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/views.d.ts +64 -8
- package/dist/read/views.js +290 -155
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +23 -0
- package/dist/surface-copy.js +23 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/automation-probe.js +14 -6
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/capabilities.d.ts +3 -0
- package/dist/write/capabilities.js +3 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/checklist.d.ts +50 -0
- package/dist/write/checklist.js +61 -0
- package/dist/write/checklist.js.map +1 -0
- package/dist/write/clear-reminder.d.ts +3 -0
- package/dist/write/clear-reminder.js +205 -0
- package/dist/write/clear-reminder.js.map +1 -0
- package/dist/write/commands.js +122 -8
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.d.ts +3 -0
- package/dist/write/edit-checklist.js +184 -0
- package/dist/write/edit-checklist.js.map +1 -0
- package/dist/write/failure-hints.d.ts +7 -0
- package/dist/write/failure-hints.js +25 -2
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +16 -0
- package/dist/write/guards.js.map +1 -1
- package/dist/write/operations.d.ts +32 -1
- package/dist/write/operations.js +3 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +3 -0
- package/dist/write/pipeline.js +23 -1
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/reversibility.d.ts +46 -0
- package/dist/write/reversibility.js +164 -0
- package/dist/write/reversibility.js.map +1 -0
- package/dist/write/undo.d.ts +29 -2
- package/dist/write/undo.js +357 -9
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.js +2 -1
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.d.ts +9 -0
- package/dist/write/vectors/shortcuts.js +90 -0
- package/dist/write/vectors/shortcuts.js.map +1 -0
- package/dist/write/vectors/types.d.ts +11 -3
- package/dist/write/vectors/url-scheme.js +16 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +1 -1
- package/dist/write/verify/delta.js +1 -1
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/shortcuts/things-proxy-find-items.shortcut +0 -0
package/dist/read/views.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { addDaysIso, encodePackedDate, localToday } from "../model/dates.js";
|
|
2
|
+
import { logBoundary, markLogged } from "./log-boundary.js";
|
|
2
3
|
import { mapProject, mapTodo } from "../model/mappers.js";
|
|
3
4
|
import { projectOccurrences } from "../model/occurrences.js";
|
|
4
5
|
import { decodeRecurrenceRule } from "../model/recurrence.js";
|
|
5
|
-
import { fetchTagsForTasks, fetchTaskRows, makeRefResolver, NOT_TEMPLATE, resolveAreaUuid, resolveProjectUuid, resolveTagUuid, tagScopeBinds, tagScopeSql, tagWithDescendants, } from "./queries.js";
|
|
6
|
+
import { fetchTagsForTasks, fetchTaskRows, makeHeadingProjectResolver, makeRefResolver, NOT_TEMPLATE, resolveAreaUuid, resolveProjectUuid, resolveTagUuid, tagScopeBinds, tagScopeSql, tagWithDescendants, } from "./queries.js";
|
|
7
|
+
import { ANYTIME_SELF, CONTAINER_UNTRASHED, EFF_PROJECT, LIVE, OPEN, PROJECT_ANYTIME_ACTIVE, } from "./predicates.js";
|
|
8
|
+
import { groupBySidebar } from "./sidebar-order.js";
|
|
9
|
+
import { areasView } from "./tags.js";
|
|
10
|
+
import { compareSearchMatches } from "./search-rank.js";
|
|
6
11
|
function tagFilter(db, filter) {
|
|
7
12
|
if (filter?.tag === undefined)
|
|
8
13
|
return { sql: "", binds: [] };
|
|
@@ -10,14 +15,22 @@ function tagFilter(db, filter) {
|
|
|
10
15
|
const uuids = filter.exactTag === true ? [target] : tagWithDescendants(db, target);
|
|
11
16
|
return { sql: ` AND ${tagScopeSql(uuids.length)}`, binds: tagScopeBinds(uuids) };
|
|
12
17
|
}
|
|
13
|
-
const LIVE = `t.type IN (0, 1) AND t.trashed = 0 AND ${NOT_TEMPLATE}`;
|
|
14
|
-
const OPEN = `${LIVE} AND t.status = 0`;
|
|
15
18
|
function materialize(db, rows) {
|
|
16
19
|
const refs = makeRefResolver(db);
|
|
20
|
+
const headingProject = makeHeadingProjectResolver(db);
|
|
17
21
|
const tags = fetchTagsForTasks(db, rows.map((r) => r.uuid));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const items = rows.map((row) => {
|
|
23
|
+
if (row.type === 1)
|
|
24
|
+
return mapProject(row, refs, tags.get(row.uuid) ?? []);
|
|
25
|
+
const todo = mapTodo(row, refs, tags.get(row.uuid) ?? []);
|
|
26
|
+
if (todo.heading !== null) {
|
|
27
|
+
const p = headingProject(todo.heading.uuid);
|
|
28
|
+
if (p !== null)
|
|
29
|
+
todo.headingProject = p;
|
|
30
|
+
}
|
|
31
|
+
return todo;
|
|
32
|
+
});
|
|
33
|
+
return markLogged(items, logBoundary(db));
|
|
21
34
|
}
|
|
22
35
|
export function todayView(db, now, filter) {
|
|
23
36
|
const todayIso = localToday(now);
|
|
@@ -34,7 +47,7 @@ export function todayView(db, now, filter) {
|
|
|
34
47
|
// (the date the item ENTERED Today: its startDate, or its deadline for
|
|
35
48
|
// deadline-driven members), then todayIndex ASC, then uuid (observed
|
|
36
49
|
// stable tiebreak).
|
|
37
|
-
const rows = fetchTaskRows(db, `${OPEN} AND (
|
|
50
|
+
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED} AND (
|
|
38
51
|
(t.startDate IS NOT NULL AND t.startDate <= ? AND t.start IN (1, 2))
|
|
39
52
|
OR (t.deadline IS NOT NULL AND t.deadline <= ? AND t.startDate IS NULL
|
|
40
53
|
AND (t.deadlineSuppressionDate IS NULL OR t.deadlineSuppressionDate < t.deadline))
|
|
@@ -58,24 +71,6 @@ export function inboxView(db, filter) {
|
|
|
58
71
|
const rows = fetchTaskRows(db, `${OPEN} AND t.start = 0${tf.sql} ORDER BY t."index" ASC`, tf.binds);
|
|
59
72
|
return materialize(db, rows);
|
|
60
73
|
}
|
|
61
|
-
/** An item's own anytime membership: unscheduled-active, or dated <= today. */
|
|
62
|
-
const ANYTIME_SELF = (col) => `((${col}.start = 1 AND (${col}.startDate IS NULL OR ${col}.startDate <= ?))
|
|
63
|
-
OR (${col}.start = 2 AND ${col}.startDate IS NOT NULL AND ${col}.startDate <= ?))`;
|
|
64
|
-
/**
|
|
65
|
-
* The item's effective project: its own link, or its heading's project for
|
|
66
|
-
* headed children (heading rows carry the project link).
|
|
67
|
-
*/
|
|
68
|
-
const EFF_PROJECT = `COALESCE(t.project, (SELECT h.project FROM TMTask h WHERE h.uuid = t.heading))`;
|
|
69
|
-
/**
|
|
70
|
-
* Container cascade (live-verified against the UI, 2026-07-09): a to-do
|
|
71
|
-
* inside a project that is NOT itself anytime-visible (someday or
|
|
72
|
-
* future-scheduled, logged, or trashed) is absent from Anytime regardless of
|
|
73
|
-
* the to-do's own start state — the project row alone represents it.
|
|
74
|
-
* Projects and container-less to-dos pass through. Two binds (packedToday ×2).
|
|
75
|
-
*/
|
|
76
|
-
const PROJECT_ANYTIME_ACTIVE = `(${EFF_PROJECT} IS NULL OR EXISTS (
|
|
77
|
-
SELECT 1 FROM TMTask p WHERE p.uuid = ${EFF_PROJECT}
|
|
78
|
-
AND p.trashed = 0 AND p.status = 0 AND ${ANYTIME_SELF("p")}))`;
|
|
79
74
|
export function anytimeView(db, now, filter) {
|
|
80
75
|
const packedToday = encodePackedDate(localToday(now));
|
|
81
76
|
const tf = tagFilter(db, filter);
|
|
@@ -86,106 +81,22 @@ export function anytimeView(db, now, filter) {
|
|
|
86
81
|
AND ${PROJECT_ANYTIME_ACTIVE}${tf.sql} ORDER BY t."index" ASC`, [packedToday, packedToday, packedToday, packedToday, ...tf.binds]);
|
|
87
82
|
return groupBySidebar(db, materialize(db, rows));
|
|
88
83
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Arranges view members into the UI's flat sidebar-mirroring order: the
|
|
91
|
-
* area-less block first (direct to-dos, then each top-level project followed
|
|
92
|
-
* by its members), then each area by its sidebar index (direct to-dos, then
|
|
93
|
-
* its projects). Project and area order here mirrors the sidebar exactly —
|
|
94
|
-
* both read the same "index" columns.
|
|
95
|
-
*/
|
|
96
|
-
function groupBySidebar(db, items) {
|
|
97
|
-
if (items.length === 0)
|
|
98
|
-
return [];
|
|
99
|
-
const inList = (n) => Array.from({ length: n }, () => "?").join(", ");
|
|
100
|
-
// Headed children: resolve heading -> project.
|
|
101
|
-
const headingUuids = [
|
|
102
|
-
...new Set(items.flatMap((i) => (i.type === "to-do" && i.heading !== null ? [i.heading.uuid] : []))),
|
|
103
|
-
];
|
|
104
|
-
const headingProject = new Map();
|
|
105
|
-
if (headingUuids.length > 0) {
|
|
106
|
-
for (const row of db
|
|
107
|
-
.prepare(`SELECT uuid, project FROM TMTask WHERE uuid IN (${inList(headingUuids.length)})`)
|
|
108
|
-
.all(...headingUuids)) {
|
|
109
|
-
headingProject.set(row.uuid, row.project);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const effProject = (i) => i.type !== "to-do"
|
|
113
|
-
? null
|
|
114
|
-
: (i.project?.uuid ??
|
|
115
|
-
(i.heading !== null ? (headingProject.get(i.heading.uuid) ?? null) : null));
|
|
116
|
-
// Sidebar rank + title for areas; index + area for every referenced project
|
|
117
|
-
// (a tag-filtered list can contain a child whose project row didn't match).
|
|
118
|
-
const areaRows = db
|
|
119
|
-
.prepare(`SELECT uuid, title, "index" FROM TMArea ORDER BY "index" ASC, uuid ASC`)
|
|
120
|
-
.all();
|
|
121
|
-
const areaRank = new Map(areaRows.map((a, rank) => [a.uuid, rank]));
|
|
122
|
-
const areaTitle = new Map(areaRows.map((a) => [a.uuid, a.title ?? ""]));
|
|
123
|
-
const projectUuids = [
|
|
124
|
-
...new Set([
|
|
125
|
-
...items.flatMap((i) => (i.type === "project" ? [i.uuid] : [])),
|
|
126
|
-
...items.flatMap((i) => {
|
|
127
|
-
const p = effProject(i);
|
|
128
|
-
return p === null ? [] : [p];
|
|
129
|
-
}),
|
|
130
|
-
]),
|
|
131
|
-
];
|
|
132
|
-
const projectMeta = new Map();
|
|
133
|
-
if (projectUuids.length > 0) {
|
|
134
|
-
for (const row of db
|
|
135
|
-
.prepare(`SELECT uuid, "index", area FROM TMTask WHERE uuid IN (${inList(projectUuids.length)})`)
|
|
136
|
-
.all(...projectUuids)) {
|
|
137
|
-
projectMeta.set(row.uuid, { index: row.index ?? 0, area: row.area });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// items arrive in SQL "index" order, so array position IS the per-container
|
|
141
|
-
// rank (the internal index is no longer exposed on the entity).
|
|
142
|
-
const sortKey = (i, pos) => {
|
|
143
|
-
const project = i.type === "project" ? i.uuid : effProject(i);
|
|
144
|
-
const meta = project === null ? undefined : projectMeta.get(project);
|
|
145
|
-
const area = i.area?.uuid ?? meta?.area ?? null;
|
|
146
|
-
return {
|
|
147
|
-
areaRank: area === null ? -1 : (areaRank.get(area) ?? areaRows.length),
|
|
148
|
-
area: area ?? "",
|
|
149
|
-
inProject: project === null ? 0 : 1,
|
|
150
|
-
projectIndex: meta?.index ?? 0,
|
|
151
|
-
project: project ?? "",
|
|
152
|
-
headerFirst: i.type === "project" ? 0 : 1,
|
|
153
|
-
pos,
|
|
154
|
-
uuid: i.uuid,
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
const keyed = items.map((item, pos) => ({ item, k: sortKey(item, pos) }));
|
|
158
|
-
keyed.sort((a, b) => a.k.areaRank - b.k.areaRank ||
|
|
159
|
-
a.k.area.localeCompare(b.k.area) ||
|
|
160
|
-
a.k.inProject - b.k.inProject ||
|
|
161
|
-
a.k.projectIndex - b.k.projectIndex ||
|
|
162
|
-
a.k.project.localeCompare(b.k.project) ||
|
|
163
|
-
a.k.headerFirst - b.k.headerFirst ||
|
|
164
|
-
a.k.pos - b.k.pos ||
|
|
165
|
-
a.k.uuid.localeCompare(b.k.uuid));
|
|
166
|
-
const sections = [];
|
|
167
|
-
for (const { item, k } of keyed) {
|
|
168
|
-
const areaUuid = k.area === "" ? null : k.area;
|
|
169
|
-
const last = sections.at(-1);
|
|
170
|
-
if (last === undefined || (last.area?.uuid ?? null) !== areaUuid) {
|
|
171
|
-
sections.push({
|
|
172
|
-
area: areaUuid === null ? null : { uuid: areaUuid, title: areaTitle.get(areaUuid) ?? "" },
|
|
173
|
-
items: [],
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
sections.at(-1)?.items.push(item);
|
|
177
|
-
}
|
|
178
|
-
return sections;
|
|
179
|
-
}
|
|
180
84
|
/** The UI's star marker in Anytime: the item is also a Today member. */
|
|
181
85
|
export function isTodayMember(item, now) {
|
|
182
86
|
return item.startDate !== null && item.startDate <= localToday(now);
|
|
183
87
|
}
|
|
184
88
|
export function upcomingView(db, now, filter) {
|
|
185
89
|
const packedToday = encodePackedDate(localToday(now));
|
|
90
|
+
const until = filter?.until;
|
|
91
|
+
const since = filter?.since;
|
|
92
|
+
const untilSql = until === undefined ? "" : " AND t.startDate <= ?";
|
|
93
|
+
const untilBinds = until === undefined ? [] : [encodePackedDate(until)];
|
|
94
|
+
const sinceSql = since === undefined ? "" : " AND t.startDate >= ?";
|
|
95
|
+
const sinceBinds = since === undefined ? [] : [encodePackedDate(since)];
|
|
186
96
|
const tf = tagFilter(db, filter);
|
|
187
|
-
const rows = fetchTaskRows(db, `${OPEN} AND
|
|
188
|
-
|
|
97
|
+
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED}
|
|
98
|
+
AND t.start = 2 AND t.startDate IS NOT NULL AND t.startDate > ?${untilSql}${sinceSql}${tf.sql}
|
|
99
|
+
ORDER BY t.startDate ASC, t."index" ASC`, [packedToday, ...untilBinds, ...sinceBinds, ...tf.binds]);
|
|
189
100
|
const items = materialize(db, rows);
|
|
190
101
|
if (filter?.repeats === false)
|
|
191
102
|
return items;
|
|
@@ -194,19 +105,22 @@ export function upcomingView(db, now, filter) {
|
|
|
194
105
|
// completion templates carry no next date until the prior instance
|
|
195
106
|
// resolves; paused templates are excluded. The occurrence deadline follows
|
|
196
107
|
// the instance-validated model: deadline = startDate − rule.startOffsetDays.
|
|
197
|
-
const templateRows = fetchTaskRows(db, `t.type IN (0, 1) AND t.trashed = 0 AND t.status = 0
|
|
108
|
+
const templateRows = fetchTaskRows(db, `t.type IN (0, 1) AND t.trashed = 0 AND t.status = 0 AND ${CONTAINER_UNTRASHED}
|
|
198
109
|
AND (t.rt1_recurrenceRule IS NOT NULL OR t.repeater IS NOT NULL)
|
|
199
110
|
AND t.rt1_instanceCreationPaused = 0
|
|
200
|
-
AND t.rt1_nextInstanceStartDate IS NOT NULL AND t.rt1_nextInstanceStartDate > ?${tf.sql}
|
|
201
|
-
ORDER BY t.rt1_nextInstanceStartDate ASC, t."index" ASC`, [packedToday, ...tf.binds]);
|
|
111
|
+
AND t.rt1_nextInstanceStartDate IS NOT NULL AND t.rt1_nextInstanceStartDate > ?${until === undefined ? "" : " AND t.rt1_nextInstanceStartDate <= ?"}${since === undefined ? "" : " AND t.rt1_nextInstanceStartDate >= ?"}${tf.sql}
|
|
112
|
+
ORDER BY t.rt1_nextInstanceStartDate ASC, t."index" ASC`, [packedToday, ...untilBinds, ...sinceBinds, ...tf.binds]);
|
|
202
113
|
const horizon = Math.max(1, Math.min(10, Math.trunc(filter?.horizon ?? 1)));
|
|
203
114
|
const rawByUuid = new Map(templateRows.map((r) => [r.uuid, r.rt1_recurrenceRule]));
|
|
204
115
|
const occurrences = materialize(db, templateRows).flatMap((template) => {
|
|
205
116
|
const startDate = template.repeating.nextOccurrence ?? null;
|
|
206
117
|
if (startDate === null)
|
|
207
118
|
return [];
|
|
208
|
-
//
|
|
209
|
-
//
|
|
119
|
+
// Whether occurrences deadline is the TEMPLATE's property, not the rule's:
|
|
120
|
+
// a deadline-less template (repeating.deadlined false) spawns instances
|
|
121
|
+
// with NO deadline even for fixed ts=0 rules — its rt1_recurrenceRule is
|
|
122
|
+
// byte-identical to a deadlined ts=0 rule (oddities §8a, UI1 2026-07-12).
|
|
123
|
+
const deadlined = template.repeating.deadlined === true;
|
|
210
124
|
let rule = null;
|
|
211
125
|
const raw = rawByUuid.get(template.uuid);
|
|
212
126
|
if (raw !== null && raw !== undefined) {
|
|
@@ -218,25 +132,60 @@ export function upcomingView(db, now, filter) {
|
|
|
218
132
|
}
|
|
219
133
|
}
|
|
220
134
|
if (rule === null || horizon === 1 || rule.type !== "fixed") {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
135
|
+
// Deadlined templates deadline the occurrence at the event date
|
|
136
|
+
// (start − ts, incl. ts=0); deadline-less ones carry no deadline.
|
|
137
|
+
const deadline = rule !== null && deadlined ? addDaysIso(startDate, -rule.startOffsetDays) : null;
|
|
224
138
|
return [{ ...template, startDate, deadline }];
|
|
225
139
|
}
|
|
226
140
|
// horizon > 1: later occurrences PROJECTED from the decoded rule,
|
|
227
|
-
// anchored on the app's own materialized next instance.
|
|
228
|
-
|
|
141
|
+
// anchored on the app's own materialized next instance. The until bound
|
|
142
|
+
// clips projections the same way it clips stored rows.
|
|
143
|
+
return (projectOccurrences(rule, startDate, {
|
|
144
|
+
count: horizon,
|
|
145
|
+
...(until !== undefined && { until }),
|
|
146
|
+
}, deadlined)
|
|
147
|
+
.filter((o) => until === undefined || o.startDate <= until)
|
|
148
|
+
.filter((o) => since === undefined || o.startDate >= since)
|
|
149
|
+
// oxlint-disable-next-line no-map-spread -- building fresh occurrence objects, not mutating
|
|
150
|
+
.map((o) => ({
|
|
229
151
|
...template,
|
|
230
152
|
startDate: o.startDate,
|
|
231
153
|
deadline: o.deadline,
|
|
232
|
-
}));
|
|
154
|
+
})));
|
|
233
155
|
});
|
|
234
|
-
|
|
156
|
+
// Within a day the UI's drag order is todayIndex ASC (live-verified
|
|
157
|
+
// 2026-07-11 against the GUI: plain `index` disagrees, todayIndex matches
|
|
158
|
+
// exactly), so the sortable Upcoming order survives the CLI.
|
|
159
|
+
const todayIndexOf = new Map([...rows, ...templateRows].map((r) => [r.uuid, r.todayIndex ?? 0]));
|
|
160
|
+
const dated = [...items, ...occurrences]
|
|
235
161
|
.map((item, pos) => ({ item, pos }))
|
|
236
162
|
.toSorted((a, b) => (a.item.startDate ?? "").localeCompare(b.item.startDate ?? "") ||
|
|
163
|
+
(todayIndexOf.get(a.item.uuid) ?? 0) - (todayIndexOf.get(b.item.uuid) ?? 0) ||
|
|
237
164
|
a.pos - b.pos ||
|
|
238
165
|
a.item.uuid.localeCompare(b.item.uuid))
|
|
239
166
|
.map((x) => x.item);
|
|
167
|
+
// The UI's trailing "Repeating To-Dos" section: templates with NO set
|
|
168
|
+
// next occurrence (after-completion rules between instances, rules past
|
|
169
|
+
// their end date) plus paused ones — startDate stays null, the decoded
|
|
170
|
+
// rule rides along so consumers can derive waiting/paused/ended.
|
|
171
|
+
const restingRows = fetchTaskRows(db, `t.type IN (0, 1) AND t.trashed = 0 AND t.status = 0 AND ${CONTAINER_UNTRASHED}
|
|
172
|
+
AND (t.rt1_recurrenceRule IS NOT NULL OR t.repeater IS NOT NULL)
|
|
173
|
+
AND (t.rt1_nextInstanceStartDate IS NULL OR t.rt1_nextInstanceStartDate <= ?
|
|
174
|
+
OR t.rt1_instanceCreationPaused = 1)${tf.sql}
|
|
175
|
+
ORDER BY t.todayIndex ASC, t."index" ASC`, [packedToday, ...tf.binds]);
|
|
176
|
+
const resting = materialize(db, restingRows).map((item) => {
|
|
177
|
+
const raw = restingRows.find((r) => r.uuid === item.uuid)?.rt1_recurrenceRule;
|
|
178
|
+
if (raw !== null && raw !== undefined) {
|
|
179
|
+
try {
|
|
180
|
+
item.repeating.rule = decodeRecurrenceRule(raw);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// undecodable rule (future Things build) — surface without it
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return item;
|
|
187
|
+
});
|
|
188
|
+
return [...dated, ...resting];
|
|
240
189
|
}
|
|
241
190
|
export function somedayView(db, now, filter) {
|
|
242
191
|
const tf = tagFilter(db, filter);
|
|
@@ -251,29 +200,130 @@ export function somedayView(db, now, filter) {
|
|
|
251
200
|
: "0";
|
|
252
201
|
const rows = fetchTaskRows(db, `${OPEN} AND t.start = 2 AND t.startDate IS NULL
|
|
253
202
|
AND (${EFF_PROJECT} IS NULL OR ${childArm})${tf.sql} ORDER BY t."index" ASC`, [...(withActiveChildren ? [packedToday, packedToday] : []), ...tf.binds]);
|
|
254
|
-
|
|
203
|
+
const sections = groupBySidebar(db, materialize(db, rows));
|
|
204
|
+
// GUI order within a Someday group (live side-by-side, 2026-07-12):
|
|
205
|
+
// PROJECT rows first (their sidebar order preserved), then direct to-dos in
|
|
206
|
+
// drag order. Someday children of active projects (the activeProjectItems
|
|
207
|
+
// toggle) trail the group, still clustered by their project — surfaces
|
|
208
|
+
// present them as a separate "From active projects" section.
|
|
209
|
+
const isChild = (i) => i.type === "to-do" && (i.project !== null || i.headingProject !== null);
|
|
210
|
+
return sections.map((s) => ({
|
|
211
|
+
area: s.area,
|
|
212
|
+
items: [
|
|
213
|
+
...s.items.filter((i) => i.type === "project"),
|
|
214
|
+
...s.items.filter((i) => i.type === "to-do" && !isChild(i)),
|
|
215
|
+
...s.items.filter(isChild),
|
|
216
|
+
],
|
|
217
|
+
}));
|
|
255
218
|
}
|
|
256
219
|
export function logbookView(db, options) {
|
|
257
|
-
const
|
|
220
|
+
const cap = options?.limit === null ? null : (options?.limit ?? 100);
|
|
258
221
|
const tf = tagFilter(db, options);
|
|
259
|
-
const
|
|
222
|
+
const where = [];
|
|
223
|
+
const binds = [];
|
|
224
|
+
if (options?.project !== undefined) {
|
|
225
|
+
where.push(`${EFF_PROJECT} = ?`);
|
|
226
|
+
binds.push(resolveProjectUuid(db, options.project));
|
|
227
|
+
}
|
|
228
|
+
if (options?.area !== undefined) {
|
|
229
|
+
const areaUuid = resolveAreaUuid(db, options.area);
|
|
230
|
+
where.push(`(t.area = ? OR EXISTS (SELECT 1 FROM TMTask p WHERE p.uuid = ${EFF_PROJECT} AND p.area = ?))`);
|
|
231
|
+
binds.push(areaUuid, areaUuid);
|
|
232
|
+
}
|
|
233
|
+
if (options?.since !== undefined) {
|
|
234
|
+
where.push("t.stopDate >= ?");
|
|
235
|
+
binds.push(options.since.getTime() / 1000);
|
|
236
|
+
}
|
|
237
|
+
if (options?.until !== undefined) {
|
|
238
|
+
where.push("t.stopDate <= ?");
|
|
239
|
+
binds.push(options.until.getTime() / 1000);
|
|
240
|
+
}
|
|
241
|
+
const extra = where.length > 0 ? ` AND ${where.join(" AND ")}` : "";
|
|
242
|
+
// Completion ≠ logged: closed items past the log-move boundary still sit
|
|
243
|
+
// checked in their original lists, exactly like the GUI's Logbook.
|
|
244
|
+
const rows = fetchTaskRows(db, `${LIVE} AND t.status IN (2, 3) AND t.stopDate <= ?${extra}${tf.sql} ORDER BY t.stopDate DESC${cap === null ? "" : " LIMIT ?"}`, [logBoundary(db).getTime() / 1000, ...binds, ...tf.binds, ...(cap === null ? [] : [cap])]);
|
|
260
245
|
return materialize(db, rows);
|
|
261
246
|
}
|
|
262
247
|
export function trashView(db, options) {
|
|
263
|
-
const
|
|
248
|
+
const cap = options?.limit === null ? null : (options?.limit ?? 200);
|
|
264
249
|
const rows = fetchTaskRows(db, `t.type IN (0, 1) AND t.trashed = 1 AND ${NOT_TEMPLATE}
|
|
265
|
-
ORDER BY t.userModificationDate DESC LIMIT
|
|
250
|
+
ORDER BY t.userModificationDate DESC${cap === null ? "" : " LIMIT ?"}`, cap === null ? [] : [cap]);
|
|
266
251
|
return materialize(db, rows);
|
|
267
252
|
}
|
|
268
253
|
export function projectsView(db, options) {
|
|
269
254
|
// areaUuid accepts a uuid OR a unique (case-insensitive) title; ambiguous
|
|
270
255
|
// or unknown references throw like every other ref resolver.
|
|
271
256
|
const area = options?.areaUuid === undefined ? null : resolveAreaUuid(db, options.areaUuid);
|
|
257
|
+
const packedToday = encodePackedDate(localToday(options?.now));
|
|
258
|
+
// Sidebar default: LATER (someday + future-scheduled) projects are hidden —
|
|
259
|
+
// active means ANYTIME_SELF (a scheduled project whose date has arrived
|
|
260
|
+
// counts active, exactly the Anytime membership test). With later=true they
|
|
261
|
+
// append AFTER the active block of their group, never intermingled.
|
|
262
|
+
const laterSql = options?.later === true ? "" : ` AND ${ANYTIME_SELF("t")}`;
|
|
263
|
+
const laterBinds = options?.later === true ? [] : [packedToday, packedToday];
|
|
264
|
+
// "Active first" within each group needs the same test as an ORDER key
|
|
265
|
+
// (two more binds when later projects are included).
|
|
266
|
+
const activeFirst = options?.later === true ? `(CASE WHEN ${ANYTIME_SELF("t")} THEN 0 ELSE 1 END) ASC, ` : "";
|
|
267
|
+
const activeFirstBinds = options?.later === true ? [packedToday, packedToday] : [];
|
|
268
|
+
// Sidebar order, not raw global index (which interleaves areas): loose
|
|
269
|
+
// (area-less) projects first in their own drag order — the GUI lists them
|
|
270
|
+
// above the areas — then each area by ITS sidebar rank (TMArea."index"),
|
|
271
|
+
// projects within an area by their drag order.
|
|
272
272
|
const where = area
|
|
273
|
-
? `${OPEN} AND t.type = 1 AND t.area =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
? `${OPEN} AND t.type = 1 AND t.area = ?${laterSql}
|
|
274
|
+
ORDER BY ${activeFirst}t."index" ASC`
|
|
275
|
+
: `${OPEN} AND t.type = 1${laterSql} ORDER BY (t.area IS NOT NULL) ASC,
|
|
276
|
+
(SELECT a."index" FROM TMArea a WHERE a.uuid = t.area) ASC, ${activeFirst}t."index" ASC`;
|
|
277
|
+
const rows = fetchTaskRows(db, where, [
|
|
278
|
+
...(area ? [area] : []),
|
|
279
|
+
...laterBinds,
|
|
280
|
+
...activeFirstBinds,
|
|
281
|
+
]);
|
|
282
|
+
const items = materialize(db, rows);
|
|
283
|
+
if (options?.later !== true)
|
|
284
|
+
return items;
|
|
285
|
+
// Each group's later sub-block reads like Upcoming: SCHEDULED projects
|
|
286
|
+
// first — date ascending, todayIndex within a day (the UI's drag order,
|
|
287
|
+
// hidden on entities but present on the raw rows) — then someday in drag
|
|
288
|
+
// order. The SQL already made group runs contiguous with actives leading,
|
|
289
|
+
// so only the later runs are re-sorted (stable within someday).
|
|
290
|
+
const todayIso = localToday(options?.now);
|
|
291
|
+
const ti = new Map(rows.map((r) => [r.uuid, r.todayIndex ?? 0]));
|
|
292
|
+
const isLater = (p) => p.startDate !== null ? p.startDate > todayIso : p.start === "someday";
|
|
293
|
+
const out = [];
|
|
294
|
+
let run = [];
|
|
295
|
+
let runArea;
|
|
296
|
+
const flush = () => {
|
|
297
|
+
out.push(...run
|
|
298
|
+
.map((item, pos) => ({ item, pos }))
|
|
299
|
+
.toSorted((a, b) => {
|
|
300
|
+
const ad = a.item.startDate;
|
|
301
|
+
const bd = b.item.startDate;
|
|
302
|
+
if (ad !== null && bd !== null)
|
|
303
|
+
return (ad.localeCompare(bd) ||
|
|
304
|
+
(ti.get(a.item.uuid) ?? 0) - (ti.get(b.item.uuid) ?? 0) ||
|
|
305
|
+
a.pos - b.pos);
|
|
306
|
+
if (ad !== null)
|
|
307
|
+
return -1;
|
|
308
|
+
if (bd !== null)
|
|
309
|
+
return 1;
|
|
310
|
+
return a.pos - b.pos; // someday keeps drag order
|
|
311
|
+
})
|
|
312
|
+
.map((x) => x.item));
|
|
313
|
+
run = [];
|
|
314
|
+
};
|
|
315
|
+
for (const item of items) {
|
|
316
|
+
const key = item.area?.uuid ?? null;
|
|
317
|
+
if (!isLater(item) || key !== runArea)
|
|
318
|
+
flush();
|
|
319
|
+
runArea = key;
|
|
320
|
+
if (isLater(item))
|
|
321
|
+
run.push(item);
|
|
322
|
+
else
|
|
323
|
+
out.push(item);
|
|
324
|
+
}
|
|
325
|
+
flush();
|
|
326
|
+
return out;
|
|
277
327
|
}
|
|
278
328
|
/**
|
|
279
329
|
* Everything that changed since a moment — created or modified TMTask rows,
|
|
@@ -284,47 +334,132 @@ export function projectsView(db, options) {
|
|
|
284
334
|
* parent task — those changes are invisible here.
|
|
285
335
|
*/
|
|
286
336
|
export function changesView(db, options) {
|
|
287
|
-
const
|
|
337
|
+
const cap = options.limit === null ? null : (options.limit ?? 200);
|
|
288
338
|
const sinceEpoch = options.since.getTime() / 1000;
|
|
289
339
|
const rows = fetchTaskRows(db, `t.type IN (0, 1) AND t.userModificationDate > ?
|
|
290
|
-
ORDER BY t.userModificationDate DESC LIMIT
|
|
340
|
+
ORDER BY t.userModificationDate DESC${cap === null ? "" : " LIMIT ?"}`, [sinceEpoch, ...(cap === null ? [] : [cap])]);
|
|
341
|
+
// oxlint-disable-next-line no-map-spread -- building fresh change objects, not mutating
|
|
291
342
|
return materialize(db, rows).map((item, i) => ({
|
|
292
343
|
...item,
|
|
293
344
|
changeKind: (rows[i]?.creationDate ?? 0) > sinceEpoch ? "created" : "modified",
|
|
294
345
|
}));
|
|
295
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* The did-you-mean fallback search: a case-insensitive SUBSTRING match on
|
|
349
|
+
* TITLES ONLY (never notes, headings, or checklist items), across areas,
|
|
350
|
+
* projects, and to-dos — open + untrashed only. Ordered per the house
|
|
351
|
+
* doctrine: containers (areas, then projects) first, then to-dos; within a
|
|
352
|
+
* group active rows precede someday rows, then most-recently-modified. `type`
|
|
353
|
+
* scopes to a single class (the typed-namespace paths). Results are capped
|
|
354
|
+
* (default 10); `total` reports the pre-cap match count so the caller can
|
|
355
|
+
* append a "… n more" tail.
|
|
356
|
+
*/
|
|
357
|
+
const somedayRank = (i) => (i.start === "someday" ? 1 : 0);
|
|
358
|
+
/** Lite-search order within a task group: active before someday, then most-recently-modified. */
|
|
359
|
+
function byStatusThenRecent(a, b) {
|
|
360
|
+
return somedayRank(a) - somedayRank(b) || b.modified.getTime() - a.modified.getTime();
|
|
361
|
+
}
|
|
362
|
+
export function liteTitleSearch(db, query, options) {
|
|
363
|
+
const cap = options?.limit ?? 10;
|
|
364
|
+
const type = options?.type;
|
|
365
|
+
const needle = query.toLowerCase();
|
|
366
|
+
const areas = type === undefined || type === "area"
|
|
367
|
+
? areasView(db)
|
|
368
|
+
.filter((a) => a.title.toLowerCase().includes(needle))
|
|
369
|
+
.toSorted((a, b) => a.title.localeCompare(b.title))
|
|
370
|
+
: [];
|
|
371
|
+
let tasks = [];
|
|
372
|
+
if (type === undefined || type === "to-do" || type === "project") {
|
|
373
|
+
const typeSql = type === "to-do" ? "t.type = 0" : type === "project" ? "t.type = 1" : "t.type IN (0, 1)";
|
|
374
|
+
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED} AND ${typeSql} AND t.title LIKE ?`, [`%${query}%`]);
|
|
375
|
+
tasks = materialize(db, rows);
|
|
376
|
+
}
|
|
377
|
+
const projects = tasks.filter((t) => t.type === "project").toSorted(byStatusThenRecent);
|
|
378
|
+
const todos = tasks.filter((t) => t.type === "to-do").toSorted(byStatusThenRecent);
|
|
379
|
+
const ordered = [
|
|
380
|
+
...areas.map((area) => ({ kind: "area", area })),
|
|
381
|
+
...projects.map((task) => ({ kind: "task", task })),
|
|
382
|
+
...todos.map((task) => ({ kind: "task", task })),
|
|
383
|
+
];
|
|
384
|
+
return { candidates: ordered.slice(0, cap), total: ordered.length };
|
|
385
|
+
}
|
|
296
386
|
export function searchView(db, query, options) {
|
|
297
|
-
const
|
|
387
|
+
const cap = options?.limit === null ? null : (options?.limit ?? 50);
|
|
298
388
|
const needle = `%${query}%`;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
389
|
+
// The scope predicates (everything except the match needle), reused verbatim
|
|
390
|
+
// for the needle query AND the heading-credited-project query so both honor
|
|
391
|
+
// the same type/status/scope constraints.
|
|
392
|
+
const scope = [NOT_TEMPLATE];
|
|
393
|
+
const scopeBinds = [];
|
|
394
|
+
scope.push(options?.type === "to-do"
|
|
302
395
|
? "t.type = 0"
|
|
303
396
|
: options?.type === "project"
|
|
304
397
|
? "t.type = 1"
|
|
305
398
|
: "t.type IN (0, 1)");
|
|
306
399
|
// Scope: open + untrashed by default; --logged/--trashed widen; --all is
|
|
307
|
-
// the legacy include-everything behavior.
|
|
400
|
+
// the legacy include-everything behavior. Untrashed means the whole
|
|
401
|
+
// container chain (derived trash, A24B), not just the row's own flag.
|
|
308
402
|
if (options?.all !== true) {
|
|
309
403
|
const statuses = options?.logged === true ? "(0, 2, 3)" : "(0)";
|
|
310
|
-
|
|
404
|
+
scope.push(`t.status IN ${statuses}`);
|
|
311
405
|
if (options?.trashed !== true)
|
|
312
|
-
|
|
406
|
+
scope.push(`t.trashed = 0 AND ${CONTAINER_UNTRASHED}`);
|
|
313
407
|
}
|
|
314
408
|
if (options?.project !== undefined) {
|
|
315
409
|
const uuid = resolveProjectUuid(db, options.project);
|
|
316
410
|
// Children incl. headed ones (heading rows carry the project link).
|
|
317
|
-
|
|
318
|
-
|
|
411
|
+
scope.push("(t.project = ? OR t.heading IN (SELECT uuid FROM TMTask WHERE type = 2 AND project = ?))");
|
|
412
|
+
scopeBinds.push(uuid, uuid);
|
|
319
413
|
}
|
|
320
414
|
if (options?.area !== undefined) {
|
|
321
415
|
const uuid = resolveAreaUuid(db, options.area);
|
|
322
|
-
|
|
323
|
-
|
|
416
|
+
scope.push("t.area = ?");
|
|
417
|
+
scopeBinds.push(uuid);
|
|
324
418
|
}
|
|
325
419
|
const tf = tagFilter(db, options);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
420
|
+
// Needle matches: title OR notes. No SQL LIMIT — ranking runs before the cap.
|
|
421
|
+
const rows = fetchTaskRows(db, `${scope.join(" AND ")} AND (t.title LIKE ? OR t.notes LIKE ?)${tf.sql}`, [...scopeBinds, needle, needle, ...tf.binds]);
|
|
422
|
+
const needleLower = query.toLowerCase();
|
|
423
|
+
const matches = new Map();
|
|
424
|
+
for (const item of materialize(db, rows)) {
|
|
425
|
+
// Field credit: title beats notes when both carry the substring.
|
|
426
|
+
const field = item.title.toLowerCase().includes(needleLower) ? "title" : "notes";
|
|
427
|
+
matches.set(item.uuid, { item, field });
|
|
428
|
+
}
|
|
429
|
+
// Heading titles are treated as if they lived in the parent PROJECT's notes:
|
|
430
|
+
// a heading-title match surfaces the parent project (never a bare heading
|
|
431
|
+
// row), credited at the heading-via-project field rank. A to-do-only search
|
|
432
|
+
// has no project rows, so headings do not apply there.
|
|
433
|
+
if (options?.type !== "to-do") {
|
|
434
|
+
const headingRows = db
|
|
435
|
+
.prepare(`SELECT project AS projectUuid, title FROM TMTask
|
|
436
|
+
WHERE type = 2 AND project IS NOT NULL AND trashed = 0 AND title LIKE ?`)
|
|
437
|
+
.all(needle);
|
|
438
|
+
const headingTitleFor = new Map();
|
|
439
|
+
for (const h of headingRows) {
|
|
440
|
+
if (!headingTitleFor.has(h.projectUuid))
|
|
441
|
+
headingTitleFor.set(h.projectUuid, h.title ?? "");
|
|
442
|
+
}
|
|
443
|
+
// Surface only the projects NOT already matched by their own title/notes
|
|
444
|
+
// (a higher-ranked field already represents them), and only those passing
|
|
445
|
+
// the view's scope.
|
|
446
|
+
const wanted = [...headingTitleFor.keys()].filter((uuid) => !matches.has(uuid));
|
|
447
|
+
if (wanted.length > 0) {
|
|
448
|
+
const placeholders = wanted.map(() => "?").join(", ");
|
|
449
|
+
const projectRows = fetchTaskRows(db, `${scope.join(" AND ")} AND t.type = 1 AND t.uuid IN (${placeholders})${tf.sql}`, [...scopeBinds, ...wanted, ...tf.binds]);
|
|
450
|
+
for (const item of materialize(db, projectRows)) {
|
|
451
|
+
const matchedVia = {
|
|
452
|
+
kind: "heading",
|
|
453
|
+
title: headingTitleFor.get(item.uuid) ?? "",
|
|
454
|
+
};
|
|
455
|
+
// Annotate the freshly materialized entity in place (owned here).
|
|
456
|
+
item.matchedVia = matchedVia;
|
|
457
|
+
matches.set(item.uuid, { item, field: "heading", matchedVia });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const ranked = [...matches.values()].toSorted(compareSearchMatches);
|
|
462
|
+
const sliced = cap === null ? ranked : ranked.slice(0, cap);
|
|
463
|
+
return sliced.map((m) => m.item);
|
|
329
464
|
}
|
|
330
465
|
//# sourceMappingURL=views.js.map
|