@kywi-software/core 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-PATTERNS.md +58 -2
- package/dist/admin/registry.d.ts +2 -2
- package/dist/admin/registry.d.ts.map +1 -1
- package/dist/admin/registry.js +41 -19
- package/dist/admin/registry.js.map +1 -1
- package/dist/admin/styles/admin.css +130 -0
- package/dist/admin/surfaces/menus/menu-editor.d.ts +3 -0
- package/dist/admin/surfaces/menus/menu-editor.d.ts.map +1 -0
- package/dist/admin/surfaces/menus/menu-editor.js +260 -0
- package/dist/admin/surfaces/menus/menu-editor.js.map +1 -0
- package/dist/admin/surfaces/menus/menu-tree.d.ts +123 -0
- package/dist/admin/surfaces/menus/menu-tree.d.ts.map +1 -0
- package/dist/admin/surfaces/menus/menu-tree.js +292 -0
- package/dist/admin/surfaces/menus/menu-tree.js.map +1 -0
- package/dist/admin/surfaces/menus/menus-data.d.ts +101 -0
- package/dist/admin/surfaces/menus/menus-data.d.ts.map +1 -0
- package/dist/admin/surfaces/menus/menus-data.js +98 -0
- package/dist/admin/surfaces/menus/menus-data.js.map +1 -0
- package/dist/admin/surfaces/menus/menus-list.d.ts +3 -0
- package/dist/admin/surfaces/menus/menus-list.d.ts.map +1 -0
- package/dist/admin/surfaces/menus/menus-list.js +105 -0
- package/dist/admin/surfaces/menus/menus-list.js.map +1 -0
- package/dist/admin/surfaces/menus/page-picker.d.ts +10 -0
- package/dist/admin/surfaces/menus/page-picker.d.ts.map +1 -0
- package/dist/admin/surfaces/menus/page-picker.js +38 -0
- package/dist/admin/surfaces/menus/page-picker.js.map +1 -0
- package/dist/admin/surfaces/settings/admin-features.d.ts.map +1 -1
- package/dist/admin/surfaces/settings/admin-features.js +1 -0
- package/dist/admin/surfaces/settings/admin-features.js.map +1 -1
- package/dist/api/feed-routes.d.ts +2 -2
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +5 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/menu-routes.d.ts +46 -0
- package/dist/api/menu-routes.d.ts.map +1 -0
- package/dist/api/menu-routes.js +247 -0
- package/dist/api/menu-routes.js.map +1 -0
- package/dist/api/openapi.d.ts.map +1 -1
- package/dist/api/openapi.js +105 -0
- package/dist/api/openapi.js.map +1 -1
- package/dist/cache/cached-queries.d.ts +22 -0
- package/dist/cache/cached-queries.d.ts.map +1 -1
- package/dist/cache/cached-queries.js +31 -1
- package/dist/cache/cached-queries.js.map +1 -1
- package/dist/cache/keys.d.ts +2 -0
- package/dist/cache/keys.d.ts.map +1 -1
- package/dist/cache/keys.js +4 -0
- package/dist/cache/keys.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/menu-queries.d.ts +182 -0
- package/dist/db/menu-queries.d.ts.map +1 -0
- package/dist/db/menu-queries.js +326 -0
- package/dist/db/menu-queries.js.map +1 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +1 -0
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/modules-extended.d.ts +32 -2
- package/dist/layout/modules-extended.d.ts.map +1 -1
- package/dist/layout/modules-extended.js +159 -39
- package/dist/layout/modules-extended.js.map +1 -1
- package/dist/layout/nav-hydration.d.ts +62 -0
- package/dist/layout/nav-hydration.d.ts.map +1 -0
- package/dist/layout/nav-hydration.js +149 -0
- package/dist/layout/nav-hydration.js.map +1 -0
- package/dist/layout/registry.d.ts +0 -26
- package/dist/layout/registry.d.ts.map +1 -1
- package/dist/layout/registry.js +22 -8
- package/dist/layout/registry.js.map +1 -1
- package/dist/nav/builder.d.ts +16 -0
- package/dist/nav/builder.d.ts.map +1 -1
- package/dist/nav/builder.js +21 -2
- package/dist/nav/builder.js.map +1 -1
- package/dist/nav/index.d.ts +1 -0
- package/dist/nav/index.d.ts.map +1 -1
- package/dist/nav/index.js +1 -0
- package/dist/nav/index.js.map +1 -1
- package/dist/nav/menu-resolver.d.ts +52 -0
- package/dist/nav/menu-resolver.d.ts.map +1 -0
- package/dist/nav/menu-resolver.js +113 -0
- package/dist/nav/menu-resolver.js.map +1 -0
- package/dist/nav/types.d.ts +30 -0
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/schema/base.d.ts +331 -0
- package/dist/schema/base.d.ts.map +1 -1
- package/dist/schema/base.js +45 -0
- package/dist/schema/base.js.map +1 -1
- package/dist/schema/mysql-tables.d.ts +303 -0
- package/dist/schema/mysql-tables.d.ts.map +1 -1
- package/dist/schema/mysql-tables.js +25 -0
- package/dist/schema/mysql-tables.js.map +1 -1
- package/dist/scope/create-scope.d.ts.map +1 -1
- package/dist/scope/create-scope.js +27 -1
- package/dist/scope/create-scope.js.map +1 -1
- package/dist/scope/types.d.ts +28 -1
- package/dist/scope/types.d.ts.map +1 -1
- package/dist/site/styles.css +380 -0
- package/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* menu-tree.ts — the client-side item model behind the Menus editor (#112).
|
|
3
|
+
*
|
|
4
|
+
* Why a client-side tree at all: `PUT /menus/:id/items` is a NESTED FULL
|
|
5
|
+
* REPLACE, so the editor owns the whole tree between loads and serializes it
|
|
6
|
+
* once on save. That is what lets this module fix the site tree's reorder
|
|
7
|
+
* semantics rather than inherit them.
|
|
8
|
+
*
|
|
9
|
+
* site-tree.tsx (naive): on drop, the dragged node is handed the *target's*
|
|
10
|
+
* `sortOrder`, leaving two siblings sharing an order and the rest of the group
|
|
11
|
+
* untouched — the resulting sequence depends on the server's tie-break.
|
|
12
|
+
*
|
|
13
|
+
* here: a drop splices the dragged subtree into its new sibling array, and
|
|
14
|
+
* sibling order IS array order. Serialization walks the arrays in order, so
|
|
15
|
+
* every affected group comes out resequenced 0..n by construction, with no
|
|
16
|
+
* duplicate or gapped positions possible.
|
|
17
|
+
*
|
|
18
|
+
* Everything in this module is pure and immutable (a move returns a new tree),
|
|
19
|
+
* which is what makes the drag semantics testable without dnd-kit.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Maximum nesting the nav renderers handle: 3 levels ⇒ depths 0, 1 and 2.
|
|
23
|
+
* Drops that would go deeper are clamped, never rejected (see {@link moveItem}).
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_MENU_DEPTH = 3;
|
|
26
|
+
/** Pixels of horizontal drag per depth step; also the row's indent unit. */
|
|
27
|
+
export const INDENT_WIDTH = 24;
|
|
28
|
+
let keySeq = 0;
|
|
29
|
+
/** Fresh client key for an item that has no server row yet. */
|
|
30
|
+
export function newItemKey() {
|
|
31
|
+
keySeq += 1;
|
|
32
|
+
return `new-${keySeq}`;
|
|
33
|
+
}
|
|
34
|
+
// ── Conversion ────────────────────────────────────────────────────────────────
|
|
35
|
+
/** Adopt the server's flagged admin tree as editor state. */
|
|
36
|
+
export function fromServerItems(nodes) {
|
|
37
|
+
return nodes.map(node => ({
|
|
38
|
+
key: node.id,
|
|
39
|
+
label: node.label,
|
|
40
|
+
description: node.description ?? '',
|
|
41
|
+
openInNewWindow: node.openInNewWindow,
|
|
42
|
+
contentId: node.contentId,
|
|
43
|
+
externalUrl: node.externalUrl,
|
|
44
|
+
href: node.href,
|
|
45
|
+
contentStatus: node.contentStatus,
|
|
46
|
+
contentMissing: node.contentMissing,
|
|
47
|
+
children: fromServerItems(node.children),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** A blank item of either kind, ready to append to the tree. */
|
|
51
|
+
export function makeItem(init) {
|
|
52
|
+
return {
|
|
53
|
+
key: newItemKey(),
|
|
54
|
+
description: '',
|
|
55
|
+
openInNewWindow: false,
|
|
56
|
+
contentId: null,
|
|
57
|
+
externalUrl: null,
|
|
58
|
+
href: null,
|
|
59
|
+
contentStatus: null,
|
|
60
|
+
contentMissing: false,
|
|
61
|
+
children: [],
|
|
62
|
+
...init,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Serialize to the nested `PUT /menus/:id/items` body.
|
|
67
|
+
*
|
|
68
|
+
* Optional keys are omitted rather than sent empty: the route's zod schema is
|
|
69
|
+
* `.strict()`, and an omitted `description` is meaningfully different from an
|
|
70
|
+
* empty string. Sibling order is array order — the resequencing contract.
|
|
71
|
+
*/
|
|
72
|
+
export function toPayload(items) {
|
|
73
|
+
return items.map(item => {
|
|
74
|
+
const payload = { label: item.label };
|
|
75
|
+
if (item.contentId)
|
|
76
|
+
payload.contentId = item.contentId;
|
|
77
|
+
else if (item.externalUrl)
|
|
78
|
+
payload.externalUrl = item.externalUrl;
|
|
79
|
+
if (item.description.trim())
|
|
80
|
+
payload.description = item.description;
|
|
81
|
+
if (item.openInNewWindow)
|
|
82
|
+
payload.openInNewWindow = true;
|
|
83
|
+
if (item.children.length > 0)
|
|
84
|
+
payload.children = toPayload(item.children);
|
|
85
|
+
return payload;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// ── Queries ───────────────────────────────────────────────────────────────────
|
|
89
|
+
/** Depth-first flatten; the render order and the drop-projection basis. */
|
|
90
|
+
export function flattenTree(items, parentKey = null, depth = 0) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
out.push({ item, key: item.key, depth, parentKey });
|
|
94
|
+
out.push(...flattenTree(item.children, item.key, depth + 1));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
export function findItem(items, key) {
|
|
99
|
+
for (const item of items) {
|
|
100
|
+
if (item.key === key)
|
|
101
|
+
return item;
|
|
102
|
+
const found = findItem(item.children, key);
|
|
103
|
+
if (found)
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** Total items at every depth. */
|
|
109
|
+
export function countItems(items) {
|
|
110
|
+
return items.reduce((n, item) => n + 1 + countItems(item.children), 0);
|
|
111
|
+
}
|
|
112
|
+
/** Levels of nesting BELOW an item (a leaf is 0). */
|
|
113
|
+
export function subtreeHeight(item) {
|
|
114
|
+
if (item.children.length === 0)
|
|
115
|
+
return 0;
|
|
116
|
+
return 1 + Math.max(...item.children.map(subtreeHeight));
|
|
117
|
+
}
|
|
118
|
+
/** Items with neither a content link nor a URL — unsavable, and always broken. */
|
|
119
|
+
export function itemsWithoutTarget(items) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const item of items) {
|
|
122
|
+
if (!item.contentId && !item.externalUrl)
|
|
123
|
+
out.push(item);
|
|
124
|
+
out.push(...itemsWithoutTarget(item.children));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
// ── Mutations (all immutable) ─────────────────────────────────────────────────
|
|
129
|
+
export function updateItem(items, key, patch) {
|
|
130
|
+
return items.map(item => item.key === key
|
|
131
|
+
? { ...item, ...patch }
|
|
132
|
+
: { ...item, children: updateItem(item.children, key, patch) });
|
|
133
|
+
}
|
|
134
|
+
/** Remove an item AND its subtree. Returns the new tree plus what was cut. */
|
|
135
|
+
export function removeItem(items, key) {
|
|
136
|
+
let removed = null;
|
|
137
|
+
const walk = (list) => {
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const item of list) {
|
|
140
|
+
if (item.key === key) {
|
|
141
|
+
removed = item;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
out.push({ ...item, children: walk(item.children) });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
};
|
|
148
|
+
const next = walk(items);
|
|
149
|
+
return { items: next, removed };
|
|
150
|
+
}
|
|
151
|
+
/** Splice an item into a parent's children (root when `parentKey` is null). */
|
|
152
|
+
export function insertItem(items, parentKey, index, item) {
|
|
153
|
+
if (parentKey === null) {
|
|
154
|
+
const next = [...items];
|
|
155
|
+
next.splice(clamp(index, 0, next.length), 0, item);
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
return items.map(candidate => {
|
|
159
|
+
if (candidate.key === parentKey) {
|
|
160
|
+
const children = [...candidate.children];
|
|
161
|
+
children.splice(clamp(index, 0, children.length), 0, item);
|
|
162
|
+
return { ...candidate, children };
|
|
163
|
+
}
|
|
164
|
+
return { ...candidate, children: insertItem(candidate.children, parentKey, index, item) };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Append a new item at the end of the root list. */
|
|
168
|
+
export function appendItem(items, item) {
|
|
169
|
+
return [...items, item];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolve a drop into a concrete position.
|
|
173
|
+
*
|
|
174
|
+
* Vertical position comes from the item dropped ON (dnd-kit's `over`), depth
|
|
175
|
+
* from how far the pointer travelled horizontally (`offsetX`) — the standard
|
|
176
|
+
* dnd-kit sortable-tree projection, which is the only way a flat sortable list
|
|
177
|
+
* can express "reparent" as well as "reorder".
|
|
178
|
+
*
|
|
179
|
+
* The projected depth is clamped three ways, so a drop is always *graceful*:
|
|
180
|
+
* - it can be at most one level deeper than the row above it (you cannot
|
|
181
|
+
* create an orphan level),
|
|
182
|
+
* - never shallower than the row below it (which would jump that row's group),
|
|
183
|
+
* - and never so deep that the dragged item's own subtree would exceed
|
|
184
|
+
* {@link MAX_MENU_DEPTH}.
|
|
185
|
+
*/
|
|
186
|
+
export function projectDrop(items, activeKey, overKey, offsetX) {
|
|
187
|
+
if (activeKey === overKey)
|
|
188
|
+
return null;
|
|
189
|
+
const flat = flattenTree(items);
|
|
190
|
+
const activeIndex = flat.findIndex(f => f.key === activeKey);
|
|
191
|
+
const overIndex = flat.findIndex(f => f.key === overKey);
|
|
192
|
+
if (activeIndex === -1 || overIndex === -1)
|
|
193
|
+
return null;
|
|
194
|
+
const active = flat[activeIndex];
|
|
195
|
+
const moved = active.item;
|
|
196
|
+
// Dropping an item into its own subtree would detach the tree from its root.
|
|
197
|
+
if (findItem(moved.children, overKey))
|
|
198
|
+
return null;
|
|
199
|
+
// Project against the tree WITHOUT the dragged subtree — that is the list the
|
|
200
|
+
// item is landing in, and it keeps indices honest when dragging downward.
|
|
201
|
+
const remaining = flattenTree(removeItem(items, activeKey).items);
|
|
202
|
+
const overRemainingIndex = remaining.findIndex(f => f.key === overKey);
|
|
203
|
+
if (overRemainingIndex === -1)
|
|
204
|
+
return null;
|
|
205
|
+
// Dragging down takes the slot AFTER the row dropped on; dragging up takes
|
|
206
|
+
// its slot, pushing it down. (dnd-kit's arrayMove, expressed on the tree.)
|
|
207
|
+
const insertIndex = activeIndex < overIndex ? overRemainingIndex + 1 : overRemainingIndex;
|
|
208
|
+
const previous = insertIndex > 0 ? remaining[insertIndex - 1] : undefined;
|
|
209
|
+
const next = remaining[insertIndex];
|
|
210
|
+
const maxDepth = previous ? previous.depth + 1 : 0;
|
|
211
|
+
const minDepth = next ? next.depth : 0;
|
|
212
|
+
const heightCap = MAX_MENU_DEPTH - 1 - subtreeHeight(moved);
|
|
213
|
+
const projected = active.depth + Math.round(offsetX / INDENT_WIDTH);
|
|
214
|
+
// Depth cap wins over `minDepth`: a too-deep drop lands shallower rather than
|
|
215
|
+
// being refused, and the tree stays valid either way.
|
|
216
|
+
const depth = Math.min(clamp(projected, minDepth, maxDepth), Math.max(heightCap, 0));
|
|
217
|
+
const parentKey = resolveParentKey(remaining, insertIndex, depth);
|
|
218
|
+
const index = siblingIndex(remaining, insertIndex, parentKey);
|
|
219
|
+
return { parentKey, index, depth };
|
|
220
|
+
}
|
|
221
|
+
/** Apply a drop. Returns the original tree unchanged when it cannot land. */
|
|
222
|
+
export function moveItem(items, activeKey, overKey, offsetX) {
|
|
223
|
+
const target = projectDrop(items, activeKey, overKey, offsetX);
|
|
224
|
+
if (!target)
|
|
225
|
+
return items;
|
|
226
|
+
const { items: without, removed } = removeItem(items, activeKey);
|
|
227
|
+
if (!removed)
|
|
228
|
+
return items;
|
|
229
|
+
return insertItem(without, target.parentKey, target.index, removed);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Keyboard/button equivalent of dragging right: become the last child of the
|
|
233
|
+
* previous sibling. No-op for a first child, or when the move would exceed
|
|
234
|
+
* {@link MAX_MENU_DEPTH}.
|
|
235
|
+
*/
|
|
236
|
+
export function indentItem(items, key) {
|
|
237
|
+
const flat = flattenTree(items);
|
|
238
|
+
const entry = flat.find(f => f.key === key);
|
|
239
|
+
if (!entry)
|
|
240
|
+
return items;
|
|
241
|
+
const siblings = flat.filter(f => f.parentKey === entry.parentKey);
|
|
242
|
+
const position = siblings.findIndex(f => f.key === key);
|
|
243
|
+
const previous = position > 0 ? siblings[position - 1] : undefined;
|
|
244
|
+
if (!previous)
|
|
245
|
+
return items;
|
|
246
|
+
if (entry.depth + 1 + subtreeHeight(entry.item) > MAX_MENU_DEPTH - 1)
|
|
247
|
+
return items;
|
|
248
|
+
const { items: without, removed } = removeItem(items, key);
|
|
249
|
+
if (!removed)
|
|
250
|
+
return items;
|
|
251
|
+
return insertItem(without, previous.key, previous.item.children.length, removed);
|
|
252
|
+
}
|
|
253
|
+
/** Become the next sibling of the current parent. No-op at the root level. */
|
|
254
|
+
export function outdentItem(items, key) {
|
|
255
|
+
const flat = flattenTree(items);
|
|
256
|
+
const entry = flat.find(f => f.key === key);
|
|
257
|
+
if (!entry || entry.parentKey === null)
|
|
258
|
+
return items;
|
|
259
|
+
const parent = flat.find(f => f.key === entry.parentKey);
|
|
260
|
+
const grandParentKey = parent.parentKey;
|
|
261
|
+
const parentSiblings = flat.filter(f => f.parentKey === grandParentKey);
|
|
262
|
+
const parentPosition = parentSiblings.findIndex(f => f.key === parent.key);
|
|
263
|
+
const { items: without, removed } = removeItem(items, key);
|
|
264
|
+
if (!removed)
|
|
265
|
+
return items;
|
|
266
|
+
return insertItem(without, grandParentKey, parentPosition + 1, removed);
|
|
267
|
+
}
|
|
268
|
+
// ── Internals ─────────────────────────────────────────────────────────────────
|
|
269
|
+
function clamp(value, min, max) {
|
|
270
|
+
return Math.min(Math.max(value, min), max);
|
|
271
|
+
}
|
|
272
|
+
/** The nearest row above the insertion point that sits one level shallower. */
|
|
273
|
+
function resolveParentKey(flat, insertIndex, depth) {
|
|
274
|
+
if (depth === 0)
|
|
275
|
+
return null;
|
|
276
|
+
for (let i = insertIndex - 1; i >= 0; i--) {
|
|
277
|
+
const candidate = flat[i];
|
|
278
|
+
if (candidate.depth === depth - 1)
|
|
279
|
+
return candidate.key;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
/** How many of the target parent's children already sit above the drop. */
|
|
284
|
+
function siblingIndex(flat, insertIndex, parentKey) {
|
|
285
|
+
let index = 0;
|
|
286
|
+
for (let i = 0; i < insertIndex; i++) {
|
|
287
|
+
if (flat[i].parentKey === parentKey)
|
|
288
|
+
index += 1;
|
|
289
|
+
}
|
|
290
|
+
return index;
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=menu-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-tree.js","sourceRoot":"","sources":["../../../../src/admin/surfaces/menus/menu-tree.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data helpers for the admin Menus surfaces (#112).
|
|
3
|
+
*
|
|
4
|
+
* Thin, typed wrappers over the menu REST surface shipped with #110. Every
|
|
5
|
+
* helper takes the cookie-aware `useAdminContext().fetch`, which appends the
|
|
6
|
+
* active `?siteId=` itself — surfaces never thread the site id by hand.
|
|
7
|
+
*
|
|
8
|
+
* The wire types below MIRROR the server shapes (`MenuRow` /
|
|
9
|
+
* `MenuItemAdminNode` in `db/menu-queries.ts`, `MenuItemPayload` in
|
|
10
|
+
* `api/menu-routes.ts`) rather than importing them: the DB types carry `Date`
|
|
11
|
+
* columns that arrive here as JSON strings, and a client surface must never
|
|
12
|
+
* reach into a module that pulls drizzle/zod into its chunk.
|
|
13
|
+
*/
|
|
14
|
+
import type { FetchLike } from '../../content-editor-api.js';
|
|
15
|
+
/** A menu row as returned by `GET /menus` (timestamps JSON-serialized). */
|
|
16
|
+
export interface MenuSummary {
|
|
17
|
+
id: string;
|
|
18
|
+
siteId: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
name: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One item of the admin (flagged) tree from `GET /menus/:id/items`.
|
|
26
|
+
*
|
|
27
|
+
* `contentMissing` / `resolvable` are the server's live resolution verdict: an
|
|
28
|
+
* item whose target was deleted or unpublished still appears here — that is the
|
|
29
|
+
* whole point of the admin read — so the editor can show it as broken instead
|
|
30
|
+
* of silently dropping it the way the public resolved endpoint does.
|
|
31
|
+
*/
|
|
32
|
+
export interface MenuItemNode {
|
|
33
|
+
id: string;
|
|
34
|
+
menuId: string;
|
|
35
|
+
parentId: string | null;
|
|
36
|
+
sortOrder: number;
|
|
37
|
+
label: string;
|
|
38
|
+
contentId: string | null;
|
|
39
|
+
externalUrl: string | null;
|
|
40
|
+
openInNewWindow: boolean;
|
|
41
|
+
description: string | null;
|
|
42
|
+
href: string | null;
|
|
43
|
+
contentStatus: string | null;
|
|
44
|
+
contentMissing: boolean;
|
|
45
|
+
resolvable: boolean;
|
|
46
|
+
children: MenuItemNode[];
|
|
47
|
+
}
|
|
48
|
+
/** One item of the nested full-replace body sent to `PUT /menus/:id/items`. */
|
|
49
|
+
export interface MenuItemPayload {
|
|
50
|
+
label: string;
|
|
51
|
+
contentId?: string;
|
|
52
|
+
externalUrl?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
openInNewWindow?: boolean;
|
|
55
|
+
children?: MenuItemPayload[];
|
|
56
|
+
}
|
|
57
|
+
/** A node of `GET /tree` — the admin content tree the page picker browses. */
|
|
58
|
+
export interface ContentTreeNode {
|
|
59
|
+
id: string;
|
|
60
|
+
parentId: string | null;
|
|
61
|
+
title: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
path: string;
|
|
64
|
+
depth: number;
|
|
65
|
+
sortOrder: number;
|
|
66
|
+
isNav: boolean;
|
|
67
|
+
isLocked: boolean;
|
|
68
|
+
status: string;
|
|
69
|
+
contentTypeName: string;
|
|
70
|
+
children: ContentTreeNode[];
|
|
71
|
+
}
|
|
72
|
+
export declare function loadMenus(fetchFn: FetchLike): Promise<MenuSummary[]>;
|
|
73
|
+
export declare function createMenu(fetchFn: FetchLike, payload: {
|
|
74
|
+
name: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
}): Promise<MenuSummary>;
|
|
77
|
+
export declare function updateMenu(fetchFn: FetchLike, id: string, payload: {
|
|
78
|
+
name?: string;
|
|
79
|
+
slug?: string;
|
|
80
|
+
}): Promise<MenuSummary>;
|
|
81
|
+
export declare function deleteMenu(fetchFn: FetchLike, id: string): Promise<void>;
|
|
82
|
+
export declare function loadMenuItems(fetchFn: FetchLike, id: string): Promise<MenuItemNode[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Full nested replace. The server rewrites the whole item tree in one
|
|
85
|
+
* transaction, so the editor never issues per-item PATCHes and a save is
|
|
86
|
+
* idempotent — sending the same tree twice leaves the menu identical.
|
|
87
|
+
*/
|
|
88
|
+
export declare function saveMenuItems(fetchFn: FetchLike, id: string, items: MenuItemPayload[]): Promise<MenuItemNode[]>;
|
|
89
|
+
export declare function loadContentTree(fetchFn: FetchLike): Promise<ContentTreeNode[]>;
|
|
90
|
+
/**
|
|
91
|
+
* `id -> nav label` from `GET /nav`, which resolves each node's `menuTitle`
|
|
92
|
+
* (falling back to its title). The picker seeds a new item's label from this so
|
|
93
|
+
* a page whose editor set a short `menuTitle` ("Pricing") enters the menu under
|
|
94
|
+
* that name rather than its long page title — the same rule the site-tree-driven
|
|
95
|
+
* nav applies. Nodes absent from the nav tree (unpublished / `isNav: false`)
|
|
96
|
+
* simply fall back to their content title.
|
|
97
|
+
*/
|
|
98
|
+
export declare function loadNavLabels(fetchFn: FetchLike): Promise<Record<string, string>>;
|
|
99
|
+
/** Total number of items in a menu tree, at every depth. */
|
|
100
|
+
export declare function countMenuItems(items: MenuItemNode[]): number;
|
|
101
|
+
//# sourceMappingURL=menus-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menus-data.d.ts","sourceRoot":"","sources":["../../../../src/admin/surfaces/menus/menus-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data helpers for the admin Menus surfaces (#112).
|
|
3
|
+
*
|
|
4
|
+
* Thin, typed wrappers over the menu REST surface shipped with #110. Every
|
|
5
|
+
* helper takes the cookie-aware `useAdminContext().fetch`, which appends the
|
|
6
|
+
* active `?siteId=` itself — surfaces never thread the site id by hand.
|
|
7
|
+
*
|
|
8
|
+
* The wire types below MIRROR the server shapes (`MenuRow` /
|
|
9
|
+
* `MenuItemAdminNode` in `db/menu-queries.ts`, `MenuItemPayload` in
|
|
10
|
+
* `api/menu-routes.ts`) rather than importing them: the DB types carry `Date`
|
|
11
|
+
* columns that arrive here as JSON strings, and a client surface must never
|
|
12
|
+
* reach into a module that pulls drizzle/zod into its chunk.
|
|
13
|
+
*/
|
|
14
|
+
// ── Response helpers (same contract as the sibling surfaces' data modules) ────
|
|
15
|
+
async function readData(res) {
|
|
16
|
+
if (!res.ok)
|
|
17
|
+
throw new Error(await errorMessage(res));
|
|
18
|
+
const body = await res.json();
|
|
19
|
+
return body.data;
|
|
20
|
+
}
|
|
21
|
+
async function expectOk(res) {
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new Error(await errorMessage(res));
|
|
24
|
+
}
|
|
25
|
+
async function errorMessage(res) {
|
|
26
|
+
let message = `Request failed (${res.status})`;
|
|
27
|
+
try {
|
|
28
|
+
const body = await res.json();
|
|
29
|
+
message = body?.error?.message ?? message;
|
|
30
|
+
// Validation errors carry per-field detail; surface the first one, since a
|
|
31
|
+
// menu save that fails on one item should name that item, not "400".
|
|
32
|
+
const first = body?.error?.details?.[0];
|
|
33
|
+
if (first?.message)
|
|
34
|
+
message = first.path ? `${first.path}: ${first.message}` : first.message;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* keep default */
|
|
38
|
+
}
|
|
39
|
+
return message;
|
|
40
|
+
}
|
|
41
|
+
// ── Menus ─────────────────────────────────────────────────────────────────────
|
|
42
|
+
export async function loadMenus(fetchFn) {
|
|
43
|
+
return readData(await fetchFn('/api/v1/menus'));
|
|
44
|
+
}
|
|
45
|
+
export async function createMenu(fetchFn, payload) {
|
|
46
|
+
return readData(await fetchFn('/api/v1/menus', { method: 'POST', body: JSON.stringify(payload) }));
|
|
47
|
+
}
|
|
48
|
+
export async function updateMenu(fetchFn, id, payload) {
|
|
49
|
+
return readData(await fetchFn(`/api/v1/menus/${id}`, { method: 'PUT', body: JSON.stringify(payload) }));
|
|
50
|
+
}
|
|
51
|
+
export async function deleteMenu(fetchFn, id) {
|
|
52
|
+
await expectOk(await fetchFn(`/api/v1/menus/${id}`, { method: 'DELETE' }));
|
|
53
|
+
}
|
|
54
|
+
// ── Menu items ────────────────────────────────────────────────────────────────
|
|
55
|
+
export async function loadMenuItems(fetchFn, id) {
|
|
56
|
+
return readData(await fetchFn(`/api/v1/menus/${id}/items`));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Full nested replace. The server rewrites the whole item tree in one
|
|
60
|
+
* transaction, so the editor never issues per-item PATCHes and a save is
|
|
61
|
+
* idempotent — sending the same tree twice leaves the menu identical.
|
|
62
|
+
*/
|
|
63
|
+
export async function saveMenuItems(fetchFn, id, items) {
|
|
64
|
+
return readData(await fetchFn(`/api/v1/menus/${id}/items`, {
|
|
65
|
+
method: 'PUT',
|
|
66
|
+
body: JSON.stringify({ items }),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
// ── Content lookup (page picker) ──────────────────────────────────────────────
|
|
70
|
+
export async function loadContentTree(fetchFn) {
|
|
71
|
+
return readData(await fetchFn('/api/v1/tree'));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `id -> nav label` from `GET /nav`, which resolves each node's `menuTitle`
|
|
75
|
+
* (falling back to its title). The picker seeds a new item's label from this so
|
|
76
|
+
* a page whose editor set a short `menuTitle` ("Pricing") enters the menu under
|
|
77
|
+
* that name rather than its long page title — the same rule the site-tree-driven
|
|
78
|
+
* nav applies. Nodes absent from the nav tree (unpublished / `isNav: false`)
|
|
79
|
+
* simply fall back to their content title.
|
|
80
|
+
*/
|
|
81
|
+
export async function loadNavLabels(fetchFn) {
|
|
82
|
+
const tree = await readData(await fetchFn('/api/v1/nav'));
|
|
83
|
+
const labels = {};
|
|
84
|
+
const walk = (nodes) => {
|
|
85
|
+
for (const n of nodes) {
|
|
86
|
+
labels[n.id] = n.title;
|
|
87
|
+
walk(n.children);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(tree);
|
|
91
|
+
return labels;
|
|
92
|
+
}
|
|
93
|
+
// ── Derived helpers ───────────────────────────────────────────────────────────
|
|
94
|
+
/** Total number of items in a menu tree, at every depth. */
|
|
95
|
+
export function countMenuItems(items) {
|
|
96
|
+
return items.reduce((n, item) => n + 1 + countMenuItems(item.children), 0);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=menus-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menus-data.js","sourceRoot":"","sources":["../../../../src/admin/surfaces/menus/menus-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menus-list.d.ts","sourceRoot":"","sources":["../../../../src/admin/surfaces/menus/menus-list.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAuCrD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,MAAM,EAAE,YAAY,2CA+N5D"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `menus.list` surface — the site's named navigation menus (#112).
|
|
5
|
+
*
|
|
6
|
+
* A menu is the owner-managed alternative to nav derived from the site tree:
|
|
7
|
+
* create one (conventionally `main` / `footer`), then edit its items in the
|
|
8
|
+
* menu editor. Layout/list conventions follow the sibling list surfaces
|
|
9
|
+
* (Layouts, Feeds): page header + primary action, `kywi-table`, row actions.
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import { useAdminContext } from '../../context.js';
|
|
13
|
+
import { LoadingState, ErrorState, ConfirmDialog, Modal, slugify } from '../../primitives.js';
|
|
14
|
+
import { loadMenus, loadMenuItems, createMenu, updateMenu, deleteMenu, countMenuItems, } from './menus-data.js';
|
|
15
|
+
function emptyForm(menu) {
|
|
16
|
+
return {
|
|
17
|
+
menu,
|
|
18
|
+
name: menu?.name ?? '',
|
|
19
|
+
slug: menu?.slug ?? '',
|
|
20
|
+
slugTouched: menu !== null,
|
|
21
|
+
error: null,
|
|
22
|
+
busy: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export default function MenusListSurface(_props) {
|
|
26
|
+
const { fetch: adminFetch, navigate } = useAdminContext();
|
|
27
|
+
const [menus, setMenus] = useState([]);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const [form, setForm] = useState(null);
|
|
31
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
32
|
+
const [deleting, setDeleting] = useState(false);
|
|
33
|
+
const load = useCallback(async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const rows = await loadMenus(adminFetch);
|
|
38
|
+
const withCounts = await Promise.all(rows.map(async (row) => {
|
|
39
|
+
try {
|
|
40
|
+
return { ...row, itemCount: countMenuItems(await loadMenuItems(adminFetch, row.id)) };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A menu whose items fail to load is still listed — losing the whole
|
|
44
|
+
// page over one unreadable count would be the worse failure.
|
|
45
|
+
return { ...row, itemCount: 0 };
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
setMenus(withCounts);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
setError(e instanceof Error ? e.message : 'Failed to load menus');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
}
|
|
56
|
+
}, [adminFetch]);
|
|
57
|
+
useEffect(() => { load(); }, [load]);
|
|
58
|
+
const handleNameChange = useCallback((value) => {
|
|
59
|
+
setForm(prev => prev ? { ...prev, name: value, slug: prev.slugTouched ? prev.slug : slugify(value) } : prev);
|
|
60
|
+
}, []);
|
|
61
|
+
const handleSubmit = useCallback(async () => {
|
|
62
|
+
if (!form)
|
|
63
|
+
return;
|
|
64
|
+
const name = form.name.trim();
|
|
65
|
+
const slug = form.slug.trim();
|
|
66
|
+
if (!name) {
|
|
67
|
+
setForm({ ...form, error: 'A menu name is required.' });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!slug) {
|
|
71
|
+
setForm({ ...form, error: 'A menu slug is required.' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setForm({ ...form, busy: true, error: null });
|
|
75
|
+
try {
|
|
76
|
+
if (form.menu)
|
|
77
|
+
await updateMenu(adminFetch, form.menu.id, { name, slug });
|
|
78
|
+
else
|
|
79
|
+
await createMenu(adminFetch, { name, slug });
|
|
80
|
+
setForm(null);
|
|
81
|
+
await load();
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
setForm({ ...form, busy: false, error: e instanceof Error ? e.message : 'Save failed' });
|
|
85
|
+
}
|
|
86
|
+
}, [form, adminFetch, load]);
|
|
87
|
+
const handleDelete = useCallback(async () => {
|
|
88
|
+
if (!pendingDelete)
|
|
89
|
+
return;
|
|
90
|
+
setDeleting(true);
|
|
91
|
+
try {
|
|
92
|
+
await deleteMenu(adminFetch, pendingDelete.id);
|
|
93
|
+
setPendingDelete(null);
|
|
94
|
+
await load();
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
setError(e instanceof Error ? e.message : 'Delete failed');
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setDeleting(false);
|
|
101
|
+
}
|
|
102
|
+
}, [pendingDelete, adminFetch, load]);
|
|
103
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kywi-admin-page-header", children: [_jsxs("div", { children: [_jsx("h1", { children: "Menus" }), _jsxs("p", { className: "kywi-text-muted", children: ["Named navigation menus. Nav modules render a menu by its slug \u2014 ", _jsx("code", { children: "main" }), " for the header, ", _jsx("code", { children: "footer" }), " for the footer \u2014 so editing a menu here changes the site\u2019s navigation everywhere it appears."] })] }), _jsx("div", { className: "kywi-admin-button-row", children: _jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: () => setForm(emptyForm(null)), children: "New Menu" }) })] }), loading ? (_jsx(LoadingState, { label: "Loading menus\u2026" })) : error ? (_jsx(ErrorState, { message: error, onRetry: load })) : menus.length === 0 ? (_jsxs("div", { className: "kywi-empty-state", children: [_jsxs("p", { children: ["No menus yet. Create one named ", _jsx("code", { children: "main" }), " to take over the site header."] }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: () => setForm(emptyForm(null)), children: "Create your first menu" })] })) : (_jsx("div", { className: "kywi-table-scroll", children: _jsxs("table", { className: "kywi-table kywi-menus-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), _jsx("th", { children: "Slug" }), _jsx("th", { children: "Items" }), _jsx("th", { children: "Updated" }), _jsx("th", { className: "kywi-col-actions", children: "Actions" })] }) }), _jsx("tbody", { children: menus.map(menu => (_jsxs("tr", { children: [_jsx("td", { children: _jsx("button", { type: "button", className: "kywi-link-button", onClick: () => navigate(`/admin/menus/${menu.id}`), children: menu.name }) }), _jsx("td", { children: _jsx("code", { children: menu.slug }) }), _jsx("td", { children: menu.itemCount }), _jsx("td", { children: menu.updatedAt ? new Date(menu.updatedAt).toLocaleDateString() : '—' }), _jsxs("td", { className: "kywi-col-actions", children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-ghost kywi-btn-sm", onClick: () => navigate(`/admin/menus/${menu.id}`), children: "Edit items" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-ghost kywi-btn-sm", onClick: () => setForm(emptyForm(menu)), children: "Rename" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-danger kywi-btn-sm", onClick: () => setPendingDelete(menu), children: "Delete" })] })] }, menu.id))) })] }) })), form && (_jsxs(Modal, { title: form.menu ? `Rename “${form.menu.name}”` : 'New menu', onClose: () => setForm(null), labelledBy: "kywi-menu-form-title", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "kywi-menu-name", className: "kywi-field-label", children: "Menu name" }), _jsx("input", { id: "kywi-menu-name", type: "text", className: "kywi-input", value: form.name, placeholder: "Main navigation", onChange: e => handleNameChange(e.target.value) })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "kywi-menu-slug", className: "kywi-field-label", children: "Slug" }), _jsx("input", { id: "kywi-menu-slug", type: "text", className: "kywi-input", value: form.slug, placeholder: "main", onChange: e => setForm(prev => prev && { ...prev, slugTouched: true, slug: slugify(e.target.value) }) }), _jsxs("span", { className: "kywi-field-hint", children: ["Nav modules reference this slug. ", _jsx("code", { children: "main" }), " and ", _jsx("code", { children: "footer" }), " are the slugs a generated site looks for."] })] }), form.error && _jsx(ErrorState, { message: form.error }), _jsxs("div", { className: "kywi-admin-form-actions", children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-secondary", onClick: () => setForm(null), disabled: form.busy, children: "Cancel" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: handleSubmit, disabled: form.busy, children: form.busy ? 'Saving…' : form.menu ? 'Save changes' : 'Create menu' })] })] })), pendingDelete && (_jsx(ConfirmDialog, { title: "Delete menu", message: _jsxs(_Fragment, { children: ["Delete \u201C", pendingDelete.name, "\u201D and its ", pendingDelete.itemCount, " item", pendingDelete.itemCount === 1 ? '' : 's', "? Any nav module pointing at", ' ', _jsx("code", { children: pendingDelete.slug }), " reverts to its fallback \u2014 a generated site header falls back to the site tree. This cannot be undone."] }), busy: deleting, onCancel: () => setPendingDelete(null), onConfirm: handleDelete }))] }));
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=menus-list.js.map
|