fix(frontend): load full tag/category lists in pickers, honoring sort
deploy / deploy (push) Successful in 19s
deploy / deploy (push) Successful in 19s
The tag pickers, filter bar and batch editor loaded only /tags?limit=200 and filtered client-side, so with more than 200 tags the rest were invisible and unsearchable. Same for the category dropdowns on the tag forms. Add fetchAllTags / fetchAllCategories helpers that page past the server's per-request cap of 200, and order results by the sort the user chose on the tags / categories page (tagSorting / categorySorting) instead of a hardcoded name-asc. Wire them into FilterBar, TagPicker, TagRuleEditor, BulkTagEditor and the tag new/edit category dropdowns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Category, CategoryOffsetPage } from '$lib/api/types';
|
||||
import { categorySorting } from '$lib/stores/sorting';
|
||||
|
||||
// The /categories endpoint caps limit at 200 per request. Category dropdowns
|
||||
// show the whole list, so page through to get them all — otherwise categories
|
||||
// past the first 200 are missing from the picker.
|
||||
const PAGE = 200;
|
||||
|
||||
/**
|
||||
* Fetches every category, paging past the server's per-request cap. Ordered by
|
||||
* the sort the user picked on the categories page (categorySorting).
|
||||
*/
|
||||
export async function fetchAllCategories(): Promise<Category[]> {
|
||||
const { sort, order } = get(categorySorting);
|
||||
const all: Category[] = [];
|
||||
for (let offset = 0; ; offset += PAGE) {
|
||||
const page = await api.get<CategoryOffsetPage>(
|
||||
`/categories?limit=${PAGE}&offset=${offset}&sort=${sort}&order=${order}`
|
||||
);
|
||||
const items = page.items ?? [];
|
||||
all.push(...items);
|
||||
const total = page.total ?? all.length;
|
||||
if (items.length < PAGE || all.length >= total) break;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Tag, TagOffsetPage } from '$lib/api/types';
|
||||
import { tagSorting } from '$lib/stores/sorting';
|
||||
|
||||
// The /tags endpoint caps limit at 200 per request. Pickers and the filter bar
|
||||
// filter the tag list client-side, so they need the *whole* list — otherwise
|
||||
// tags past the first 200 are invisible and unsearchable. Page through until we
|
||||
// have them all.
|
||||
const PAGE = 200;
|
||||
|
||||
/**
|
||||
* Fetches every tag, paging past the server's per-request cap. Ordered by the
|
||||
* sort the user picked on the tags page (tagSorting), so the pickers and filter
|
||||
* bar show tags in the same order as that page.
|
||||
*/
|
||||
export async function fetchAllTags(): Promise<Tag[]> {
|
||||
const { sort, order } = get(tagSorting);
|
||||
const all: Tag[] = [];
|
||||
for (let offset = 0; ; offset += PAGE) {
|
||||
const page = await api.get<TagOffsetPage>(
|
||||
`/tags?limit=${PAGE}&offset=${offset}&sort=${sort}&order=${order}`
|
||||
);
|
||||
const items = page.items ?? [];
|
||||
all.push(...items);
|
||||
const total = page.total ?? all.length;
|
||||
if (items.length < PAGE || all.length >= total) break;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Tag, TagOffsetPage } from '$lib/api/types';
|
||||
import type { Tag } from '$lib/api/types';
|
||||
import { fetchAllTags } from '$lib/api/tags';
|
||||
|
||||
interface Props {
|
||||
fileIds: string[];
|
||||
@@ -30,13 +31,13 @@
|
||||
error = '';
|
||||
try {
|
||||
const [tagsRes, commonRes] = await Promise.all([
|
||||
api.get<TagOffsetPage>('/tags?limit=200&sort=name&order=asc'),
|
||||
fetchAllTags(),
|
||||
api.post<{ common_tag_ids: string[]; partial_tag_ids: string[] }>(
|
||||
'/files/bulk/common-tags',
|
||||
{ file_ids: fileIds }
|
||||
)
|
||||
]);
|
||||
allTags = tagsRes.items ?? [];
|
||||
allTags = tagsRes;
|
||||
commonIds = new Set(commonRes.common_tag_ids ?? []);
|
||||
partialIds = new Set(commonRes.partial_tag_ids ?? []);
|
||||
} catch {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Tag, TagOffsetPage } from '$lib/api/types';
|
||||
import type { Tag } from '$lib/api/types';
|
||||
import { fetchAllTags } from '$lib/api/tags';
|
||||
import { buildDslFilter, parseDslFilter, tokenLabel } from '$lib/utils/dsl';
|
||||
|
||||
interface Props {
|
||||
@@ -26,8 +26,8 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
api.get<TagOffsetPage>('/tags?limit=200&sort=name&order=asc').then((page) => {
|
||||
tags = page.items ?? [];
|
||||
fetchAllTags().then((all) => {
|
||||
tags = all;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Tag, TagOffsetPage } from '$lib/api/types';
|
||||
import type { Tag } from '$lib/api/types';
|
||||
import { fetchAllTags } from '$lib/api/tags';
|
||||
|
||||
interface Props {
|
||||
fileTags: Tag[];
|
||||
@@ -15,8 +15,8 @@
|
||||
let busy = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
api.get<TagOffsetPage>('/tags?limit=200&sort=name&order=asc').then((p) => {
|
||||
allTags = p.items ?? [];
|
||||
fetchAllTags().then((all) => {
|
||||
allTags = all;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { api, ApiError } from '$lib/api/client';
|
||||
import type { Tag, TagOffsetPage, TagRule } from '$lib/api/types';
|
||||
import type { Tag, TagRule } from '$lib/api/types';
|
||||
import { fetchAllTags } from '$lib/api/tags';
|
||||
import TagBadge from './TagBadge.svelte';
|
||||
import { appSettings } from '$lib/stores/appSettings';
|
||||
|
||||
@@ -18,8 +19,8 @@
|
||||
let error = $state('');
|
||||
|
||||
$effect(() => {
|
||||
api.get<TagOffsetPage>('/tags?limit=200&sort=name&order=asc').then((p) => {
|
||||
allTags = p.items ?? [];
|
||||
fetchAllTags().then((all) => {
|
||||
allTags = all;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { api, ApiError } from '$lib/api/client';
|
||||
import type { Category, CategoryOffsetPage, Tag, TagRule } from '$lib/api/types';
|
||||
import type { Category, Tag, TagRule } from '$lib/api/types';
|
||||
import { fetchAllCategories } from '$lib/api/categories';
|
||||
import TagRuleEditor from '$lib/components/tag/TagRuleEditor.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
@@ -32,12 +33,12 @@
|
||||
loadError = '';
|
||||
void Promise.all([
|
||||
api.get<Tag>(`/tags/${id}`),
|
||||
api.get<CategoryOffsetPage>('/categories?limit=200&sort=name&order=asc'),
|
||||
fetchAllCategories(),
|
||||
api.get<TagRule[]>(`/tags/${id}/rules`).catch(() => [] as TagRule[])
|
||||
])
|
||||
.then(([t, cats, r]) => {
|
||||
tag = t;
|
||||
categories = cats.items ?? [];
|
||||
categories = cats;
|
||||
rules = r;
|
||||
|
||||
name = t.name ?? '';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { api, ApiError } from '$lib/api/client';
|
||||
import type { Category, CategoryOffsetPage } from '$lib/api/types';
|
||||
import type { Category } from '$lib/api/types';
|
||||
import { fetchAllCategories } from '$lib/api/categories';
|
||||
|
||||
let name = $state('');
|
||||
let notes = $state('');
|
||||
@@ -13,8 +14,8 @@
|
||||
let categories = $state<Category[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
api.get<CategoryOffsetPage>('/categories?limit=200&sort=name&order=asc').then((p) => {
|
||||
categories = p.items ?? [];
|
||||
fetchAllCategories().then((all) => {
|
||||
categories = all;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user