From 17700db5985521787a139fb25db369b39708dac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Marseault?= <22910497+Larsluph@users.noreply.github.com> Date: Sun, 28 Jul 2024 08:11:51 -0500 Subject: [PATCH] fix: Sort values correctly (#1811) --- package-lock.json | 8 ++++++ package.json | 1 + src/components/Dashboard/RightClick.vue | 4 +-- .../Dialogs/AddTorrentParamsForm.vue | 2 +- .../Navbar/SideWidgets/FilterSelect.vue | 9 +++---- src/components/Settings/TagsAndCategories.vue | 4 +-- .../TorrentDetail/TagsAndCategories.vue | 2 +- src/helpers/datetime.spec.ts | 11 +++++++- src/pages/SearchEngine.vue | 6 ++--- src/services/qbit/MockProvider.ts | 10 +++---- src/stores/categories.ts | 26 ++++++++++++------- src/stores/tags.ts | 19 +++++++++----- src/stores/trackers.ts | 19 ++++++++------ 13 files changed, 76 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index f49c096c..d8e0b731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "jsdom": "^24.1.1", "prettier": "^3.3.3", "sass": "^1.77.8", + "timezone-mock": "^1.3.6", "typescript": "^5.5.4", "vite": "^5.3.4", "vite-plugin-top-level-await": "^1.4.2", @@ -4677,6 +4678,13 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/timezone-mock": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.3.6.tgz", + "integrity": "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", diff --git a/package.json b/package.json index 3e15eb38..91cb6d9b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "jsdom": "^24.1.1", "prettier": "^3.3.3", "sass": "^1.77.8", + "timezone-mock": "^1.3.6", "typescript": "^5.5.4", "vite": "^5.3.4", "vite-plugin-top-level-await": "^1.4.2", diff --git a/src/components/Dashboard/RightClick.vue b/src/components/Dashboard/RightClick.vue index 9f2da2a4..f52b6f34 100644 --- a/src/components/Dashboard/RightClick.vue +++ b/src/components/Dashboard/RightClick.vue @@ -32,7 +32,7 @@ const hashes = computed(() => dashboardStore.selectedTorrents) const hash = computed(() => hashes.value[0]) const torrent = computed(() => torrentStore.getTorrentByHash(hash.value)) const torrents = computed(() => dashboardStore.selectedTorrents.map(torrentStore.getTorrentByHash).filter(torrent => !!torrent)) -const availableCategories = computed(() => ['', ...categoryStore.categories.keys()]) +const availableCategories = computed(() => ['', ...categoryStore.categories.map(cat => cat.name)]) async function resumeTorrents() { await torrentStore.resumeTorrents(hashes) @@ -243,7 +243,7 @@ const menuData = computed(() => [ { text: t('dashboard.right_click.category.title'), icon: 'mdi-label', - disabled: categoryStore.categories.size === 0, + disabled: categoryStore.categories.length === 0, disabledText: t('dashboard.right_click.category.disabled_title'), disabledIcon: 'mdi-label-off', children: availableCategories.value.map(category => ({ diff --git a/src/components/Dialogs/AddTorrentParamsForm.vue b/src/components/Dialogs/AddTorrentParamsForm.vue index b3989a47..0446a70c 100644 --- a/src/components/Dialogs/AddTorrentParamsForm.vue +++ b/src/components/Dialogs/AddTorrentParamsForm.vue @@ -33,7 +33,7 @@ const savePathField = ref() const tagSearch = ref('') const categorySearch = ref('') -const categoryNames = computed(() => Array.from(categoryStore.categories.keys())) +const categoryNames = computed(() => categoryStore.categories.map(cat => cat.name)) const category = computed({ get: () => form.value.category || categorySearch.value || undefined, set: value => (form.value.category = value || undefined) diff --git a/src/components/Navbar/SideWidgets/FilterSelect.vue b/src/components/Navbar/SideWidgets/FilterSelect.vue index f9087adf..c22c1b84 100644 --- a/src/components/Navbar/SideWidgets/FilterSelect.vue +++ b/src/components/Navbar/SideWidgets/FilterSelect.vue @@ -17,12 +17,9 @@ const statuses = computed(() => .filter(state => typeof state === 'number') .map(state => ({ title: t(`torrent.state.${getTorrentStateValue(state as TorrentState)}`), value: state })) ) -const categories = computed(() => [{ title: t('navbar.side.filters.uncategorized'), value: '' }, ...Array.from(categoryStore.categories.keys()).map(c => ({ title: c, value: c }))]) -const tags = computed(() => [{ title: t('navbar.side.filters.untagged'), value: null }, ...tagStore.tags.map(tag => ({ title: tag, value: tag }))]) -const trackers = computed(() => [ - { title: t('navbar.side.filters.untracked'), value: null }, - ...Array.from(trackerStore.trackers.keys()).map(tracker => ({ title: tracker, value: tracker })) -]) +const categories = computed(() => [{ title: t('navbar.side.filters.uncategorized'), value: '' }, ...categoryStore.categories.map(c => c.name)]) +const tags = computed(() => [{ title: t('navbar.side.filters.untagged'), value: null }, ...tagStore.tags]) +const trackers = computed(() => [{ title: t('navbar.side.filters.untracked'), value: null }, ...trackerStore.trackers]) function selectAllStatuses() { statusFilter.value = [] diff --git a/src/components/Settings/TagsAndCategories.vue b/src/components/Settings/TagsAndCategories.vue index ab4d6868..94927564 100644 --- a/src/components/Settings/TagsAndCategories.vue +++ b/src/components/Settings/TagsAndCategories.vue @@ -56,7 +56,7 @@ function openCategoryFormDialog(initialCategory?: Category) { {{ $t('settings.tagsAndCategories.categoriesSubheader') }} - +
{{ category.name }}
@@ -65,7 +65,7 @@ function openCategoryFormDialog(initialCategory?: Category) {
- + {{ $t('settings.tagsAndCategories.noCategories') }} diff --git a/src/components/TorrentDetail/TagsAndCategories.vue b/src/components/TorrentDetail/TagsAndCategories.vue index 10384a6f..2eb37825 100644 --- a/src/components/TorrentDetail/TagsAndCategories.vue +++ b/src/components/TorrentDetail/TagsAndCategories.vue @@ -42,7 +42,7 @@ async function toggleTag(tag: string) { {{ $t('torrentDetail.tagsAndCategories.categories') }} { + timezoneMock.register('UTC') +}) + +afterAll(() => { + timezoneMock.unregister() +}) test('helpers/datetime/formatEta', () => { // seconds diff --git a/src/pages/SearchEngine.vue b/src/pages/SearchEngine.vue index 7bae812c..b28ea88b 100644 --- a/src/pages/SearchEngine.vue +++ b/src/pages/SearchEngine.vue @@ -33,7 +33,7 @@ const headers = [ { title: t('searchEngine.headers.siteUrl'), key: 'siteUrl' }, { title: '', key: 'actions', sortable: false } ] -const cats = [ +const categories = [ { title: t('searchEngine.filters.category.movies'), value: 'movies' }, { title: t('searchEngine.filters.category.tv'), value: 'tv' }, { title: t('searchEngine.filters.category.music'), value: 'music' }, @@ -43,8 +43,8 @@ const cats = [ { title: t('searchEngine.filters.category.pictures'), value: 'pictures' }, { title: t('searchEngine.filters.category.books'), value: 'books' } ] -cats.sort((a, b) => a.title.localeCompare(b.title)) -const categories = [{ title: t('searchEngine.filters.category.all'), value: 'all' }, ...cats] + .sort((a, b) => a.title.localeCompare(b.title)) + .splice(0, 0, { title: t('searchEngine.filters.category.all'), value: 'all' }) const plugins = computed(() => { const plugins = [ diff --git a/src/services/qbit/MockProvider.ts b/src/services/qbit/MockProvider.ts index 652cfedf..1559691f 100644 --- a/src/services/qbit/MockProvider.ts +++ b/src/services/qbit/MockProvider.ts @@ -26,13 +26,13 @@ import IProvider from './IProvider' export default class MockProvider implements IProvider { private static instance: MockProvider private readonly categories: Record = { - ISO: { name: 'ISO', savePath: faker.system.directoryPath() }, - Other: { name: 'Other', savePath: faker.system.directoryPath() }, Movie: { name: 'Movie', savePath: faker.system.directoryPath() }, - Music: { name: 'Music', savePath: faker.system.directoryPath() }, - TV: { name: 'TV', savePath: faker.system.directoryPath() } + TV: { name: 'TV', savePath: faker.system.directoryPath() }, + Other: { name: 'Other', savePath: faker.system.directoryPath() }, + ISO: { name: 'ISO', savePath: faker.system.directoryPath() }, + Music: { name: 'Music', savePath: faker.system.directoryPath() } } - private readonly tags: string[] = ['sorted', 'pending_sort'] + private readonly tags: string[] = ['pending', 'sorted', 'pending_sort'] private readonly trackers: Record = faker.helpers .multiple(() => faker.internet.url(), { count: 5 }) .reduce( diff --git a/src/stores/categories.ts b/src/stores/categories.ts index e7ef9941..c724e073 100644 --- a/src/stores/categories.ts +++ b/src/stores/categories.ts @@ -1,40 +1,46 @@ +import { comparators } from '@/helpers' import qbit from '@/services/qbit' import { Category } from '@/types/qbit/models' +import { useSorted } from '@vueuse/core' import { acceptHMRUpdate, defineStore } from 'pinia' import { shallowRef, triggerRef } from 'vue' export const useCategoryStore = defineStore('categories', () => { /** Key: Category name */ - const categories = shallowRef>(new Map()) + const _categoryMap = shallowRef>(new Map()) + const categories = useSorted( + () => Array.from(_categoryMap.value.values()), + (a, b) => comparators.text.asc(a.name, b.name) + ) function syncFromMaindata(fullUpdate: boolean, entries: [string, Partial][], removed?: string[]) { if (fullUpdate) { - categories.value = new Map(entries as [string, Category][]) + _categoryMap.value = new Map(entries as [string, Category][]) return } for (const [catName, qbitCat] of entries) { - const oldCat = categories.value.get(catName) + const oldCat = _categoryMap.value.get(catName) if (oldCat) { const newCat = { name: qbitCat.name ?? oldCat.name, savePath: qbitCat.savePath ?? oldCat.savePath } - categories.value.set(catName, newCat) + _categoryMap.value.set(catName, newCat) } else { - categories.value.set(catName, { + _categoryMap.value.set(catName, { name: qbitCat.name ?? catName, savePath: qbitCat.savePath ?? '' }) } } - removed?.forEach(c => categories.value.delete(c)) - triggerRef(categories) + removed?.forEach(c => _categoryMap.value.delete(c)) + triggerRef(_categoryMap) } function getCategoryFromName(categoryName?: string) { if (!categoryName) return - return categories.value.get(categoryName) + return _categoryMap.value.get(categoryName) } async function createCategory(category: Category) { @@ -78,8 +84,8 @@ export const useCategoryStore = defineStore('categories', () => { editCategory, deleteCategories, $reset: () => { - categories.value.clear() - triggerRef(categories) + _categoryMap.value.clear() + triggerRef(_categoryMap) } } }) diff --git a/src/stores/tags.ts b/src/stores/tags.ts index ba67f255..5d8f1208 100644 --- a/src/stores/tags.ts +++ b/src/stores/tags.ts @@ -1,21 +1,27 @@ +import { comparators } from '@/helpers' import qbit from '@/services/qbit' +import { useSorted } from '@vueuse/core' import { acceptHMRUpdate, defineStore } from 'pinia' -import { shallowRef } from 'vue' +import { shallowRef, triggerRef } from 'vue' export const useTagStore = defineStore('tags', () => { - const tags = shallowRef([]) + const _tags = shallowRef>(new Set()) + const tags = useSorted( + () => Array.from(_tags.value.values()), + comparators.text.asc + ) function syncFromMaindata(fullUpdate: boolean, values: string[], removed?: string[]) { if (fullUpdate) { - tags.value = values + _tags.value = new Set(values) return } if (values) { - tags.value = [...tags.value, ...values] + _tags.value = _tags.value.union(new Set(values)) } - tags.value = tags.value.filter(tag => !removed || !removed.includes(tag)) + _tags.value = _tags.value.difference(new Set(removed)) } async function createTags(tags: string[]) { @@ -52,7 +58,8 @@ export const useTagStore = defineStore('tags', () => { editTag, deleteTags, $reset: () => { - tags.value = [] + _tags.value.clear() + triggerRef(_tags) } } }) diff --git a/src/stores/trackers.ts b/src/stores/trackers.ts index 8356bcc3..fc29b77d 100644 --- a/src/stores/trackers.ts +++ b/src/stores/trackers.ts @@ -1,13 +1,16 @@ +import { comparators } from '@/helpers' import qbit from '@/services/qbit' +import { useSorted } from '@vueuse/core' import { acceptHMRUpdate, defineStore } from 'pinia' import { computed, shallowRef, triggerRef } from 'vue' export const useTrackerStore = defineStore('trackers', () => { /** Key: tracker domain, values: torrent hashes */ - const trackers = shallowRef>(new Map()) + const _trackerMap = shallowRef>(new Map()) + const trackers = useSorted(() => Array.from(_trackerMap.value.keys()), comparators.text.asc) /** Key: torrent hash, values: tracker domains */ const torrentTrackers = computed(() => - Array.from(trackers.value.entries()).reduce((tot, val) => { + Array.from(_trackerMap.value.entries()).reduce((tot, val) => { const [domain, hashes] = val hashes.forEach(hash => { const domains = tot.get(hash) @@ -23,16 +26,16 @@ export const useTrackerStore = defineStore('trackers', () => { function syncFromMaindata(fullUpdate: boolean, entries: [string, string[]][], removed?: string[]) { if (fullUpdate) { - trackers.value = new Map(entries) + _trackerMap.value = new Map(entries) return } for (const [trackerUrl, linkedTorrents] of entries) { - trackers.value.set(trackerUrl, linkedTorrents) + _trackerMap.value.set(trackerUrl, linkedTorrents) } - removed?.forEach(t => trackers.value.delete(t)) - triggerRef(trackers) + removed?.forEach(t => _trackerMap.value.delete(t)) + triggerRef(_trackerMap) } async function getTorrentTrackers(hash: string) { @@ -60,8 +63,8 @@ export const useTrackerStore = defineStore('trackers', () => { editTorrentTracker, removeTorrentTrackers, $reset: () => { - trackers.value.clear() - triggerRef(trackers) + _trackerMap.value.clear() + triggerRef(_trackerMap) } } })