mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): preserve plugin install identity
This commit is contained in:
@@ -230,37 +230,35 @@ describe("PluginsPage", () => {
|
||||
throw new Error(`Unexpected method ${method}`);
|
||||
});
|
||||
const harness = createGateway(client);
|
||||
const lobsterCatalog = createResult(
|
||||
createPlugin({
|
||||
id: "lobster",
|
||||
name: "Lobster",
|
||||
installed: false,
|
||||
install: { source: "clawhub", packageName: "@openclaw/lobster" },
|
||||
}),
|
||||
);
|
||||
const lobsterCatalog = createResult();
|
||||
const { page } = await mountPage(
|
||||
createContext(harness.gateway),
|
||||
createPluginsRouteData(harness.gateway, lobsterCatalog),
|
||||
);
|
||||
const rowKey = "plugin:lobster";
|
||||
const rowKey = "clawhub:@openclaw/lobster";
|
||||
const installIdentity = "plugin:lobster";
|
||||
const installRequest = {
|
||||
source: "clawhub",
|
||||
packageName: "@openclaw/lobster",
|
||||
} satisfies PluginInstallRequest;
|
||||
|
||||
await page.install(rowKey, installRequest);
|
||||
await page.install(rowKey, installRequest, installIdentity);
|
||||
expect(page.messages[rowKey]?.installPolicyWarning?.details.acknowledgementToken).toBe(
|
||||
"approval-token",
|
||||
);
|
||||
|
||||
const pendingRetry = page.install(rowKey, {
|
||||
...installRequest,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
});
|
||||
const pendingRetry = page.install(
|
||||
rowKey,
|
||||
{
|
||||
...installRequest,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
},
|
||||
installIdentity,
|
||||
);
|
||||
await waitForFast(() =>
|
||||
expect(request.mock.calls.filter(([method]) => method === "plugins.install")).toHaveLength(2),
|
||||
);
|
||||
await page.install("clawhub:@openclaw/lobster", installRequest);
|
||||
await page.install("plugin:lobster", installRequest, installIdentity);
|
||||
expect(request.mock.calls.filter(([method]) => method === "plugins.install")).toHaveLength(2);
|
||||
expect(page.messages[rowKey]?.installPolicyWarning).toBeDefined();
|
||||
page.messages["plugin:workboard"] = { kind: "success", text: "Unrelated message." };
|
||||
@@ -272,7 +270,7 @@ describe("PluginsPage", () => {
|
||||
);
|
||||
expect(page.messages[rowKey]).toBeUndefined();
|
||||
expect(page.messages["plugin:workboard"]?.text).toBe("Unrelated message.");
|
||||
expect(page.installOutcomeReconciliations[rowKey]).toBe("checking");
|
||||
expect(page.installOutcomeReconciliations[installIdentity]).toBe("checking");
|
||||
|
||||
retry.resolve({
|
||||
ok: true,
|
||||
@@ -285,7 +283,9 @@ describe("PluginsPage", () => {
|
||||
createPlugin({ id: "lobster", name: "Lobster", installed: true, enabled: true }),
|
||||
),
|
||||
);
|
||||
await waitForFast(() => expect(page.installOutcomeReconciliations[rowKey]).toBeUndefined());
|
||||
await waitForFast(() =>
|
||||
expect(page.installOutcomeReconciliations[installIdentity]).toBeUndefined(),
|
||||
);
|
||||
expect(page.messages[rowKey]).toBeUndefined();
|
||||
expect(page.result?.plugins[0]?.installed).toBe(true);
|
||||
});
|
||||
@@ -349,11 +349,15 @@ describe("PluginsPage", () => {
|
||||
packageName: "@openclaw/lobster",
|
||||
} satisfies PluginInstallRequest;
|
||||
|
||||
await page.install(rowKey, installRequest);
|
||||
const pendingRetry = page.install(rowKey, {
|
||||
...installRequest,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
});
|
||||
await page.install(rowKey, installRequest, rowKey);
|
||||
const pendingRetry = page.install(
|
||||
rowKey,
|
||||
{
|
||||
...installRequest,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
},
|
||||
rowKey,
|
||||
);
|
||||
await waitForFast(() => expect(installCalls).toBe(2));
|
||||
|
||||
harness.emit(client, false);
|
||||
@@ -586,10 +590,14 @@ describe("PluginsPage", () => {
|
||||
runtimeConfig.patchForm(["pending"], true);
|
||||
|
||||
if (action === "install") {
|
||||
await page.install("search:example-plugin", {
|
||||
source: "clawhub",
|
||||
packageName: "example-plugin",
|
||||
} as PluginInstallRequest);
|
||||
await page.install(
|
||||
"search:example-plugin",
|
||||
{
|
||||
source: "clawhub",
|
||||
packageName: "example-plugin",
|
||||
} as PluginInstallRequest,
|
||||
"clawhub:example-plugin",
|
||||
);
|
||||
} else if (action === "enable") {
|
||||
await page.updateEnabled("workboard", true);
|
||||
} else {
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
installPlugin,
|
||||
pluginInstallNeedsRiskAcknowledgement,
|
||||
readPluginInstallTrustError,
|
||||
resolvePluginInstallIdentity,
|
||||
runPluginConfigMutation,
|
||||
setPluginEnabled,
|
||||
uninstallPlugin,
|
||||
@@ -838,8 +837,11 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async install(rowKey: string, request: PluginInstallRequest): Promise<void> {
|
||||
const operationKey = resolvePluginInstallIdentity(request, this.result?.plugins ?? []);
|
||||
private async install(
|
||||
rowKey: string,
|
||||
request: PluginInstallRequest,
|
||||
installIdentity: string,
|
||||
): Promise<void> {
|
||||
await this.runPluginMutation(
|
||||
rowKey,
|
||||
(client) => installPlugin(client, request),
|
||||
@@ -880,8 +882,8 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
});
|
||||
},
|
||||
{
|
||||
operationKey,
|
||||
pendingInstallTarget: operationKey,
|
||||
operationKey: installIdentity,
|
||||
pendingInstallTarget: installIdentity,
|
||||
preserveMessageWhilePending: request.installPolicyWarningAcknowledgement !== undefined,
|
||||
},
|
||||
);
|
||||
@@ -1114,7 +1116,8 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
},
|
||||
onSetEnabled: (pluginId, enabled, rowKey) =>
|
||||
void this.updateEnabled(pluginId, enabled, rowKey),
|
||||
onInstall: (rowKey, request) => void this.install(rowKey, request),
|
||||
onInstall: (rowKey, request, installIdentity) =>
|
||||
void this.install(rowKey, request, installIdentity),
|
||||
onDismissMessage: (rowKey) => this.setMessage(rowKey, null),
|
||||
onRetryInstallOutcome: () => void this.refreshCatalog(),
|
||||
onRequestUninstall: (rowKey) => this.setPendingRemoval(rowKey, true),
|
||||
|
||||
@@ -834,13 +834,19 @@ describeControlUiE2e("Control UI Plugins mocked Gateway E2E", () => {
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: pluginMethods,
|
||||
methodResponses: pluginMethodResponses(),
|
||||
methodResponses: {
|
||||
...pluginMethodResponses(),
|
||||
"plugins.list": inventory([workboardDisabled, remoteIconPlugin]),
|
||||
"plugins.search": lobsterSearchResponse,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}settings/plugins`);
|
||||
await page.getByRole("tab", { name: /^Discover/u }).click();
|
||||
const row = page.locator('[data-plugin-id="lobster"]');
|
||||
await page.getByRole("searchbox", { name: "Search plugins" }).fill("lobster");
|
||||
await gateway.waitForRequest("plugins.search");
|
||||
const row = page.locator('[data-package-name="@openclaw/lobster"]');
|
||||
await row.waitFor({ state: "visible" });
|
||||
|
||||
await gateway.deferNext("plugins.install");
|
||||
@@ -876,9 +882,6 @@ describeControlUiE2e("Control UI Plugins mocked Gateway E2E", () => {
|
||||
);
|
||||
expect((await gateway.getRequests("plugins.install")).length).toBe(2);
|
||||
|
||||
await gateway.setMethodResponse("plugins.search", lobsterSearchResponse);
|
||||
await page.getByRole("searchbox", { name: "Search plugins" }).fill("lobster");
|
||||
await gateway.waitForRequest("plugins.search");
|
||||
const searchRow = page.locator('[data-package-name="@openclaw/lobster"]');
|
||||
await searchRow.waitFor({ state: "visible" });
|
||||
await searchRow.getByText("Checking plugin status…", { exact: false }).waitFor();
|
||||
|
||||
@@ -441,10 +441,14 @@ describe("renderPlugins", () => {
|
||||
container
|
||||
.querySelector<HTMLButtonElement>('[data-plugin-id="tavily"] .plugins-install')
|
||||
?.click();
|
||||
expect(onInstall).toHaveBeenCalledWith(pluginRowKey("tavily"), {
|
||||
source: "official",
|
||||
pluginId: "tavily",
|
||||
});
|
||||
expect(onInstall).toHaveBeenCalledWith(
|
||||
pluginRowKey("tavily"),
|
||||
{
|
||||
source: "official",
|
||||
pluginId: "tavily",
|
||||
},
|
||||
pluginRowKey("tavily"),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders featured plugins newest-featured first", () => {
|
||||
@@ -578,10 +582,14 @@ describe("renderPlugins", () => {
|
||||
expect(normalizedText(result)).toContain("149.3K");
|
||||
expect(normalizedText(result)).toContain("Code plugin");
|
||||
result?.querySelector<HTMLButtonElement>('[aria-label="Install Calendar Plus"]')?.click();
|
||||
expect(onInstall).toHaveBeenCalledWith(clawHubKey("@openclaw/calendar-plus"), {
|
||||
source: "clawhub",
|
||||
packageName: "@openclaw/calendar-plus",
|
||||
});
|
||||
expect(onInstall).toHaveBeenCalledWith(
|
||||
clawHubKey("@openclaw/calendar-plus"),
|
||||
{
|
||||
source: "clawhub",
|
||||
packageName: "@openclaw/calendar-plus",
|
||||
},
|
||||
clawHubKey("@openclaw/calendar-plus"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps discovery available while disabling all read-only mutations", () => {
|
||||
@@ -654,12 +662,16 @@ describe("renderPlugins", () => {
|
||||
expect(row?.getAttribute("aria-busy")).toBe("false");
|
||||
expect(row?.querySelector('[role="alert"]')?.textContent).toContain("Review required.");
|
||||
row?.querySelector<HTMLButtonElement>(".plugins-row-message button")?.click();
|
||||
expect(onInstall).toHaveBeenCalledWith(key, {
|
||||
source: "clawhub",
|
||||
packageName,
|
||||
version: "2.0.0",
|
||||
acknowledgeClawHubRisk: true,
|
||||
});
|
||||
expect(onInstall).toHaveBeenCalledWith(
|
||||
key,
|
||||
{
|
||||
source: "clawhub",
|
||||
packageName,
|
||||
version: "2.0.0",
|
||||
acknowledgeClawHubRisk: true,
|
||||
},
|
||||
key,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders install policy findings with cancel and acknowledged retry actions", () => {
|
||||
@@ -760,10 +772,14 @@ describe("renderPlugins", () => {
|
||||
expect(onDismissMessage).toHaveBeenCalledWith(key);
|
||||
|
||||
actionButton(alert, "Install anyway")?.click();
|
||||
expect(onInstall).toHaveBeenCalledWith(key, {
|
||||
...request,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
});
|
||||
expect(onInstall).toHaveBeenCalledWith(
|
||||
key,
|
||||
{
|
||||
...request,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
},
|
||||
key,
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks a repeated install while its reconnect outcome is unresolved", () => {
|
||||
@@ -821,6 +837,38 @@ describe("renderPlugins", () => {
|
||||
expect(onRetryInstallOutcome).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves a search-only runtime identity when installing", () => {
|
||||
const onInstall = vi.fn();
|
||||
const container = mount(
|
||||
createProps({
|
||||
activeTab: "discover",
|
||||
query: "lobster",
|
||||
result: createResult([]),
|
||||
searchResults: [
|
||||
{
|
||||
score: 1,
|
||||
package: {
|
||||
name: "@openclaw/lobster",
|
||||
displayName: "Lobster",
|
||||
family: "code-plugin",
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
runtimeId: "lobster",
|
||||
},
|
||||
},
|
||||
],
|
||||
onInstall,
|
||||
}),
|
||||
);
|
||||
|
||||
actionButton(container, "Install Lobster")?.click();
|
||||
expect(onInstall).toHaveBeenCalledWith(
|
||||
"clawhub:@openclaw/lobster",
|
||||
{ source: "clawhub", packageName: "@openclaw/lobster" },
|
||||
"plugin:lobster",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the not-installed outcome visible for reason-only policy warnings", () => {
|
||||
const plugin = createPlugin({
|
||||
id: "reason-only",
|
||||
|
||||
+103
-27
@@ -106,7 +106,7 @@ type PluginsViewProps = {
|
||||
onIconError: (pluginId: string) => void;
|
||||
onShowDetails: (pluginId: string | null) => void;
|
||||
onSetEnabled: (pluginId: string, enabled: boolean, rowKey: string) => void;
|
||||
onInstall: (rowKey: string, request: PluginInstallRequest) => void;
|
||||
onInstall: (rowKey: string, request: PluginInstallRequest, installIdentity: string) => void;
|
||||
onDismissMessage: (rowKey: string) => void;
|
||||
onRetryInstallOutcome: () => void;
|
||||
onRequestUninstall: (rowKey: string) => void;
|
||||
@@ -160,17 +160,26 @@ function clawHubRowKey(packageName: string): string {
|
||||
return `clawhub:${packageName}`;
|
||||
}
|
||||
|
||||
function resolveInstallIdentity(
|
||||
props: PluginsViewProps,
|
||||
request: PluginInstallRequest,
|
||||
runtimeId?: string,
|
||||
): string {
|
||||
return resolvePluginInstallIdentity(request, props.result?.plugins ?? [], runtimeId);
|
||||
}
|
||||
|
||||
function installOperationState(
|
||||
props: PluginsViewProps,
|
||||
request: PluginInstallRequest | undefined,
|
||||
runtimeId?: string,
|
||||
identity: string | undefined,
|
||||
): { busy: boolean; outcome?: InstallOutcomeReconciliation } {
|
||||
if (!request) {
|
||||
if (!identity) {
|
||||
return { busy: false };
|
||||
}
|
||||
const identity = resolvePluginInstallIdentity(request, props.result?.plugins ?? [], runtimeId);
|
||||
const outcome = props.installOutcomeReconciliations[identity];
|
||||
return { busy: Boolean(props.busy[identity] || outcome), ...(outcome ? { outcome } : {}) };
|
||||
return {
|
||||
busy: Boolean(props.busy[identity] || outcome),
|
||||
...(outcome ? { outcome } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function connectorRowKey(connectorId: string): string {
|
||||
@@ -396,6 +405,17 @@ function originLabel(origin: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function requestInstall(
|
||||
props: PluginsViewProps,
|
||||
rowKey: string,
|
||||
request: PluginInstallRequest,
|
||||
installIdentity?: string,
|
||||
) {
|
||||
if (installIdentity) {
|
||||
props.onInstall(rowKey, request, installIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
/** Dot-separated plain-text meta line under a row description. */
|
||||
function renderMetaLine(parts: ReadonlyArray<TemplateResult | string | typeof nothing>) {
|
||||
const visible = parts.filter((part) => part !== nothing && part !== "");
|
||||
@@ -416,6 +436,7 @@ function renderRowMessage(
|
||||
busy: boolean,
|
||||
props: PluginsViewProps,
|
||||
installOutcome?: InstallOutcomeReconciliation,
|
||||
installIdentity?: string,
|
||||
) {
|
||||
if (installOutcome) {
|
||||
return html`
|
||||
@@ -533,10 +554,15 @@ function renderRowMessage(
|
||||
title=${props.mutationBlockedReason ?? ""}
|
||||
?disabled=${busy || !props.canMutate}
|
||||
@click=${() =>
|
||||
props.onInstall(key, {
|
||||
...request,
|
||||
installPolicyWarningAcknowledgement: details.acknowledgementToken,
|
||||
})}
|
||||
requestInstall(
|
||||
props,
|
||||
key,
|
||||
{
|
||||
...request,
|
||||
installPolicyWarningAcknowledgement: details.acknowledgementToken,
|
||||
},
|
||||
installIdentity,
|
||||
)}
|
||||
>
|
||||
${busy ? t("pluginsPage.installing") : t("pluginsPage.installAnyway")}
|
||||
</button>
|
||||
@@ -556,12 +582,19 @@ function renderRowMessage(
|
||||
title=${props.mutationBlockedReason ?? ""}
|
||||
?disabled=${busy || !props.canMutate}
|
||||
@click=${() =>
|
||||
props.onInstall(key, {
|
||||
source: "clawhub",
|
||||
packageName: message.acknowledge?.packageName ?? "",
|
||||
...(message.acknowledge?.version ? { version: message.acknowledge.version } : {}),
|
||||
acknowledgeClawHubRisk: true,
|
||||
})}
|
||||
requestInstall(
|
||||
props,
|
||||
key,
|
||||
{
|
||||
source: "clawhub",
|
||||
packageName: message.acknowledge?.packageName ?? "",
|
||||
...(message.acknowledge?.version
|
||||
? { version: message.acknowledge.version }
|
||||
: {}),
|
||||
acknowledgeClawHubRisk: true,
|
||||
},
|
||||
installIdentity,
|
||||
)}
|
||||
>
|
||||
${busy ? t("pluginsPage.installing") : t("pluginsPage.acknowledgeRisk")}
|
||||
</button>
|
||||
@@ -635,8 +668,9 @@ function renderInstallButton(
|
||||
key: string,
|
||||
name: string,
|
||||
request: PluginInstallRequest,
|
||||
installIdentity: string,
|
||||
) {
|
||||
const { outcome: installOutcome } = installOperationState(props, request);
|
||||
const installOutcome = props.installOutcomeReconciliations[installIdentity];
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
@@ -646,7 +680,7 @@ function renderInstallButton(
|
||||
?disabled=${!props.canMutate || busy}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
props.onInstall(key, request);
|
||||
props.onInstall(key, request, installIdentity);
|
||||
}}
|
||||
>
|
||||
${busy
|
||||
@@ -709,7 +743,14 @@ function renderCatalogActions(
|
||||
if (!plugin.installed) {
|
||||
const install = plugin.install;
|
||||
return install
|
||||
? renderInstallButton(props, busy, rowKey, plugin.name, install)
|
||||
? renderInstallButton(
|
||||
props,
|
||||
busy,
|
||||
rowKey,
|
||||
plugin.name,
|
||||
install,
|
||||
resolveInstallIdentity(props, install),
|
||||
)
|
||||
: html`<span class="plugins-action-note">${t("pluginsPage.unavailable")}</span>`;
|
||||
}
|
||||
return html`
|
||||
@@ -779,7 +820,10 @@ function renderPluginRow(
|
||||
includePackageName = false,
|
||||
): TemplateResult {
|
||||
const key = pluginRowKey(plugin.id);
|
||||
const installOperation = installOperationState(props, plugin.install);
|
||||
const installIdentity = plugin.install
|
||||
? resolveInstallIdentity(props, plugin.install)
|
||||
: undefined;
|
||||
const installOperation = installOperationState(props, installIdentity);
|
||||
const busy = props.busy[key] || installOperation.busy;
|
||||
return html`
|
||||
<article
|
||||
@@ -829,7 +873,14 @@ function renderPluginRow(
|
||||
${plugin.error}
|
||||
</div>`
|
||||
: nothing}
|
||||
${renderRowMessage(key, props.messages[key], busy, props, installOperation.outcome)}
|
||||
${renderRowMessage(
|
||||
key,
|
||||
props.messages[key],
|
||||
busy,
|
||||
props,
|
||||
installOperation.outcome,
|
||||
installIdentity,
|
||||
)}
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -1047,7 +1098,8 @@ function renderClawHubResult(item: PluginSearchResult, props: PluginsViewProps):
|
||||
const installed = findInstalledSearchPlugin(item, props.result?.plugins ?? []);
|
||||
const key = clawHubRowKey(pkg.name);
|
||||
const installRequest = { source: "clawhub", packageName: pkg.name } as const;
|
||||
const installOperation = installOperationState(props, installRequest, pkg.runtimeId);
|
||||
const installIdentity = resolveInstallIdentity(props, installRequest, pkg.runtimeId);
|
||||
const installOperation = installOperationState(props, installIdentity);
|
||||
const busy = props.busy[key] || installOperation.busy;
|
||||
const artSlug = pkg.runtimeId ?? pkg.name;
|
||||
return html`
|
||||
@@ -1093,9 +1145,16 @@ function renderClawHubResult(item: PluginSearchResult, props: PluginsViewProps):
|
||||
<div class="settings-row__control">
|
||||
${installed
|
||||
? html`${rowStateStatus(installed)}${renderCatalogActions(installed, props, busy, key)}`
|
||||
: renderInstallButton(props, busy, key, pkg.displayName, installRequest)}
|
||||
: renderInstallButton(props, busy, key, pkg.displayName, installRequest, installIdentity)}
|
||||
</div>
|
||||
${renderRowMessage(key, props.messages[key], busy, props, installOperation.outcome)}
|
||||
${renderRowMessage(
|
||||
key,
|
||||
props.messages[key],
|
||||
busy,
|
||||
props,
|
||||
installOperation.outcome,
|
||||
installIdentity,
|
||||
)}
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -1218,7 +1277,10 @@ function renderDetailOverlay(props: PluginsViewProps) {
|
||||
return nothing;
|
||||
}
|
||||
const key = pluginRowKey(plugin.id);
|
||||
const installOperation = installOperationState(props, plugin.install);
|
||||
const installIdentity = plugin.install
|
||||
? resolveInstallIdentity(props, plugin.install)
|
||||
: undefined;
|
||||
const installOperation = installOperationState(props, installIdentity);
|
||||
const busy = props.busy[key] || installOperation.busy;
|
||||
return html`
|
||||
<openclaw-modal-dialog
|
||||
@@ -1274,7 +1336,14 @@ function renderDetailOverlay(props: PluginsViewProps) {
|
||||
</button>
|
||||
`
|
||||
: plugin.install
|
||||
? renderInstallButton(props, busy, key, plugin.name, plugin.install)
|
||||
? renderInstallButton(
|
||||
props,
|
||||
busy,
|
||||
key,
|
||||
plugin.name,
|
||||
plugin.install,
|
||||
resolveInstallIdentity(props, plugin.install),
|
||||
)
|
||||
: nothing}
|
||||
${plugin.removable
|
||||
? html`
|
||||
@@ -1297,7 +1366,14 @@ function renderDetailOverlay(props: PluginsViewProps) {
|
||||
${plugin.error}
|
||||
</div>`
|
||||
: nothing}
|
||||
${renderRowMessage(key, props.messages[key], busy, props, installOperation.outcome)}
|
||||
${renderRowMessage(
|
||||
key,
|
||||
props.messages[key],
|
||||
busy,
|
||||
props,
|
||||
installOperation.outcome,
|
||||
installIdentity,
|
||||
)}
|
||||
<div class="plugins-detail__meta">
|
||||
${plugin.origin
|
||||
? detailMetaRow(t("pluginsPage.detailOrigin"), originLabel(plugin.origin))
|
||||
|
||||
Reference in New Issue
Block a user