feat(spa): wildcard sub-categories define + assign per option
This commit is contained in:
@@ -17,16 +17,40 @@ const moduleStore = useModuleStore();
|
||||
const categoryStore = useCategoryStore();
|
||||
const toast = useToast();
|
||||
|
||||
interface Option { id: string; value: string; weight: number; }
|
||||
interface Option { id: string; value: string; weight: number; sub_category?: string | null; }
|
||||
|
||||
const name = ref("");
|
||||
const description = ref("");
|
||||
const categoryId = ref<string | null>(null);
|
||||
const tags = ref<string[]>([]);
|
||||
const options = ref<Option[]>([]);
|
||||
const subCategories = ref<string[]>([]);
|
||||
const newSubCategory = ref("");
|
||||
const saving = ref(false);
|
||||
const isEdit = computed(() => !!props.id);
|
||||
|
||||
const subCategoryOptions = computed(() => [
|
||||
{ label: "(none)", value: null as string | null },
|
||||
...subCategories.value.map((s) => ({ label: s, value: s })),
|
||||
]);
|
||||
|
||||
function addSubCategory() {
|
||||
const v = newSubCategory.value.trim();
|
||||
if (!v) return;
|
||||
if (subCategories.value.includes(v)) {
|
||||
toast.add({ severity: "warn", summary: "Duplicate sub-category", life: 1500 });
|
||||
return;
|
||||
}
|
||||
subCategories.value.push(v);
|
||||
newSubCategory.value = "";
|
||||
}
|
||||
|
||||
function removeSubCategory(s: string) {
|
||||
subCategories.value = subCategories.value.filter((x) => x !== s);
|
||||
// Also clear it from any option assignment
|
||||
for (const o of options.value) if (o.sub_category === s) o.sub_category = null;
|
||||
}
|
||||
|
||||
const tagSuggestions = ref<string[]>([]);
|
||||
function searchTags(event: { query: string }) {
|
||||
const q = event.query.toLowerCase();
|
||||
@@ -48,7 +72,9 @@ onMounted(async () => {
|
||||
description.value = row.description;
|
||||
categoryId.value = row.category_id;
|
||||
tags.value = row.tags;
|
||||
options.value = (row.payload as { options?: Option[] }).options ?? [];
|
||||
const payload = row.payload as { options?: Option[]; sub_categories?: string[] };
|
||||
options.value = payload.options ?? [];
|
||||
subCategories.value = payload.sub_categories ?? [];
|
||||
} catch {
|
||||
toast.add({ severity: "error", summary: "Wildcard not found", life: 3000 });
|
||||
router.replace("/wildcards");
|
||||
@@ -69,7 +95,10 @@ async function save() {
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = { options: options.value };
|
||||
const payload = {
|
||||
options: options.value,
|
||||
sub_categories: subCategories.value,
|
||||
};
|
||||
if (isEdit.value && props.id) {
|
||||
await moduleStore.update(props.id, {
|
||||
name: name.value, description: description.value,
|
||||
@@ -137,6 +166,36 @@ async function save() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="form-section__label m-0">Sub-categories</h2>
|
||||
</div>
|
||||
<p class="text-xs text-wp-text2 mb-2">
|
||||
Optional groupings inside this wildcard. Tag each option below with one of these to allow filtering at resolve time.
|
||||
</p>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<InputText
|
||||
v-model="newSubCategory"
|
||||
placeholder="e.g. warm-tones"
|
||||
class="flex-1"
|
||||
aria-label="Sub-category name"
|
||||
@keydown.enter="addSubCategory"
|
||||
/>
|
||||
<Button label="Add" icon="pi pi-plus" size="small" severity="secondary" outlined @click="addSubCategory" />
|
||||
</div>
|
||||
<div v-if="subCategories.length" class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="s in subCategories"
|
||||
:key="s"
|
||||
class="sub-cat-chip"
|
||||
>
|
||||
{{ s }}
|
||||
<i class="pi pi-times cursor-pointer" @click="removeSubCategory(s)" />
|
||||
</span>
|
||||
</div>
|
||||
<p v-else class="text-xs text-wp-text3">No sub-categories yet.</p>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="form-section__label m-0">Options</h2>
|
||||
@@ -145,8 +204,9 @@ async function save() {
|
||||
<table class="w-full text-sm border border-wp-border rounded">
|
||||
<thead>
|
||||
<tr class="bg-wp-bg2">
|
||||
<th class="px-3 py-2 text-left text-wp-text2 text-xs uppercase">Weight</th>
|
||||
<th class="px-3 py-2 text-left text-wp-text2 text-xs uppercase w-24">Weight</th>
|
||||
<th class="px-3 py-2 text-left text-wp-text2 text-xs uppercase">Value</th>
|
||||
<th class="px-3 py-2 text-left text-wp-text2 text-xs uppercase w-44">Sub-category</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -156,13 +216,24 @@ async function save() {
|
||||
<InputNumber v-model="opt.weight" :min="0" :max="999" size="small" class="w-full" />
|
||||
</td>
|
||||
<td class="px-3 py-2"><InputText v-model="opt.value" class="w-full" /></td>
|
||||
<td class="px-3 py-2 w-44">
|
||||
<Select
|
||||
v-model="opt.sub_category"
|
||||
:options="subCategoryOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
placeholder="(none)"
|
||||
class="w-full"
|
||||
aria-label="Sub-category for option"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger"
|
||||
aria-label="Remove option" @click="removeOption(idx)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!options.length">
|
||||
<td colspan="3" class="text-center text-wp-text2 py-4">No options yet.</td>
|
||||
<td colspan="4" class="text-center text-wp-text2 py-4">No options yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -193,4 +264,17 @@ async function save() {
|
||||
letter-spacing: 0.5px; color: var(--wp-text2);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.sub-cat-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 11px;
|
||||
font-size: 11px;
|
||||
background: var(--wp-violet-bg);
|
||||
color: var(--wp-violet);
|
||||
border: 1px solid var(--wp-violet);
|
||||
}
|
||||
.sub-cat-chip i { opacity: 0.7; font-size: 10px; }
|
||||
.sub-cat-chip i:hover { opacity: 1; }
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/wp/images/site.webmanifest" />
|
||||
<script type="module" crossorigin src="/wp/assets/manager-DjdGrtNo.js"></script>
|
||||
<script type="module" crossorigin src="/wp/assets/manager-BZjxC98I.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/wp/assets/manager-DzNhfNc6.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user