260 lines
8.5 KiB
Vue
260 lines
8.5 KiB
Vue
<script setup>
|
|
import { ref, reactive, computed, onMounted } from 'vue';
|
|
import AppListShell from '../_shared/AppListShell.vue';
|
|
import AppListItem from '../_shared/AppListItem.vue';
|
|
|
|
const props = defineProps({
|
|
serverData: { type: Object, required: true },
|
|
});
|
|
|
|
const api = (path, opts = {}) =>
|
|
fetch(`${props.serverData.restUrl}aurora-admin/v1${path}`, {
|
|
...opts,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-WP-Nonce': props.serverData.restNonce,
|
|
...(opts.headers || {}),
|
|
},
|
|
}).then((r) => {
|
|
if (!r.ok) throw new Error('Request failed');
|
|
return r.json();
|
|
});
|
|
|
|
const menus = ref([]);
|
|
const nativeItems = ref([]);
|
|
const loading = ref(true);
|
|
const search = ref('');
|
|
const statusFilter = ref('all');
|
|
const selectedId = ref(null);
|
|
const roleOptions = ref([]);
|
|
|
|
const load = async () => {
|
|
loading.value = true;
|
|
try {
|
|
menus.value = await api('/menus');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
load();
|
|
api('/menus/native-items').then((items) => (nativeItems.value = items));
|
|
roleOptions.value = props.serverData.roles || [];
|
|
});
|
|
|
|
const filteredMenus = computed(() => {
|
|
let list = menus.value;
|
|
if (statusFilter.value !== 'all') {
|
|
list = list.filter((m) => m.status === statusFilter.value);
|
|
}
|
|
const q = search.value.trim().toLowerCase();
|
|
if (q) list = list.filter((m) => m.name.toLowerCase().includes(q));
|
|
return list;
|
|
});
|
|
|
|
const selected = computed(() => menus.value.find((m) => m.id === selectedId.value) || null);
|
|
|
|
const draft = reactive({ name: '', status: 'draft', roles: [], items: [] });
|
|
|
|
const selectMenu = (menu) => {
|
|
selectedId.value = menu.id;
|
|
Object.assign(draft, {
|
|
name: menu.name,
|
|
status: menu.status,
|
|
roles: [...menu.roles],
|
|
items: menu.items.map((i) => ({ ...i })),
|
|
});
|
|
};
|
|
|
|
const isCreating = ref(false);
|
|
const newMenu = () => {
|
|
selectedId.value = null;
|
|
isCreating.value = true;
|
|
Object.assign(draft, { name: 'New menu', status: 'draft', roles: [], items: [] });
|
|
};
|
|
|
|
const itemState = (nativeId) => draft.items.find((i) => i.native_id === nativeId);
|
|
const toggleHidden = (nativeId) => {
|
|
let item = itemState(nativeId);
|
|
if (!item) {
|
|
item = { native_id: nativeId, label: '', hidden: false, order: 0 };
|
|
draft.items.push(item);
|
|
}
|
|
item.hidden = !item.hidden;
|
|
};
|
|
const setLabel = (nativeId, label) => {
|
|
let item = itemState(nativeId);
|
|
if (!item) {
|
|
item = { native_id: nativeId, label: '', hidden: false, order: 0 };
|
|
draft.items.push(item);
|
|
}
|
|
item.label = label;
|
|
};
|
|
|
|
const saving = ref(false);
|
|
const save = async () => {
|
|
saving.value = true;
|
|
try {
|
|
const payload = { name: draft.name, status: draft.status, roles: draft.roles, items: draft.items };
|
|
if (isCreating.value) {
|
|
const created = await api('/menus', { method: 'POST', body: JSON.stringify(payload) });
|
|
await load();
|
|
selectMenu(created);
|
|
isCreating.value = false;
|
|
} else if (selectedId.value) {
|
|
await api(`/menus/${selectedId.value}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
await load();
|
|
}
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
const remove = async () => {
|
|
if (!selectedId.value) return;
|
|
await api(`/menus/${selectedId.value}`, { method: 'DELETE' });
|
|
selectedId.value = null;
|
|
await load();
|
|
};
|
|
|
|
const toggleRole = (slug) => {
|
|
const idx = draft.roles.indexOf(slug);
|
|
if (idx === -1) draft.roles.push(slug);
|
|
else draft.roles.splice(idx, 1);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<AppListShell
|
|
v-model:search="search"
|
|
title="Menu Creator"
|
|
search-placeholder="Search menus…"
|
|
:count-label="`${filteredMenus.length} MENUS`"
|
|
detail-icon="dashicons-menu-alt"
|
|
detail-title="Menu Editor"
|
|
detail-text="Select a menu from the list to edit its structure and settings."
|
|
:has-selection="!!selected || isCreating"
|
|
>
|
|
<template #header-actions>
|
|
<button type="button" @click="newMenu">+</button>
|
|
</template>
|
|
|
|
<template #filters>
|
|
<button
|
|
v-for="f in ['all', 'published', 'draft']"
|
|
:key="f"
|
|
type="button"
|
|
:class="{ 'is-active': statusFilter === f }"
|
|
@click="statusFilter = f"
|
|
>
|
|
{{ f === 'all' ? 'All' : f[0].toUpperCase() + f.slice(1) }}
|
|
</button>
|
|
</template>
|
|
|
|
<template #list>
|
|
<p v-if="!loading && !filteredMenus.length" style="font-size: 0.8125rem; color: var(--aurora-text-muted); padding: 20px 4px; text-align: center;">
|
|
No menus yet
|
|
</p>
|
|
<AppListItem
|
|
v-for="m in filteredMenus"
|
|
:key="m.id"
|
|
icon="dashicons-menu-alt"
|
|
:title="m.name"
|
|
:subtitle="m.status"
|
|
:active="m.id === selectedId"
|
|
@click="selectMenu(m)"
|
|
/>
|
|
</template>
|
|
|
|
<template #detail>
|
|
<div class="menu-editor">
|
|
<div class="menu-editor__row">
|
|
<input v-model="draft.name" type="text" class="menu-editor__name" placeholder="Menu name" />
|
|
<select v-model="draft.status" class="menu-editor__status">
|
|
<option value="draft">Draft</option>
|
|
<option value="published">Published</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="menu-editor__section">
|
|
<h3>Target roles</h3>
|
|
<div class="menu-editor__roles">
|
|
<label v-for="r in roleOptions" :key="r.value" class="menu-editor__role">
|
|
<input type="checkbox" :checked="draft.roles.includes(r.value)" @change="toggleRole(r.value)" />
|
|
{{ r.label }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="menu-editor__section">
|
|
<h3>Menu items</h3>
|
|
<div class="menu-editor__items">
|
|
<div v-for="ni in nativeItems" :key="ni.native_id" class="menu-editor__item">
|
|
<span class="dashicons" :class="ni.icon?.value || 'dashicons-admin-generic'" />
|
|
<input
|
|
type="text"
|
|
:value="itemState(ni.native_id)?.label || ni.title"
|
|
@input="setLabel(ni.native_id, $event.target.value)"
|
|
/>
|
|
<label class="menu-editor__hide">
|
|
<input
|
|
type="checkbox"
|
|
:checked="!!itemState(ni.native_id)?.hidden"
|
|
@change="toggleHidden(ni.native_id)"
|
|
/>
|
|
Hide
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="menu-editor__actions">
|
|
<button type="button" class="is-danger" :disabled="!selectedId" @click="remove">Delete</button>
|
|
<button type="button" class="is-primary" :disabled="saving" @click="save">
|
|
{{ saving ? 'Saving…' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</AppListShell>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.menu-editor { display: flex; flex-direction: column; gap: 22px; }
|
|
.menu-editor__row { display: flex; gap: 10px; }
|
|
.menu-editor__name {
|
|
flex: 1; font-size: 1.1rem; font-weight: 700; padding: 8px 10px;
|
|
border: 1px solid var(--aurora-border); border-radius: 8px;
|
|
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
|
}
|
|
.menu-editor__status {
|
|
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
|
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
|
}
|
|
.menu-editor__section h3 {
|
|
font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.03em;
|
|
color: var(--aurora-text-muted); margin: 0 0 10px;
|
|
}
|
|
.menu-editor__roles { display: flex; flex-wrap: wrap; gap: 12px; }
|
|
.menu-editor__role { display: flex; align-items: center; gap: 6px; font-size: 0.8125rem; color: var(--aurora-text); }
|
|
.menu-editor__items { display: flex; flex-direction: column; gap: 6px; max-height: 360px; overflow-y: auto; }
|
|
.menu-editor__item {
|
|
display: flex; align-items: center; gap: 10px; padding: 6px 4px;
|
|
border-bottom: 1px solid var(--aurora-frame-border);
|
|
}
|
|
.menu-editor__item input[type='text'] {
|
|
flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--aurora-border);
|
|
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.8125rem;
|
|
}
|
|
.menu-editor__hide { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
|
.menu-editor__actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
.menu-editor__actions button {
|
|
border: none; border-radius: 8px; padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
|
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
|
}
|
|
.menu-editor__actions .is-primary { background: var(--aurora-accent); color: #fff; }
|
|
.menu-editor__actions .is-danger { color: #ef4444; }
|
|
.menu-editor__actions button:disabled { opacity: 0.5; cursor: default; }
|
|
</style>
|