Prevent self-referencing locations and items as parents (#773)

* prevent current location and descendants from being selected as parent

* prevent an item from showing up in the parent items drop-down for itself

* pass location object to filter function to allow for more flexible filtering

* align exclude prop and fix type comparison, change item filter to array of ItemsObjects to allow for descendant filtering in future

* fix linting prop reference
This commit is contained in:
Nikolai Oakfield
2025-06-28 18:58:46 -04:00
committed by GitHub
parent c1c8eb649c
commit c9d055fe03
5 changed files with 45 additions and 10 deletions

View File

@@ -65,6 +65,7 @@
searchPlaceholder?: string;
noResultsText?: string;
placeholder?: string;
excludeItems?: ItemsObject[];
}
const emit = defineEmits(["update:modelValue", "update:search"]);
@@ -78,6 +79,7 @@
searchPlaceholder: undefined,
noResultsText: undefined,
placeholder: undefined,
excludeItems: undefined,
});
const id = useId();
@@ -137,12 +139,19 @@
}
const filtered = computed(() => {
if (!search.value) return props.items;
if (isStrings(props.items)) {
return props.items.filter(item => item.toLowerCase().includes(search.value.toLowerCase()));
let baseItems = props.items;
if (!isStrings(baseItems) && props.excludeItems) {
const excludeIds = props.excludeItems.map(i => i.id);
baseItems = baseItems.filter(item => !excludeIds?.includes(item.id));
}
if (!search.value) return baseItems;
if (isStrings(baseItems)) {
return baseItems.filter(item => item.toLowerCase().includes(search.value.toLowerCase()));
} else {
// Fuzzy search on itemText
return fuzzysort.go(search.value, props.items, { key: props.itemText, all: true }).map(i => i.obj);
return fuzzysort.go(search.value, baseItems, { key: props.itemText, all: true }).map(i => i.obj);
}
});
</script>

View File

@@ -58,6 +58,7 @@
type Props = {
modelValue?: LocationSummary | null;
currentLocation?: LocationSummary;
};
const props = defineProps<Props>();
@@ -66,7 +67,7 @@
const open = ref(false);
const search = ref("");
const id = useId();
const locations = useFlatLocations();
const locations = useFlatLocations(props.currentLocation);
const value = useVModel(props, "modelValue", emit);
function selectLocation(location: LocationSummary) {

View File

@@ -1,5 +1,5 @@
import type { Ref } from "vue";
import type { TreeItem } from "~~/lib/api/types/data-contracts";
import type { TreeItem, LocationSummary } from "~~/lib/api/types/data-contracts";
export interface FlatTreeItem {
id: string;
@@ -35,9 +35,29 @@ function flatTree(tree: TreeItem[]): FlatTreeItem[] {
return v;
}
export function useFlatLocations(): Ref<FlatTreeItem[]> {
const locations = useLocationStore();
function filterOutSubtree(tree: TreeItem[], excludeId: string): TreeItem[] {
// Recursively filters out a subtree starting from excludeId
const result: TreeItem[] = [];
for (const item of tree) {
if (item.id === excludeId) {
continue;
}
const newItem = { ...item };
if (item.children) {
newItem.children = filterOutSubtree(item.children, excludeId);
}
result.push(newItem);
}
return result;
}
export function useFlatLocations(excludeSubtreeForLocation?: LocationSummary): Ref<FlatTreeItem[]> {
const locations = useLocationStore();
if (locations.tree === null) {
locations.refreshTree();
}
@@ -47,6 +67,10 @@ export function useFlatLocations(): Ref<FlatTreeItem[]> {
return [];
}
return flatTree(locations.tree);
const filteredTree = excludeSubtreeForLocation
? filterOutSubtree(locations.tree, excludeSubtreeForLocation.id)
: locations.tree;
return flatTree(filteredTree);
});
}

View File

@@ -577,6 +577,7 @@
item-text="name"
:label="$t('items.parent_item')"
no-results-text="Type to search..."
:exclude-items="[item]"
@update:model-value="maybeSyncWithParentLocation()"
/>
<div class="flex flex-col gap-2">

View File

@@ -147,7 +147,7 @@
:label="$t('components.location.create_modal.location_description')"
:max-length="1000"
/>
<LocationSelector v-model="parent" />
<LocationSelector v-model="parent" :current-location="location" />
<DialogFooter>
<Button type="submit" :disabled="updating">
<MdiLoading v-if="updating" class="animate-spin" />