feat(marketplace): UI fuer Plugin-Marketplace — Modul 5/16 des UI-Backlogs
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
ERSTES Modul ueber die Phase-Q-Plugin-Architektur: Registrierung komplett ueber das Plugin-Manifest (page_routes + menu_items) — routes/index.tsx und Sidebar.tsx wurden NICHT angefasst. Der Komponenten-Map-Generator wired den lazy import automatisch (38 Komponenten). Backend existierte vollstaendig (listings mit search/tags/pagination, listing-detail, install mit Ed25519-Signatur-Verify, verify, categories; marketplace:read/admin + require_admin fuer Install), Frontend hatte 0% Abdeckung. - api/marketplace.ts: TanStack-Hooks (useMarketplaceListings mit search/tags/pagination, useMarketplaceListing, useMarketplaceCategories, useInstallFromMarketplace, useVerifyMarketplacePlugin) - pages/Marketplace.tsx: Suche, Tag-Filter-Chips, Listing-Karten (Name, Version, Author, Beschreibung, Tags, Download-Counter, Verified-Badge, Preis/Kostenlos), Install mit Confirm (Admin-only via is_system_admin), Signatur-Verify, Ergebnis-Banner, Pagination — No-Access-Card ohne marketplace:read - Manifest: page_route /marketplace + menu_item (Store-Icon, order 85, permission marketplace:read) - i18n marketplace.* + nav.marketplace de/en Verifikation: Vitest 10/10 (Rendering, No-Access, Empty/Error, Karten, Search+Tags, Admin-Gating, Install-Flow mit+ohne Confirm, Verify-Flow) · tsc exit 0 · production build exit 0 · Backend-Regressionen (route-order, m5-miniapps) 10/10 · compileall sauber · Manifest-Check: page_route + menu_item korrekt.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Marketplace API client — browse, search, verify and install plugins.
|
||||
*
|
||||
* Backend: /api/v1/marketplace (listings, listing detail, install, verify,
|
||||
* categories). Permissions: marketplace:read (browse/verify), admin
|
||||
* (install).
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost } from '@/api/client';
|
||||
|
||||
export interface MarketplaceListing {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
download_url: string;
|
||||
icon: string;
|
||||
screenshots: string[];
|
||||
tags: string[];
|
||||
price: number;
|
||||
is_verified: boolean;
|
||||
download_count: number;
|
||||
min_app_version: string;
|
||||
license: string;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface MarketplaceListResponse {
|
||||
listings: MarketplaceListing[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface MarketplaceCategoriesResponse {
|
||||
categories: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MarketplaceInstallResponse {
|
||||
success: boolean;
|
||||
name: string;
|
||||
display_name: string;
|
||||
version: string;
|
||||
installed: boolean;
|
||||
activated: boolean;
|
||||
message: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface MarketplaceVerifyResponse {
|
||||
name: string;
|
||||
version: string;
|
||||
signature_valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MarketplaceListParams {
|
||||
search?: string;
|
||||
tags?: string[];
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// ─── Query hooks ─────────────────────────────────────────────
|
||||
|
||||
export function useMarketplaceListings(params: MarketplaceListParams = {}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.search) searchParams.set('search', params.search);
|
||||
if (params.tags && params.tags.length > 0) searchParams.set('tags', params.tags.join(','));
|
||||
if (params.page) searchParams.set('page', String(params.page));
|
||||
if (params.pageSize) searchParams.set('page_size', String(params.pageSize));
|
||||
const qs = searchParams.toString();
|
||||
|
||||
return useQuery<MarketplaceListResponse>({
|
||||
queryKey: ['marketplace-listings', params.search, params.tags, params.page, params.pageSize],
|
||||
queryFn: () => apiGet<MarketplaceListResponse>(`/marketplace/listings${qs ? `?${qs}` : ''}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarketplaceListing(name: string | null) {
|
||||
return useQuery<MarketplaceListing>({
|
||||
queryKey: ['marketplace-listings', name],
|
||||
queryFn: () => apiGet<MarketplaceListing>(`/marketplace/listings/${name}`),
|
||||
enabled: !!name,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarketplaceCategories() {
|
||||
return useQuery<MarketplaceCategoriesResponse>({
|
||||
queryKey: ['marketplace-categories'],
|
||||
queryFn: () => apiGet<MarketplaceCategoriesResponse>('/marketplace/categories'),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Mutation hooks ──────────────────────────────────────────
|
||||
|
||||
export function useInstallFromMarketplace() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<
|
||||
MarketplaceInstallResponse,
|
||||
Error,
|
||||
{ name: string; signature?: string; publicKey?: string; activate?: boolean }
|
||||
>({
|
||||
mutationFn: ({ name, signature, publicKey, activate }) =>
|
||||
apiPost<MarketplaceInstallResponse>(`/marketplace/install/${name}`, {
|
||||
name,
|
||||
signature: signature ?? null,
|
||||
public_key: publicKey ?? null,
|
||||
activate: activate ?? false,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['marketplace-listings'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useVerifyMarketplacePlugin() {
|
||||
return useMutation<
|
||||
MarketplaceVerifyResponse,
|
||||
Error,
|
||||
{ name: string; signature?: string; publicKey?: string }
|
||||
>({
|
||||
mutationFn: ({ name, signature, publicKey }) =>
|
||||
apiPost<MarketplaceVerifyResponse>(`/marketplace/verify/${name}`, {
|
||||
name,
|
||||
signature: signature ?? null,
|
||||
public_key: publicKey ?? null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user