feat: plugins v1
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
<!-- eslint-disable @angular-eslint/template/cyclomatic-complexity -->
|
||||
<section
|
||||
class="flex h-full min-h-0 flex-col bg-background text-foreground"
|
||||
data-testid="plugin-manager"
|
||||
>
|
||||
<header class="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label="Back to settings"
|
||||
(click)="close()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideArrowLeft"
|
||||
size="18"
|
||||
/>
|
||||
</button>
|
||||
<div class="min-w-0">
|
||||
<h2 class="truncate text-base font-semibold">Plugins</h2>
|
||||
<p class="truncate text-xs text-muted-foreground">Local runtime, store install, capabilities, logs, extension points.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted disabled:opacity-50"
|
||||
[disabled]="busyAll()"
|
||||
(click)="activateAll()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePlay"
|
||||
size="16"
|
||||
/>
|
||||
Activate ready plugins
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted"
|
||||
(click)="openStore()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideStore"
|
||||
size="16"
|
||||
/>
|
||||
Open Plugin Store
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<nav
|
||||
class="flex gap-2 border-b border-border px-4 py-2"
|
||||
aria-label="Plugin manager sections"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'installed'"
|
||||
(click)="setTab('installed')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePackage"
|
||||
size="16"
|
||||
/>
|
||||
Installed
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'extensions'"
|
||||
(click)="setTab('extensions')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSettings"
|
||||
size="16"
|
||||
/>
|
||||
Extension points
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'requirements'"
|
||||
(click)="setTab('requirements')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideShield"
|
||||
size="16"
|
||||
/>
|
||||
Requirements
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'settings'"
|
||||
(click)="setTab('settings')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSettings"
|
||||
size="16"
|
||||
/>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'docs'"
|
||||
(click)="setTab('docs')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePackage"
|
||||
size="16"
|
||||
/>
|
||||
Docs
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-2 rounded-md px-3 text-sm"
|
||||
[class.bg-muted]="activeTab() === 'logs'"
|
||||
(click)="setTab('logs')"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideBug"
|
||||
size="16"
|
||||
/>
|
||||
Logs
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto p-4">
|
||||
@switch (activeTab()) {
|
||||
@case ('extensions') {
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="grid gap-3 md:grid-cols-2 xl:grid-cols-4"
|
||||
data-testid="plugin-extension-counts"
|
||||
>
|
||||
@for (
|
||||
item of [
|
||||
{ label: 'Settings pages', value: extensionCounts().settingsPages },
|
||||
{ label: 'App pages', value: extensionCounts().appPages },
|
||||
{ label: 'Side panels', value: extensionCounts().sidePanels },
|
||||
{ label: 'Channel sections', value: extensionCounts().channelSections },
|
||||
{ label: 'Composer actions', value: extensionCounts().composerActions },
|
||||
{ label: 'Profile actions', value: extensionCounts().profileActions },
|
||||
{ label: 'Toolbar actions', value: extensionCounts().toolbarActions },
|
||||
{ label: 'Embed renderers', value: extensionCounts().embeds }
|
||||
];
|
||||
track item.label
|
||||
) {
|
||||
<article class="rounded-lg border border-border bg-card p-3">
|
||||
<p class="text-sm text-muted-foreground">{{ item.label }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold">{{ item.value }}</p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="rounded-lg border border-border bg-card p-4"
|
||||
data-testid="plugin-conflict-diagnostics"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">Conflict diagnostics</h3>
|
||||
@if (uiConflicts().length === 0) {
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
No duplicate route, action, embed, channel, panel, or settings contribution ids detected.
|
||||
</p>
|
||||
} @else {
|
||||
<div class="mt-3 space-y-2">
|
||||
@for (conflict of uiConflicts(); track conflict.kind + conflict.contributionId) {
|
||||
<div class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm">
|
||||
<span class="font-medium">{{ conflict.kind }} / {{ conflict.contributionId }}</span>
|
||||
<span class="text-muted-foreground"> conflicts in {{ conflict.pluginIds.join(', ') }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
@case ('requirements') {
|
||||
<div
|
||||
class="space-y-3"
|
||||
data-testid="plugin-server-requirements"
|
||||
>
|
||||
@if (requirementComparisons().length === 0) {
|
||||
<p class="rounded-lg border border-border bg-card p-4 text-sm text-muted-foreground">
|
||||
No server plugin requirements for the current room.
|
||||
</p>
|
||||
} @else {
|
||||
@for (comparison of requirementComparisons(); track comparison.pluginId) {
|
||||
<article class="rounded-lg border border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">{{ comparison.installed?.title ?? comparison.pluginId }}</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">{{ comparison.pluginId }}</p>
|
||||
</div>
|
||||
<span class="rounded bg-muted px-2 py-1 text-xs text-muted-foreground">{{ comparison.status }}</span>
|
||||
</div>
|
||||
@if (comparison.requirement) {
|
||||
<p class="mt-3 text-sm text-muted-foreground">Server status: {{ comparison.requirement.status }}</p>
|
||||
@if (comparison.requirement.versionRange) {
|
||||
<p class="mt-1 text-sm text-muted-foreground">Version range: {{ comparison.requirement.versionRange }}</p>
|
||||
}
|
||||
@if (comparison.requirement.reason) {
|
||||
<p class="mt-1 text-sm text-muted-foreground">{{ comparison.requirement.reason }}</p>
|
||||
}
|
||||
}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@case ('settings') {
|
||||
<div
|
||||
class="grid gap-4 xl:grid-cols-[260px_minmax(0,1fr)]"
|
||||
data-testid="plugin-generated-settings"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
@for (entry of entries(); track trackEntry($index, entry)) {
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-md border border-border px-3 py-2 text-left text-sm hover:bg-muted"
|
||||
[class.bg-muted]="isSelected(entry)"
|
||||
(click)="selectPlugin(entry.manifest.id)"
|
||||
>
|
||||
{{ entry.manifest.title }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<section class="rounded-lg border border-border bg-card p-4">
|
||||
@if (selectedPlugin(); as plugin) {
|
||||
<h3 class="text-sm font-semibold">{{ plugin.manifest.title }} settings</h3>
|
||||
@if (selectedSettingsPages().length > 0) {
|
||||
<div class="mt-4 space-y-3">
|
||||
@for (page of selectedSettingsPages(); track page.id) {
|
||||
<article class="rounded-md border border-border bg-background/40 p-3">
|
||||
<h4 class="mb-2 text-sm font-medium">{{ page.contribution.label }}</h4>
|
||||
<app-plugin-render-host [render]="page.contribution.render"></app-plugin-render-host>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (selectedSettingsSchema()) {
|
||||
<pre class="mt-3 max-h-[420px] overflow-auto rounded-md bg-muted p-3 text-xs">{{ selectedSettingsSchema() | json }}</pre>
|
||||
} @else {
|
||||
<p class="mt-2 text-sm text-muted-foreground">This plugin does not declare a settings schema.</p>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
@case ('docs') {
|
||||
<div
|
||||
class="grid gap-4 xl:grid-cols-[260px_minmax(0,1fr)]"
|
||||
data-testid="plugin-installed-docs"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
@for (entry of entries(); track trackEntry($index, entry)) {
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-md border border-border px-3 py-2 text-left text-sm hover:bg-muted"
|
||||
[class.bg-muted]="isSelected(entry)"
|
||||
(click)="selectPlugin(entry.manifest.id)"
|
||||
>
|
||||
{{ entry.manifest.title }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<section class="rounded-lg border border-border bg-card p-4">
|
||||
@if (selectedPlugin(); as plugin) {
|
||||
<h3 class="text-sm font-semibold">{{ plugin.manifest.title }}</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">{{ plugin.manifest.description }}</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@for (doc of selectedDocs(); track doc.label) {
|
||||
<a
|
||||
class="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
|
||||
[href]="doc.url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ doc.label }}</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<pre class="mt-4 max-h-[420px] overflow-auto rounded-md bg-muted p-3 text-xs">{{ plugin.manifest | json }}</pre>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
@case ('logs') {
|
||||
<div class="space-y-3">
|
||||
@if (!selectedPlugin()) {
|
||||
<p class="text-sm text-muted-foreground">No plugins installed.</p>
|
||||
} @else {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (entry of entries(); track trackEntry($index, entry)) {
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border px-3 py-1 text-sm hover:bg-muted"
|
||||
[class.bg-muted]="isSelected(entry)"
|
||||
(click)="selectPlugin(entry.manifest.id)"
|
||||
>
|
||||
{{ entry.manifest.title }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-card">
|
||||
@if (selectedLogs().length === 0) {
|
||||
<p class="p-4 text-sm text-muted-foreground">No logs for selected plugin.</p>
|
||||
} @else {
|
||||
@for (log of selectedLogs(); track log.timestamp) {
|
||||
<div class="border-b border-border px-4 py-3 last:border-b-0">
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="uppercase">{{ log.level }}</span>
|
||||
<span>{{ log.timestamp | date: 'short' }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm">{{ log.message }}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<div class="grid gap-4 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div class="space-y-3">
|
||||
@if (entries().length === 0) {
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-border p-8 text-center"
|
||||
data-testid="plugin-empty-state"
|
||||
>
|
||||
<ng-icon
|
||||
class="mx-auto text-muted-foreground"
|
||||
name="lucidePackage"
|
||||
size="28"
|
||||
/>
|
||||
<p class="mt-3 text-sm font-medium">No plugins installed.</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Use Store tab or local plugin folder discovery.</p>
|
||||
</div>
|
||||
} @else {
|
||||
@for (entry of entries(); track trackEntry($index, entry)) {
|
||||
<article
|
||||
class="rounded-lg border border-border bg-card p-4"
|
||||
[class.ring-2]="isSelected(entry)"
|
||||
[class.ring-primary]="isSelected(entry)"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="truncate text-sm font-semibold">{{ entry.manifest.title }}</h3>
|
||||
<span class="rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ entry.state }}</span>
|
||||
<span class="rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">v{{ entry.manifest.version }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-muted-foreground">{{ entry.manifest.description }}</p>
|
||||
<p class="mt-2 text-xs text-muted-foreground">{{ entry.manifest.id }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2 text-sm hover:bg-muted"
|
||||
(click)="selectPlugin(entry.manifest.id)"
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2 text-sm hover:bg-muted"
|
||||
(click)="setEnabled(entry, !entry.enabled)"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="entry.enabled ? 'lucideX' : 'lucideCheck'"
|
||||
size="14"
|
||||
/>
|
||||
{{ entry.enabled ? 'Disable' : 'Enable' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2 text-sm hover:bg-muted disabled:opacity-50"
|
||||
[disabled]="busyPluginId() === entry.manifest.id"
|
||||
(click)="reload(entry)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideRefreshCw"
|
||||
size="14"
|
||||
/>
|
||||
Reload
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2 text-sm hover:bg-muted disabled:opacity-50"
|
||||
[disabled]="busyPluginId() === entry.manifest.id"
|
||||
(click)="unload(entry)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
size="14"
|
||||
/>
|
||||
Unload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (entry.error) {
|
||||
<p class="mt-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ entry.error }}</p>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside class="rounded-lg border border-border bg-card p-4">
|
||||
@if (selectedPlugin(); as plugin) {
|
||||
<div class="flex items-center gap-2">
|
||||
<ng-icon
|
||||
name="lucideShield"
|
||||
size="18"
|
||||
/>
|
||||
<h3 class="text-sm font-semibold">Capabilities</h3>
|
||||
</div>
|
||||
@if ((plugin.manifest.capabilities?.length ?? 0) === 0) {
|
||||
<p class="mt-3 text-sm text-muted-foreground">Plugin requests no capabilities.</p>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 h-8 rounded-md border border-border px-3 text-sm hover:bg-muted"
|
||||
(click)="grantAll(plugin)"
|
||||
>
|
||||
Grant all requested
|
||||
</button>
|
||||
<div class="mt-3 space-y-2">
|
||||
@for (capability of plugin.manifest.capabilities; track trackCapability($index, capability)) {
|
||||
<label class="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4"
|
||||
[checked]="capabilities.has(plugin.manifest.id, capability)"
|
||||
(change)="toggleCapability(plugin, capability)"
|
||||
/>
|
||||
<span>{{ capability }}</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (missingCapabilities().length > 0) {
|
||||
<p class="mt-3 text-xs text-muted-foreground">Missing: {{ missingCapabilities().join(', ') }}</p>
|
||||
}
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,208 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Output,
|
||||
computed,
|
||||
inject,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideArrowLeft,
|
||||
lucideBug,
|
||||
lucideCheck,
|
||||
lucidePackage,
|
||||
lucidePlay,
|
||||
lucideRefreshCw,
|
||||
lucideSettings,
|
||||
lucideShield,
|
||||
lucideStore,
|
||||
lucideX
|
||||
} from '@ng-icons/lucide';
|
||||
import type { PluginCapabilityId } from '../../../../shared-kernel';
|
||||
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
|
||||
import { PluginHostService } from '../../application/services/plugin-host.service';
|
||||
import { PluginLoggerService } from '../../application/services/plugin-logger.service';
|
||||
import { PluginRegistryService } from '../../application/services/plugin-registry.service';
|
||||
import { PluginRequirementStateService } from '../../application/services/plugin-requirement-state.service';
|
||||
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
|
||||
import type { RegisteredPlugin } from '../../domain/models/plugin-runtime.models';
|
||||
import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component';
|
||||
|
||||
type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirements' | 'settings';
|
||||
|
||||
@Component({
|
||||
selector: 'app-plugin-manager',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
PluginRenderHostComponent
|
||||
],
|
||||
templateUrl: './plugin-manager.component.html',
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideArrowLeft,
|
||||
lucideBug,
|
||||
lucideCheck,
|
||||
lucidePackage,
|
||||
lucidePlay,
|
||||
lucideRefreshCw,
|
||||
lucideSettings,
|
||||
lucideShield,
|
||||
lucideStore,
|
||||
lucideX
|
||||
})
|
||||
]
|
||||
})
|
||||
export class PluginManagerComponent {
|
||||
@Output() readonly closed = new EventEmitter<void>();
|
||||
|
||||
readonly capabilities = inject(PluginCapabilityService);
|
||||
readonly host = inject(PluginHostService);
|
||||
readonly logger = inject(PluginLoggerService);
|
||||
readonly registry = inject(PluginRegistryService);
|
||||
readonly requirementState = inject(PluginRequirementStateService);
|
||||
readonly router = inject(Router);
|
||||
readonly uiRegistry = inject(PluginUiRegistryService);
|
||||
readonly activeTab = signal<PluginManagerTab>('installed');
|
||||
readonly busyPluginId = signal<string | null>(null);
|
||||
readonly busyAll = signal(false);
|
||||
readonly selectedPluginId = signal<string | null>(null);
|
||||
readonly entries = this.registry.entries;
|
||||
readonly selectedPlugin = computed(() => {
|
||||
const selectedPluginId = this.selectedPluginId();
|
||||
|
||||
return this.entries().find((entry) => entry.manifest.id === selectedPluginId) ?? this.entries()[0] ?? null;
|
||||
});
|
||||
readonly missingCapabilities = computed(() => {
|
||||
const selectedPlugin = this.selectedPlugin();
|
||||
|
||||
return selectedPlugin ? this.capabilities.missing(selectedPlugin.manifest) : [];
|
||||
});
|
||||
readonly selectedLogs = computed(() => {
|
||||
const selectedPlugin = this.selectedPlugin();
|
||||
|
||||
return selectedPlugin ? this.logger.entries().filter((entry) => entry.pluginId === selectedPlugin.manifest.id)
|
||||
.slice(-20) : [];
|
||||
});
|
||||
readonly extensionCounts = computed(() => ({
|
||||
appPages: this.uiRegistry.appPages().length,
|
||||
channelSections: this.uiRegistry.channelSections().length,
|
||||
composerActions: this.uiRegistry.composerActions().length,
|
||||
embeds: this.uiRegistry.embeds().length,
|
||||
profileActions: this.uiRegistry.profileActions().length,
|
||||
settingsPages: this.uiRegistry.settingsPages().length,
|
||||
sidePanels: this.uiRegistry.sidePanels().length,
|
||||
toolbarActions: this.uiRegistry.toolbarActions().length
|
||||
}));
|
||||
readonly requirementComparisons = this.requirementState.comparisons;
|
||||
readonly uiConflicts = this.uiRegistry.conflicts;
|
||||
readonly selectedRequirement = computed(() => {
|
||||
const selectedPlugin = this.selectedPlugin();
|
||||
|
||||
return selectedPlugin ? this.requirementState.comparisonFor(selectedPlugin.manifest.id) : null;
|
||||
});
|
||||
readonly selectedSettingsSchema = computed(() => this.selectedPlugin()?.manifest.settings ?? null);
|
||||
readonly selectedSettingsPages = computed(() => {
|
||||
const selectedPlugin = this.selectedPlugin();
|
||||
|
||||
return selectedPlugin
|
||||
? this.uiRegistry.settingsPageRecords().filter((record) => record.pluginId === selectedPlugin.manifest.id)
|
||||
: [];
|
||||
});
|
||||
readonly selectedDocs = computed(() => {
|
||||
const manifest = this.selectedPlugin()?.manifest;
|
||||
|
||||
if (!manifest) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: 'Readme', url: manifest.readme },
|
||||
{ label: 'Homepage', url: manifest.homepage },
|
||||
{ label: 'Changelog', url: manifest.changelog },
|
||||
{ label: 'Support', url: manifest.bugs }
|
||||
].filter((item): item is { label: string; url: string } => typeof item.url === 'string' && item.url.length > 0);
|
||||
});
|
||||
|
||||
setTab(tab: PluginManagerTab): void {
|
||||
this.activeTab.set(tab);
|
||||
}
|
||||
|
||||
openStore(): void {
|
||||
const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url;
|
||||
|
||||
this.closed.emit();
|
||||
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
|
||||
}
|
||||
|
||||
selectPlugin(pluginId: string): void {
|
||||
this.selectedPluginId.set(pluginId);
|
||||
}
|
||||
|
||||
grantAll(entry: RegisteredPlugin): void {
|
||||
this.capabilities.grantAll(entry.manifest);
|
||||
}
|
||||
|
||||
toggleCapability(entry: RegisteredPlugin, capability: PluginCapabilityId): void {
|
||||
if (this.capabilities.has(entry.manifest.id, capability)) {
|
||||
this.capabilities.revoke(entry.manifest.id, capability);
|
||||
return;
|
||||
}
|
||||
|
||||
this.capabilities.grant(entry.manifest.id, capability);
|
||||
}
|
||||
|
||||
async activateAll(): Promise<void> {
|
||||
this.busyAll.set(true);
|
||||
|
||||
try {
|
||||
await this.host.activateReadyPlugins();
|
||||
} finally {
|
||||
this.busyAll.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async reload(entry: RegisteredPlugin): Promise<void> {
|
||||
this.busyPluginId.set(entry.manifest.id);
|
||||
|
||||
try {
|
||||
await this.host.reloadPlugin(entry.manifest.id);
|
||||
} finally {
|
||||
this.busyPluginId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
async unload(entry: RegisteredPlugin): Promise<void> {
|
||||
this.busyPluginId.set(entry.manifest.id);
|
||||
|
||||
try {
|
||||
await this.host.deactivatePlugin(entry.manifest.id);
|
||||
} finally {
|
||||
this.busyPluginId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(entry: RegisteredPlugin, enabled: boolean): void {
|
||||
this.registry.setEnabled(entry.manifest.id, enabled);
|
||||
}
|
||||
|
||||
isSelected(entry: RegisteredPlugin): boolean {
|
||||
return this.selectedPlugin()?.manifest.id === entry.manifest.id;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed.emit();
|
||||
}
|
||||
|
||||
trackEntry(index: number, entry: RegisteredPlugin): string {
|
||||
return entry.manifest.id;
|
||||
}
|
||||
|
||||
trackCapability(index: number, capability: PluginCapabilityId): string {
|
||||
return capability;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user