mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(smart-scope): preserve draft filters while editing (#54)
Keep incomplete root filters visible in the SmartScope editor. This lets users choose a field before setting an operator or value. Fixes #52
This commit is contained in:
@@ -12,6 +12,7 @@ import FilterTextTypeahead from './FilterTextTypeahead.vue'
|
||||
const props = defineProps<{
|
||||
modelValue: GroupRule | undefined
|
||||
depth?: number
|
||||
preserveIncompleteRoot?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -166,7 +167,7 @@ function emitUpdate() {
|
||||
}
|
||||
})
|
||||
const isSubGroup = (props.depth ?? 0) > 0
|
||||
if (!isSubGroup && rules.length === 0) {
|
||||
if (!isSubGroup && rules.length === 0 && !props.preserveIncompleteRoot) {
|
||||
// Top-level: no complete rules means no active filter — don't send an empty group to the API.
|
||||
emit('update:modelValue', undefined)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { GroupRule } from '@bookorbit/types'
|
||||
import BookFilterBuilder from '../BookFilterBuilder.vue'
|
||||
|
||||
vi.mock('@/features/library/composables/useLibraries', () => ({
|
||||
useLibraries: () => ({
|
||||
libraries: ref([]),
|
||||
loading: ref(false),
|
||||
fetchLibraries: vi.fn<() => Promise<void>>(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const incompleteFilter: GroupRule = {
|
||||
type: 'group',
|
||||
join: 'AND',
|
||||
rules: [{ type: 'rule', field: 'title', operator: 'contains' }],
|
||||
}
|
||||
|
||||
function lastUpdate(wrapper: ReturnType<typeof mount<typeof BookFilterBuilder>>) {
|
||||
const events = wrapper.emitted('update:modelValue')
|
||||
return events?.[events.length - 1]?.[0]
|
||||
}
|
||||
|
||||
describe('BookFilterBuilder', () => {
|
||||
it('emits undefined for an incomplete root filter by default', async () => {
|
||||
const wrapper = mount(BookFilterBuilder, {
|
||||
props: {
|
||||
modelValue: incompleteFilter,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.get('select').setValue('collection')
|
||||
|
||||
expect(lastUpdate(wrapper)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves an incomplete root group when requested', async () => {
|
||||
const wrapper = mount(BookFilterBuilder, {
|
||||
props: {
|
||||
modelValue: incompleteFilter,
|
||||
preserveIncompleteRoot: true,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.get('select').setValue('collection')
|
||||
|
||||
expect(lastUpdate(wrapper)).toEqual({ type: 'group', join: 'AND', rules: [] })
|
||||
})
|
||||
})
|
||||
@@ -105,6 +105,10 @@ function initFilter() {
|
||||
draftFilter.value = { type: 'group', join: 'AND', rules: [{ type: 'rule', field: 'title', operator: 'contains' }] }
|
||||
}
|
||||
|
||||
function hasCompleteRules(group: GroupRule | undefined): boolean {
|
||||
return !!group?.rules.some((rule) => rule.type === 'rule' || hasCompleteRules(rule as GroupRule))
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!props.smartScope) return
|
||||
if (!trimmedDraftName.value) {
|
||||
@@ -121,7 +125,7 @@ async function save() {
|
||||
await updateSmartScope(props.smartScope.id, {
|
||||
name: trimmedDraftName.value,
|
||||
icon: trimmedDraftIcon.value,
|
||||
filter: draftFilter.value,
|
||||
filter: hasCompleteRules(draftFilter.value) ? draftFilter.value : undefined,
|
||||
defaultSort: draftSort.value,
|
||||
})
|
||||
emit('saved')
|
||||
@@ -218,7 +222,7 @@ async function save() {
|
||||
|
||||
<!-- Filter builder -->
|
||||
<template v-else>
|
||||
<BookFilterBuilder v-model="draftFilter" />
|
||||
<BookFilterBuilder v-model="draftFilter" preserve-incomplete-root />
|
||||
<div class="flex flex-wrap items-center gap-1.5 pt-3 border-t border-border/50">
|
||||
<span class="text-xs text-muted-foreground shrink-0">Replace with template:</span>
|
||||
<button
|
||||
|
||||
+1
-31
@@ -4,37 +4,7 @@ module.exports = {
|
||||
"type-enum": [2, "always", ["feat", "fix", "db", "perf", "refactor", "style", "docs", "test", "build", "ci", "chore", "security", "revert"]],
|
||||
"header-max-length": [2, "always", 100],
|
||||
"subject-full-stop": [2, "never", "."],
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"auth",
|
||||
"library",
|
||||
"books",
|
||||
"authors",
|
||||
"series",
|
||||
"cover",
|
||||
"metadata",
|
||||
"kobo",
|
||||
"opds",
|
||||
"reader",
|
||||
"collections",
|
||||
"annotations",
|
||||
"users",
|
||||
"stats",
|
||||
"notifications",
|
||||
"settings",
|
||||
"scanner",
|
||||
"email",
|
||||
"audit",
|
||||
"smart-scope",
|
||||
"types",
|
||||
"docker",
|
||||
"deps",
|
||||
"server",
|
||||
"client",
|
||||
],
|
||||
],
|
||||
"scope-enum": [0],
|
||||
"scope-empty": [0],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user