@nextclaw/ui 0.5.1 → 0.5.3
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 +13 -0
- package/dist/assets/index-BHGBLfqi.css +1 -0
- package/dist/assets/index-CAFcPSll.js +332 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/api/marketplace.ts +10 -0
- package/src/api/types.ts +25 -8
- package/src/components/config/ChannelForm.tsx +14 -10
- package/src/components/config/ProviderForm.tsx +17 -16
- package/src/components/config/RuntimeConfig.tsx +33 -27
- package/src/components/doc-browser/DocBrowser.tsx +51 -5
- package/src/components/doc-browser/DocBrowserContext.tsx +33 -10
- package/src/components/marketplace/MarketplacePage.tsx +397 -205
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/hooks/useMarketplace.ts +18 -1
- package/dist/assets/index-B6sMhZ9q.css +0 -1
- package/dist/assets/index-B_-o3-kG.js +0 -337
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
/* eslint-disable max-lines-per-function */
|
|
2
|
+
import type { MarketplaceInstalledRecord, MarketplaceItemSummary, MarketplaceManageAction, MarketplaceSort } from '@/api/types';
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
4
4
|
import { Tabs } from '@/components/ui/tabs-custom';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
6
|
+
import {
|
|
7
|
+
useInstallMarketplaceItem,
|
|
8
|
+
useManageMarketplaceItem,
|
|
9
|
+
useMarketplaceInstalled,
|
|
10
|
+
useMarketplaceItems
|
|
11
|
+
} from '@/hooks/useMarketplace';
|
|
12
|
+
import { cn } from '@/lib/utils';
|
|
13
|
+
import { PackageSearch } from 'lucide-react';
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
15
|
|
|
8
16
|
const PAGE_SIZE = 12;
|
|
9
17
|
|
|
@@ -15,42 +23,147 @@ type InstallState = {
|
|
|
15
23
|
installingSpec?: string;
|
|
16
24
|
};
|
|
17
25
|
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
type ManageState = {
|
|
27
|
+
isPending: boolean;
|
|
28
|
+
targetId?: string;
|
|
29
|
+
action?: MarketplaceManageAction;
|
|
21
30
|
};
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
type InstalledRenderEntry = {
|
|
33
|
+
key: string;
|
|
34
|
+
record: MarketplaceInstalledRecord;
|
|
35
|
+
item?: MarketplaceItemSummary;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function normalizeMarketplaceKey(value: string | undefined): string {
|
|
39
|
+
return (value ?? '').trim().toLowerCase();
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
: sets.skill.has(item.install.spec);
|
|
42
|
+
function toLookupKey(type: MarketplaceItemSummary['type'], value: string | undefined): string {
|
|
43
|
+
const normalized = normalizeMarketplaceKey(value);
|
|
44
|
+
return normalized.length > 0 ? `${type}:${normalized}` : '';
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
function buildCatalogLookup(items: MarketplaceItemSummary[]): Map<string, MarketplaceItemSummary> {
|
|
48
|
+
const lookup = new Map<string, MarketplaceItemSummary>();
|
|
49
|
+
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const candidates = [item.install.spec, item.slug, item.id];
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const lookupKey = toLookupKey(item.type, candidate);
|
|
54
|
+
if (!lookupKey || lookup.has(lookupKey)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
lookup.set(lookupKey, item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lookup;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildInstalledRecordLookup(records: MarketplaceInstalledRecord[]): Map<string, MarketplaceInstalledRecord> {
|
|
65
|
+
const lookup = new Map<string, MarketplaceInstalledRecord>();
|
|
66
|
+
|
|
67
|
+
for (const record of records) {
|
|
68
|
+
const candidates = [record.spec, record.id, record.label];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const lookupKey = toLookupKey(record.type, candidate);
|
|
71
|
+
if (!lookupKey || lookup.has(lookupKey)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
lookup.set(lookupKey, record);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return lookup;
|
|
47
79
|
}
|
|
48
80
|
|
|
49
|
-
function
|
|
50
|
-
|
|
81
|
+
function findInstalledRecordForItem(
|
|
82
|
+
item: MarketplaceItemSummary,
|
|
83
|
+
installedRecordLookup: Map<string, MarketplaceInstalledRecord>
|
|
84
|
+
): MarketplaceInstalledRecord | undefined {
|
|
85
|
+
const candidates = [item.install.spec, item.slug, item.id];
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const lookupKey = toLookupKey(item.type, candidate);
|
|
88
|
+
if (!lookupKey) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const record = installedRecordLookup.get(lookupKey);
|
|
92
|
+
if (record) {
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findCatalogItemForRecord(
|
|
100
|
+
record: MarketplaceInstalledRecord,
|
|
101
|
+
catalogLookup: Map<string, MarketplaceItemSummary>
|
|
102
|
+
): MarketplaceItemSummary | undefined {
|
|
103
|
+
const bySpec = catalogLookup.get(toLookupKey(record.type, record.spec));
|
|
104
|
+
if (bySpec) {
|
|
105
|
+
return bySpec;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const byId = catalogLookup.get(toLookupKey(record.type, record.id));
|
|
109
|
+
if (byId) {
|
|
110
|
+
return byId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return catalogLookup.get(toLookupKey(record.type, record.label));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function matchInstalledSearch(
|
|
117
|
+
record: MarketplaceInstalledRecord,
|
|
118
|
+
item: MarketplaceItemSummary | undefined,
|
|
119
|
+
query: string
|
|
120
|
+
): boolean {
|
|
121
|
+
const normalizedQuery = normalizeMarketplaceKey(query);
|
|
122
|
+
if (!normalizedQuery) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const values = [
|
|
127
|
+
record.id,
|
|
128
|
+
record.spec,
|
|
129
|
+
record.label,
|
|
130
|
+
item?.name,
|
|
131
|
+
item?.slug,
|
|
132
|
+
item?.summary,
|
|
133
|
+
...(item?.tags ?? [])
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return values
|
|
137
|
+
.map((value) => normalizeMarketplaceKey(value))
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.some((value) => value.includes(normalizedQuery));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getAvatarColor(text: string) {
|
|
143
|
+
const colors = [
|
|
144
|
+
'bg-blue-500', 'bg-indigo-500', 'bg-purple-500', 'bg-pink-500',
|
|
145
|
+
'bg-rose-500', 'bg-orange-500', 'bg-emerald-500', 'bg-teal-500', 'bg-cyan-500'
|
|
146
|
+
];
|
|
147
|
+
let hash = 0;
|
|
148
|
+
for (let i = 0; i < text.length; i++) {
|
|
149
|
+
hash = text.charCodeAt(i) + ((hash << 5) - hash);
|
|
150
|
+
}
|
|
151
|
+
return colors[Math.abs(hash) % colors.length];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
|
|
155
|
+
const displayName = name || fallback;
|
|
156
|
+
const letters = displayName.substring(0, 2).toUpperCase();
|
|
157
|
+
const colorClass = getAvatarColor(displayName);
|
|
158
|
+
return (
|
|
159
|
+
<div className={cn("flex items-center justify-center w-11 h-11 rounded-2xl text-white font-bold text-base shrink-0 shadow-sm", colorClass)}>
|
|
160
|
+
{letters}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
51
163
|
}
|
|
52
164
|
|
|
53
165
|
function FilterPanel(props: {
|
|
166
|
+
scope: ScopeType;
|
|
54
167
|
searchText: string;
|
|
55
168
|
typeFilter: FilterType;
|
|
56
169
|
sort: MarketplaceSort;
|
|
@@ -59,162 +172,160 @@ function FilterPanel(props: {
|
|
|
59
172
|
onSortChange: (value: MarketplaceSort) => void;
|
|
60
173
|
}) {
|
|
61
174
|
return (
|
|
62
|
-
<div className="bg-white border border-gray-200 rounded-
|
|
63
|
-
<div className="
|
|
64
|
-
<div className="
|
|
175
|
+
<div className="bg-white border border-gray-200 rounded-xl p-4 mb-4">
|
|
176
|
+
<div className="flex gap-3 items-center">
|
|
177
|
+
<div className="flex-1 min-w-0 relative">
|
|
65
178
|
<PackageSearch className="h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
66
179
|
<input
|
|
67
180
|
value={props.searchText}
|
|
68
181
|
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
69
|
-
placeholder="Search
|
|
70
|
-
className="w-full h-
|
|
182
|
+
placeholder="Search extensions..."
|
|
183
|
+
className="w-full h-9 border border-gray-200 rounded-lg pl-9 pr-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
71
184
|
/>
|
|
72
185
|
</div>
|
|
73
186
|
|
|
74
|
-
<div className="flex
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
value
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
187
|
+
<div className="inline-flex h-9 rounded-lg bg-gray-100 p-0.5 shrink-0">
|
|
188
|
+
{([
|
|
189
|
+
{ value: 'all', label: 'All' },
|
|
190
|
+
{ value: 'plugin', label: 'Plugins' },
|
|
191
|
+
{ value: 'skill', label: 'Skills' },
|
|
192
|
+
] as const).map((opt) => (
|
|
193
|
+
<button
|
|
194
|
+
key={opt.value}
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={() => props.onTypeFilterChange(opt.value)}
|
|
197
|
+
className={cn(
|
|
198
|
+
'px-3 rounded-md text-sm font-medium transition-all whitespace-nowrap',
|
|
199
|
+
props.typeFilter === opt.value
|
|
200
|
+
? 'bg-white text-gray-900 shadow-sm'
|
|
201
|
+
: 'text-gray-500 hover:text-gray-700'
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{opt.label}
|
|
205
|
+
</button>
|
|
206
|
+
))}
|
|
93
207
|
</div>
|
|
208
|
+
|
|
209
|
+
{props.scope === 'all' && (
|
|
210
|
+
<Select value={props.sort} onValueChange={(v) => props.onSortChange(v as MarketplaceSort)}>
|
|
211
|
+
<SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
|
|
212
|
+
<SelectValue />
|
|
213
|
+
</SelectTrigger>
|
|
214
|
+
<SelectContent>
|
|
215
|
+
<SelectItem value="relevance">Relevance</SelectItem>
|
|
216
|
+
<SelectItem value="updated">Recently Updated</SelectItem>
|
|
217
|
+
</SelectContent>
|
|
218
|
+
</Select>
|
|
219
|
+
)}
|
|
94
220
|
</div>
|
|
95
221
|
</div>
|
|
96
222
|
);
|
|
97
223
|
}
|
|
98
224
|
|
|
99
|
-
function
|
|
100
|
-
item
|
|
225
|
+
function MarketplaceListCard(props: {
|
|
226
|
+
item?: MarketplaceItemSummary;
|
|
227
|
+
record?: MarketplaceInstalledRecord;
|
|
101
228
|
installState: InstallState;
|
|
102
|
-
|
|
229
|
+
manageState: ManageState;
|
|
103
230
|
onInstall: (item: MarketplaceItemSummary) => void;
|
|
231
|
+
onManage: (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => void;
|
|
104
232
|
}) {
|
|
105
|
-
const
|
|
233
|
+
const record = props.record;
|
|
234
|
+
const type = props.item?.type ?? record?.type;
|
|
235
|
+
const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? 'Unknown Item';
|
|
236
|
+
const summary = props.item?.summary ?? (record ? 'Installed locally. Details are currently unavailable from marketplace.' : '');
|
|
237
|
+
const spec = props.item?.install.spec ?? record?.spec ?? '';
|
|
106
238
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<button
|
|
110
|
-
disabled
|
|
111
|
-
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg text-xs font-semibold bg-gray-100 text-gray-500 cursor-not-allowed"
|
|
112
|
-
>
|
|
113
|
-
Installed
|
|
114
|
-
</button>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
239
|
+
const targetId = record?.id || record?.spec;
|
|
240
|
+
const busyForRecord = Boolean(targetId) && props.manageState.isPending && props.manageState.targetId === targetId;
|
|
117
241
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg text-xs font-semibold bg-gray-900 text-white hover:bg-black disabled:opacity-50"
|
|
123
|
-
>
|
|
124
|
-
<Download className="h-3.5 w-3.5" />
|
|
125
|
-
{isInstalling ? 'Installing...' : 'Install'}
|
|
126
|
-
</button>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
242
|
+
const canToggle = record?.type === 'plugin';
|
|
243
|
+
const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
|
|
244
|
+
const canUninstallSkill = record?.type === 'skill' && record.source === 'workspace';
|
|
245
|
+
const canUninstall = Boolean(canUninstallPlugin || canUninstallSkill);
|
|
129
246
|
|
|
130
|
-
|
|
131
|
-
items: MarketplaceItemSummary[];
|
|
132
|
-
loading: boolean;
|
|
133
|
-
installState: InstallState;
|
|
134
|
-
installedSets: InstalledSpecSets;
|
|
135
|
-
onInstall: (item: MarketplaceItemSummary) => void;
|
|
136
|
-
}) {
|
|
137
|
-
return (
|
|
138
|
-
<section className="mb-6">
|
|
139
|
-
<div className="flex items-center gap-2 mb-3">
|
|
140
|
-
<Sparkles className="h-4 w-4 text-amber-500" />
|
|
141
|
-
<h3 className="text-[15px] font-bold text-gray-900">Recommended</h3>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
145
|
-
{props.items.map((item) => {
|
|
146
|
-
const installed = isInstalled(item, props.installedSets);
|
|
147
|
-
return (
|
|
148
|
-
<div key={item.id} className="bg-white border border-gray-200 rounded-xl p-4">
|
|
149
|
-
<div className="flex items-start justify-between gap-3">
|
|
150
|
-
<div>
|
|
151
|
-
<div className="text-[14px] font-semibold text-gray-900">{item.name}</div>
|
|
152
|
-
<div className="text-[12px] text-gray-500 mt-0.5">{item.summary}</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div className="flex items-center gap-2">
|
|
155
|
-
<TypeBadge type={item.type} />
|
|
156
|
-
{installed && <InstalledBadge />}
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
<div className="mt-3 flex items-center justify-between">
|
|
160
|
-
<code className="text-[11px] text-gray-500 bg-gray-100 rounded px-2 py-1">{item.install.spec}</code>
|
|
161
|
-
<InstallButton
|
|
162
|
-
item={item}
|
|
163
|
-
installed={installed}
|
|
164
|
-
installState={props.installState}
|
|
165
|
-
onInstall={props.onInstall}
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
);
|
|
170
|
-
})}
|
|
247
|
+
const isInstalling = props.installState.isPending && props.item && props.installState.installingSpec === props.item.install.spec;
|
|
171
248
|
|
|
172
|
-
|
|
173
|
-
{!props.loading && props.items.length === 0 && <div className="text-[13px] text-gray-500">No recommendations yet.</div>}
|
|
174
|
-
</div>
|
|
175
|
-
</section>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function MarketplaceItemCard(props: {
|
|
180
|
-
item: MarketplaceItemSummary;
|
|
181
|
-
installState: InstallState;
|
|
182
|
-
installed: boolean;
|
|
183
|
-
onInstall: (item: MarketplaceItemSummary) => void;
|
|
184
|
-
}) {
|
|
185
|
-
const downloads = props.item.metrics?.downloads30d;
|
|
249
|
+
const displayType = type === 'plugin' ? 'Plugin' : type === 'skill' ? 'Skill' : 'Extension';
|
|
186
250
|
|
|
187
251
|
return (
|
|
188
|
-
<article className="bg-white border border-gray-200 rounded-xl
|
|
189
|
-
<div className="flex
|
|
190
|
-
<
|
|
191
|
-
<div className="flex
|
|
192
|
-
<
|
|
193
|
-
|
|
252
|
+
<article className="group bg-white border border-transparent hover:border-gray-200 rounded-xl px-4 py-3 hover:shadow-sm transition-all flex items-start gap-4 justify-between cursor-default h-[90px]">
|
|
253
|
+
<div className="flex gap-3 min-w-0 flex-1 h-full items-start">
|
|
254
|
+
<ItemIcon name={title} fallback={spec || 'Ext'} />
|
|
255
|
+
<div className="min-w-0 flex-1 flex flex-col justify-center h-full">
|
|
256
|
+
<TooltipProvider delayDuration={400}>
|
|
257
|
+
<Tooltip>
|
|
258
|
+
<TooltipTrigger asChild>
|
|
259
|
+
<div className="text-[14px] font-semibold text-gray-900 truncate leading-tight cursor-default">{title}</div>
|
|
260
|
+
</TooltipTrigger>
|
|
261
|
+
<TooltipContent className="max-w-[300px] text-xs">
|
|
262
|
+
{title}
|
|
263
|
+
</TooltipContent>
|
|
264
|
+
</Tooltip>
|
|
265
|
+
|
|
266
|
+
<div className="flex items-center gap-1.5 mt-0.5 mb-1.5">
|
|
267
|
+
<span className="text-[11px] text-gray-500 font-medium">{displayType}</span>
|
|
268
|
+
{spec && (
|
|
269
|
+
<>
|
|
270
|
+
<span className="text-[10px] text-gray-300">•</span>
|
|
271
|
+
<Tooltip>
|
|
272
|
+
<TooltipTrigger asChild>
|
|
273
|
+
<span className="text-[11px] text-gray-400 truncate max-w-full font-mono cursor-default">{spec}</span>
|
|
274
|
+
</TooltipTrigger>
|
|
275
|
+
<TooltipContent className="max-w-[300px] text-xs font-mono break-all">
|
|
276
|
+
{spec}
|
|
277
|
+
</TooltipContent>
|
|
278
|
+
</Tooltip>
|
|
279
|
+
</>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<Tooltip>
|
|
284
|
+
<TooltipTrigger asChild>
|
|
285
|
+
<p className="text-[12px] text-gray-500/90 line-clamp-1 transition-colors leading-relaxed text-left cursor-default">{summary}</p>
|
|
286
|
+
</TooltipTrigger>
|
|
287
|
+
{summary && (
|
|
288
|
+
<TooltipContent className="max-w-[400px] text-xs leading-relaxed">
|
|
289
|
+
{summary}
|
|
290
|
+
</TooltipContent>
|
|
291
|
+
)}
|
|
292
|
+
</Tooltip>
|
|
293
|
+
</TooltipProvider>
|
|
194
294
|
</div>
|
|
195
295
|
</div>
|
|
196
296
|
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
297
|
+
<div className="shrink-0 flex items-center h-full">
|
|
298
|
+
{props.item && !record && (
|
|
299
|
+
<button
|
|
300
|
+
onClick={() => props.onInstall(props.item as MarketplaceItemSummary)}
|
|
301
|
+
disabled={props.installState.isPending}
|
|
302
|
+
className="inline-flex items-center gap-1.5 h-8 px-4 rounded-full text-xs font-semibold bg-gray-900 text-white hover:bg-black disabled:opacity-50 transition-colors"
|
|
303
|
+
>
|
|
304
|
+
{isInstalling ? 'Installing...' : 'Install'}
|
|
305
|
+
</button>
|
|
306
|
+
)}
|
|
206
307
|
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
{record && canToggle && (
|
|
309
|
+
<button
|
|
310
|
+
disabled={props.manageState.isPending}
|
|
311
|
+
onClick={() => props.onManage(record.enabled === false ? 'enable' : 'disable', record)}
|
|
312
|
+
className="inline-flex items-center h-8 px-4 rounded-full text-xs font-semibold border border-gray-200 text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
|
|
313
|
+
>
|
|
314
|
+
{busyForRecord && props.manageState.action !== 'uninstall'
|
|
315
|
+
? (props.manageState.action === 'enable' ? 'Enabling...' : 'Disabling...')
|
|
316
|
+
: (record.enabled === false ? 'Enable' : 'Disable')}
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
209
319
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
320
|
+
{record && canUninstall && (
|
|
321
|
+
<button
|
|
322
|
+
disabled={props.manageState.isPending}
|
|
323
|
+
onClick={() => props.onManage('uninstall', record)}
|
|
324
|
+
className="inline-flex items-center h-8 px-4 rounded-full text-xs font-semibold border border-rose-100 text-rose-600 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
|
|
325
|
+
>
|
|
326
|
+
{busyForRecord && props.manageState.action === 'uninstall' ? 'Removing...' : 'Uninstall'}
|
|
327
|
+
</button>
|
|
328
|
+
)}
|
|
218
329
|
</div>
|
|
219
330
|
</article>
|
|
220
331
|
);
|
|
@@ -228,7 +339,7 @@ function PaginationBar(props: {
|
|
|
228
339
|
onNext: () => void;
|
|
229
340
|
}) {
|
|
230
341
|
return (
|
|
231
|
-
<div className="mt-
|
|
342
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
232
343
|
<button
|
|
233
344
|
className="h-8 px-3 rounded-lg border border-gray-200 text-sm text-gray-700 disabled:opacity-40"
|
|
234
345
|
onClick={props.onPrev}
|
|
@@ -267,46 +378,93 @@ export function MarketplacePage() {
|
|
|
267
378
|
}, [searchText]);
|
|
268
379
|
|
|
269
380
|
const installedQuery = useMarketplaceInstalled();
|
|
270
|
-
const requestPage = scope === 'installed' ? 1 : page;
|
|
271
|
-
const requestPageSize = scope === 'installed' ? 100 : PAGE_SIZE;
|
|
272
381
|
|
|
273
382
|
const itemsQuery = useMarketplaceItems({
|
|
274
383
|
q: query || undefined,
|
|
275
384
|
type: typeFilter === 'all' ? undefined : typeFilter,
|
|
276
385
|
sort,
|
|
277
|
-
page
|
|
278
|
-
pageSize:
|
|
386
|
+
page,
|
|
387
|
+
pageSize: PAGE_SIZE
|
|
279
388
|
});
|
|
280
|
-
|
|
389
|
+
|
|
281
390
|
const installMutation = useInstallMarketplaceItem();
|
|
391
|
+
const manageMutation = useManageMarketplaceItem();
|
|
282
392
|
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
393
|
+
const installedRecords = useMemo(
|
|
394
|
+
() => installedQuery.data?.records ?? [],
|
|
395
|
+
[installedQuery.data?.records]
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const allItems = useMemo(
|
|
399
|
+
() => itemsQuery.data?.items ?? [],
|
|
400
|
+
[itemsQuery.data?.items]
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const catalogLookup = useMemo(
|
|
404
|
+
() => buildCatalogLookup(allItems),
|
|
405
|
+
[allItems]
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const installedRecordLookup = useMemo(
|
|
409
|
+
() => buildInstalledRecordLookup(installedRecords),
|
|
410
|
+
[installedRecords]
|
|
411
|
+
);
|
|
288
412
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
413
|
+
const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
|
|
414
|
+
const entries = installedRecords
|
|
415
|
+
.filter((record) => (typeFilter === 'all' ? true : record.type === typeFilter))
|
|
416
|
+
.map((record) => ({
|
|
417
|
+
key: `${record.type}:${record.spec}:${record.id ?? ''}`,
|
|
418
|
+
record,
|
|
419
|
+
item: findCatalogItemForRecord(record, catalogLookup)
|
|
420
|
+
}))
|
|
421
|
+
.filter((entry) => matchInstalledSearch(entry.record, entry.item, query));
|
|
422
|
+
|
|
423
|
+
entries.sort((left, right) => {
|
|
424
|
+
const leftTs = left.record.installedAt ? Date.parse(left.record.installedAt) : Number.NaN;
|
|
425
|
+
const rightTs = right.record.installedAt ? Date.parse(right.record.installedAt) : Number.NaN;
|
|
426
|
+
const leftValid = !Number.isNaN(leftTs);
|
|
427
|
+
const rightValid = !Number.isNaN(rightTs);
|
|
428
|
+
|
|
429
|
+
if (leftValid && rightValid && leftTs !== rightTs) {
|
|
430
|
+
return rightTs - leftTs;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return left.record.spec.localeCompare(right.record.spec);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return entries;
|
|
437
|
+
}, [installedRecords, typeFilter, catalogLookup, query]);
|
|
438
|
+
|
|
439
|
+
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
293
440
|
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
294
441
|
|
|
295
442
|
const listSummary = useMemo(() => {
|
|
443
|
+
if (scope === 'installed') {
|
|
444
|
+
if (installedQuery.isLoading) {
|
|
445
|
+
return 'Loading...';
|
|
446
|
+
}
|
|
447
|
+
return `${installedEntries.length} installed`;
|
|
448
|
+
}
|
|
449
|
+
|
|
296
450
|
if (!itemsQuery.data) {
|
|
297
451
|
return 'Loading...';
|
|
298
452
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return `Showing ${items.length} / ${total}`;
|
|
303
|
-
}, [items.length, itemsQuery.data, scope, total]);
|
|
453
|
+
|
|
454
|
+
return `${allItems.length} / ${total}`;
|
|
455
|
+
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
304
456
|
|
|
305
457
|
const installState: InstallState = {
|
|
306
458
|
isPending: installMutation.isPending,
|
|
307
459
|
installingSpec: installMutation.variables?.spec
|
|
308
460
|
};
|
|
309
461
|
|
|
462
|
+
const manageState: ManageState = {
|
|
463
|
+
isPending: manageMutation.isPending,
|
|
464
|
+
targetId: manageMutation.variables?.id || manageMutation.variables?.spec,
|
|
465
|
+
action: manageMutation.variables?.action
|
|
466
|
+
};
|
|
467
|
+
|
|
310
468
|
const tabs = [
|
|
311
469
|
{ id: 'all', label: 'Marketplace' },
|
|
312
470
|
{ id: 'installed', label: 'Installed', count: installedQuery.data?.total ?? 0 }
|
|
@@ -316,20 +474,39 @@ export function MarketplacePage() {
|
|
|
316
474
|
if (installMutation.isPending) {
|
|
317
475
|
return;
|
|
318
476
|
}
|
|
319
|
-
installMutation.mutate({ type: item.type, spec: item.install.spec });
|
|
477
|
+
installMutation.mutate({ type: item.type, spec: item.install.spec, kind: item.install.kind });
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const handleManage = (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => {
|
|
481
|
+
if (manageMutation.isPending) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const targetId = record.id || record.spec;
|
|
486
|
+
if (!targetId) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (action === 'uninstall') {
|
|
491
|
+
const confirmed = window.confirm(`Confirm ${action} ${targetId}?`);
|
|
492
|
+
if (!confirmed) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
manageMutation.mutate({
|
|
498
|
+
type: record.type,
|
|
499
|
+
action,
|
|
500
|
+
id: targetId,
|
|
501
|
+
spec: record.spec
|
|
502
|
+
});
|
|
320
503
|
};
|
|
321
504
|
|
|
322
505
|
return (
|
|
323
506
|
<div className="animate-fade-in pb-20">
|
|
324
|
-
<div className="
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
<p className="text-[13px] text-gray-500 mt-1">Search, discover and install plugins/skills.</p>
|
|
328
|
-
</div>
|
|
329
|
-
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-indigo-50 text-indigo-600 text-xs font-semibold">
|
|
330
|
-
<Store className="h-3.5 w-3.5" />
|
|
331
|
-
Read-only Catalog
|
|
332
|
-
</div>
|
|
507
|
+
<div className="mb-5">
|
|
508
|
+
<h2 className="text-2xl font-bold text-gray-900">Marketplace</h2>
|
|
509
|
+
<p className="text-[13px] text-gray-500 mt-1">A cleaner extension list focused on install / enable / disable.</p>
|
|
333
510
|
</div>
|
|
334
511
|
|
|
335
512
|
<Tabs
|
|
@@ -339,10 +516,11 @@ export function MarketplacePage() {
|
|
|
339
516
|
setScope(value as ScopeType);
|
|
340
517
|
setPage(1);
|
|
341
518
|
}}
|
|
342
|
-
className="mb-
|
|
519
|
+
className="mb-4"
|
|
343
520
|
/>
|
|
344
521
|
|
|
345
522
|
<FilterPanel
|
|
523
|
+
scope={scope}
|
|
346
524
|
searchText={searchText}
|
|
347
525
|
typeFilter={typeFilter}
|
|
348
526
|
sort={sort}
|
|
@@ -357,41 +535,55 @@ export function MarketplacePage() {
|
|
|
357
535
|
}}
|
|
358
536
|
/>
|
|
359
537
|
|
|
360
|
-
<RecommendationSection
|
|
361
|
-
items={recommendations}
|
|
362
|
-
loading={recommendationsQuery.isLoading}
|
|
363
|
-
installState={installState}
|
|
364
|
-
installedSets={installedSets}
|
|
365
|
-
onInstall={handleInstall}
|
|
366
|
-
/>
|
|
367
|
-
|
|
368
538
|
<section>
|
|
369
539
|
<div className="flex items-center justify-between mb-3">
|
|
370
|
-
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed
|
|
540
|
+
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed' : 'Extensions'}</h3>
|
|
371
541
|
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
372
542
|
</div>
|
|
373
543
|
|
|
374
|
-
{itemsQuery.isError && (
|
|
544
|
+
{scope === 'all' && itemsQuery.isError && (
|
|
375
545
|
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
376
546
|
Failed to load marketplace data: {itemsQuery.error.message}
|
|
377
547
|
</div>
|
|
378
548
|
)}
|
|
549
|
+
{scope === 'installed' && installedQuery.isError && (
|
|
550
|
+
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
551
|
+
Failed to load installed items: {installedQuery.error.message}
|
|
552
|
+
</div>
|
|
553
|
+
)}
|
|
379
554
|
|
|
380
|
-
<div className="grid grid-cols-1
|
|
381
|
-
{
|
|
382
|
-
<
|
|
555
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
|
|
556
|
+
{scope === 'all' && allItems.map((item) => (
|
|
557
|
+
<MarketplaceListCard
|
|
383
558
|
key={item.id}
|
|
384
559
|
item={item}
|
|
385
|
-
|
|
560
|
+
record={findInstalledRecordForItem(item, installedRecordLookup)}
|
|
386
561
|
installState={installState}
|
|
562
|
+
manageState={manageState}
|
|
387
563
|
onInstall={handleInstall}
|
|
564
|
+
onManage={handleManage}
|
|
565
|
+
/>
|
|
566
|
+
))}
|
|
567
|
+
|
|
568
|
+
{scope === 'installed' && installedEntries.map((entry) => (
|
|
569
|
+
<MarketplaceListCard
|
|
570
|
+
key={entry.key}
|
|
571
|
+
item={entry.item}
|
|
572
|
+
record={entry.record}
|
|
573
|
+
installState={installState}
|
|
574
|
+
manageState={manageState}
|
|
575
|
+
onInstall={handleInstall}
|
|
576
|
+
onManage={handleManage}
|
|
388
577
|
/>
|
|
389
578
|
))}
|
|
390
579
|
</div>
|
|
391
580
|
|
|
392
|
-
{!itemsQuery.isLoading && !itemsQuery.isError &&
|
|
581
|
+
{scope === 'all' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
|
|
393
582
|
<div className="text-[13px] text-gray-500 py-8 text-center">No items found.</div>
|
|
394
583
|
)}
|
|
584
|
+
{scope === 'installed' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
|
|
585
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">No installed items found.</div>
|
|
586
|
+
)}
|
|
395
587
|
</section>
|
|
396
588
|
|
|
397
589
|
{scope === 'all' && (
|