mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
* fix: show plugin names and descriptions in the UI language Read plugin.name and plugin.description from each plugin's own locale namespace, falling back to the manifest, everywhere core shows a plugin's name: the Plugins manager, onboarding picker, host editor tabs and sections, host filters and bulk actions, beta updates, interface presets and plugin crash/loading notices. * docs(sdk): document plugin.name and plugin.description locale keys --------- Co-authored-by: ZacharyZcR <zkl205305@gmail.com>
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
import { CircleSlash } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/button";
|
|
import { usePermissions } from "@/hooks/use-permissions";
|
|
import { pluginName } from "@/lib/plugin-i18n";
|
|
import { OPEN_PLUGINS_EVENT } from "@/plugins/open-plugins";
|
|
import { usePluginStore } from "./plugin-store";
|
|
import { unregisteredViewStatus, type ViewKind } from "./view-ownership";
|
|
|
|
/**
|
|
* Stands in for a tab, panel or card whose plugin is not running.
|
|
*
|
|
* A saved tab of a plugin that was switched off or removed must neither crash
|
|
* the shell nor vanish without a word, so it renders this until the plugin is
|
|
* back, at which point the real view takes over in place.
|
|
*/
|
|
export function PluginViewPlaceholder({
|
|
kind,
|
|
viewId,
|
|
compact = false,
|
|
}: {
|
|
kind: ViewKind;
|
|
viewId: string;
|
|
compact?: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
// Subscribing re-renders this when plugin state changes.
|
|
usePluginStore();
|
|
const { has } = usePermissions();
|
|
const { status, owner } = unregisteredViewStatus(kind, viewId);
|
|
const name = owner ? pluginName(t, owner.summary) : viewId;
|
|
|
|
if (status === "loading") {
|
|
return (
|
|
<div
|
|
className="flex h-full w-full items-center justify-center"
|
|
aria-label={t("plugins.runtime.loading", { name })}
|
|
>
|
|
<div className="size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const title =
|
|
status === "failed"
|
|
? t("plugins.runtime.failed", { name })
|
|
: owner
|
|
? t("plugins.runtime.needsPlugin", { name })
|
|
: t("plugins.runtime.needsUnknownPlugin");
|
|
const hint =
|
|
status === "failed"
|
|
? t("plugins.runtime.failedHint")
|
|
: status === "disabled"
|
|
? t("plugins.runtime.disabledHint")
|
|
: t("plugins.runtime.missingHint");
|
|
|
|
return (
|
|
<div
|
|
data-testid="plugin-view-placeholder"
|
|
data-status={status}
|
|
className={`flex h-full w-full flex-col items-center justify-center gap-2 text-center ${compact ? "p-3" : "p-6"}`}
|
|
>
|
|
<CircleSlash className="size-6 text-muted-foreground/40" />
|
|
<span className="text-sm font-semibold text-muted-foreground">
|
|
{title}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground/70">{hint}</span>
|
|
{!compact && status !== "failed" && has("admin.plugins.manage") && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-1"
|
|
onClick={() =>
|
|
window.dispatchEvent(new CustomEvent(OPEN_PLUGINS_EVENT))
|
|
}
|
|
>
|
|
{t("plugins.manager.browsePlugins")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|