Initial commit: Aurora Admin plugin

This commit is contained in:
Kris Parish
2026-07-03 02:53:29 -05:00
commit 03cabe2fe4
73 changed files with 9591 additions and 0 deletions
+259
View File
@@ -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>