From 0f5429131ad3ad0780c102c6914a0674c8a7a413 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:54:00 +0000 Subject: [PATCH] feat(smart-scope): filter by community review count Allow provider-aware review count criteria so rating thresholds can be qualified by community review volume. Closes #1041 --- .../book/components/BookFilterBuilder.vue | 30 +- .../__tests__/BookFilterBuilder.spec.ts | 40 + .../book/composables/useTableQuickFilters.ts | 2 +- .../book/lib/__tests__/filter-labels.spec.ts | 9 + client/src/features/book/lib/filter-labels.ts | 7 +- client/src/locales/en.json | 1 + packages/types/src/query.ts | 8 +- ..._community_rating_count_filter_indexes.sql | 2 + .../src/db/migrations/meta/0082_snapshot.json | 16752 ++++++++++++++++ server/src/db/migrations/meta/_journal.json | 7 + server/src/db/schema/metadata.ts | 2 + .../book/book-query-builder.service.test.ts | 94 +- .../book/book-query-builder.service.ts | 40 +- .../book/utils/group-rule.validator.test.ts | 20 + .../book/utils/group-rule.validator.ts | 8 +- 15 files changed, 16998 insertions(+), 24 deletions(-) create mode 100644 server/src/db/migrations/0082_add_community_rating_count_filter_indexes.sql create mode 100644 server/src/db/migrations/meta/0082_snapshot.json diff --git a/client/src/features/book/components/BookFilterBuilder.vue b/client/src/features/book/components/BookFilterBuilder.vue index b197df550..60c844864 100644 --- a/client/src/features/book/components/BookFilterBuilder.vue +++ b/client/src/features/book/components/BookFilterBuilder.vue @@ -11,6 +11,7 @@ import { isCustomRuleField, parseCustomRuleFieldId, type CommunityRatingProvider, + type CommunityRatingRuleField, type CustomMetadataFieldType, type CustomRuleField, type GroupRule, @@ -44,7 +45,16 @@ const emit = defineEmits<{ const MAX_DEPTH = 5 const BYTES_PER_MIB = 1024 ** 2 -const NUMERIC_FIELDS: RuleField[] = ['seriesIndex', 'publishedYear', 'pageCount', 'fileSize', 'rating', 'communityRating', 'metadataScore'] +const NUMERIC_FIELDS: RuleField[] = [ + 'seriesIndex', + 'publishedYear', + 'pageCount', + 'fileSize', + 'rating', + 'communityRating', + 'communityRatingCount', + 'metadataScore', +] const DATE_FIELDS: RuleField[] = ['publishedDate', 'addedAt', 'startedAt', 'finishedAt'] const NO_VALUE_OPERATORS: RuleOperator[] = [ 'isEmpty', @@ -135,6 +145,15 @@ function isNumericField(field: RuleField): boolean { return NUMERIC_FIELDS.includes(field) || customFieldType(field) === 'number' } +function isCommunityRatingField(field: RuleField): field is CommunityRatingRuleField { + return field === 'communityRating' || field === 'communityRatingCount' +} + +function providerForRule(rule: Rule): CommunityRatingProvider { + if (rule.field === 'communityRating' || rule.field === 'communityRatingCount') return rule.provider ?? 'any' + return 'any' +} + function isDateField(field: RuleField): boolean { return DATE_FIELDS.includes(field) || customFieldType(field) === 'date' } @@ -173,7 +192,7 @@ function toEditableRule(r: Rule): EditableRule { return { field: r.field, operator: r.operator, - provider: r.field === 'communityRating' ? (r.provider ?? 'any') : 'any', + provider: providerForRule(r), value: usesChips ? '' : value, valueChips: usesChips ? (r.value as string[]) : [], valueTo, @@ -257,7 +276,7 @@ function emitUpdate() { : n.rule.valueTo : undefined, } - return n.rule.field === 'communityRating' ? ({ ...rule, provider: n.rule.provider } as Rule) : (rule as Rule) + return isCommunityRatingField(n.rule.field) ? ({ ...rule, provider: n.rule.provider } as Rule) : (rule as Rule) }) const isSubGroup = (props.depth ?? 0) > 0 if (!isSubGroup && rules.length === 0 && !props.preserveIncompleteRoot) { @@ -393,7 +412,7 @@ function valueInputType(field: RuleField, operator: RuleOperator): string { function numericInputMin(field: RuleField): string | undefined { if (field === 'fileSize') return '0' - if (field === 'communityRating') return '0' + if (isCommunityRatingField(field)) return '0' return undefined } @@ -406,6 +425,7 @@ function numericInputMax(field: RuleField): string | undefined { function numericInputStep(field: RuleField): string | undefined { if (field === 'fileSize') return '0.1' if (field === 'communityRating') return '0.1' + if (field === 'communityRatingCount') return '1' return undefined } @@ -465,7 +485,7 @@ function showValueToInput(operator: RuleOperator): boolean {