@asteby/metacore-runtime-react 39.2.0 → 39.2.1
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/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SidebarLayoutManager —
|
|
3
|
-
*
|
|
4
|
-
* PermissionsManager. Hosts wire fetchers to /api/org/sidebar-layout.
|
|
2
|
+
* SidebarLayoutManager — Discord-style org sidebar editor.
|
|
3
|
+
* The canvas IS the sidebar: sections, folders, leaves — drag, rename, nest.
|
|
5
4
|
*/
|
|
6
5
|
import * as React from 'react';
|
|
7
6
|
import { type CatalogLeaf, type SidebarLayoutDoc, type SidebarLayoutNode } from './apply-sidebar-layout';
|
|
@@ -9,12 +8,7 @@ export interface SidebarLayoutManagerProps {
|
|
|
9
8
|
loadLayout: () => Promise<SidebarLayoutDoc>;
|
|
10
9
|
saveLayout: (doc: SidebarLayoutDoc) => Promise<void>;
|
|
11
10
|
resetLayout: () => Promise<void>;
|
|
12
|
-
/** Live catalog of every leaf the shell can show (core + addons). */
|
|
13
11
|
catalog: CatalogLeaf[];
|
|
14
|
-
/**
|
|
15
|
-
* Optional: current live nav groups — used by "Partir del menú actual" to
|
|
16
|
-
* seed a tree when the org has no layout yet.
|
|
17
|
-
*/
|
|
18
12
|
currentGroups?: Array<{
|
|
19
13
|
title: string;
|
|
20
14
|
items: Array<{
|
|
@@ -28,9 +22,11 @@ export interface SidebarLayoutManagerProps {
|
|
|
28
22
|
}>;
|
|
29
23
|
}>;
|
|
30
24
|
}>;
|
|
25
|
+
/** Resolve i18n keys / raw titles to human labels. */
|
|
26
|
+
resolveLabel?: (title: string) => string;
|
|
31
27
|
title?: string;
|
|
32
28
|
description?: string;
|
|
33
29
|
}
|
|
34
|
-
export declare function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, title, description, }: SidebarLayoutManagerProps): React.JSX.Element;
|
|
30
|
+
export declare function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, resolveLabel, title, description, }: SidebarLayoutManagerProps): React.JSX.Element;
|
|
35
31
|
export type { CatalogLeaf, SidebarLayoutDoc, SidebarLayoutNode };
|
|
36
32
|
//# sourceMappingURL=sidebar-layout-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar-layout-manager.d.ts","sourceRoot":"","sources":["../src/sidebar-layout-manager.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sidebar-layout-manager.d.ts","sourceRoot":"","sources":["../src/sidebar-layout-manager.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAwC9B,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC3C,UAAU,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,KAAK,CAAC;YACX,KAAK,EAAE,MAAM,CAAA;YACb,GAAG,EAAE,MAAM,CAAA;YACX,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,KAAK,CAAC,EAAE,KAAK,CAAC;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7D,CAAC,CAAA;KACH,CAAC,CAAA;IACF,sDAAsD;IACtD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AA4TD,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAuB,EACvB,KAAsB,EACtB,WAA8F,GAC/F,EAAE,yBAAyB,qBAgb3B;AAED,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAA"}
|
|
@@ -1,14 +1,84 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* SidebarLayoutManager —
|
|
4
|
-
*
|
|
5
|
-
* PermissionsManager. Hosts wire fetchers to /api/org/sidebar-layout.
|
|
3
|
+
* SidebarLayoutManager — Discord-style org sidebar editor.
|
|
4
|
+
* The canvas IS the sidebar: sections, folders, leaves — drag, rename, nest.
|
|
6
5
|
*/
|
|
7
6
|
import * as React from 'react';
|
|
8
|
-
import {
|
|
7
|
+
import { DndContext, DragOverlay, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
|
|
8
|
+
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
|
|
9
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
10
|
+
import { ChevronDown, ChevronRight, FolderPlus, GripVertical, Pencil, Plus, RotateCcw, Save, Trash2, Eye, EyeOff, Hash, Folder, } from 'lucide-react';
|
|
9
11
|
import { toast } from 'sonner';
|
|
10
|
-
import { Button,
|
|
11
|
-
import {
|
|
12
|
+
import { Button, Input, ScrollArea, } from '@asteby/metacore-ui/primitives';
|
|
13
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
14
|
+
import { collectFolderKeys, folderAccessCapability, layoutTreeFromNavGroups, leafRefFor, slugifyFolderKey, } from './apply-sidebar-layout';
|
|
15
|
+
function pathKey(path) {
|
|
16
|
+
return path.length ? path.join('.') : 'root';
|
|
17
|
+
}
|
|
18
|
+
function getAt(tree, path) {
|
|
19
|
+
let cur = tree;
|
|
20
|
+
let node = null;
|
|
21
|
+
for (const i of path) {
|
|
22
|
+
node = cur[i] ?? null;
|
|
23
|
+
if (!node)
|
|
24
|
+
return null;
|
|
25
|
+
cur = node.children ?? [];
|
|
26
|
+
}
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
function setChildren(tree, parentPath, children) {
|
|
30
|
+
if (parentPath.length === 0)
|
|
31
|
+
return children;
|
|
32
|
+
const clone = structuredClone(tree);
|
|
33
|
+
let cur = clone;
|
|
34
|
+
for (let d = 0; d < parentPath.length - 1; d++) {
|
|
35
|
+
const n = cur[parentPath[d]];
|
|
36
|
+
if (!n)
|
|
37
|
+
return tree;
|
|
38
|
+
n.children = n.children ?? [];
|
|
39
|
+
cur = n.children;
|
|
40
|
+
}
|
|
41
|
+
const parent = cur[parentPath[parentPath.length - 1]];
|
|
42
|
+
if (!parent)
|
|
43
|
+
return tree;
|
|
44
|
+
parent.children = children;
|
|
45
|
+
return clone;
|
|
46
|
+
}
|
|
47
|
+
function removeAt(tree, path) {
|
|
48
|
+
if (path.length === 0)
|
|
49
|
+
return { tree, node: null };
|
|
50
|
+
const parentPath = path.slice(0, -1);
|
|
51
|
+
const idx = path[path.length - 1];
|
|
52
|
+
const parentKids = parentPath.length === 0
|
|
53
|
+
? [...tree]
|
|
54
|
+
: [...(getAt(tree, parentPath)?.children ?? [])];
|
|
55
|
+
const [node] = parentKids.splice(idx, 1);
|
|
56
|
+
return { tree: setChildren(tree, parentPath, parentKids), node: node ?? null };
|
|
57
|
+
}
|
|
58
|
+
function insertAt(tree, parentPath, index, node) {
|
|
59
|
+
const kids = parentPath.length === 0
|
|
60
|
+
? [...tree]
|
|
61
|
+
: [...(getAt(tree, parentPath)?.children ?? [])];
|
|
62
|
+
const i = Math.max(0, Math.min(index, kids.length));
|
|
63
|
+
kids.splice(i, 0, node);
|
|
64
|
+
return setChildren(tree, parentPath, kids);
|
|
65
|
+
}
|
|
66
|
+
function updateAt(tree, path, patch) {
|
|
67
|
+
const clone = structuredClone(tree);
|
|
68
|
+
let cur = clone;
|
|
69
|
+
for (let d = 0; d < path.length - 1; d++) {
|
|
70
|
+
const n = cur[path[d]];
|
|
71
|
+
if (!n)
|
|
72
|
+
return tree;
|
|
73
|
+
n.children = n.children ?? [];
|
|
74
|
+
cur = n.children;
|
|
75
|
+
}
|
|
76
|
+
const i = path[path.length - 1];
|
|
77
|
+
if (!cur[i])
|
|
78
|
+
return tree;
|
|
79
|
+
cur[i] = { ...cur[i], ...patch };
|
|
80
|
+
return clone;
|
|
81
|
+
}
|
|
12
82
|
function collectUsedRefs(tree) {
|
|
13
83
|
const used = new Set();
|
|
14
84
|
const walk = (nodes) => {
|
|
@@ -22,86 +92,177 @@ function collectUsedRefs(tree) {
|
|
|
22
92
|
walk(tree);
|
|
23
93
|
return used;
|
|
24
94
|
}
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return n;
|
|
33
|
-
if (rest.length === 0)
|
|
34
|
-
return fn(n);
|
|
35
|
-
const kids = updateAt(n.children ?? [], rest, fn);
|
|
36
|
-
return { ...n, children: kids };
|
|
37
|
-
})
|
|
38
|
-
.filter((n) => n != null);
|
|
39
|
-
}
|
|
40
|
-
function removeAt(tree, path) {
|
|
41
|
-
return updateAt(tree, path, () => null).filter(Boolean);
|
|
95
|
+
function labelOf(node, catalogByRef, resolveLabel) {
|
|
96
|
+
if (node.type === 'leaf') {
|
|
97
|
+
const leaf = node.ref ? catalogByRef.get(node.ref) : undefined;
|
|
98
|
+
const raw = node.title || leaf?.title || node.ref || '…';
|
|
99
|
+
return resolveLabel(raw);
|
|
100
|
+
}
|
|
101
|
+
return resolveLabel(node.title || node.key || '…');
|
|
42
102
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
103
|
+
// ── Sortable row ──────────────────────────────────────────────────────────
|
|
104
|
+
function SortableRow({ id, depth, kind, label, hidden, collapsed, onToggleCollapse, onRename, onToggleHidden, onDelete, onAddFolder, onAddLeaf, unusedLeaves, children, }) {
|
|
105
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
|
106
|
+
const [editing, setEditing] = React.useState(false);
|
|
107
|
+
const [draft, setDraft] = React.useState(label);
|
|
108
|
+
const inputRef = React.useRef(null);
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (editing) {
|
|
111
|
+
setDraft(label);
|
|
112
|
+
requestAnimationFrame(() => inputRef.current?.select());
|
|
113
|
+
}
|
|
114
|
+
}, [editing, label]);
|
|
115
|
+
const style = {
|
|
116
|
+
transform: CSS.Transform.toString(transform),
|
|
117
|
+
transition,
|
|
118
|
+
opacity: isDragging ? 0.45 : hidden ? 0.45 : 1,
|
|
119
|
+
paddingLeft: 8 + depth * 14,
|
|
56
120
|
};
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
121
|
+
const isCategory = kind === 'group' || kind === 'folder';
|
|
122
|
+
const commitRename = () => {
|
|
123
|
+
const next = draft.trim();
|
|
124
|
+
setEditing(false);
|
|
125
|
+
if (next && next !== label)
|
|
126
|
+
onRename(next);
|
|
127
|
+
};
|
|
128
|
+
return (_jsxs("div", { ref: setNodeRef, style: style, className: "group/row", children: [_jsxs("div", { className: cn('flex items-center gap-1 rounded-md py-1 pr-1', isCategory ? 'mt-2 first:mt-0' : 'hover:bg-muted/50', isDragging && 'bg-muted/80'), children: [_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground cursor-grab px-0.5 active:cursor-grabbing", "aria-label": "Arrastrar", ...attributes, ...listeners, children: _jsx(GripVertical, { className: "size-3.5 opacity-40 group-hover/row:opacity-100" }) }), isCategory ? (_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground p-0.5", onClick: onToggleCollapse, children: collapsed ? (_jsx(ChevronRight, { className: "size-3.5" })) : (_jsx(ChevronDown, { className: "size-3.5" })) })) : (_jsx(Hash, { className: "text-muted-foreground size-3.5 shrink-0 opacity-60" })), kind === 'folder' ? (_jsx(Folder, { className: "size-3.5 shrink-0 text-amber-500/90" })) : null, editing ? (_jsx(Input, { ref: inputRef, value: draft, onChange: (e) => setDraft(e.target.value), onBlur: commitRename, onKeyDown: (e) => {
|
|
129
|
+
if (e.key === 'Enter')
|
|
130
|
+
commitRename();
|
|
131
|
+
if (e.key === 'Escape')
|
|
132
|
+
setEditing(false);
|
|
133
|
+
}, className: "h-7 flex-1 text-sm" })) : (_jsx("button", { type: "button", className: cn('min-w-0 flex-1 truncate text-left text-sm', isCategory
|
|
134
|
+
? 'text-muted-foreground text-[11px] font-semibold uppercase tracking-wider'
|
|
135
|
+
: 'font-medium'), onDoubleClick: () => setEditing(true), title: "Doble clic para renombrar", children: label })), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100", children: [kind === 'group' || kind === 'folder' ? (_jsxs(_Fragment, { children: [onAddFolder ? (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: "Nueva carpeta", onClick: onAddFolder, children: _jsx(FolderPlus, { className: "size-3" }) })) : null, onAddLeaf && unusedLeaves && unusedLeaves.length > 0 ? (_jsxs("select", { className: "border-input bg-background h-6 max-w-[110px] rounded border px-1 text-[10px]", defaultValue: "", title: "Agregar \u00EDtem", onChange: (e) => {
|
|
136
|
+
const ref = e.target.value;
|
|
137
|
+
e.target.value = '';
|
|
138
|
+
if (ref)
|
|
139
|
+
onAddLeaf(ref);
|
|
140
|
+
}, children: [_jsx("option", { value: "", children: "+ \u00EDtem" }), unusedLeaves.map((l) => (_jsx("option", { value: l.ref, children: l.title }, l.ref)))] })) : null] })) : null, _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: "Renombrar", onClick: () => setEditing(true), children: _jsx(Pencil, { className: "size-3" }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "size-6", title: hidden ? 'Mostrar' : 'Ocultar', onClick: onToggleHidden, children: hidden ? _jsx(EyeOff, { className: "size-3" }) : _jsx(Eye, { className: "size-3" }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", className: "text-destructive size-6", title: "Quitar", onClick: onDelete, children: _jsx(Trash2, { className: "size-3" }) })] })] }), !collapsed ? children : null] }));
|
|
73
141
|
}
|
|
74
|
-
|
|
142
|
+
// ── Manager ───────────────────────────────────────────────────────────────
|
|
143
|
+
export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, catalog, currentGroups, resolveLabel = (s) => s, title = 'Menú lateral', description = 'Arrastrá, renombrá y agrupá como en Discord. Así lo verá toda la organización.', }) {
|
|
75
144
|
const [tree, setTree] = React.useState([]);
|
|
76
145
|
const [baseline, setBaseline] = React.useState('[]');
|
|
77
146
|
const [loading, setLoading] = React.useState(true);
|
|
78
147
|
const [saving, setSaving] = React.useState(false);
|
|
79
|
-
const [
|
|
80
|
-
const [
|
|
148
|
+
const [collapsed, setCollapsed] = React.useState({});
|
|
149
|
+
const [activeId, setActiveId] = React.useState(null);
|
|
150
|
+
const [newCategory, setNewCategory] = React.useState('');
|
|
151
|
+
const catalogByRef = React.useMemo(() => {
|
|
152
|
+
const m = new Map();
|
|
153
|
+
for (const l of catalog) {
|
|
154
|
+
m.set(l.ref, { ...l, title: resolveLabel(l.title) });
|
|
155
|
+
for (const a of l.aliases ?? [])
|
|
156
|
+
m.set(a, m.get(l.ref));
|
|
157
|
+
}
|
|
158
|
+
return m;
|
|
159
|
+
}, [catalog, resolveLabel]);
|
|
160
|
+
const unused = React.useMemo(() => {
|
|
161
|
+
const used = collectUsedRefs(tree);
|
|
162
|
+
return catalog
|
|
163
|
+
.filter((l) => !used.has(l.ref) && !(l.aliases ?? []).some((a) => used.has(a)))
|
|
164
|
+
.map((l) => ({ ...l, title: resolveLabel(l.title) }));
|
|
165
|
+
}, [catalog, tree, resolveLabel]);
|
|
166
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
|
|
167
|
+
const seedFromCurrent = React.useCallback(() => {
|
|
168
|
+
if (!currentGroups?.length) {
|
|
169
|
+
toast.message('No hay menú actual para copiar');
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
return layoutTreeFromNavGroups(currentGroups, (u) => leafRefFor(u));
|
|
173
|
+
}, [currentGroups]);
|
|
81
174
|
const reload = React.useCallback(async () => {
|
|
82
175
|
setLoading(true);
|
|
83
176
|
try {
|
|
84
177
|
const doc = await loadLayout();
|
|
85
|
-
|
|
178
|
+
let t = doc.tree ?? [];
|
|
179
|
+
// Empty overlay → show live menu as editable draft (Discord empty-server feel).
|
|
180
|
+
if (t.length === 0 && currentGroups?.length) {
|
|
181
|
+
t = seedFromCurrent();
|
|
182
|
+
setBaseline('[]'); // still "unsaved overlay"
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
setBaseline(JSON.stringify(t));
|
|
186
|
+
}
|
|
86
187
|
setTree(t);
|
|
87
|
-
setBaseline(JSON.stringify(t));
|
|
88
188
|
}
|
|
89
189
|
catch (e) {
|
|
90
|
-
toast.error(e instanceof Error ? e.message : 'No se pudo cargar el
|
|
190
|
+
toast.error(e instanceof Error ? e.message : 'No se pudo cargar el menú');
|
|
91
191
|
}
|
|
92
192
|
finally {
|
|
93
193
|
setLoading(false);
|
|
94
194
|
}
|
|
95
|
-
}, [loadLayout]);
|
|
195
|
+
}, [loadLayout, currentGroups, seedFromCurrent]);
|
|
96
196
|
React.useEffect(() => {
|
|
97
197
|
void reload();
|
|
98
198
|
}, [reload]);
|
|
99
199
|
const dirty = JSON.stringify(tree) !== baseline;
|
|
100
|
-
const used = React.useMemo(() => collectUsedRefs(tree), [tree]);
|
|
101
|
-
const unused = React.useMemo(() => catalog.filter((l) => !used.has(l.ref) && !(l.aliases ?? []).some((a) => used.has(a))), [catalog, used]);
|
|
102
200
|
const folderKeys = React.useMemo(() => collectFolderKeys(tree), [tree]);
|
|
103
|
-
|
|
104
|
-
const
|
|
201
|
+
// Flat id → path index for DnD (ids are path keys)
|
|
202
|
+
const idToPath = React.useMemo(() => {
|
|
203
|
+
const map = new Map();
|
|
204
|
+
const walk = (nodes, parent) => {
|
|
205
|
+
nodes.forEach((_, i) => {
|
|
206
|
+
const path = [...parent, i];
|
|
207
|
+
map.set(pathKey(path), path);
|
|
208
|
+
const n = nodes[i];
|
|
209
|
+
if (n.children?.length)
|
|
210
|
+
walk(n.children, path);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
walk(tree, []);
|
|
214
|
+
return map;
|
|
215
|
+
}, [tree]);
|
|
216
|
+
const handleDragStart = (e) => {
|
|
217
|
+
setActiveId(String(e.active.id));
|
|
218
|
+
};
|
|
219
|
+
const handleDragEnd = (e) => {
|
|
220
|
+
setActiveId(null);
|
|
221
|
+
const { active, over } = e;
|
|
222
|
+
if (!over || active.id === over.id)
|
|
223
|
+
return;
|
|
224
|
+
const fromPath = idToPath.get(String(active.id));
|
|
225
|
+
const toPath = idToPath.get(String(over.id));
|
|
226
|
+
if (!fromPath || !toPath)
|
|
227
|
+
return;
|
|
228
|
+
// Same parent → reorder siblings
|
|
229
|
+
const fromParent = fromPath.slice(0, -1);
|
|
230
|
+
const toParent = toPath.slice(0, -1);
|
|
231
|
+
if (pathKey(fromParent) === pathKey(toParent)) {
|
|
232
|
+
const kids = fromParent.length === 0
|
|
233
|
+
? [...tree]
|
|
234
|
+
: [...(getAt(tree, fromParent)?.children ?? [])];
|
|
235
|
+
const oldIndex = fromPath[fromPath.length - 1];
|
|
236
|
+
const newIndex = toPath[toPath.length - 1];
|
|
237
|
+
setTree(setChildren(tree, fromParent, arrayMove(kids, oldIndex, newIndex)));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Different parent → move node under target's parent at target index
|
|
241
|
+
// (or into target if it's a folder/group)
|
|
242
|
+
const overNode = getAt(tree, toPath);
|
|
243
|
+
let destParent = toParent;
|
|
244
|
+
let destIndex = toPath[toPath.length - 1];
|
|
245
|
+
if (overNode && (overNode.type === 'group' || overNode.type === 'folder')) {
|
|
246
|
+
destParent = toPath;
|
|
247
|
+
destIndex = overNode.children?.length ?? 0;
|
|
248
|
+
}
|
|
249
|
+
// Prevent dropping into own descendant
|
|
250
|
+
const fromKey = pathKey(fromPath);
|
|
251
|
+
if (pathKey(destParent).startsWith(fromKey + '.') || pathKey(destParent) === fromKey) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
setTree((prev) => {
|
|
255
|
+
const { tree: without, node } = removeAt(prev, fromPath);
|
|
256
|
+
if (!node)
|
|
257
|
+
return prev;
|
|
258
|
+
// Recompute dest index if we removed from earlier sibling in same parent
|
|
259
|
+
let idx = destIndex;
|
|
260
|
+
if (pathKey(fromParent) === pathKey(destParent) && fromPath[fromPath.length - 1] < destIndex) {
|
|
261
|
+
idx = Math.max(0, destIndex - 1);
|
|
262
|
+
}
|
|
263
|
+
return insertAt(without, destParent, idx, node);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
105
266
|
const handleSave = async () => {
|
|
106
267
|
setSaving(true);
|
|
107
268
|
try {
|
|
@@ -120,9 +281,10 @@ export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, cata
|
|
|
120
281
|
setSaving(true);
|
|
121
282
|
try {
|
|
122
283
|
await resetLayout();
|
|
123
|
-
|
|
284
|
+
const seeded = seedFromCurrent();
|
|
285
|
+
setTree(seeded);
|
|
124
286
|
setBaseline('[]');
|
|
125
|
-
toast.success('
|
|
287
|
+
toast.success('Overlay restaurado — mostrando el menú default');
|
|
126
288
|
}
|
|
127
289
|
catch (e) {
|
|
128
290
|
toast.error(e instanceof Error ? e.message : 'Error al resetear');
|
|
@@ -131,74 +293,92 @@ export function SidebarLayoutManager({ loadLayout, saveLayout, resetLayout, cata
|
|
|
131
293
|
setSaving(false);
|
|
132
294
|
}
|
|
133
295
|
};
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const seeded = layoutTreeFromNavGroups(currentGroups);
|
|
140
|
-
setTree(seeded);
|
|
141
|
-
toast.message('Árbol partido del menú actual — guardá para aplicar');
|
|
142
|
-
};
|
|
143
|
-
const addRootFolder = () => {
|
|
144
|
-
const titleText = newFolderTitle.trim() || 'Nueva carpeta';
|
|
145
|
-
const key = slugifyFolderKey(titleText);
|
|
296
|
+
const addRootCategory = () => {
|
|
297
|
+
const name = newCategory.trim() || 'Nueva sección';
|
|
298
|
+
const key = slugifyFolderKey(name);
|
|
146
299
|
setTree((t) => [
|
|
147
300
|
...t,
|
|
148
301
|
{
|
|
149
302
|
type: 'group',
|
|
150
|
-
key: `group_${key}`,
|
|
151
|
-
title:
|
|
152
|
-
children: [
|
|
303
|
+
key: `group_${key}_${Date.now() % 10000}`,
|
|
304
|
+
title: name,
|
|
305
|
+
children: [],
|
|
153
306
|
},
|
|
154
307
|
]);
|
|
155
|
-
|
|
156
|
-
};
|
|
157
|
-
const addLeafTo = (parentPath, leaf) => {
|
|
158
|
-
setTree((t) => appendChild(t, parentPath, {
|
|
159
|
-
type: 'leaf',
|
|
160
|
-
ref: leaf.ref,
|
|
161
|
-
title: leaf.title,
|
|
162
|
-
icon: leaf.icon,
|
|
163
|
-
}));
|
|
308
|
+
setNewCategory('');
|
|
164
309
|
};
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
310
|
+
const renderList = (nodes, parentPath) => {
|
|
311
|
+
const ids = nodes.map((_, i) => pathKey([...parentPath, i]));
|
|
312
|
+
return (_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: nodes.map((node, i) => {
|
|
313
|
+
const path = [...parentPath, i];
|
|
314
|
+
const id = pathKey(path);
|
|
315
|
+
const depth = parentPath.length;
|
|
316
|
+
const isOpen = !(collapsed[id] ?? false);
|
|
317
|
+
const lbl = labelOf(node, catalogByRef, resolveLabel);
|
|
318
|
+
return (_jsx(SortableRow, { id: id, depth: depth, kind: node.type, label: lbl, hidden: node.hidden, collapsed: !isOpen, onToggleCollapse: () => setCollapsed((c) => ({ ...c, [id]: !(c[id] ?? false) })), onRename: (next) => setTree((t) => updateAt(t, path, { title: next })), onToggleHidden: () => setTree((t) => updateAt(t, path, { hidden: !node.hidden })), onDelete: () => setTree((t) => removeAt(t, path).tree), onAddFolder: node.type === 'group' || node.type === 'folder'
|
|
319
|
+
? () => {
|
|
320
|
+
const key = slugifyFolderKey(`carpeta_${Date.now() % 1000}`);
|
|
321
|
+
setTree((t) => insertAt(t, path, node.children?.length ?? 0, {
|
|
322
|
+
type: 'folder',
|
|
323
|
+
key,
|
|
324
|
+
title: 'Nueva carpeta',
|
|
325
|
+
icon: 'Folder',
|
|
326
|
+
children: [],
|
|
327
|
+
}));
|
|
328
|
+
setCollapsed((c) => ({ ...c, [id]: false }));
|
|
329
|
+
}
|
|
330
|
+
: undefined, onAddLeaf: node.type === 'group' || node.type === 'folder'
|
|
331
|
+
? (ref) => {
|
|
332
|
+
const leaf = catalog.find((l) => l.ref === ref);
|
|
333
|
+
setTree((t) => insertAt(t, path, node.children?.length ?? 0, {
|
|
334
|
+
type: 'leaf',
|
|
335
|
+
ref,
|
|
336
|
+
title: leaf?.title,
|
|
337
|
+
icon: leaf?.icon,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
: undefined, unusedLeaves: unused, children: node.type !== 'leaf' && isOpen
|
|
341
|
+
? renderList(node.children ?? [], path)
|
|
342
|
+
: null }, id));
|
|
343
|
+
}) }));
|
|
186
344
|
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
345
|
+
const activeLabel = activeId
|
|
346
|
+
? (() => {
|
|
347
|
+
const p = idToPath.get(activeId);
|
|
348
|
+
if (!p)
|
|
349
|
+
return null;
|
|
350
|
+
const n = getAt(tree, p);
|
|
351
|
+
return n ? labelOf(n, catalogByRef, resolveLabel) : null;
|
|
352
|
+
})()
|
|
353
|
+
: null;
|
|
354
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title }), _jsx("p", { className: "text-muted-foreground max-w-xl text-sm", children: description })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: saving, onClick: () => {
|
|
355
|
+
setTree(seedFromCurrent());
|
|
356
|
+
toast.message('Árbol recargado desde el menú actual');
|
|
357
|
+
}, children: "Recargar men\u00FA actual" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: saving, onClick: () => void handleReset(), children: [_jsx(RotateCcw, { className: "mr-1 size-3.5" }), "Restaurar default"] }), _jsxs(Button, { type: "button", size: "sm", disabled: !dirty || saving, onClick: () => void handleSave(), children: [_jsx(Save, { className: "mr-1 size-3.5" }), saving ? 'Guardando…' : dirty ? 'Guardar cambios' : 'Guardado'] })] })] }), loading ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Cargando men\u00FA\u2026" })) : (_jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]", children: [_jsxs("div", { className: "bg-muted/20 border-border overflow-hidden rounded-xl border", children: [_jsxs("div", { className: "border-border/60 flex items-center justify-between border-b px-3 py-2", children: [_jsx("span", { className: "text-muted-foreground text-[11px] font-semibold uppercase tracking-wider", children: "Vista del men\u00FA" }), baseline === '[]' && dirty ? (_jsx("span", { className: "text-amber-600 text-[11px]", children: "Sin guardar \u00B7 borrador del men\u00FA actual" })) : dirty ? (_jsx("span", { className: "text-amber-600 text-[11px]", children: "Cambios sin guardar" })) : (_jsx("span", { className: "text-muted-foreground text-[11px]", children: folderKeys.length
|
|
358
|
+
? `${folderKeys.length} carpeta(s) con permiso`
|
|
359
|
+
: 'Layout activo' }))] }), _jsx(ScrollArea, { className: "h-[min(70vh,640px)]", children: _jsxs("div", { className: "p-2 pb-4", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragEnd: handleDragEnd, children: [tree.length === 0 ? (_jsx("p", { className: "text-muted-foreground px-3 py-8 text-center text-sm", children: "Men\u00FA vac\u00EDo. Cre\u00E1 una secci\u00F3n o recarg\u00E1 el men\u00FA actual." })) : (renderList(tree, [])), _jsx(DragOverlay, { children: activeLabel ? (_jsx("div", { className: "bg-background border-border rounded-md border px-3 py-1.5 text-sm shadow-lg", children: activeLabel })) : null })] }), _jsxs("div", { className: "mt-3 flex gap-2 px-2", children: [_jsx(Input, { value: newCategory, onChange: (e) => setNewCategory(e.target.value), placeholder: "Nombre de secci\u00F3n\u2026", className: "h-8 text-sm", onKeyDown: (e) => {
|
|
360
|
+
if (e.key === 'Enter')
|
|
361
|
+
addRootCategory();
|
|
362
|
+
} }), _jsxs(Button, { type: "button", size: "sm", className: "h-8 shrink-0", onClick: addRootCategory, children: [_jsx(Plus, { className: "mr-1 size-3.5" }), "Secci\u00F3n"] })] })] }) })] }), _jsxs("div", { className: "bg-muted/10 border-border flex flex-col overflow-hidden rounded-xl border", children: [_jsxs("div", { className: "border-border/60 border-b px-3 py-2", children: [_jsx("p", { className: "text-muted-foreground text-[11px] font-semibold uppercase tracking-wider", children: "Sin colocar" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-[11px]", children: "Agregalos desde el men\u00FA + de cada secci\u00F3n" })] }), _jsx(ScrollArea, { className: "flex-1", children: _jsx("ul", { className: "space-y-1 p-2", children: unused.length === 0 ? (_jsx("li", { className: "text-muted-foreground px-1 py-4 text-center text-xs", children: "Todo est\u00E1 en el men\u00FA" })) : (unused.map((l) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "hover:bg-muted/60 flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs", title: l.ref, onClick: () => {
|
|
363
|
+
if (tree.length === 0) {
|
|
364
|
+
setTree([
|
|
365
|
+
{
|
|
366
|
+
type: 'group',
|
|
367
|
+
key: 'sidebar.general',
|
|
368
|
+
title: 'General',
|
|
369
|
+
children: [
|
|
370
|
+
{ type: 'leaf', ref: l.ref, title: l.title, icon: l.icon },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
setTree((t) => insertAt(t, [0], t[0]?.children?.length ?? 0, {
|
|
377
|
+
type: 'leaf',
|
|
378
|
+
ref: l.ref,
|
|
379
|
+
title: l.title,
|
|
380
|
+
icon: l.icon,
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
}, children: [_jsx(Hash, { className: "text-muted-foreground size-3 shrink-0" }), _jsx("span", { className: "truncate font-medium", children: l.title }), _jsx(Plus, { className: "text-muted-foreground ml-auto size-3 shrink-0 opacity-50" })] }) }, l.ref)))) }) }), folderKeys.length > 0 ? (_jsxs("div", { className: "border-border/60 text-muted-foreground border-t px-3 py-2 text-[10px] leading-relaxed", children: ["Permisos:", ' ', folderKeys.map((k) => folderAccessCapability(k)).join(', ')] })) : null] })] }))] }));
|
|
204
384
|
}
|