@peers-app/peers-ui 0.7.39 → 0.7.40
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/dist/command-palette/command-palette-ui.js +190 -35
- package/dist/command-palette/command-palette.js +0 -121
- package/dist/mention-configs.js +1 -1
- package/dist/screens/console-logs/console-logs-list.js +45 -47
- package/dist/screens/search/global-search.js +110 -36
- package/package.json +3 -3
- package/src/command-palette/command-palette-ui.tsx +245 -12
- package/src/command-palette/command-palette.ts +0 -121
- package/src/mention-configs.ts +1 -1
- package/src/screens/console-logs/console-logs-list.tsx +47 -56
- package/src/screens/search/global-search.tsx +126 -7
|
@@ -98,127 +98,6 @@ const coreCommands: Command[] = [
|
|
|
98
98
|
closeCommandPalette();
|
|
99
99
|
goToTabPath('threads');
|
|
100
100
|
}
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'go-settings',
|
|
104
|
-
label: 'Open Settings',
|
|
105
|
-
description: 'Open application settings',
|
|
106
|
-
iconClassName: 'bi-gear-fill',
|
|
107
|
-
category: 'Navigation',
|
|
108
|
-
action: () => {
|
|
109
|
-
closeCommandPalette();
|
|
110
|
-
goToTabPath('settings');
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
id: 'go-assistants',
|
|
115
|
-
label: 'Go to Assistants',
|
|
116
|
-
description: 'View and manage assistants',
|
|
117
|
-
iconClassName: 'bi-person-fill-gear',
|
|
118
|
-
category: 'Navigation',
|
|
119
|
-
action: () => {
|
|
120
|
-
closeCommandPalette();
|
|
121
|
-
goToTabPath('assistants');
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: 'go-workflows',
|
|
126
|
-
label: 'Go to Workflows',
|
|
127
|
-
description: 'View and manage workflows',
|
|
128
|
-
iconClassName: 'bi-database-fill-gear',
|
|
129
|
-
category: 'Navigation',
|
|
130
|
-
action: () => {
|
|
131
|
-
closeCommandPalette();
|
|
132
|
-
goToTabPath('workflows');
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: 'go-tools',
|
|
137
|
-
label: 'Go to Tools',
|
|
138
|
-
description: 'View and manage tools',
|
|
139
|
-
iconClassName: 'bi-tools',
|
|
140
|
-
category: 'Navigation',
|
|
141
|
-
action: () => {
|
|
142
|
-
closeCommandPalette();
|
|
143
|
-
goToTabPath('tools');
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
id: 'go-events',
|
|
148
|
-
label: 'Go to Events',
|
|
149
|
-
description: 'View and manage events',
|
|
150
|
-
iconClassName: 'bi-lightning-charge-fill',
|
|
151
|
-
category: 'Navigation',
|
|
152
|
-
action: () => {
|
|
153
|
-
closeCommandPalette();
|
|
154
|
-
goToTabPath('events');
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
id: 'go-predicates',
|
|
159
|
-
label: 'Go to Predicates',
|
|
160
|
-
description: 'View and manage predicates',
|
|
161
|
-
iconClassName: 'bi-node-plus-fill',
|
|
162
|
-
category: 'Navigation',
|
|
163
|
-
action: () => {
|
|
164
|
-
closeCommandPalette();
|
|
165
|
-
goToTabPath('predicates');
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: 'go-peer-types',
|
|
170
|
-
label: 'Go to Peer Types',
|
|
171
|
-
description: 'View and manage peer types',
|
|
172
|
-
iconClassName: 'bi-code-square',
|
|
173
|
-
category: 'Navigation',
|
|
174
|
-
action: () => {
|
|
175
|
-
closeCommandPalette();
|
|
176
|
-
goToTabPath('peer-types');
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
id: 'go-packages',
|
|
181
|
-
label: 'Go to Packages',
|
|
182
|
-
description: 'View and manage packages',
|
|
183
|
-
iconClassName: 'bi-box-fill',
|
|
184
|
-
category: 'Navigation',
|
|
185
|
-
action: () => {
|
|
186
|
-
closeCommandPalette();
|
|
187
|
-
goToTabPath('packages');
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
id: 'go-variables',
|
|
192
|
-
label: 'Go to Variables',
|
|
193
|
-
description: 'View and manage variables',
|
|
194
|
-
iconClassName: 'bi-braces',
|
|
195
|
-
category: 'Navigation',
|
|
196
|
-
action: () => {
|
|
197
|
-
closeCommandPalette();
|
|
198
|
-
goToTabPath('variables');
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
id: 'go-knowledge-values',
|
|
203
|
-
label: 'Go to Knowledge Values',
|
|
204
|
-
description: 'View and manage knowledge values',
|
|
205
|
-
iconClassName: 'bi-journal-bookmark-fill',
|
|
206
|
-
category: 'Navigation',
|
|
207
|
-
action: () => {
|
|
208
|
-
closeCommandPalette();
|
|
209
|
-
goToTabPath('knowledge-values');
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
id: 'go-knowledge-frames',
|
|
214
|
-
label: 'Go to Knowledge Frames',
|
|
215
|
-
description: 'View and manage knowledge frames',
|
|
216
|
-
iconClassName: 'bi-window-dock',
|
|
217
|
-
category: 'Navigation',
|
|
218
|
-
action: () => {
|
|
219
|
-
closeCommandPalette();
|
|
220
|
-
goToTabPath('knowledge-frames');
|
|
221
|
-
}
|
|
222
101
|
}
|
|
223
102
|
];
|
|
224
103
|
|
package/src/mention-configs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConsoleLogs, DataFilter, IConsoleLog } from "@peers-app/peers-sdk";
|
|
1
|
+
import { ConsoleLogs, DataFilter, IConsoleLog, ISubscriptionResult, newid, sleep } from "@peers-app/peers-sdk";
|
|
2
2
|
import { min, sortBy, uniqBy } from 'lodash';
|
|
3
3
|
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
|
@@ -29,14 +29,15 @@ const DEFAULT_COLUMNS: Column[] = [
|
|
|
29
29
|
export const ConsoleLogsList = () => {
|
|
30
30
|
const logs = useObservableState<IConsoleLog[]>([]);
|
|
31
31
|
const [allLogsLoaded, setAllLogsLoaded] = useState(false);
|
|
32
|
+
const loadMoreId = useObservableState<string>(newid(), true);
|
|
32
33
|
const [levelFilter, setLevelFilter] = useState<string>('all');
|
|
33
34
|
const [processFilter, setProcessFilter] = useState<string>('all');
|
|
34
|
-
const
|
|
35
|
+
const searchText = useObservableState<string>('');
|
|
35
36
|
const [columns, setColumns] = useState<Column[]>(DEFAULT_COLUMNS);
|
|
36
37
|
const [totalLogCount, setTotalLogCount] = useState<number>(0);
|
|
37
38
|
const [_colorMode] = useObservable(colorMode);
|
|
38
39
|
const logsEndRef = React.useRef<HTMLDivElement>(null);
|
|
39
|
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
40
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
40
41
|
|
|
41
42
|
const batchSize = 50;
|
|
42
43
|
|
|
@@ -82,6 +83,9 @@ export const ConsoleLogsList = () => {
|
|
|
82
83
|
if (processFilter !== 'all') {
|
|
83
84
|
filter.process = processFilter;
|
|
84
85
|
}
|
|
86
|
+
if (searchText()) {
|
|
87
|
+
filter.message = { $matchWords: searchText() }
|
|
88
|
+
}
|
|
85
89
|
return filter;
|
|
86
90
|
};
|
|
87
91
|
|
|
@@ -90,15 +94,6 @@ export const ConsoleLogsList = () => {
|
|
|
90
94
|
const table = await ConsoleLogs();
|
|
91
95
|
const filter = buildFilter();
|
|
92
96
|
let count = await table.count(filter);
|
|
93
|
-
|
|
94
|
-
// If search text is applied, we need to count manually since it's client-side filtering
|
|
95
|
-
if (searchText) {
|
|
96
|
-
const allLogs = await table.list(filter);
|
|
97
|
-
count = allLogs.filter(log =>
|
|
98
|
-
log.message.toLowerCase().includes(searchText.toLowerCase())
|
|
99
|
-
).length;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
97
|
setTotalLogCount(count);
|
|
103
98
|
}
|
|
104
99
|
|
|
@@ -106,63 +101,57 @@ export const ConsoleLogsList = () => {
|
|
|
106
101
|
async function fetchLogs(lastLog?: IConsoleLog): Promise<IConsoleLog[]> {
|
|
107
102
|
const table = await ConsoleLogs();
|
|
108
103
|
const filter: any = buildFilter();
|
|
109
|
-
|
|
110
104
|
if (lastLog) {
|
|
111
105
|
filter.logId = { $lt: lastLog.logId };
|
|
112
106
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
sortBy: ['-timestamp', '-logId'],
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const fetchedLogs: IConsoleLog[] = [];
|
|
119
|
-
for await (const log of cursor) {
|
|
120
|
-
// Apply text search filter (if search is implemented in cursor, this can be removed)
|
|
121
|
-
if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase())) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
fetchedLogs.push(log);
|
|
125
|
-
if (fetchedLogs.length >= batchSize) {
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return fetchedLogs;
|
|
107
|
+
const results = await table.list(filter, { pageSize: batchSize, sortBy: ['-timestamp', '-logId'] });
|
|
108
|
+
return results;
|
|
130
109
|
}
|
|
131
110
|
|
|
132
111
|
// Load older logs (prepend to list)
|
|
133
|
-
function
|
|
112
|
+
async function loadMoreLogs(startLoadId?: string) {
|
|
113
|
+
if (startLoadId && startLoadId !== loadMoreId()) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
startLoadId ??= loadMoreId();
|
|
134
117
|
const oldestLog = logs()[0];
|
|
135
|
-
fetchLogs(oldestLog)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
118
|
+
const fetchedLogs = await fetchLogs(oldestLog);
|
|
119
|
+
if (loadMoreId() !== startLoadId) {
|
|
120
|
+
loadMoreLogs(loadMoreId());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (fetchedLogs.length === 0) {
|
|
124
|
+
setAllLogsLoaded(true);
|
|
125
|
+
}
|
|
126
|
+
let _logs = sortBy([...logs(), ...fetchedLogs], 'timestamp');
|
|
127
|
+
_logs = uniqBy(_logs, l => l.logId);
|
|
128
|
+
logs(_logs);
|
|
145
129
|
}
|
|
146
130
|
|
|
147
131
|
// Initial load and ensure screen is filled
|
|
148
132
|
const minHeightOfLog = 30;
|
|
149
133
|
useEffect(() => {
|
|
150
134
|
if (!allLogsLoaded && (!logs.length || logs.length * minHeightOfLog < windowHeight())) {
|
|
151
|
-
|
|
135
|
+
loadMoreLogs();
|
|
152
136
|
}
|
|
153
|
-
}, [logs, levelFilter, processFilter, searchText]);
|
|
137
|
+
}, [logs, levelFilter, processFilter, searchText()]);
|
|
154
138
|
|
|
155
139
|
// Reset when filters change
|
|
156
140
|
useEffect(() => {
|
|
141
|
+
loadMoreId(newid());
|
|
157
142
|
logs([]);
|
|
158
|
-
setAllLogsLoaded(false);
|
|
159
143
|
updateLogCount();
|
|
160
|
-
|
|
144
|
+
if (allLogsLoaded) {
|
|
145
|
+
setAllLogsLoaded(false);
|
|
146
|
+
loadMoreLogs()
|
|
147
|
+
}
|
|
148
|
+
}, [levelFilter, processFilter, searchText()]);
|
|
161
149
|
|
|
162
150
|
// Subscribe to new logs
|
|
163
151
|
useEffect(() => {
|
|
152
|
+
let sub: ISubscriptionResult | undefined = undefined;
|
|
164
153
|
ConsoleLogs().then(table => {
|
|
165
|
-
|
|
154
|
+
sub = table.dataChanged.subscribe(evt => {
|
|
166
155
|
// Update count whenever data changes
|
|
167
156
|
updateLogCount();
|
|
168
157
|
|
|
@@ -171,7 +160,11 @@ export const ConsoleLogsList = () => {
|
|
|
171
160
|
// Check if log matches current filters
|
|
172
161
|
if (levelFilter !== 'all' && log.level !== levelFilter) return;
|
|
173
162
|
if (processFilter !== 'all' && log.process !== processFilter) return;
|
|
174
|
-
if (searchText
|
|
163
|
+
if (searchText()) {
|
|
164
|
+
const logMessage = log.message.toLowerCase();
|
|
165
|
+
const filterOut = searchText().toLowerCase().split(' ').some(word => !logMessage.includes(word));
|
|
166
|
+
if (filterOut) return;
|
|
167
|
+
}
|
|
175
168
|
|
|
176
169
|
// Don't add we're only showing a limited batch and this is older
|
|
177
170
|
if (logs().length > batchSize && min(logs().map(l => l.timestamp))! > log.timestamp) return;
|
|
@@ -183,12 +176,11 @@ export const ConsoleLogsList = () => {
|
|
|
183
176
|
scrollToBottom('smooth');
|
|
184
177
|
}
|
|
185
178
|
});
|
|
186
|
-
|
|
187
|
-
return () => {
|
|
188
|
-
sub.unsubscribe();
|
|
189
|
-
};
|
|
190
179
|
});
|
|
191
|
-
|
|
180
|
+
return () => {
|
|
181
|
+
sub?.unsubscribe();
|
|
182
|
+
};
|
|
183
|
+
}, [levelFilter, processFilter]);
|
|
192
184
|
|
|
193
185
|
function scrollToBottom(behavior: 'instant' | 'smooth', delay = 100) {
|
|
194
186
|
setTimeout(() => {
|
|
@@ -202,7 +194,6 @@ export const ConsoleLogsList = () => {
|
|
|
202
194
|
const table = await ConsoleLogs();
|
|
203
195
|
await table.deleteOldLogs(Date.now());
|
|
204
196
|
logs([]);
|
|
205
|
-
setAllLogsLoaded(true);
|
|
206
197
|
setTotalLogCount(0);
|
|
207
198
|
} catch (err) {
|
|
208
199
|
console.error('Failed to clear logs:', err);
|
|
@@ -219,8 +210,8 @@ export const ConsoleLogsList = () => {
|
|
|
219
210
|
setLevelFilter={setLevelFilter}
|
|
220
211
|
processFilter={processFilter}
|
|
221
212
|
setProcessFilter={setProcessFilter}
|
|
222
|
-
searchText={searchText}
|
|
223
|
-
setSearchText={
|
|
213
|
+
searchText={searchText()}
|
|
214
|
+
setSearchText={searchText}
|
|
224
215
|
/>
|
|
225
216
|
|
|
226
217
|
<div
|
|
@@ -254,7 +245,7 @@ export const ConsoleLogsList = () => {
|
|
|
254
245
|
>
|
|
255
246
|
<InfiniteScroll
|
|
256
247
|
dataLength={_logs.length}
|
|
257
|
-
next={
|
|
248
|
+
next={loadMoreLogs}
|
|
258
249
|
style={{ display: 'flex', flexDirection: 'column-reverse', overflow: 'hidden' }}
|
|
259
250
|
inverse={true}
|
|
260
251
|
hasMore={!allLogsLoaded}
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
valueTypeMentionConfig,
|
|
13
13
|
IMentionConfig
|
|
14
14
|
} from '../../mention-configs';
|
|
15
|
-
import { IMentionData } from "@peers-app/peers-sdk";
|
|
15
|
+
import { IMentionData, IAppNav } from "@peers-app/peers-sdk";
|
|
16
|
+
import { allPackages } from '../../ui-router/routes-loader';
|
|
17
|
+
import { systemPackage } from '../../system-apps';
|
|
16
18
|
|
|
17
19
|
interface SearchResult {
|
|
18
20
|
config: IMentionConfig;
|
|
@@ -20,10 +22,22 @@ interface SearchResult {
|
|
|
20
22
|
category: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
interface AppSearchItem {
|
|
26
|
+
packageId: string;
|
|
27
|
+
packageName: string;
|
|
28
|
+
navItem: IAppNav;
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
displayName: string;
|
|
32
|
+
iconClassName: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
export function GlobalSearch() {
|
|
24
36
|
const [_colorMode] = useObservable(colorMode);
|
|
37
|
+
const [packages] = useObservable(allPackages);
|
|
25
38
|
const [searchQuery, setSearchQuery] = useState('');
|
|
26
39
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
|
40
|
+
const [appResults, setAppResults] = useState<AppSearchItem[]>([]);
|
|
27
41
|
const [isSearching, setIsSearching] = useState(false);
|
|
28
42
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
29
43
|
|
|
@@ -47,10 +61,42 @@ export function GlobalSearch() {
|
|
|
47
61
|
{ config: userMentionConfig, category: 'Users', navigationPath: 'profile' },
|
|
48
62
|
];
|
|
49
63
|
|
|
64
|
+
// Get all apps (system and user)
|
|
65
|
+
const getAllApps = (): AppSearchItem[] => {
|
|
66
|
+
const allPackages_ = [...packages, systemPackage];
|
|
67
|
+
return allPackages_
|
|
68
|
+
.filter(p => !p.disabled && p.appNavs && p.appNavs.length > 0)
|
|
69
|
+
.flatMap(pkg =>
|
|
70
|
+
pkg.appNavs!.map(navItem => {
|
|
71
|
+
// Construct path - use direct path for system apps, package-nav for others
|
|
72
|
+
let path: string;
|
|
73
|
+
if (pkg.packageId === 'system-apps') {
|
|
74
|
+
path = navItem.navigationPath ?? navItem.name.replace(/\s/g, '-').toLowerCase();
|
|
75
|
+
} else {
|
|
76
|
+
path = `package-nav/${pkg.packageId}/${(navItem.navigationPath ?? navItem.name).replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
|
|
77
|
+
while (path.includes('//')) {
|
|
78
|
+
path = path.replace('//', '/');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
packageId: pkg.packageId,
|
|
84
|
+
packageName: pkg.name,
|
|
85
|
+
navItem,
|
|
86
|
+
path,
|
|
87
|
+
name: navItem.name,
|
|
88
|
+
displayName: navItem.displayName || navItem.name,
|
|
89
|
+
iconClassName: navItem.iconClassName || 'bi-box-seam'
|
|
90
|
+
};
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
50
95
|
// Debounced search effect
|
|
51
96
|
useEffect(() => {
|
|
52
97
|
if (!searchQuery.trim()) {
|
|
53
98
|
setSearchResults([]);
|
|
99
|
+
setAppResults([]);
|
|
54
100
|
return;
|
|
55
101
|
}
|
|
56
102
|
|
|
@@ -75,13 +121,23 @@ export function GlobalSearch() {
|
|
|
75
121
|
const filteredResults = searchResults.filter(Boolean) as SearchResult[];
|
|
76
122
|
|
|
77
123
|
setSearchResults(filteredResults);
|
|
124
|
+
|
|
125
|
+
// Search apps
|
|
126
|
+
const allApps = getAllApps();
|
|
127
|
+
const lowerQuery = searchQuery.toLowerCase();
|
|
128
|
+
const filteredApps = allApps.filter(app =>
|
|
129
|
+
app.name.toLowerCase().includes(lowerQuery) ||
|
|
130
|
+
app.displayName.toLowerCase().includes(lowerQuery) ||
|
|
131
|
+
app.packageName.toLowerCase().includes(lowerQuery)
|
|
132
|
+
);
|
|
133
|
+
setAppResults(filteredApps);
|
|
78
134
|
} finally {
|
|
79
135
|
setIsSearching(false);
|
|
80
136
|
}
|
|
81
137
|
}, 300); // 300ms debounce
|
|
82
138
|
|
|
83
139
|
return () => clearTimeout(timeoutId);
|
|
84
|
-
}, [searchQuery]);
|
|
140
|
+
}, [searchQuery, packages]);
|
|
85
141
|
|
|
86
142
|
const handleItemClick = (result: SearchResult, item: IMentionData) => {
|
|
87
143
|
// Try using the config's onClick first
|
|
@@ -103,7 +159,11 @@ export function GlobalSearch() {
|
|
|
103
159
|
}
|
|
104
160
|
};
|
|
105
161
|
|
|
106
|
-
const
|
|
162
|
+
const handleAppClick = (app: AppSearchItem) => {
|
|
163
|
+
goToTabPath(app.path);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const totalResults = searchResults.reduce((sum, result) => sum + result.items.length, 0) + appResults.length;
|
|
107
167
|
|
|
108
168
|
return (
|
|
109
169
|
<div className="container-fluid h-100 p-4" style={{ maxHeight: '100vh', overflowY: 'auto' }}>
|
|
@@ -131,7 +191,7 @@ export function GlobalSearch() {
|
|
|
131
191
|
ref={inputRef}
|
|
132
192
|
type="text"
|
|
133
193
|
className={`form-control form-control-lg ${isDark ? 'bg-dark text-light border-secondary' : ''}`}
|
|
134
|
-
placeholder="Search across tools, assistants, workflows, events, and more..."
|
|
194
|
+
placeholder="Search across apps, tools, assistants, workflows, events, and more..."
|
|
135
195
|
value={searchQuery}
|
|
136
196
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
137
197
|
style={{
|
|
@@ -162,7 +222,7 @@ export function GlobalSearch() {
|
|
|
162
222
|
{isSearching ? (
|
|
163
223
|
'Searching...'
|
|
164
224
|
) : totalResults > 0 ? (
|
|
165
|
-
`Found ${totalResults} result${totalResults !== 1 ? 's' : ''}
|
|
225
|
+
`Found ${totalResults} result${totalResults !== 1 ? 's' : ''}`
|
|
166
226
|
) : searchQuery.trim() ? (
|
|
167
227
|
'No results found'
|
|
168
228
|
) : null}
|
|
@@ -173,7 +233,7 @@ export function GlobalSearch() {
|
|
|
173
233
|
{/* Search Results */}
|
|
174
234
|
{searchQuery && !isSearching && (
|
|
175
235
|
<div>
|
|
176
|
-
{searchResults.length === 0 ? (
|
|
236
|
+
{searchResults.length === 0 && appResults.length === 0 ? (
|
|
177
237
|
<div className="text-center py-5">
|
|
178
238
|
<i className="bi-search mb-3 d-block text-muted" style={{ fontSize: '48px' }} />
|
|
179
239
|
<h4 className="text-muted">No results found</h4>
|
|
@@ -183,6 +243,65 @@ export function GlobalSearch() {
|
|
|
183
243
|
</div>
|
|
184
244
|
) : (
|
|
185
245
|
<div>
|
|
246
|
+
{/* Apps Section */}
|
|
247
|
+
{appResults.length > 0 && (
|
|
248
|
+
<div className="mb-5">
|
|
249
|
+
<div className="d-flex align-items-center mb-3">
|
|
250
|
+
<i className="bi-grid-3x3-gap me-3" style={{ fontSize: '20px', color: '#6c757d' }} />
|
|
251
|
+
<h4 className="mb-0 me-3">Apps</h4>
|
|
252
|
+
<span className="badge bg-secondary">{appResults.length}</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="row g-3">
|
|
255
|
+
{appResults.map((app) => (
|
|
256
|
+
<div key={`${app.packageId}-${app.path}`} className="col-12 col-md-6 col-lg-4">
|
|
257
|
+
<div
|
|
258
|
+
className={`card h-100 ${isDark ? 'bg-dark border-secondary' : 'bg-light'}`}
|
|
259
|
+
style={{
|
|
260
|
+
cursor: 'pointer',
|
|
261
|
+
transition: 'all 0.15s ease',
|
|
262
|
+
borderRadius: '8px'
|
|
263
|
+
}}
|
|
264
|
+
onClick={() => handleAppClick(app)}
|
|
265
|
+
onMouseEnter={(e) => {
|
|
266
|
+
e.currentTarget.style.transform = 'translateY(-2px)';
|
|
267
|
+
e.currentTarget.style.boxShadow = isDark
|
|
268
|
+
? '0 4px 12px rgba(0,0,0,0.3)'
|
|
269
|
+
: '0 4px 12px rgba(0,0,0,0.1)';
|
|
270
|
+
}}
|
|
271
|
+
onMouseLeave={(e) => {
|
|
272
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
273
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<div className="card-body p-3">
|
|
277
|
+
<div className="d-flex align-items-start">
|
|
278
|
+
<i
|
|
279
|
+
className={`${app.iconClassName} me-3 mt-1`}
|
|
280
|
+
style={{
|
|
281
|
+
fontSize: '16px',
|
|
282
|
+
color: isDark ? '#0d6efd' : '#0d6efd',
|
|
283
|
+
minWidth: '16px'
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
<div className="flex-grow-1">
|
|
287
|
+
<h6 className="card-title mb-1 fw-medium">
|
|
288
|
+
{app.displayName}
|
|
289
|
+
</h6>
|
|
290
|
+
<small className="text-muted text-uppercase" style={{ fontSize: '11px', letterSpacing: '0.5px' }}>
|
|
291
|
+
{app.packageId === 'system-apps' ? 'System App' : app.packageName}
|
|
292
|
+
</small>
|
|
293
|
+
</div>
|
|
294
|
+
<i className="bi-arrow-right text-muted" style={{ fontSize: '12px' }} />
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{/* Entity Results */}
|
|
186
305
|
{searchResults.map((result) => (
|
|
187
306
|
<div key={result.category} className="mb-5">
|
|
188
307
|
{/* Category Header */}
|
|
@@ -253,7 +372,7 @@ export function GlobalSearch() {
|
|
|
253
372
|
<i className="bi-search mb-3 d-block text-muted" style={{ fontSize: '64px' }} />
|
|
254
373
|
<h3 className="text-muted mb-3">Search across everything</h3>
|
|
255
374
|
<p className="text-muted mb-4" style={{ maxWidth: '400px', margin: '0 auto' }}>
|
|
256
|
-
Find tools, assistants, workflows, events, predicates, types, and users all in one place.
|
|
375
|
+
Find apps, tools, assistants, workflows, events, predicates, types, and users all in one place.
|
|
257
376
|
</p>
|
|
258
377
|
<div className="d-flex flex-wrap justify-content-center gap-2">
|
|
259
378
|
{searchConfigs.map(({ config, category }) => (
|