mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -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<{
|
const props = defineProps<{
|
||||||
modelValue: GroupRule | undefined
|
modelValue: GroupRule | undefined
|
||||||
depth?: number
|
depth?: number
|
||||||
|
preserveIncompleteRoot?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -166,7 +167,7 @@ function emitUpdate() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
const isSubGroup = (props.depth ?? 0) > 0
|
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.
|
// Top-level: no complete rules means no active filter — don't send an empty group to the API.
|
||||||
emit('update:modelValue', undefined)
|
emit('update:modelValue', undefined)
|
||||||
return
|
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' }] }
|
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() {
|
async function save() {
|
||||||
if (!props.smartScope) return
|
if (!props.smartScope) return
|
||||||
if (!trimmedDraftName.value) {
|
if (!trimmedDraftName.value) {
|
||||||
@@ -121,7 +125,7 @@ async function save() {
|
|||||||
await updateSmartScope(props.smartScope.id, {
|
await updateSmartScope(props.smartScope.id, {
|
||||||
name: trimmedDraftName.value,
|
name: trimmedDraftName.value,
|
||||||
icon: trimmedDraftIcon.value,
|
icon: trimmedDraftIcon.value,
|
||||||
filter: draftFilter.value,
|
filter: hasCompleteRules(draftFilter.value) ? draftFilter.value : undefined,
|
||||||
defaultSort: draftSort.value,
|
defaultSort: draftSort.value,
|
||||||
})
|
})
|
||||||
emit('saved')
|
emit('saved')
|
||||||
@@ -218,7 +222,7 @@ async function save() {
|
|||||||
|
|
||||||
<!-- Filter builder -->
|
<!-- Filter builder -->
|
||||||
<template v-else>
|
<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">
|
<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>
|
<span class="text-xs text-muted-foreground shrink-0">Replace with template:</span>
|
||||||
<button
|
<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"]],
|
"type-enum": [2, "always", ["feat", "fix", "db", "perf", "refactor", "style", "docs", "test", "build", "ci", "chore", "security", "revert"]],
|
||||||
"header-max-length": [2, "always", 100],
|
"header-max-length": [2, "always", 100],
|
||||||
"subject-full-stop": [2, "never", "."],
|
"subject-full-stop": [2, "never", "."],
|
||||||
"scope-enum": [
|
"scope-enum": [0],
|
||||||
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-empty": [0],
|
"scope-empty": [0],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user