fix: Sort values correctly (#1811)
Some checks failed
Build project and release / Build VueTorrent (vuetorrent-build, ./vuetorrent, build) (push) Has been cancelled
Build project and release / Run Release Please action (push) Has been cancelled
Build project and release / Build VueTorrent (vuetorrent-demo, ./vuetorrent-demo, build-demo) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Build project and release / Push to nightly branch (push) Has been cancelled
Build project and release / Push to demo repo (push) Has been cancelled
Build project and release / Upload release to GitHub (push) Has been cancelled
Build project and release / Push to latest branch (push) Has been cancelled
Build project and release / Push docker mod to GHCR (push) Has been cancelled

This commit is contained in:
Rémi Marseault 2024-07-28 08:11:51 -05:00 committed by GitHub
parent 2863b1dc85
commit 17700db598
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 76 additions and 45 deletions

8
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<RightClickMenuEntryType[]>(() => [
{
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 => ({

View file

@ -33,7 +33,7 @@ const savePathField = ref<typeof HistoryField>()
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<string | undefined>({
get: () => form.value.category || categorySearch.value || undefined,
set: value => (form.value.category = value || undefined)

View file

@ -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 = []

View file

@ -56,7 +56,7 @@ function openCategoryFormDialog(initialCategory?: Category) {
<v-col cols="12" sm="6">
<v-list-subheader class="ml-2">{{ $t('settings.tagsAndCategories.categoriesSubheader') }}</v-list-subheader>
<v-sheet rounded="xl" class="d-flex align-center gap" v-for="category in categoryStore.categories.values()">
<v-sheet rounded="xl" class="d-flex align-center gap" v-for="category in categoryStore.categories">
<div class="pl-4 py-1 wrap-anywhere">{{ category.name }}</div>
<v-spacer />
<div class="d-flex">
@ -65,7 +65,7 @@ function openCategoryFormDialog(initialCategory?: Category) {
</div>
</v-sheet>
<v-card v-if="categoryStore.categories.size === 0">
<v-card v-if="categoryStore.categories.length === 0">
<v-card-text>{{ $t('settings.tagsAndCategories.noCategories') }}</v-card-text>
</v-card>

View file

@ -42,7 +42,7 @@ async function toggleTag(tag: string) {
<v-list-subheader>{{ $t('torrentDetail.tagsAndCategories.categories') }}</v-list-subheader>
<v-list-item
v-for="category in categoryStore.categories.values()"
v-for="category in categoryStore.categories"
variant="text"
color="accent"
:title="category.name"

View file

@ -1,5 +1,14 @@
import { formatEta, formatTimeMs, formatTimeSec } from './datetime'
import timezoneMock from 'timezone-mock'
import { expect, test } from 'vitest'
import { formatEta, formatTimeMs, formatTimeSec } from './datetime'
beforeAll(() => {
timezoneMock.register('UTC')
})
afterAll(() => {
timezoneMock.unregister()
})
test('helpers/datetime/formatEta', () => {
// seconds

View file

@ -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 = [

View file

@ -26,13 +26,13 @@ import IProvider from './IProvider'
export default class MockProvider implements IProvider {
private static instance: MockProvider
private readonly categories: Record<string, Category> = {
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<string, string[]> = faker.helpers
.multiple(() => faker.internet.url(), { count: 5 })
.reduce(

View file

@ -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<Map<string, Category>>(new Map())
const _categoryMap = shallowRef<Map<string, Category>>(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<Category>][], 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)
}
}
})

View file

@ -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<string[]>([])
const _tags = shallowRef<Set<string>>(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)
}
}
})

View file

@ -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<Map<string, string[]>>(new Map())
const _trackerMap = shallowRef<Map<string, string[]>>(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)
}
}
})