Add frontend support for label parent/child relationships

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-14 02:16:49 +00:00
parent a23e0f5909
commit 8463b70229
4 changed files with 94 additions and 1 deletions

View File

@@ -15,6 +15,11 @@
default: "md",
},
});
// Type guard to check if label is LabelOut (has parent/children)
const isLabelOut = (label: LabelOut | LabelSummary): label is LabelOut => {
return 'parent' in label || 'children' in label;
};
</script>
<template>
@@ -42,6 +47,10 @@
<MdiArrowUp class="hidden group-hover/label-chip:block" />
</div>
</div>
<template v-if="isLabelOut(label) && label.parent">
<span class="opacity-70">{{ label.parent.name }}</span>
<span class="opacity-50">/</span>
</template>
{{ label.name }}
</NuxtLink>
</template>

View File

@@ -15,6 +15,7 @@
:max-length="1000"
/>
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
<LabelParentSelector v-model="form.parentId" :labels="labels" />
<div class="mt-4 flex flex-row-reverse">
<ButtonGroup>
<Button :disabled="loading" type="submit">{{ $t("global.create") }}</Button>
@@ -37,6 +38,8 @@
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import { Button, ButtonGroup } from "~/components/ui/button";
import LabelParentSelector from "@/components/Label/ParentSelector.vue";
import type { LabelOut } from "~/lib/api/types/data-contracts";
const { t } = useI18n();
@@ -49,13 +52,25 @@
const form = reactive({
name: "",
description: "",
color: "", // Future!
color: "",
parentId: null as string | null,
});
const labels = ref<LabelOut[]>([]);
// Load labels for parent selection
onMounted(async () => {
const { data } = await api.labels.getAll();
if (data) {
labels.value = data;
}
});
function reset() {
form.name = "";
form.description = "";
form.color = "";
form.parentId = null;
focused.value = false;
loading.value = false;
}

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col gap-1">
<Label :for="id">
{{ $t('components.label.parent_selector.label') }}
</Label>
<Select v-model="modelValue">
<SelectTrigger :id="id">
<SelectValue :placeholder="$t('components.label.parent_selector.placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{{ $t('components.label.parent_selector.no_parent') }}</SelectItem>
<SelectItem v-for="label in props.labels" :key="label.id" :value="label.id">
{{ label.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</template>
<script setup lang="ts">
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { LabelOut } from "~/lib/api/types/data-contracts";
const id = useId();
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
type: String as () => string | null,
default: null,
},
labels: {
type: Array as () => LabelOut[],
required: true,
},
});
const modelValue = useVModel(props, "modelValue", emit);
</script>

View File

@@ -21,6 +21,8 @@
import PageQRCode from "~/components/global/PageQRCode.vue";
import Markdown from "~/components/global/Markdown.vue";
import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
import LabelParentSelector from "@/components/Label/ParentSelector.vue";
import LabelChip from "@/components/Label/Chip.vue";
definePageMeta({
middleware: ["auth"],
@@ -69,12 +71,19 @@
name: "",
description: "",
color: "",
parentId: null as string | null,
});
const { data: allLabels } = useAsyncData("all-labels", async () => {
const { data } = await api.labels.getAll();
return data || [];
});
function openUpdate() {
updateData.name = label.value?.name || "";
updateData.description = label.value?.description || "";
updateData.color = "";
updateData.parentId = label.value?.parent?.id || null;
openDialog(DialogID.UpdateLabel);
}
@@ -151,6 +160,7 @@
:show-hex="true"
:starting-color="label.color"
/>
<LabelParentSelector v-if="allLabels" v-model="updateData.parentId" :labels="allLabels.filter(l => l.id !== labelId)" />
<DialogFooter>
<Button type="submit" :loading="updating"> {{ $t("global.update") }} </Button>
</DialogFooter>
@@ -204,6 +214,25 @@
</header>
<Separator v-if="label && label.description" />
<Markdown v-if="label && label.description" class="mt-3 text-base" :source="label.description" />
<!-- Display parent and children -->
<div v-if="label && (label.parent || (label.children && label.children.length > 0))" class="mt-3">
<Separator />
<div class="mt-3">
<div v-if="label.parent" class="mb-2">
<span class="text-sm font-medium">{{ $t("labels.parent_label") }}:</span>
<div class="mt-1">
<LabelChip :label="label.parent" size="sm" />
</div>
</div>
<div v-if="label.children && label.children.length > 0">
<span class="text-sm font-medium">{{ $t("labels.child_labels") }}:</span>
<div class="mt-1 flex flex-wrap gap-2">
<LabelChip v-for="child in label.children" :key="child.id" :label="child" size="sm" />
</div>
</div>
</div>
</div>
</Card>
<section v-if="label && items">
<ItemViewSelectable :items="items.items" @refresh="refreshItemList" />