From 35f38d7ceeed11f41c796a3bc591a05845b1f959 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 22 Apr 2026 19:38:35 +0300 Subject: [PATCH] refactor: update presets UI to follow same rules as other pages. Co-authored-by: Copilot --- API.md | 4 +- app/features/presets/repository.py | 19 ++-- app/features/presets/router.py | 5 +- .../presets/tests/test_presets_repository.py | 26 ++++++ ui/app/components/PresetForm.vue | 3 +- ui/app/composables/usePresets.ts | 5 ++ ui/app/pages/presets.vue | 86 +++++++++++++++---- ui/tests/composables/usePresets.test.ts | 36 ++++++-- 8 files changed, 147 insertions(+), 37 deletions(-) diff --git a/API.md b/API.md index 8b625811..907dd2d3 100644 --- a/API.md +++ b/API.md @@ -1777,6 +1777,7 @@ Binary image data with appropriate headers - `per_page` (optional): Items per page. Default: `config.default_pagination`. - `sort` (optional): Comma-separated sort fields. Accepted values: `id`, `name`, `priority`, `default`, `created_at`, `updated_at`. Default: `priority,name`. - `order` (optional): Comma-separated sort directions matching `sort`, or a single direction applied to every requested sort field. Accepted values: `asc`, `desc`. Default: `desc,asc`. +- `exclude_defaults` (optional): When `true`, excludes system presets from the results. Default: `false`. **Response**: ```json @@ -1806,10 +1807,9 @@ Binary image data with appropriate headers **Notes**: - `default: true` indicates this is a system default preset (cannot be modified or deleted) -- Default ordering remains `priority desc, name asc` **Error Responses**: -- `400 Bad Request` - Invalid pagination or sorting query parameters +- `400 Bad Request` - Invalid data was provided. --- diff --git a/app/features/presets/repository.py b/app/features/presets/repository.py index 3f867677..94567d03 100644 --- a/app/features/presets/repository.py +++ b/app/features/presets/repository.py @@ -180,25 +180,32 @@ class PresetsRepository(metaclass=Singleton): per_page: int, sort: str | None = None, order: str | None = None, + exclude_defaults: bool = False, ) -> tuple[list[PresetModel], int, int, int]: order_by = self._build_order_by(sort, order) async with self.session() as session: - total: int = await self.count() + total: int = await self.count(exclude_defaults=exclude_defaults) total_pages: int = (total + per_page - 1) // per_page if total > 0 else 1 if page > total_pages and total > 0: page = total_pages - query: Select[tuple[PresetModel]] = ( - select(PresetModel).order_by(*order_by).limit(per_page).offset((page - 1) * per_page) - ) + query: Select[tuple[PresetModel]] = select(PresetModel) + if exclude_defaults: + query = query.where(PresetModel.default.is_(False)) + + query = query.order_by(*order_by).limit(per_page).offset((page - 1) * per_page) result: Result[tuple[PresetModel]] = await session.execute(query) return list(result.scalars().all()), total, page, total_pages - async def count(self) -> int: + async def count(self, exclude_defaults: bool = False) -> int: async with self.session() as session: - result: Result[tuple[int]] = await session.execute(select(func.count()).select_from(PresetModel)) + query = select(func.count()).select_from(PresetModel) + if exclude_defaults: + query = query.where(PresetModel.default.is_(False)) + + result: Result[tuple[int]] = await session.execute(query) return int(result.scalar_one()) async def get(self, identifier: int | str) -> PresetModel | None: diff --git a/app/features/presets/router.py b/app/features/presets/router.py index 28105e13..fd30521e 100644 --- a/app/features/presets/router.py +++ b/app/features/presets/router.py @@ -26,10 +26,11 @@ async def presets_list(request: Request, encoder: Encoder, repo: PresetsReposito try: page, per_page = normalize_pagination(request) items, total, current_page, total_pages = await repo.list_paginated( - page, - per_page, + page=page, + per_page=per_page, sort=request.query.get("sort"), order=request.query.get("order"), + exclude_defaults=bool(request.query.get("exclude_defaults", False)), ) except ValueError as exc: return web.json_response(data={"error": str(exc)}, status=web.HTTPBadRequest.status_code) diff --git a/app/features/presets/tests/test_presets_repository.py b/app/features/presets/tests/test_presets_repository.py index 11c83add..656ac446 100644 --- a/app/features/presets/tests/test_presets_repository.py +++ b/app/features/presets/tests/test_presets_repository.py @@ -88,6 +88,18 @@ class TestPresetsRepository: assert [item.name for item in items] == ["gamma", "beta", "alpha"], "Should sort by requested field" + @pytest.mark.asyncio + async def test_list_paginated_excludes_defaults(self, repo): + await repo.create({"name": "System Default", "default": True, "priority": 10}) + await repo.create({"name": "Custom Preset", "priority": 1}) + + items, total, page, total_pages = await repo.list_paginated(page=1, per_page=10, exclude_defaults=True) + + assert [item.name for item in items] == ["custom_preset"], "Should exclude default presets" + assert total == 1, "Should count only custom presets" + assert page == 1, "Should keep current page when filtered results exist" + assert total_pages == 1, "Should compute pages from the filtered total" + @pytest.mark.asyncio async def test_list_paginated_supports_multiple_sort_fields(self, repo): await repo.create({"name": "Charlie", "priority": 2}) @@ -153,3 +165,17 @@ class TestPresetRoutes: assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort direction" assert "order" in payload["error"], "Should explain invalid sort direction" + + async def test_list_route_supports_excluding_defaults(self, repo): + await repo.create({"name": "System Default", "default": True, "priority": 10}) + await repo.create({"name": "Custom Preset", "priority": 1}) + + request = MagicMock(spec=Request) + request.query = {"page": "1", "per_page": "10", "exclude_defaults": "true"} + + response = await presets_list(request, Encoder(), repo) + payload = json.loads(response.text) + + assert response.status == web.HTTPOk.status_code, "Should return 200 for valid default exclusion" + assert [item["name"] for item in payload["items"]] == ["custom_preset"], "Should exclude default presets" + assert payload["pagination"]["total"] == 1, "Should report filtered total" diff --git a/ui/app/components/PresetForm.vue b/ui/app/components/PresetForm.vue index cb5240d6..bb6f6aa4 100644 --- a/ui/app/components/PresetForm.vue +++ b/ui/app/components/PresetForm.vue @@ -367,13 +367,12 @@ const props = defineProps<{ reference?: number | null; preset: Partial; addInProgress?: boolean; - presets?: Preset[]; }>(); const config = useYtpConfig(); const toast = useNotification(); const dialog = useDialog(); -const { presets, findPreset, selectItems } = usePresetOptions(() => props.presets); +const { presets, findPreset, selectItems } = usePresetOptions(); const form = reactive({ name: '', diff --git a/ui/app/composables/usePresets.ts b/ui/app/composables/usePresets.ts index e9021129..50872fd4 100644 --- a/ui/app/composables/usePresets.ts +++ b/ui/app/composables/usePresets.ts @@ -129,6 +129,7 @@ const removePreset = (id: number) => { const loadPresets = async ( page: number = 1, perPage: number | undefined = undefined, + options: { excludeDefaults?: boolean } = {}, ): Promise => { isLoading.value = true; try { @@ -136,6 +137,10 @@ const loadPresets = async ( if (perPage !== undefined) { url += `&per_page=${perPage}`; } + if (options.excludeDefaults) { + url += '&exclude_defaults=true'; + } + const response = await request(url); await ensureSuccess(response); diff --git a/ui/app/pages/presets.vue b/ui/app/pages/presets.vue index 4f3f3082..16c784e8 100644 --- a/ui/app/pages/presets.vue +++ b/ui/app/pages/presets.vue @@ -23,7 +23,7 @@
Reload
+ +
+
+ +
+