Studies: Finished & archived section with Archive/Restore
Build and Push Docker Image / build-and-push (push) Successful in 6m3s
Build and Push Docker Image / build-and-push (push) Successful in 6m3s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
afc3206da4
commit
843056b963
6 files changed
+140
-74
No files matched your search
@@ -200,17 +200,16 @@ export function useProjectLibrary({
|
||||
}
|
||||
};
|
||||
|
||||
// Puts a study in a series ("Romans", "Advent 2026"), or takes it out (an empty name).
|
||||
const setProjectSeries = (id, series) => {
|
||||
// Changes one top-level field of a stored study (from Home, without opening it) and syncs it.
|
||||
const patchStoredStudy = (id, patch) => {
|
||||
const loaded = loadProjectById(id);
|
||||
if (!loaded) return;
|
||||
const name = String(series ?? '').trim().slice(0, 80);
|
||||
if ((loaded.series ?? '') === name) return;
|
||||
const updated = { ...loaded, series: name, lastEdited: Date.now() };
|
||||
if (Object.entries(patch).every(([k, v]) => (loaded[k] ?? (typeof v === 'boolean' ? false : '')) === v)) return;
|
||||
const updated = { ...loaded, ...patch, lastEdited: Date.now() };
|
||||
saveProjectToStorage(updated);
|
||||
unsyncedStudies.add(id);
|
||||
setProjectIndex(loadProjectIndex());
|
||||
if (project?.id === id) setProject((current) => (current ? { ...current, series: name } : current));
|
||||
if (project?.id === id) setProject((current) => (current ? { ...current, ...patch } : current));
|
||||
saveRemoteProject(updated, { baseEdited: getSyncBase(id) }).then((result) => {
|
||||
if (result.ok) {
|
||||
const serverEdited = result.data?.lastEdited ?? updated.lastEdited;
|
||||
@@ -221,6 +220,12 @@ export function useProjectLibrary({
|
||||
});
|
||||
};
|
||||
|
||||
// Puts a study in a series ("Romans", "Advent 2026"), or takes it out (an empty name).
|
||||
const setProjectSeries = (id, series) => patchStoredStudy(id, { series: String(series ?? '').trim().slice(0, 80) });
|
||||
|
||||
// Moves a study to (or back from) the "Finished & archived" section on Home.
|
||||
const setProjectArchived = (id, archived) => patchStoredStudy(id, { archived: !!archived });
|
||||
|
||||
// Changes one chunk of a stored study (from Home, without opening it) and syncs it. `change` takes the chunk, returns the new one.
|
||||
const updateStoredChunk = (id, chunkId, change) => {
|
||||
const loaded = loadProjectById(id);
|
||||
@@ -318,6 +323,7 @@ export function useProjectLibrary({
|
||||
openStudyChunk,
|
||||
renameProjectInStorage,
|
||||
setProjectSeries,
|
||||
setProjectArchived,
|
||||
saveChunkRecall,
|
||||
saveChunkPatch,
|
||||
deleteProject,
|
||||
|
||||
@@ -105,3 +105,17 @@ test('studies only on the server are pulled down', async () => {
|
||||
setup();
|
||||
await waitFor(() => expect(loadProjectById('far')?.title).toBe('From elsewhere'));
|
||||
});
|
||||
|
||||
test('archiving a study keeps it, flags it in the index, syncs it, and restoring clears the flag', async () => {
|
||||
saveProjectToStorage(study('s1'));
|
||||
const hook = setup();
|
||||
act(() => hook.result.current.setProjectArchived('s1', true));
|
||||
expect(loadProjectById('s1').archived).toBe(true);
|
||||
expect(loadProjectIndex().find((e) => e.id === 's1').archived).toBe(true);
|
||||
await waitFor(() => expect(sync.saveRemoteProject).toHaveBeenCalledWith(expect.objectContaining({ id: 's1', archived: true }), expect.anything()));
|
||||
sync.saveRemoteProject.mockClear();
|
||||
act(() => hook.result.current.setProjectArchived('s1', true)); // already archived: nothing to do
|
||||
expect(sync.saveRemoteProject).not.toHaveBeenCalled();
|
||||
act(() => hook.result.current.setProjectArchived('s1', false));
|
||||
expect(loadProjectIndex().find((e) => e.id === 's1').archived).toBe(false);
|
||||
});
|
||||
+102
-67
@@ -50,6 +50,7 @@ export default function HomePage() {
|
||||
resumeProject,
|
||||
renameProjectInStorage,
|
||||
setProjectSeries,
|
||||
setProjectArchived,
|
||||
deleteProject,
|
||||
handlePlayBookAudio,
|
||||
handleStopBookAudio,
|
||||
@@ -68,6 +69,7 @@ export default function HomePage() {
|
||||
}); // 'recent' | 'title' | 'passage'
|
||||
const [homeTags, setHomeTags] = useState([]); // tag chips switched on: a study must have all of them
|
||||
const [showAllTags, setShowAllTags] = useState(false);
|
||||
const [showArchived, setShowArchived] = useState(false); // the "Finished & archived" section is folded away until opened
|
||||
const [seriesFilter, setSeriesFilter] = useState(''); // '' = all, '__none' = not in a series, or a series name
|
||||
const [seriesEditingId, setSeriesEditingId] = useState(null);
|
||||
const [seriesValue, setSeriesValue] = useState('');
|
||||
@@ -119,21 +121,90 @@ export default function HomePage() {
|
||||
}, 300);
|
||||
}, [homeSearch]);
|
||||
|
||||
const tagList = tagCounts(projectIndex);
|
||||
// Finished studies are set aside in their own section; the tag and series filters follow the active ones
|
||||
const activeIndex = projectIndex.filter((e) => !e.archived);
|
||||
const tagList = tagCounts(activeIndex);
|
||||
const shownTags = showAllTags ? tagList : tagList.slice(0, 12);
|
||||
const seriesNames = seriesList(projectIndex);
|
||||
const visible = filterStudies(projectIndex, { query: homeSearch, tags: homeTags, series: seriesFilter })
|
||||
.sort((a, b) => {
|
||||
if (homeSort === 'title') return (a.title ?? '').localeCompare(b.title ?? '');
|
||||
if (homeSort === 'passage') return (a.chapterSummary ?? '').localeCompare(b.chapterSummary ?? '');
|
||||
return (b.lastEdited ?? 0) - (a.lastEdited ?? 0);
|
||||
});
|
||||
const seriesNames = seriesList(activeIndex);
|
||||
const byHomeSort = (a, b) => {
|
||||
if (homeSort === 'title') return (a.title ?? '').localeCompare(b.title ?? '');
|
||||
if (homeSort === 'passage') return (a.chapterSummary ?? '').localeCompare(b.chapterSummary ?? '');
|
||||
return (b.lastEdited ?? 0) - (a.lastEdited ?? 0);
|
||||
};
|
||||
const visible = filterStudies(projectIndex, { query: homeSearch, tags: homeTags, series: seriesFilter }).sort(byHomeSort);
|
||||
const archivedStudies = filterStudies(projectIndex, { query: homeSearch, tags: homeTags, archived: true }).sort(byHomeSort);
|
||||
const archivedCount = projectIndex.length - activeIndex.length;
|
||||
const filtering = !!homeSearch.trim() || homeTags.length > 0 || seriesFilter !== '';
|
||||
const archivedOpen = showArchived || ((!!homeSearch.trim() || homeTags.length > 0) && archivedStudies.length > 0);
|
||||
// With series in use (and no single series chosen) the studies are grouped under their series name
|
||||
const sections = seriesFilter === '' && seriesNames.length > 0 ? groupBySeries(visible) : [{ name: '', items: visible, flat: true }];
|
||||
const fieldCls = 'rounded-lg border border-[var(--r-line)] bg-[var(--r-bar)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
const linkBtn = 'rounded-lg px-3 py-1.5 text-sm text-[var(--r-sub)] hover:bg-black/5';
|
||||
const notice = 'mb-4 rounded-xl border border-[var(--r-line)] bg-[var(--r-bar)] px-4 py-3 text-sm';
|
||||
|
||||
const renderEntry = (entry) => (
|
||||
<div key={entry.id} className={homeView === 'list'
|
||||
? 'flex flex-col gap-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-3 sm:flex-row sm:items-center sm:gap-4'
|
||||
: 'flex flex-col gap-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5'}>
|
||||
{homeView === 'list'
|
||||
? <BookCover summary={entry.chapterSummary} compact className="h-14 w-full sm:w-28 sm:shrink-0" />
|
||||
: <BookCover summary={entry.chapterSummary} className="h-24" />}
|
||||
<div className="min-w-0 flex-1">
|
||||
{renamingId === entry.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="text" autoFocus value={renameValue} onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); const t = renameValue.trim(); if (t) renameProjectInStorage(entry.id, t); setRenamingId(null); }
|
||||
else if (e.key === 'Escape') setRenamingId(null);
|
||||
}}
|
||||
className={`${fieldCls} flex-1`} />
|
||||
<button type="button" onClick={() => { const t = renameValue.trim(); if (t) renameProjectInStorage(entry.id, t); setRenamingId(null); }} className="text-sm text-[var(--r-accent)] hover:underline">Save</button>
|
||||
<button type="button" onClick={() => setRenamingId(null)} className="text-sm text-[var(--r-sub)]">×</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex items-start gap-2 ${homeView === 'list' ? 'flex-wrap items-baseline' : 'justify-between'}`}>
|
||||
<h2 className="text-base font-medium">{entry.title}</h2>
|
||||
{sharedStudies?.[entry.id] !== undefined && (
|
||||
<span className="rounded-full border border-[var(--r-line)] px-2 py-0.5 text-[11px] text-[var(--r-sub)]" title="Another account invited you to edit this study">Shared{sharedStudies[entry.id] ? ` by ${sharedStudies[entry.id]}` : ''}</span>
|
||||
)}
|
||||
<button type="button" onClick={() => { setRenamingId(entry.id); setRenameValue(entry.title ?? ''); }} className="shrink-0 text-xs text-[var(--r-sub)] hover:text-[var(--r-text)]" title="Rename project">✎ Rename</button>
|
||||
<button type="button" onClick={() => { setSeriesEditingId(entry.id); setSeriesValue(entry.series ?? ''); }} className="shrink-0 text-xs text-[var(--r-sub)] hover:text-[var(--r-text)]" title="Put this study in a series">{entry.series ? '⧉ Series' : '⧉ Add to series'}</button>
|
||||
</div>
|
||||
)}
|
||||
{seriesEditingId === entry.id && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input type="text" autoFocus list="series-names" aria-label="Series name" placeholder="Series name (blank to remove)" value={seriesValue} onChange={(e) => setSeriesValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); setProjectSeries(entry.id, seriesValue); setSeriesEditingId(null); } else if (e.key === 'Escape') setSeriesEditingId(null); }}
|
||||
className={`${fieldCls} flex-1`} />
|
||||
<button type="button" onClick={() => { setProjectSeries(entry.id, seriesValue); setSeriesEditingId(null); }} className="text-sm text-[var(--r-accent)] hover:underline">Save</button>
|
||||
<button type="button" onClick={() => setSeriesEditingId(null)} aria-label="Cancel" className="text-sm text-[var(--r-sub)]">×</button>
|
||||
</div>
|
||||
)}
|
||||
{entry.chapterSummary && <p className="mt-1 text-sm text-[var(--r-sub)]" title={entry.chapterSummary}>{compactPassages(entry.chapterSummary)}</p>}
|
||||
{entry.tags?.length > 0 && (
|
||||
// A few tags and a "+N" for the rest (hover for all; the Tag filter above finds any of them)
|
||||
<div className="mt-2 flex flex-wrap gap-1" title={entry.tags.length > 3 ? entry.tags.join(', ') : undefined}>
|
||||
{entry.tags.slice(0, 3).map((tag) => (
|
||||
<button key={tag} type="button" onClick={() => toggleTag(tag)} aria-pressed={homeTags.includes(tag)} title={`Show studies tagged “${tag}”`}
|
||||
className={`rounded-full border px-2 py-0.5 text-[11px] ${homeTags.includes(tag) ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>{tag}</button>
|
||||
))}
|
||||
{entry.tags.length > 3 && <span className="rounded-full px-1.5 py-0.5 text-[11px] text-[var(--r-sub)]">+{entry.tags.length - 3} more</span>}
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-[var(--r-sub)]">{formatRelativeDate(entry.lastEdited)}</p>
|
||||
<OfflineBadge project={studyBooks[entry.id]} offline={offline} />
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 ${homeView === 'list' ? 'sm:w-56 sm:shrink-0' : ''}`}>
|
||||
<button type="button" onClick={() => resumeProject(entry.id)} className="flex-1 rounded-xl bg-[var(--r-text)] px-4 py-2 text-sm font-medium text-[var(--r-bg)] hover:opacity-90">Resume</button>
|
||||
{sharedStudies?.[entry.id] === undefined && (
|
||||
<button type="button" onClick={() => setProjectArchived(entry.id, !entry.archived)} title={entry.archived ? 'Move back to your studies' : 'Set this study aside as finished'}
|
||||
className="rounded-xl border border-[var(--r-line)] px-3 py-2 text-sm text-[var(--r-sub)] hover:bg-black/5">{entry.archived ? 'Restore' : 'Archive'}</button>
|
||||
)}
|
||||
<button type="button" onClick={() => deleteProject(entry.id)} className="rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm text-[var(--r-danger,#b42318)] hover:bg-black/5">{sharedStudies?.[entry.id] !== undefined ? 'Leave' : 'Delete'}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title="My Studies"
|
||||
@@ -232,7 +303,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visible.length === 0 && (
|
||||
{visible.length === 0 && archivedStudies.length === 0 && (
|
||||
<p role="status" className="mb-4 rounded-xl border border-dashed border-[var(--r-line)] px-4 py-6 text-center text-sm text-[var(--r-sub)]">
|
||||
No studies match{homeTags.length ? ` ${homeTags.map((t) => `“${t}”`).join(' + ')}` : ''}{homeSearch.trim() ? ` “${homeSearch.trim()}”` : ''}.{' '}
|
||||
<button type="button" onClick={() => { setHomeTags([]); setHomeSearch(''); }} className="text-[var(--r-accent)] hover:underline">Clear filters</button>
|
||||
@@ -273,67 +344,31 @@ export default function HomePage() {
|
||||
</h3>
|
||||
)}
|
||||
<div className={homeView === 'list' ? 'flex flex-col gap-2' : 'grid gap-3 sm:grid-cols-2'}>
|
||||
{section.items.map((entry) => (
|
||||
<div key={entry.id} className={homeView === 'list'
|
||||
? 'flex flex-col gap-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-3 sm:flex-row sm:items-center sm:gap-4'
|
||||
: 'flex flex-col gap-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5'}>
|
||||
{homeView === 'list'
|
||||
? <BookCover summary={entry.chapterSummary} compact className="h-14 w-full sm:w-28 sm:shrink-0" />
|
||||
: <BookCover summary={entry.chapterSummary} className="h-24" />}
|
||||
<div className="min-w-0 flex-1">
|
||||
{renamingId === entry.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="text" autoFocus value={renameValue} onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); const t = renameValue.trim(); if (t) renameProjectInStorage(entry.id, t); setRenamingId(null); }
|
||||
else if (e.key === 'Escape') setRenamingId(null);
|
||||
}}
|
||||
className={`${fieldCls} flex-1`} />
|
||||
<button type="button" onClick={() => { const t = renameValue.trim(); if (t) renameProjectInStorage(entry.id, t); setRenamingId(null); }} className="text-sm text-[var(--r-accent)] hover:underline">Save</button>
|
||||
<button type="button" onClick={() => setRenamingId(null)} className="text-sm text-[var(--r-sub)]">×</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex items-start gap-2 ${homeView === 'list' ? 'flex-wrap items-baseline' : 'justify-between'}`}>
|
||||
<h2 className="text-base font-medium">{entry.title}</h2>
|
||||
{sharedStudies?.[entry.id] !== undefined && (
|
||||
<span className="rounded-full border border-[var(--r-line)] px-2 py-0.5 text-[11px] text-[var(--r-sub)]" title="Another account invited you to edit this study">Shared{sharedStudies[entry.id] ? ` by ${sharedStudies[entry.id]}` : ''}</span>
|
||||
)}
|
||||
<button type="button" onClick={() => { setRenamingId(entry.id); setRenameValue(entry.title ?? ''); }} className="shrink-0 text-xs text-[var(--r-sub)] hover:text-[var(--r-text)]" title="Rename project">✎ Rename</button>
|
||||
<button type="button" onClick={() => { setSeriesEditingId(entry.id); setSeriesValue(entry.series ?? ''); }} className="shrink-0 text-xs text-[var(--r-sub)] hover:text-[var(--r-text)]" title="Put this study in a series">{entry.series ? '⧉ Series' : '⧉ Add to series'}</button>
|
||||
</div>
|
||||
)}
|
||||
{seriesEditingId === entry.id && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input type="text" autoFocus list="series-names" aria-label="Series name" placeholder="Series name (blank to remove)" value={seriesValue} onChange={(e) => setSeriesValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); setProjectSeries(entry.id, seriesValue); setSeriesEditingId(null); } else if (e.key === 'Escape') setSeriesEditingId(null); }}
|
||||
className={`${fieldCls} flex-1`} />
|
||||
<button type="button" onClick={() => { setProjectSeries(entry.id, seriesValue); setSeriesEditingId(null); }} className="text-sm text-[var(--r-accent)] hover:underline">Save</button>
|
||||
<button type="button" onClick={() => setSeriesEditingId(null)} aria-label="Cancel" className="text-sm text-[var(--r-sub)]">×</button>
|
||||
</div>
|
||||
)}
|
||||
{entry.chapterSummary && <p className="mt-1 text-sm text-[var(--r-sub)]" title={entry.chapterSummary}>{compactPassages(entry.chapterSummary)}</p>}
|
||||
{entry.tags?.length > 0 && (
|
||||
// A few tags and a "+N" for the rest (hover for all; the Tag filter above finds any of them)
|
||||
<div className="mt-2 flex flex-wrap gap-1" title={entry.tags.length > 3 ? entry.tags.join(', ') : undefined}>
|
||||
{entry.tags.slice(0, 3).map((tag) => (
|
||||
<button key={tag} type="button" onClick={() => toggleTag(tag)} aria-pressed={homeTags.includes(tag)} title={`Show studies tagged “${tag}”`}
|
||||
className={`rounded-full border px-2 py-0.5 text-[11px] ${homeTags.includes(tag) ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>{tag}</button>
|
||||
))}
|
||||
{entry.tags.length > 3 && <span className="rounded-full px-1.5 py-0.5 text-[11px] text-[var(--r-sub)]">+{entry.tags.length - 3} more</span>}
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-[var(--r-sub)]">{formatRelativeDate(entry.lastEdited)}</p>
|
||||
<OfflineBadge project={studyBooks[entry.id]} offline={offline} />
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 ${homeView === 'list' ? 'sm:w-56 sm:shrink-0' : ''}`}>
|
||||
<button type="button" onClick={() => resumeProject(entry.id)} className="flex-1 rounded-xl bg-[var(--r-text)] px-4 py-2 text-sm font-medium text-[var(--r-bg)] hover:opacity-90">Resume</button>
|
||||
<button type="button" onClick={() => deleteProject(entry.id)} className="rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm text-[var(--r-danger,#b42318)] hover:bg-black/5">{sharedStudies?.[entry.id] !== undefined ? 'Leave' : 'Delete'}</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{section.items.map(renderEntry)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{visible.length === 0 && archivedStudies.length > 0 && !filtering && (
|
||||
<p role="status" className="mb-4 rounded-xl border border-dashed border-[var(--r-line)] px-4 py-6 text-center text-sm text-[var(--r-sub)]">Everything is finished or archived. Open the section below to bring a study back.</p>
|
||||
)}
|
||||
|
||||
{archivedCount > 0 && (
|
||||
<div className="mt-6 border-t border-[var(--r-line)] pt-4">
|
||||
<button type="button" onClick={() => setShowArchived((o) => !o)} aria-expanded={archivedOpen} aria-controls="archived-studies"
|
||||
className="flex w-full items-center gap-2 text-left text-sm font-medium">
|
||||
<span aria-hidden="true" className="text-xs text-[var(--r-sub)]">{archivedOpen ? '▾' : '▸'}</span>
|
||||
Finished & archived <span className="text-xs font-normal text-[var(--r-sub)]">{filtering && archivedStudies.length !== archivedCount ? `${archivedStudies.length} of ${archivedCount}` : archivedCount}</span>
|
||||
</button>
|
||||
{archivedOpen && (
|
||||
<div id="archived-studies" className={`mt-3 ${homeView === 'list' ? 'flex flex-col gap-2' : 'grid gap-3 sm:grid-cols-2'}`}>
|
||||
{archivedStudies.length === 0
|
||||
? <p className="text-sm text-[var(--r-sub)]">No archived studies match.</p>
|
||||
: archivedStudies.map(renderEntry)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -94,6 +94,7 @@ export function saveProjectToStorage(project) {
|
||||
lastEdited: updated.lastEdited,
|
||||
chapterSummary: buildChapterSummary(updated),
|
||||
series: (updated.series ?? '').trim(),
|
||||
archived: !!updated.archived,
|
||||
tags,
|
||||
};
|
||||
if (existing >= 0) {
|
||||
|
||||
@@ -30,11 +30,13 @@ export function groupBySeries(entries) {
|
||||
return groups.has('') ? [...named, { name: '', items: groups.get('') }] : named;
|
||||
}
|
||||
|
||||
export function filterStudies(index, { query = '', tags = [], series = '' } = {}) {
|
||||
// archived: false (default) leaves out finished/archived studies, true keeps only them, null keeps all.
|
||||
export function filterStudies(index, { query = '', tags = [], series = '', archived = false } = {}) {
|
||||
const raw = String(query).trim().toLowerCase();
|
||||
const tagOnly = raw.startsWith('#');
|
||||
const q = tagOnly ? raw.slice(1).trim() : raw;
|
||||
return index.filter((entry) => {
|
||||
if (archived !== null && !!entry.archived !== archived) return false;
|
||||
if (series === '__none') { if ((entry.series ?? '').trim()) return false; } else if (series && (entry.series ?? '').trim() !== series) return false;
|
||||
const own = (entry.tags ?? []).map((t) => t.toLowerCase());
|
||||
if (tags.length && !tags.every((t) => own.includes(t.toLowerCase()))) return false;
|
||||
|
||||
@@ -52,3 +52,11 @@ test('a series can be chosen as a filter, and the search finds studies by series
|
||||
expect(filterStudies(idx, { series: '__none' }).map((e) => e.id)).toEqual(['b']);
|
||||
expect(filterStudies(idx, { query: 'advent' }).map((e) => e.id)).toEqual(['c']);
|
||||
});
|
||||
|
||||
test('archived studies are left out unless asked for', () => {
|
||||
const idx = [{ id: 'a', title: 'Live' }, { id: 'b', title: 'Done', archived: true }, { id: 'c', title: 'Done too', archived: true, tags: ['hope'] }];
|
||||
expect(filterStudies(idx).map((e) => e.id)).toEqual(['a']);
|
||||
expect(filterStudies(idx, { archived: true }).map((e) => e.id)).toEqual(['b', 'c']);
|
||||
expect(filterStudies(idx, { archived: true, tags: ['hope'] }).map((e) => e.id)).toEqual(['c']);
|
||||
expect(filterStudies(idx, { archived: null }).map((e) => e.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
Reference in new issue
Block a user