feat(sidebar): tallet ved et projekt er opgaver, og folden har en tast
"Hvorfor står der 2 ud for et tomt projekt?" Fordi tallet var member_count — hvor mange mennesker. Den samme grå kolonne betød tre ting: personer ved projekter, projekter ved grupper, opgaver ved etiketter. I en opgaveapp læses et tal ved siden af et projekt som opgaver, og det gjorde manden, der har skrevet appen. Nu er det åbne opgaver, og det vises når der er noget tilbage frem for når projektet er delt — som var den gamle betingelse, og grunden til at netop de tre delte havde et tal. Hvem der ellers er med står på projektets egen side. Tællingen hentes fra serveren igen, når opgaver flytter sig. Det ville være hurtigere at lægge til og trække fra lokalt, og det ville være forkert inden for en dag: en opgave flyttet mellem projekter, en lukket i en anden fane, en gentagende der lukker og åbner sig selv. Og sidebjælken kunne allerede foldes — der var bare kun en knap med et rektangel med en streg i, og det er ikke et ord. Nu gør ⌘B det samme, knappen siger det, og genvejssiden nævner det. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
997dffe01c
commit
62498c2765
@@ -412,8 +412,16 @@
|
||||
>
|
||||
<span class="dot" style="background: {colorVar(project.color)}" aria-hidden="true"></span>
|
||||
{projectName(project)}
|
||||
{#if project.shared}
|
||||
<span class="count">{project.member_count}</span>
|
||||
<!-- Open tasks, not people. The same grey number meant three things in this
|
||||
one column — people at a project, projects at a group, tasks at a label
|
||||
— and in a task app a number beside a project is read as tasks. Who
|
||||
else is in it is on the project's own page.
|
||||
|
||||
Shown when there is something left rather than when the project is
|
||||
shared, which is what it keyed on before: a nought beside every empty
|
||||
project is a column of noughts. -->
|
||||
{#if project.open_count > 0}
|
||||
<span class="count">{project.open_count}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
@@ -377,6 +377,7 @@ export const da = {
|
||||
'data.templatesHint':
|
||||
'Et projekt gemt som en form, der kan bruges igen. Datoerne i skabelonen er relative, så de bliver regnet ud fra den startdato, du vælger.',
|
||||
|
||||
'keys.foldSidebar': 'fold sidebjælken væk',
|
||||
'keys.anyLetter': 'et bogstav',
|
||||
'keys.typeToCapture': 'begynder en opgave, med bogstavet i feltet',
|
||||
'keys.navigation': 'Navigation',
|
||||
|
||||
@@ -372,6 +372,7 @@ export const en = {
|
||||
'data.templatesHint':
|
||||
'A project saved as a shape you can use again. The dates in a template are relative, so they are worked out from the start date you pick.',
|
||||
|
||||
'keys.foldSidebar': 'fold the sidebar away',
|
||||
'keys.anyLetter': 'any letter',
|
||||
'keys.typeToCapture': 'starts a task, with the letter already in the field',
|
||||
'keys.navigation': 'Navigation',
|
||||
|
||||
@@ -156,6 +156,7 @@ class AppState {
|
||||
break;
|
||||
case 'task.deleted':
|
||||
this.tasks = this.tasks.filter((t) => t.id !== task?.id);
|
||||
this.refreshCounts();
|
||||
break;
|
||||
|
||||
// Projects and labels belong to a person rather than to a project, so
|
||||
@@ -246,6 +247,11 @@ class AppState {
|
||||
*/
|
||||
upsert(task) {
|
||||
if (!task?.id) return;
|
||||
// Before the branch, not after it. A task that is already in the list can
|
||||
// still change the counts — moved to another project, finished in another
|
||||
// tab, a recurring one that closed and opened itself — and the branch below
|
||||
// returns early for exactly those.
|
||||
this.refreshCounts();
|
||||
if (this.tasks.some((t) => t.id === task.id)) {
|
||||
this.replace(task.id, task);
|
||||
return;
|
||||
@@ -301,6 +307,7 @@ class AppState {
|
||||
const what = task?.content ?? '';
|
||||
|
||||
await this.#optimistic(id, { completed: true }, () => api.completeTask(id));
|
||||
this.refreshCounts();
|
||||
|
||||
if (!repeats) {
|
||||
this.toast(t('task.completedUndo', { what: truncate(what) }), {
|
||||
@@ -312,6 +319,7 @@ class AppState {
|
||||
|
||||
async reopen(id) {
|
||||
await this.#optimistic(id, { completed: false }, () => api.reopenTask(id));
|
||||
this.refreshCounts();
|
||||
}
|
||||
|
||||
async update(id, patch) {
|
||||
@@ -373,6 +381,7 @@ class AppState {
|
||||
this.tasks = this.tasks.filter((t) => t.id !== id);
|
||||
try {
|
||||
await api.deleteTask(id);
|
||||
this.refreshCounts();
|
||||
} catch (e) {
|
||||
this.tasks = previous;
|
||||
this.toast(humanMessage(e));
|
||||
@@ -417,6 +426,56 @@ class AppState {
|
||||
this.projects = projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings the sidebar's per-project counts back in line after tasks have moved.
|
||||
*
|
||||
* The count is open tasks now, which changes on every add, finish and delete —
|
||||
* where the old one, how many people are in it, changed about twice a year. It
|
||||
* would be quicker to add and subtract locally, and it would be wrong within a
|
||||
* day: a task moved between projects, one finished in another tab, a recurring
|
||||
* one that closed and reopened itself. Asking the server is one request and
|
||||
* cannot drift.
|
||||
*
|
||||
* Coalesced, because a paste of five tasks is five calls in the same tick and
|
||||
* the answer to all of them is the same.
|
||||
*/
|
||||
#countsRunning = false;
|
||||
#countsAgain = false;
|
||||
#countsTimer = null;
|
||||
|
||||
refreshCounts() {
|
||||
// Two windows to coalesce over, not one. Collapsing calls that arrive within
|
||||
// a tick is the easy half; the half that matters is a change arriving while
|
||||
// the request is already in flight. Dropping those left the sidebar showing
|
||||
// the count as it was one task ago, which is the state the test caught and
|
||||
// which nothing but a reload would have corrected.
|
||||
if (this.#countsRunning) {
|
||||
this.#countsAgain = true;
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.#countsTimer);
|
||||
this.#countsTimer = setTimeout(async () => {
|
||||
this.#countsRunning = true;
|
||||
try {
|
||||
const { projects } = await api.listProjects();
|
||||
const counts = new Map(projects.map((p) => [p.id, p.open_count]));
|
||||
this.projects = this.projects.map((p) =>
|
||||
counts.has(p.id) ? { ...p, open_count: counts.get(p.id) } : p
|
||||
);
|
||||
} catch {
|
||||
// The number is a convenience; failing to update it must not put an
|
||||
// error in front of somebody who was only ticking something off.
|
||||
} finally {
|
||||
this.#countsRunning = false;
|
||||
}
|
||||
if (this.#countsAgain) {
|
||||
this.#countsAgain = false;
|
||||
this.refreshCounts();
|
||||
}
|
||||
}, 120);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Puts the projects in the given order.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user