Initial commit: Aurora Admin plugin
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user