style: add better default sorting to custom format conditions

This commit is contained in:
Sam Chau
2026-01-21 10:00:56 +10:30
parent 353fe3832f
commit ebced3e5b6
5 changed files with 62 additions and 53 deletions

View File

@@ -36,10 +36,8 @@ export async function list(cache: PCDCache): Promise<CustomFormatTableRow[]> {
// 3. Get all conditions for all custom formats
const allConditions = await db
.selectFrom('custom_format_conditions')
.select(['custom_format_name', 'name', 'required', 'negate'])
.select(['custom_format_name', 'name', 'type', 'required', 'negate'])
.where('custom_format_name', 'in', formatNames)
.orderBy('custom_format_name')
.orderBy('name')
.execute();
// 4. Get test counts for all custom formats
@@ -77,6 +75,7 @@ export async function list(cache: PCDCache): Promise<CustomFormatTableRow[]> {
}
conditionsMap.get(condition.custom_format_name)!.push({
name: condition.name,
type: condition.type,
required: condition.required === 1,
negate: condition.negate === 1
});

View File

@@ -7,6 +7,7 @@ import type { Tag } from '../../types.ts';
/** Condition reference for display */
export interface ConditionRef {
name: string;
type: string;
required: boolean;
negate: boolean;
}

View File

@@ -5,21 +5,55 @@
// Arr type for filtering
export type ArrType = 'all' | 'radarr' | 'sonarr';
// Condition type definitions
// Condition type definitions (ordered for display sorting)
export const CONDITION_TYPES = [
{ value: 'resolution', label: 'Resolution', arrType: 'all' as ArrType },
{ value: 'source', label: 'Source', arrType: 'all' as ArrType },
{ value: 'quality_modifier', label: 'Quality Modifier', arrType: 'radarr' as ArrType },
{ value: 'release_title', label: 'Release Title', arrType: 'all' as ArrType },
{ value: 'release_group', label: 'Release Group', arrType: 'all' as ArrType },
{ value: 'edition', label: 'Edition', arrType: 'radarr' as ArrType },
{ value: 'language', label: 'Language', arrType: 'all' as ArrType },
{ value: 'source', label: 'Source', arrType: 'all' as ArrType },
{ value: 'resolution', label: 'Resolution', arrType: 'all' as ArrType },
{ value: 'quality_modifier', label: 'Quality Modifier', arrType: 'radarr' as ArrType },
{ value: 'release_type', label: 'Release Type', arrType: 'sonarr' as ArrType },
{ value: 'indexer_flag', label: 'Indexer Flag', arrType: 'all' as ArrType },
{ value: 'size', label: 'Size', arrType: 'all' as ArrType },
{ value: 'year', label: 'Year', arrType: 'all' as ArrType }
] as const;
// Type order index for sorting
const TYPE_ORDER: Map<string, number> = new Map(CONDITION_TYPES.map((t, i) => [t.value, i]));
/**
* Get the status priority for sorting: required=0, negated=1, optional=2
*/
function getStatusPriority(required: boolean, negate: boolean): number {
if (required && !negate) return 0; // Required
if (negate) return 1; // Negated (includes required+negate)
return 2; // Optional
}
/**
* Sort conditions by: required/negated/optional, then type order, then alphabetical
*/
export function sortConditions<T extends { required: boolean; negate: boolean; type: string; name: string }>(
conditions: T[]
): T[] {
return [...conditions].sort((a, b) => {
// Primary: status (required -> negated -> optional)
const statusA = getStatusPriority(a.required, a.negate);
const statusB = getStatusPriority(b.required, b.negate);
if (statusA !== statusB) return statusA - statusB;
// Secondary: type order
const typeA = TYPE_ORDER.get(a.type) ?? 999;
const typeB = TYPE_ORDER.get(b.type) ?? 999;
if (typeA !== typeB) return typeA - typeB;
// Tertiary: alphabetical by name
return a.name.localeCompare(b.name);
});
}
// Pattern-based types (use regex patterns as values)
export const PATTERN_TYPES = ['release_title', 'release_group', 'edition'] as const;

View File

@@ -9,7 +9,7 @@
import StickyCard from '$ui/card/StickyCard.svelte';
import SaveTargetModal from '$ui/modal/SaveTargetModal.svelte';
import { alertStore } from '$alerts/store';
import { CONDITION_TYPES } from '$lib/shared/conditionTypes';
import { sortConditions } from '$lib/shared/conditionTypes';
import { current, isDirty, initEdit, update } from '$lib/client/stores/dirty';
import type { PageData } from './$types';
import type { ConditionData } from '$pcd/queries/customFormats/index';
@@ -104,23 +104,8 @@
}
}
// Group conditions by type
$: groupedConditions = conditions.reduce(
(acc, condition) => {
const type = condition.type;
if (!acc[type]) acc[type] = [];
acc[type].push(condition);
return acc;
},
{} as Record<string, KeyedCondition[]>
);
// Get ordered types (only those that have conditions)
$: orderedTypes = CONDITION_TYPES.filter((t) => groupedConditions[t.value]).map((t) => ({
value: t.value,
label: t.label,
conditions: groupedConditions[t.value]
}));
// Sort conditions by status (required -> negated -> optional), then type, then alphabetical
$: sortedConditions = sortConditions(conditions);
function handleRemove(key: string) {
update(
@@ -277,34 +262,23 @@
</div>
{/if}
<!-- Existing conditions grouped by type -->
<!-- Existing conditions sorted by status, type, then name -->
{#if conditions.length === 0 && draftConditions.length === 0}
<p class="text-sm text-neutral-500 dark:text-neutral-400">No conditions defined</p>
{:else}
{#each orderedTypes as group (group.value)}
<div class="space-y-2">
<!-- Group header -->
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{group.label}
</span>
<Badge variant="neutral" size="sm">{group.conditions.length}</Badge>
</div>
<!-- Conditions -->
{#each group.conditions as condition (condition._key)}
<ConditionCard
{condition}
availablePatterns={data.availablePatterns}
availableLanguages={data.availableLanguages}
invalid={!isConditionValid(condition)}
nameConflict={hasNameConflict(condition)}
on:remove={() => handleRemove(condition._key)}
on:change={(e) => handleConditionChange(e.detail, condition._key)}
/>
{/each}
</div>
{/each}
<div class="space-y-2">
{#each sortedConditions as condition (condition._key)}
<ConditionCard
{condition}
availablePatterns={data.availablePatterns}
availableLanguages={data.availableLanguages}
invalid={!isConditionValid(condition)}
nameConflict={hasNameConflict(condition)}
on:remove={() => handleRemove(condition._key)}
on:change={(e) => handleConditionChange(e.detail, condition._key)}
/>
{/each}
</div>
{/if}
</div>
</form>

View File

@@ -6,6 +6,7 @@
import { marked } from 'marked';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { sortConditions } from '$lib/shared/conditionTypes';
export let formats: CustomFormatTableRow[];
@@ -80,11 +81,11 @@
cell: (row: CustomFormatTableRow) => ({
html:
row.conditions.length > 0
? `<div class="flex flex-wrap gap-1">${row.conditions
? `<div class="flex flex-wrap gap-1">${sortConditions(row.conditions)
.map((c) => {
// Color based on required/negate:
// required + negate = red (must NOT match)
// required + !negate = accent (must match)
// required + !negate = green (must match)
// !required + negate = amber (optional negative)
// !required + !negate = neutral (optional)
let colorClass: string;
@@ -92,7 +93,7 @@
colorClass = 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
} else if (c.required) {
colorClass =
'bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-200';
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
} else if (c.negate) {
colorClass =
'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200';