fix: apply game order to dailies and routines
Account creation order leaked into daily rows and routine groups even when the reader had arranged their games. Apply the shared display rule while preserving each game's server and task order.
This commit is contained in:
+5
-2
@@ -609,7 +609,8 @@ legend on the same baseline as "Next: …".
|
||||
|
||||
### Ticking things off
|
||||
|
||||
- **Today's dailies.** The whole row is the target. A tick washes the row in its game's hue, pops the
|
||||
- **Today's dailies.** Rows and progress pips follow Game order, falling back to A–Z by game name;
|
||||
each game's servers and tasks keep their relative order. The whole row is the target. A tick washes the row in its game's hue, pops the
|
||||
box and draws the check. A pip bar and "N of M done" sit under the heading. A check-in goal (for
|
||||
example "5/5 wk") counts as done once met, with a dotted check, and shows its goal count instead of
|
||||
a streak.
|
||||
@@ -655,7 +656,9 @@ legend on the same baseline as "Next: …".
|
||||
|
||||
### Other pages
|
||||
|
||||
- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. The table has tick
|
||||
- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. Game groups,
|
||||
rows within each reset band, history and Checklist pins follow Game order with the same A–Z fallback.
|
||||
The table has tick
|
||||
and click-to-edit amount cells. The library and create/edit drawers are shelves.
|
||||
- **Timeline.** Two looks (new and classic) chosen in Appearance. Dailies are not on the timeline.
|
||||
- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
import { metaFor } from "../shared/games.ts";
|
||||
import { readGoal } from "../shared/goal.ts";
|
||||
import { isDaily, resolveDaily } from "../shared/daily.ts";
|
||||
import { orderGames } from "./state/gameOrder.ts";
|
||||
import { orderGameItems, orderGames } from "./state/gameOrder.ts";
|
||||
import { advanceFocus } from "./state/lens.ts";
|
||||
import { restorePrefsValue } from "./state/usePrefs.ts";
|
||||
import { readableAmounts } from "./state/useAmounts.ts";
|
||||
@@ -532,12 +532,12 @@ function ReaderContent({
|
||||
setSettingsPage(next);
|
||||
setPage("settings");
|
||||
};
|
||||
const accounts = Object.values(state.accounts).filter(
|
||||
const accounts = orderGameItems(Object.values(state.accounts).filter(
|
||||
(a) =>
|
||||
!a.retiredAt &&
|
||||
!prefs.hiddenGames.includes(a.game) &&
|
||||
(a.intent.events || Object.values(a.routines).some((r) => r.followed)),
|
||||
);
|
||||
), prefs.gameOrder, (game) => meta(game).name);
|
||||
const ordered = orderGames(
|
||||
[...new Set(accounts.map((a) => a.game))],
|
||||
prefs.gameOrder,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useGameMeta } from "../../state/gameMeta.tsx";
|
||||
import { orderGameItems } from "../../state/gameOrder.ts";
|
||||
import { serverSuffix } from "../../state/reader/servers.ts";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -193,6 +194,11 @@ export function DailyPanel({
|
||||
now: number;
|
||||
}) {
|
||||
const meta = useGameMeta();
|
||||
rows = orderGameItems(
|
||||
rows,
|
||||
Array.isArray(state.prefs.gameOrder) ? state.prefs.gameOrder as string[] : undefined,
|
||||
(game) => meta(game).name,
|
||||
);
|
||||
const total = rows.length;
|
||||
const done = rows.filter((r) =>
|
||||
doneToday(r, readerDayKey(now, r.clock)),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { routinePeriod } from "./TimelinePage.tsx";
|
||||
import { manyServers, serverSuffix } from "../../state/reader/servers.ts";
|
||||
import { formatRemaining } from "../../../shared/time.ts";
|
||||
import { useGameMeta } from "../../state/gameMeta.tsx";
|
||||
import { orderGameItems } from "../../state/gameOrder.ts";
|
||||
import type { CSSProperties } from "react";
|
||||
import { RoutineHistory } from "./RoutineHistory.tsx";
|
||||
import { useState } from "react";
|
||||
@@ -120,14 +121,15 @@ export function RoutinesPage({
|
||||
setEditing(task);
|
||||
setDrawer(pane);
|
||||
};
|
||||
const accounts = Object.values(state.accounts).filter(
|
||||
const accounts = orderGameItems(Object.values(state.accounts).filter(
|
||||
(a) =>
|
||||
!a.retiredAt &&
|
||||
!(
|
||||
Array.isArray(state.prefs.hiddenGames) &&
|
||||
state.prefs.hiddenGames.includes(a.game)
|
||||
),
|
||||
);
|
||||
), Array.isArray(state.prefs.gameOrder) ? state.prefs.gameOrder as string[] : undefined,
|
||||
(game) => meta(game).name);
|
||||
const save = async (draft: TaskDraft, placement: CreatePlacement) => {
|
||||
const at = new Date().toISOString(),
|
||||
id = editing ? editing.id : mintTaskId();
|
||||
|
||||
@@ -64,6 +64,16 @@ export function orderGames(
|
||||
return [...placed, ...alphabetical(lanes.filter((id) => !seen.has(id)))];
|
||||
}
|
||||
|
||||
/** Apply the same display order to accounts or tasks, keeping ties stable. */
|
||||
export function orderGameItems<T extends { game: LaneId }>(
|
||||
items: readonly T[],
|
||||
stored: readonly LaneId[] | undefined,
|
||||
nameOf: (id: LaneId) => string,
|
||||
): T[] {
|
||||
const games = orderGames([...new Set(items.map((item) => item.game))], stored, nameOf);
|
||||
return [...items].sort((a, b) => games.indexOf(a.game) - games.indexOf(b.game));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one entry, shifting the rest.
|
||||
*
|
||||
|
||||
+15
-1
@@ -1,8 +1,22 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { moveGame, orderGames } from "../src/client/state/gameOrder.ts";
|
||||
import { moveGame, orderGames, orderGameItems } from "../src/client/state/gameOrder.ts";
|
||||
import { metaFor } from "../src/shared/games.ts";
|
||||
import type { LaneId } from "../src/shared/custom.ts";
|
||||
|
||||
test("ordering tasks keeps a game's servers and tasks stable without changing its input", () => {
|
||||
const items = [
|
||||
{ game: "hsr", id: "first" },
|
||||
{ game: "genshin", id: "asia-daily" },
|
||||
{ game: "hsr", id: "second" },
|
||||
{ game: "genshin", id: "asia-weekly" },
|
||||
{ game: "genshin", id: "europe-daily" },
|
||||
];
|
||||
const original = items.slice();
|
||||
expect(orderGameItems(items, ["genshin", "hsr"], (g) => metaFor(g, {}).name).map((r) => r.id))
|
||||
.toEqual(["asia-daily", "asia-weekly", "europe-daily", "first", "second"]);
|
||||
expect(items).toEqual(original);
|
||||
});
|
||||
|
||||
/**
|
||||
* The reader's game order.
|
||||
*
|
||||
|
||||
@@ -7,6 +7,26 @@ import { readerDayKey } from "../src/shared/readerClock.ts";
|
||||
|
||||
const at = "2026-10-16T12:00:00.000Z";
|
||||
const now = Date.parse(at);
|
||||
|
||||
test.each([
|
||||
{ order: ["zzz", "genshin", "hsr"], expected: ["ZZZ", "Genshin", "Star Rail"] },
|
||||
{ order: undefined, expected: ["Genshin", "Star Rail", "ZZZ"] },
|
||||
{ order: ["zzz"], expected: ["ZZZ", "Genshin", "Star Rail"] },
|
||||
])("dailies and progress pips follow game order $order", ({ order, expected }) => {
|
||||
let id = 0;
|
||||
const state = migrateLegacy({}, ["hsr", "genshin", "zzz"], at, () => `account:${++id}`);
|
||||
state.prefs.gameOrder = order;
|
||||
const rows = Object.values(state.accounts).flatMap((account) =>
|
||||
routineRows(state, account.id, now).filter((r) => r.id === `dailies:${account.game}`),
|
||||
);
|
||||
const html = render(<DailyPanel rows={rows} state={state} now={now} onAction={async () => ({ ok: true })} />);
|
||||
expect([...html.matchAll(/<strong>(.*?)<\/strong>/g)].map((m) => m[1])).toEqual([...expected]);
|
||||
const pips = [...html.matchAll(/<i[^>]*style="([^"]*)"/g)].map((m) => m[1]);
|
||||
const rowHues = [...html.matchAll(/class="daily-row daily-hit"[^>]*style="([^"]*)"/g)].map((m) => m[1]);
|
||||
expect(pips).toHaveLength(3);
|
||||
expect(pips).toEqual(rowHues);
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const state = migrateLegacy({}, ["genshin"], at, () => "account:daily");
|
||||
state.accounts["account:daily"]!.routines["dailies:genshin"] = {
|
||||
|
||||
@@ -45,6 +45,17 @@ test("bulk pins combine changes per account and preserve the other placement", (
|
||||
});
|
||||
|
||||
import { RoutinesPage } from "../src/client/components/redesign/RoutinesPage.tsx";
|
||||
test.each(["game", "reset"])("routines follow Game order when grouped by %s", (group) => {
|
||||
const at = "2026-10-16T12:00:00.000Z";
|
||||
let id = 0;
|
||||
const state = migrateLegacy({}, ["hsr", "genshin", "zzz"], at, () => `account:${++id}`);
|
||||
state.prefs.gameOrder = ["zzz", "genshin", "hsr"];
|
||||
state.prefs.routineGroup = group;
|
||||
const html = render(<RoutinesPage state={state} now={Date.parse(at)} onAction={async () => ({ ok: true })} />);
|
||||
const cells = [...html.matchAll(/class="gcell"><span>([^<]*)/g)].map((m) => m[1]);
|
||||
expect(cells).toEqual(["ZZZ", "Genshin", "Star Rail"]);
|
||||
});
|
||||
|
||||
test("the routines table shows progress as text with an edit affordance, not inputs", () => {
|
||||
const at = "2026-10-16T12:00:00.000Z";
|
||||
const state = migrateLegacy({}, ["genshin"], at, () => "account:table");
|
||||
|
||||
Reference in New Issue
Block a user