@object-ui/app-shell 6.1.0 → 6.2.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/CHANGELOG.md +110 -0
- package/README.md +10 -1
- package/dist/console/AppContent.js +24 -2
- package/dist/console/ai/AiChatPage.d.ts +8 -0
- package/dist/console/ai/AiChatPage.js +188 -0
- package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
- package/dist/console/ai/ConversationsSidebar.js +111 -0
- package/dist/console/auth/LoginPage.js +19 -2
- package/dist/console/auth/RegisterPage.js +30 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
- package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
- package/dist/console/marketplace/MarketplacePage.js +55 -18
- package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
- package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
- package/dist/console/marketplace/usePackageL10n.js +110 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
- package/dist/console/organizations/OrganizationsPage.js +24 -3
- package/dist/context/FavoritesProvider.d.ts +40 -2
- package/dist/context/FavoritesProvider.js +201 -20
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +7 -0
- package/dist/hooks/useChatConversation.js +37 -5
- package/dist/hooks/useConversationList.d.ts +25 -0
- package/dist/hooks/useConversationList.js +131 -0
- package/dist/hooks/useNavPins.d.ts +11 -4
- package/dist/hooks/useNavPins.js +104 -53
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +20 -1
- package/dist/layout/UnifiedSidebar.js +1 -1
- package/dist/providers/ExpressionProvider.d.ts +11 -1
- package/dist/providers/ExpressionProvider.js +11 -6
- package/dist/services/builtinComponents.d.ts +1 -0
- package/dist/services/builtinComponents.js +166 -0
- package/dist/services/componentRegistry.d.ts +63 -0
- package/dist/services/componentRegistry.js +36 -0
- package/dist/views/ComponentNavView.d.ts +6 -0
- package/dist/views/ComponentNavView.js +26 -0
- package/dist/views/RecordDetailView.js +66 -6
- package/dist/views/RecordFormPage.js +15 -3
- package/dist/views/SearchResultsPage.js +4 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +58 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
- package/dist/views/metadata-admin/DirectoryPage.js +135 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
- package/dist/views/metadata-admin/LayeredDiff.js +26 -0
- package/dist/views/metadata-admin/PageShell.d.ts +34 -0
- package/dist/views/metadata-admin/PageShell.js +33 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
- package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
- package/dist/views/metadata-admin/QuickFind.js +152 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
- package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
- package/dist/views/metadata-admin/ResourceListPage.js +144 -0
- package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
- package/dist/views/metadata-admin/ResourceRouter.js +47 -0
- package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
- package/dist/views/metadata-admin/SchemaForm.js +556 -0
- package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
- package/dist/views/metadata-admin/default-schemas.js +207 -0
- package/dist/views/metadata-admin/i18n.d.ts +33 -0
- package/dist/views/metadata-admin/i18n.js +303 -0
- package/dist/views/metadata-admin/index.d.ts +31 -0
- package/dist/views/metadata-admin/index.js +33 -0
- package/dist/views/metadata-admin/predicate.d.ts +31 -0
- package/dist/views/metadata-admin/predicate.js +150 -0
- package/dist/views/metadata-admin/registry.d.ts +125 -0
- package/dist/views/metadata-admin/registry.js +48 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
- package/dist/views/metadata-admin/useMetadata.js +96 -0
- package/dist/views/metadata-admin/widgets.d.ts +68 -0
- package/dist/views/metadata-admin/widgets.js +287 -0
- package/package.json +27 -26
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Loads the signed-in user's AI conversation history.
|
|
4
|
+
*
|
|
5
|
+
* Backed by `GET /api/v1/ai/conversations` exposed by
|
|
6
|
+
* `@objectstack/service-ai`. The endpoint already filters by
|
|
7
|
+
* `req.user.userId` server-side so we just forward credentials.
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
10
|
+
function extractPreview(messages) {
|
|
11
|
+
if (!messages || messages.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
// Prefer the most recent user message, fall back to last message.
|
|
14
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15
|
+
const m = messages[i];
|
|
16
|
+
if (m.role === 'user') {
|
|
17
|
+
const text = stringifyContent(m.content);
|
|
18
|
+
if (text)
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const last = messages[messages.length - 1];
|
|
23
|
+
return stringifyContent(last?.content);
|
|
24
|
+
}
|
|
25
|
+
function stringifyContent(content) {
|
|
26
|
+
if (typeof content === 'string')
|
|
27
|
+
return content.slice(0, 140);
|
|
28
|
+
if (Array.isArray(content)) {
|
|
29
|
+
const text = content
|
|
30
|
+
.map((p) => {
|
|
31
|
+
if (typeof p === 'string')
|
|
32
|
+
return p;
|
|
33
|
+
if (p && typeof p === 'object' && 'text' in p && typeof p.text === 'string') {
|
|
34
|
+
return p.text;
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
})
|
|
38
|
+
.join('');
|
|
39
|
+
return text ? text.slice(0, 140) : undefined;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function normalize(row) {
|
|
44
|
+
return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
title: row.title,
|
|
47
|
+
agentId: row.agentId,
|
|
48
|
+
createdAt: row.createdAt,
|
|
49
|
+
updatedAt: row.updatedAt,
|
|
50
|
+
preview: extractPreview(row.messages),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function useConversationList(options) {
|
|
54
|
+
const { userId, apiBase, limit = 50, refreshKey } = options;
|
|
55
|
+
const [conversations, setConversations] = useState([]);
|
|
56
|
+
const [isLoading, setIsLoading] = useState(Boolean(userId));
|
|
57
|
+
const [error, setError] = useState(undefined);
|
|
58
|
+
const [internalKey, setInternalKey] = useState(0);
|
|
59
|
+
const refetch = useCallback(async () => {
|
|
60
|
+
setInternalKey((k) => k + 1);
|
|
61
|
+
}, []);
|
|
62
|
+
const remove = useCallback(async (id) => {
|
|
63
|
+
try {
|
|
64
|
+
await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
setConversations((rows) => rows.filter((r) => r.id !== id));
|
|
71
|
+
}
|
|
72
|
+
}, [apiBase]);
|
|
73
|
+
const rename = useCallback(async (id, title) => {
|
|
74
|
+
const trimmed = title.trim();
|
|
75
|
+
// Optimistic update so the sidebar reflects the new title immediately.
|
|
76
|
+
setConversations((rows) => rows.map((r) => (r.id === id ? { ...r, title: trimmed || undefined } : r)));
|
|
77
|
+
const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
|
|
78
|
+
method: 'PATCH',
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ title: trimmed }),
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
// Roll back by refetching.
|
|
85
|
+
setInternalKey((k) => k + 1);
|
|
86
|
+
throw new Error(`PATCH conversation failed: ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
}, [apiBase]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!userId) {
|
|
91
|
+
setConversations([]);
|
|
92
|
+
setIsLoading(false);
|
|
93
|
+
setError(undefined);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let cancelled = false;
|
|
97
|
+
setIsLoading(true);
|
|
98
|
+
setError(undefined);
|
|
99
|
+
(async () => {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${apiBase}/conversations?limit=${encodeURIComponent(String(limit))}`, { credentials: 'include' });
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error(`GET conversations failed: ${res.status}`);
|
|
104
|
+
const body = (await res.json());
|
|
105
|
+
const rows = Array.isArray(body)
|
|
106
|
+
? body
|
|
107
|
+
: (body.conversations ?? body.items ?? []);
|
|
108
|
+
if (cancelled)
|
|
109
|
+
return;
|
|
110
|
+
const sorted = rows
|
|
111
|
+
.map(normalize)
|
|
112
|
+
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
|
|
113
|
+
setConversations(sorted);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (cancelled)
|
|
117
|
+
return;
|
|
118
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
119
|
+
setConversations([]);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (!cancelled)
|
|
123
|
+
setIsLoading(false);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
return () => {
|
|
127
|
+
cancelled = true;
|
|
128
|
+
};
|
|
129
|
+
}, [userId, apiBase, limit, internalKey, refreshKey]);
|
|
130
|
+
return { conversations, isLoading, error, refetch, remove, rename };
|
|
131
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useNavPins
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin shim over `useFavorites` that exposes the legacy pin API used by
|
|
5
|
+
* `NavigationRenderer`. Pin state for sidebar navigation items is stored as
|
|
6
|
+
* `type: 'nav'` favorites with `pinned: true` and `navId` pointing back at
|
|
7
|
+
* the originating `NavigationItem.id`. Backed by the same backend channel as
|
|
8
|
+
* regular favorites (UserDataAdapter), so pins now sync across devices.
|
|
9
|
+
*
|
|
10
|
+
* Migration from the old `objectui-nav-pins` localStorage key happens once
|
|
11
|
+
* on `<FavoritesProvider>` mount — see `migrateLegacyNavPins`.
|
|
12
|
+
*
|
|
7
13
|
* @module
|
|
8
14
|
*/
|
|
9
15
|
import type { NavigationItem } from '@object-ui/types';
|
|
10
16
|
export declare function useNavPins(): {
|
|
11
17
|
pinnedIds: string[];
|
|
12
|
-
togglePin: (itemId: string, pinned: boolean) => void;
|
|
18
|
+
togglePin: (itemId: string, pinned: boolean, item?: NavigationItem, basePath?: string) => void;
|
|
13
19
|
isPinned: (itemId: string) => boolean;
|
|
14
20
|
applyPins: (items: NavigationItem[]) => NavigationItem[];
|
|
15
21
|
clearPins: () => void;
|
|
16
22
|
};
|
|
23
|
+
export type { FavoriteItem } from '../context/FavoritesProvider';
|
package/dist/hooks/useNavPins.js
CHANGED
|
@@ -1,72 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useNavPins
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin shim over `useFavorites` that exposes the legacy pin API used by
|
|
5
|
+
* `NavigationRenderer`. Pin state for sidebar navigation items is stored as
|
|
6
|
+
* `type: 'nav'` favorites with `pinned: true` and `navId` pointing back at
|
|
7
|
+
* the originating `NavigationItem.id`. Backed by the same backend channel as
|
|
8
|
+
* regular favorites (UserDataAdapter), so pins now sync across devices.
|
|
9
|
+
*
|
|
10
|
+
* Migration from the old `objectui-nav-pins` localStorage key happens once
|
|
11
|
+
* on `<FavoritesProvider>` mount — see `migrateLegacyNavPins`.
|
|
12
|
+
*
|
|
7
13
|
* @module
|
|
8
14
|
*/
|
|
9
|
-
import {
|
|
10
|
-
|
|
15
|
+
import { useCallback, useMemo } from 'react';
|
|
16
|
+
import { useFavorites } from '../context/FavoritesProvider';
|
|
11
17
|
const MAX_PINS = 20;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Synthesize the `href` for a NavigationItem in isolation from a sidebar.
|
|
20
|
+
*
|
|
21
|
+
* This duplicates a small portion of `NavigationRenderer.resolveHref` to keep
|
|
22
|
+
* the favorite record self-describing — useful if a future iteration surfaces
|
|
23
|
+
* nav-pins outside the sidebar (e.g. command palette / quick-jump). Returns
|
|
24
|
+
* an empty string for non-routable item types; consumers should treat empty
|
|
25
|
+
* href as "fall back to live nav tree".
|
|
26
|
+
*/
|
|
27
|
+
function deriveHref(item, basePath = '') {
|
|
28
|
+
switch (item.type) {
|
|
29
|
+
case 'object':
|
|
30
|
+
if (!item.objectName)
|
|
31
|
+
return '';
|
|
32
|
+
return item.viewName
|
|
33
|
+
? `${basePath}/${item.objectName}/view/${item.viewName}`
|
|
34
|
+
: `${basePath}/${item.objectName}`;
|
|
35
|
+
case 'dashboard':
|
|
36
|
+
return item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '';
|
|
37
|
+
case 'page':
|
|
38
|
+
return item.pageName ? `${basePath}/page/${item.pageName}` : '';
|
|
39
|
+
case 'report':
|
|
40
|
+
return item.reportName ? `${basePath}/report/${item.reportName}` : '';
|
|
41
|
+
case 'url':
|
|
42
|
+
return item.url ?? '';
|
|
43
|
+
default:
|
|
44
|
+
return '';
|
|
24
45
|
}
|
|
25
46
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// Storage full — silently ignore
|
|
32
|
-
}
|
|
47
|
+
function favoriteIdFor(navId) {
|
|
48
|
+
return `nav:${navId}`;
|
|
33
49
|
}
|
|
34
50
|
export function useNavPins() {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const { favorites, addFavorite, removeFavorite, setPinned, pinnedNavIds, } = useFavorites();
|
|
52
|
+
const togglePin = useCallback((itemId, pinned, item, basePath) => {
|
|
53
|
+
const favId = favoriteIdFor(itemId);
|
|
54
|
+
if (pinned) {
|
|
55
|
+
// If a NavigationItem is provided, register/refresh the favorite with
|
|
56
|
+
// proper label/href so backend sync carries portable data. Otherwise
|
|
57
|
+
// just flip the flag — the existing favorite (if any) keeps its data.
|
|
58
|
+
if (item) {
|
|
59
|
+
addFavorite({
|
|
60
|
+
id: favId,
|
|
61
|
+
label: item.label,
|
|
62
|
+
href: deriveHref(item, basePath ?? ''),
|
|
63
|
+
type: 'nav',
|
|
64
|
+
navId: itemId,
|
|
65
|
+
pinned: true,
|
|
66
|
+
});
|
|
67
|
+
// addFavorite is idempotent by id — for an already-present favorite
|
|
68
|
+
// that may have been unpinned via setPinned(false), flip the flag
|
|
69
|
+
// back on explicitly.
|
|
70
|
+
setPinned(favId, true);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
setPinned(favId, true);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Unpinning a nav favorite removes it entirely — nav-pins exist
|
|
78
|
+
// solely to surface the item in the sidebar Pinned section.
|
|
79
|
+
removeFavorite(favId);
|
|
80
|
+
}
|
|
81
|
+
}, [addFavorite, removeFavorite, setPinned]);
|
|
82
|
+
const isPinned = useCallback((itemId) => pinnedNavIds.has(itemId), [pinnedNavIds]);
|
|
51
83
|
/**
|
|
52
|
-
* Apply pinned state to a navigation item tree.
|
|
53
|
-
*
|
|
84
|
+
* Apply pinned state to a navigation item tree. Returns new items with
|
|
85
|
+
* `pinned` property set based on stored pin state. Caps the displayed pin
|
|
86
|
+
* count at `MAX_PINS` — additional `pinnedNavIds` (e.g. coming from another
|
|
87
|
+
* device with a larger cap) are silently ignored at render time.
|
|
54
88
|
*/
|
|
55
89
|
const applyPins = useCallback((items) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
if (pinnedNavIds.size === 0) {
|
|
91
|
+
// Fast path: drop any stale `pinned: true` left by previous renders.
|
|
92
|
+
let anyStale = false;
|
|
93
|
+
for (const it of items)
|
|
94
|
+
if (it.pinned) {
|
|
95
|
+
anyStale = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (!anyStale)
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
let pinCount = 0;
|
|
102
|
+
const walk = (list) => list.map(item => {
|
|
103
|
+
const shouldPin = pinnedNavIds.has(item.id) && pinCount < MAX_PINS;
|
|
104
|
+
if (shouldPin)
|
|
105
|
+
pinCount++;
|
|
106
|
+
const children = item.children?.length ? walk(item.children) : item.children;
|
|
107
|
+
if (shouldPin !== (item.pinned ?? false) || children !== item.children) {
|
|
108
|
+
return { ...item, pinned: shouldPin, children };
|
|
63
109
|
}
|
|
64
110
|
return item;
|
|
65
111
|
});
|
|
66
|
-
|
|
112
|
+
return walk(items);
|
|
113
|
+
}, [pinnedNavIds]);
|
|
67
114
|
const clearPins = useCallback(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
// Remove every nav-pin favorite — content favorites are untouched.
|
|
116
|
+
for (const f of favorites) {
|
|
117
|
+
if (f.type === 'nav')
|
|
118
|
+
removeFavorite(f.id);
|
|
119
|
+
}
|
|
120
|
+
}, [favorites, removeFavorite]);
|
|
121
|
+
const pinnedIds = useMemo(() => Array.from(pinnedNavIds), [pinnedNavIds]);
|
|
71
122
|
return { pinnedIds, togglePin, isPinned, applyPins, clearPins };
|
|
72
123
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -43,3 +43,10 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
|
|
|
43
43
|
export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
|
|
44
44
|
export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
|
|
45
45
|
export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
|
|
46
|
+
export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
|
|
47
|
+
export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
|
|
48
|
+
export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
|
|
49
|
+
export type { AppComponentRegistryEntry } from './services/componentRegistry';
|
|
50
|
+
import './services/builtinComponents';
|
|
51
|
+
export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './views/metadata-admin';
|
|
52
|
+
export type { MetadataResourceConfig, MetadataDomain, RichMetadataTypeEntry, } from './views/metadata-admin';
|
package/dist/index.js
CHANGED
|
@@ -49,3 +49,17 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
|
|
|
49
49
|
export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
|
|
50
50
|
export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
|
|
51
51
|
export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
|
|
52
|
+
export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
|
|
53
|
+
export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
|
|
54
|
+
// Phase 3b: Component nav registry — plugins use this to register
|
|
55
|
+
// admin/setup UI surfaces that are addressable from App metadata via
|
|
56
|
+
// `{ type: 'component', componentRef: 'ns:name' }` nav items.
|
|
57
|
+
export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
|
|
58
|
+
// Side-effect import: registers built-in admin components
|
|
59
|
+
// (metadata:directory, metadata:resource) at module load.
|
|
60
|
+
import './services/builtinComponents';
|
|
61
|
+
// Phase 3c — generic metadata admin engine. Re-exported so plugins
|
|
62
|
+
// can call `registerMetadataResource()` to override the per-type
|
|
63
|
+
// list / edit / create components, and host apps can compose the
|
|
64
|
+
// page primitives directly when needed.
|
|
65
|
+
export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './views/metadata-admin';
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -19,7 +19,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
19
19
|
*/
|
|
20
20
|
import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
|
|
21
21
|
import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
|
|
22
|
-
import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
|
|
22
|
+
import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, Bot, } from 'lucide-react';
|
|
23
23
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
24
24
|
import { useOffline } from '@object-ui/react';
|
|
25
25
|
import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
|
|
@@ -389,5 +389,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
389
389
|
if (!isActive)
|
|
390
390
|
mobileSwitcher.onChange(v.id);
|
|
391
391
|
}, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
|
|
392
|
-
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] })] })] })] }));
|
|
392
|
+
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] })] })] })] }));
|
|
393
393
|
}
|
|
@@ -204,7 +204,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
204
204
|
const AreaIcon = getIcon(area.icon);
|
|
205
205
|
const isActiveArea = area.id === activeAreaId;
|
|
206
206
|
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
|
|
207
|
-
}) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.
|
|
207
|
+
}) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.some(f => f.type !== 'nav') && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.filter(f => f.type !== 'nav').slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
|
|
208
208
|
const NavIcon = getIcon(item.icon);
|
|
209
209
|
return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
|
|
210
210
|
}) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('user.settings', { defaultValue: 'Settings' })] }) }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (() => {
|
|
@@ -237,6 +237,25 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
237
237
|
href = item.dashboardName ? `${baseUrl}/dashboard/${item.dashboardName}` : '#';
|
|
238
238
|
else if (item.type === 'page')
|
|
239
239
|
href = item.pageName ? `${baseUrl}/page/${item.pageName}` : '#';
|
|
240
|
+
else if (item.type === 'component') {
|
|
241
|
+
const ref = item.componentRef;
|
|
242
|
+
if (ref) {
|
|
243
|
+
const segs = ref.split(':').filter(Boolean);
|
|
244
|
+
href = `${baseUrl}/component/${segs.join('/')}`;
|
|
245
|
+
const navParams = item.params;
|
|
246
|
+
if (navParams) {
|
|
247
|
+
const usp = new URLSearchParams();
|
|
248
|
+
for (const [k, v] of Object.entries(navParams)) {
|
|
249
|
+
if (v === undefined || v === null)
|
|
250
|
+
continue;
|
|
251
|
+
usp.set(k, typeof v === 'string' ? v : JSON.stringify(v));
|
|
252
|
+
}
|
|
253
|
+
const qs = usp.toString();
|
|
254
|
+
if (qs)
|
|
255
|
+
href += `?${qs}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
240
259
|
return (_jsxs(Link, { to: href, className: "flex flex-col items-center gap-0.5 px-2 py-1.5 text-muted-foreground hover:text-foreground transition-colors min-w-[44px] min-h-[44px] justify-center", children: [_jsx(NavIcon, { className: "h-5 w-5" }), _jsx("span", { className: "text-[10px] truncate max-w-[60px]", children: resolveI18nLabel(item.label, t) })] }, item.id));
|
|
241
260
|
});
|
|
242
261
|
})() }))] }));
|
|
@@ -175,7 +175,7 @@ export function UnifiedSidebar({ activeAppName }) {
|
|
|
175
175
|
const AreaIcon = getIcon(area.icon);
|
|
176
176
|
const isActiveArea = area.id === activeAreaId;
|
|
177
177
|
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
|
|
178
|
-
}) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.
|
|
178
|
+
}) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, resolveItemLabel: activeApp ? (itemId, fallback) => resolveNavGroupLabel(activeApp.name, itemId, fallback) : undefined, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.some(f => f.type !== 'nav') && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.filter(f => f.type !== 'nav').slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
|
|
179
179
|
const NavIcon = getIcon(item.icon);
|
|
180
180
|
const isActive = location.pathname === item.url;
|
|
181
181
|
return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, isActive: isActive, children: _jsxs(Link, { to: item.url || '/home', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
|
|
@@ -22,6 +22,15 @@ export interface ExpressionContextValue {
|
|
|
22
22
|
app: Record<string, any>;
|
|
23
23
|
/** Additional data scope */
|
|
24
24
|
data: Record<string, any>;
|
|
25
|
+
/**
|
|
26
|
+
* Deployment-level feature flags surfaced by `/api/v1/auth/config`
|
|
27
|
+
* (e.g. `multiOrgEnabled`). Used by CEL/template predicates on
|
|
28
|
+
* metadata actions and views to hide entries that would otherwise
|
|
29
|
+
* hit a forbidden endpoint. Empty `{}` when auth config hasn't
|
|
30
|
+
* loaded yet — predicates should default to "visible" in that case
|
|
31
|
+
* (see `sys_organization.create_organization.visible`).
|
|
32
|
+
*/
|
|
33
|
+
features: Record<string, any>;
|
|
25
34
|
/** The evaluator instance (for imperative use) */
|
|
26
35
|
evaluator: ExpressionEvaluator;
|
|
27
36
|
}
|
|
@@ -30,8 +39,9 @@ interface ExpressionProviderProps {
|
|
|
30
39
|
user?: Record<string, any>;
|
|
31
40
|
app?: Record<string, any>;
|
|
32
41
|
data?: Record<string, any>;
|
|
42
|
+
features?: Record<string, any>;
|
|
33
43
|
}
|
|
34
|
-
export declare function ExpressionProvider({ children, user, app, data }: ExpressionProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
export declare function ExpressionProvider({ children, user, app, data, features }: ExpressionProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
35
45
|
/**
|
|
36
46
|
* Hook to access the expression context.
|
|
37
47
|
* Returns the full context value or a default empty context.
|
|
@@ -16,14 +16,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
16
16
|
*/
|
|
17
17
|
import { createContext, useContext, useMemo } from 'react';
|
|
18
18
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
19
|
+
import { PredicateScopeProvider } from '@object-ui/react';
|
|
19
20
|
const ExprCtx = createContext(null);
|
|
20
|
-
export function ExpressionProvider({ children, user = {}, app = {}, data = {} }) {
|
|
21
|
+
export function ExpressionProvider({ children, user = {}, app = {}, data = {}, features = {} }) {
|
|
21
22
|
const value = useMemo(() => {
|
|
22
|
-
const context = { user, app, data };
|
|
23
|
+
const context = { user, app, data, features };
|
|
23
24
|
const evaluator = new ExpressionEvaluator(context);
|
|
24
|
-
return { user, app, data, evaluator };
|
|
25
|
-
}, [user, app, data]);
|
|
26
|
-
|
|
25
|
+
return { user, app, data, features, evaluator };
|
|
26
|
+
}, [user, app, data, features]);
|
|
27
|
+
// Also feed the predicate scope used by useCondition/useExpression in
|
|
28
|
+
// @object-ui/react so action visibility predicates (e.g. on toolbar
|
|
29
|
+
// buttons) can see deployment-level flags like features.multiOrgEnabled.
|
|
30
|
+
const scope = useMemo(() => ({ user, app, data, features }), [user, app, data, features]);
|
|
31
|
+
return (_jsx(ExprCtx.Provider, { value: value, children: _jsx(PredicateScopeProvider, { scope: scope, children: children }) }));
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Hook to access the expression context.
|
|
@@ -33,7 +38,7 @@ export function useExpressionContext() {
|
|
|
33
38
|
const ctx = useContext(ExprCtx);
|
|
34
39
|
if (!ctx) {
|
|
35
40
|
// Return a safe default so components can be used outside the provider
|
|
36
|
-
const fallback = { user: {}, app: {}, data: {} };
|
|
41
|
+
const fallback = { user: {}, app: {}, data: {}, features: {} };
|
|
37
42
|
return { ...fallback, evaluator: new ExpressionEvaluator(fallback) };
|
|
38
43
|
}
|
|
39
44
|
return ctx;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|