Public Access
feat(sync): show sync changes in banner and details dialog
This commit is contained in:
1 parent
e054e13a59
commit
dabb0c941c
9 files changed
+425
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import {apiFetch} from './api.js';
|
||||
|
||||
const CORE = () => import.meta.env.VITE_CORE_API_URL;
|
||||
|
||||
// The order of the groups in the details.
|
||||
const KINDS = ['ADDED', 'REMOVED', 'PLAYTIME', 'ACHIEVEMENTS', 'STATUS'];
|
||||
|
||||
/**
|
||||
* `{syncedAt, startedBy, seen, importedGames, changes: [{kind, appid, name, imageUrl, amount, fromStatus, toStatus}]}`,
|
||||
* or null when the user has no result (204) or the answer is not 200. PLAYTIME: amount in minutes, ACHIEVEMENTS:
|
||||
* amount is the number of new unlocks.
|
||||
*/
|
||||
export async function fetchSyncChanges() {
|
||||
const response = await apiFetch(`${CORE()}/api/library/sync-changes`);
|
||||
return response.status === 200 ? response.json() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result with this `syncedAt` stays seen on every device. A newer result is not touched.
|
||||
*/
|
||||
export const markSyncChangesSeen = (syncedAt) =>
|
||||
apiFetch(`${CORE()}/api/library/sync-changes/seen`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({syncedAt}),
|
||||
});
|
||||
|
||||
/**
|
||||
* What the dashboard shows of a result: null (nothing), `{mode: 'nothing'}` for a manual sync that changed nothing, or
|
||||
* `{mode: 'changes', imported, added, removed, minutes, achievements, moves}`. A result the user saw, and an empty
|
||||
* result of an automatic sync, show nothing.
|
||||
*/
|
||||
export function bannerOf(result) {
|
||||
if (!result || result.seen) return null;
|
||||
const changes = result.changes ?? [];
|
||||
const imported = result.importedGames ?? 0;
|
||||
if (changes.length === 0 && imported === 0) {
|
||||
return result.startedBy === 'MANUAL' ? {mode: 'nothing'} : null;
|
||||
}
|
||||
const of = (kind) => changes.filter((change) => change.kind === kind);
|
||||
const sum = (kind) => of(kind).reduce((total, change) => total + (change.amount ?? 0), 0);
|
||||
return {
|
||||
mode: 'changes', imported, added: of('ADDED').length, removed: of('REMOVED').length,
|
||||
minutes: sum('PLAYTIME'), achievements: sum('ACHIEVEMENTS'), moves: of('STATUS').length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `[{kind, changes}]` for the kinds that have changes, in the order of KINDS.
|
||||
*/
|
||||
export const groupByKind = (changes) => KINDS
|
||||
.map((kind) => ({kind, changes: changes.filter((change) => change.kind === kind)}))
|
||||
.filter((group) => group.changes.length > 0);
|
||||
@@ -0,0 +1,49 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {bannerOf, groupByKind} from './syncChanges.js';
|
||||
|
||||
const result = (extra = {}) => ({syncedAt: '2026-10-04T10:00:00Z', startedBy: 'AUTO', seen: false, importedGames: 0, changes: [], ...extra});
|
||||
|
||||
describe('bannerOf', () => {
|
||||
it('showsNothingWithoutAResult', () => {
|
||||
expect(bannerOf(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('showsNothingForASeenResult', () => {
|
||||
expect(bannerOf(result({seen: true, changes: [{kind: 'ADDED', appid: 1}]}))).toBeNull();
|
||||
});
|
||||
|
||||
it('showsNothingForAnEmptyAutomaticSync', () => {
|
||||
expect(bannerOf(result())).toBeNull();
|
||||
});
|
||||
|
||||
it('saysNothingChangedForAnEmptyManualSync', () => {
|
||||
expect(bannerOf(result({startedBy: 'MANUAL'}))).toEqual({mode: 'nothing'});
|
||||
});
|
||||
|
||||
it('countsTheGamesOfAnImport', () => {
|
||||
expect(bannerOf(result({importedGames: 12}))).toMatchObject({mode: 'changes', imported: 12, added: 0});
|
||||
});
|
||||
|
||||
it('sumsMinutesAndUnlocksAndCountsTheRest', () => {
|
||||
const changes = [
|
||||
{kind: 'ADDED', appid: 1}, {kind: 'ADDED', appid: 2}, {kind: 'REMOVED', appid: 3},
|
||||
{kind: 'PLAYTIME', appid: 4, amount: 30}, {kind: 'PLAYTIME', appid: 5, amount: 45},
|
||||
{kind: 'ACHIEVEMENTS', appid: 4, amount: 2}, {kind: 'ACHIEVEMENTS', appid: 5, amount: 3},
|
||||
{kind: 'STATUS', appid: 5, fromStatus: 'BACKLOG', toStatus: 'PLAYING'},
|
||||
];
|
||||
|
||||
expect(bannerOf(result({changes}))).toEqual(
|
||||
{mode: 'changes', imported: 0, added: 2, removed: 1, minutes: 75, achievements: 5, moves: 1});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupByKind', () => {
|
||||
it('putsTheGroupsInTheFixedOrderAndLeavesOutEmptyOnes', () => {
|
||||
const changes = [{kind: 'STATUS', appid: 1}, {kind: 'ADDED', appid: 2}, {kind: 'PLAYTIME', appid: 3}, {kind: 'ADDED', appid: 4}];
|
||||
|
||||
const groups = groupByKind(changes);
|
||||
|
||||
expect(groups.map((group) => group.kind)).toEqual(['ADDED', 'PLAYTIME', 'STATUS']);
|
||||
expect(groups[0].changes.map((change) => change.appid)).toEqual([2, 4]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user