Compare commits

...
Author SHA1 Message Date
l0ng-ai 84abdc1885 fix(plugins): move the repository URL to the gear menu and drop the no-repository hint
The Available tab always showed a "There is no online plugin repository"
notice under the search field when no index was set. Release builds use
the official index by default, so the notice only appeared in development
builds, where the empty URL field above it already said the same thing.
The URL field itself sat at the top of the list although most people never
change it.

- The notice is gone, and the search placeholder reads "Search plugins"
  because the list holds marketplace plugins as well as local ones.
- With a repository configured, the empty details panel says "No plugin
  selected" instead of claiming there is no online repository.
- The marketplace index URL is edited from the gear menu's "Plugin
  Repository…" dialog; leaving it empty restores the default.
2026-11-04 21:37:16 +08:00
6 changed files with 74 additions and 62 deletions

No files matched your search

@@ -369,17 +369,6 @@
padding: 4px 8px;
}
.marketBar {
display: flex;
gap: 4px;
align-items: center;
}
.marketBar > :first-child {
flex: 1;
min-width: 0;
}
.categorySelect {
width: 100%;
}
+21 -35
View File
@@ -16,7 +16,6 @@ import {
EmptyState,
Icon,
IconButton,
Input,
SearchField,
Select,
Spinner,
@@ -48,6 +47,7 @@ import {
} from "./store";
import { useProject } from "@/platform/project";
import { importIntellijTheme, showInstalledIntellijThemes } from "./intellijImport";
import { promptInput } from "@/workbench/inputDialogs";
import styles from "./PluginsPage.module.css";
import { tx } from "@/platform/i18n";
@@ -537,6 +537,21 @@ interface GearActions {
install: (packageFile: boolean) => void;
importIntellijFile: () => void;
importInstalledIntellij: () => void;
editRepository: () => void;
}
/** Asks for the marketplace index URL; an empty one restores the default. */
async function editMarketUrl() {
const url = await promptInput({
title: "Plugin Repository",
label: "Marketplace index URL:",
initial: usePlugins.getState().marketUrl ?? "",
placeholder: "https://…/index.json or a folder path",
okLabel: "Save",
validate: () => null,
hint: <T>Leave empty to use the default repository.</T>,
});
if (url !== null) await setMarketUrl(url);
}
function gearMenu(plugins: readonly PluginInfo[], updates: number, actions: GearActions): MenuItem[] {
@@ -550,6 +565,7 @@ function gearMenu(plugins: readonly PluginInfo[], updates: number, actions: Gear
{ kind: "separator" },
{ label: "Install Plugin from Disk…", icon: "package", run: () => actions.install(false) },
{ label: "Install Plugin from Package File…", run: () => actions.install(true) },
{ label: "Plugin Repository…", run: actions.editRepository },
...(actions.canImport
? [
{ kind: "separator" as const },
@@ -654,30 +670,6 @@ function CategoryFilter({
);
}
/** The marketplace index URL, editable in place. */
function MarketUrlBar() {
const current = usePlugins((s) => s.marketUrl);
const [value, setValue] = useState(current ?? "");
useEffect(() => setValue(current ?? ""), [current]);
return (
<form
className={styles.marketBar}
onSubmit={(e) => {
e.preventDefault();
void setMarketUrl(value);
}}
>
<Input
value={value}
placeholder="Marketplace index URL (https://…/index.json or a folder path)"
aria-label="Marketplace index URL"
onChange={(e) => setValue(e.target.value)}
/>
<IconButton icon="refresh" title="Refresh Marketplace" type="submit" />
</form>
);
}
function MarketRow({ entry, selected, onSelect }: { entry: MarketPlugin; selected: boolean; onSelect: () => void }) {
const update = isUpdate(entry);
return (
@@ -832,6 +824,7 @@ export function PluginsPage() {
install: (packageFile) => void installFromDisk(undefined, packageFile).then(showInstalled),
importIntellijFile: () => void importIntellijTheme().then(showInstalled),
importInstalledIntellij: () => showInstalledIntellijThemes(showInstalled),
editRepository: () => void editMarketUrl(),
}),
);
};
@@ -876,7 +869,7 @@ export function PluginsPage() {
className={styles.search}
value={query}
onChange={setQuery}
placeholder={tab === "installed" ? "Search installed plugins" : "Search local plugins"}
placeholder={tab === "installed" ? "Search installed plugins" : "Search plugins"}
aria-label="Filter plugins"
/>
{tab === "installed" ? (
@@ -916,20 +909,11 @@ export function PluginsPage() {
</ul>
) : (
<ul className={styles.list} aria-label={tx("Available plugins")}>
<li className={styles.marketItem}>
<MarketUrlBar />
</li>
{(market?.length ?? 0) > 0 && (
<li className={styles.marketItem}>
<CategoryFilter market={market ?? []} value={category} onChange={setCategory} />
</li>
)}
{!marketUrl && !marketError && (
<li className={styles.notice} role="note">
<Icon name="info" size={14} />
<span>{tx`${NO_REPOSITORY}. Showing plugin packages available on this machine.`}</span>
</li>
)}
{marketError && <li className={clsx(styles.listEmpty, styles.errorText)}>{marketError}</li>}
{marketShown.length > 0 && <ListHeader title="Marketplace" count={marketShown.length} />}
{marketShown.map((m) => (
@@ -983,6 +967,8 @@ export function PluginsPage() {
/>
) : selectedEntry ? (
<CatalogDetails key={selectedEntry.id} entry={selectedEntry} />
) : marketUrl ? (
<EmptyState className={styles.detailsEmpty} title="No plugin selected" />
) : (
<EmptyState className={styles.detailsEmpty} title={NO_REPOSITORY}>
<T>Install a plugin folder with</T> <code>plugin.toml</code>{" "}
@@ -9,6 +9,7 @@ import { executeAction, executeActionFrom, getAction, registerAction } from "@/p
import { useProject } from "@/platform/project";
import { pluginThemeRef, useContributedIconThemes } from "@/platform/themes";
import { ContextMenuHost, hideContextMenu } from "@/ui";
import { InputDialogsHost } from "@/workbench/inputDialogs";
import { provideNotificationService, type NotificationService } from "@/workbench/contributions";
import { applyThemeAttributes, previewTheme, useThemeOverride } from "@/workbench/theme";
import { createMemoryPluginsBackend, sampleCatalog, setPluginsBackend, type PluginsBackend } from "../backend";
@@ -561,7 +562,6 @@ describe("PluginsPage", () => {
render(<PluginsPage />);
await user.click(screen.getByRole("tab", { name: /Available/ }));
const list = await screen.findByRole("list", { name: "Available plugins" });
expect(within(list).getByText(new RegExp(NO_REPOSITORY))).toBeInTheDocument();
await waitFor(() => expect(within(list).getByText("Toy Language")).toBeInTheDocument());
await user.click(within(list).getByText("Toy Language"));
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Toy Language");
@@ -663,6 +663,53 @@ describe("marketplace", () => {
expect(asked[0]).toMatchObject({ pluginId: "hello", kind: "install" });
});
it("doesn't claim there is no repository while one is configured", async () => {
setPluginsBackend({
...createMemoryPluginsBackend([], []),
marketUrl: async () => "https://plugins.example/index.json",
market: async () => [],
});
const user = userEvent.setup();
render(<PluginsPage />);
await user.click(screen.getByRole("tab", { name: /Available/ }));
expect(await screen.findByText("No plugin selected")).toBeInTheDocument();
expect(screen.queryByText(new RegExp(NO_REPOSITORY))).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Filter plugins" })).toHaveAttribute("placeholder", "Search plugins");
});
it("edits the repository from the gear menu; an empty URL restores the default", async () => {
const base = createMemoryPluginsBackend([], []);
const set: (string | null)[] = [];
setPluginsBackend({
...base,
marketUrl: async () => "https://plugins.example/index.json",
market: async () => [],
setMarketUrl: async (url) => void set.push(url),
});
const user = userEvent.setup();
render(
<>
<PluginsPage />
<ContextMenuHost />
<InputDialogsHost />
</>,
);
await user.click(screen.getByRole("tab", { name: /Available/ }));
expect(screen.queryByRole("textbox", { name: /Marketplace index URL/ })).not.toBeInTheDocument();
const edit = async (value: string) => {
await user.click(screen.getByRole("button", { name: "Manage Plugins" }));
await user.click(screen.getByRole("menuitem", { name: "Plugin Repository…" }));
const field = await screen.findByRole("textbox", { name: /Marketplace index URL/ });
await waitFor(() => expect(field).toHaveValue("https://plugins.example/index.json"));
await user.clear(field);
if (value) await user.type(field, value);
await user.click(screen.getByRole("button", { name: "Save" }));
};
await edit("https://mirror.example/index.json");
await edit("");
await waitFor(() => expect(set).toEqual(["https://mirror.example/index.json", null]));
});
it("filters the marketplace by category", async () => {
const entry = (id: string, name: string, categories: string[]) => ({
id,
-5
View File
@@ -1859,7 +1859,6 @@
"Search frames": "Rechercher des cadres",
"Search in comments and strings": "Rechercher dans les commentaires et les chaînes",
"Search installed plugins": "Rechercher parmi les plugins installés",
"Search local plugins": "Rechercher parmi les plugins locaux",
"Search packages": "Rechercher des paquets",
"Search projects": "Rechercher des projets",
"Search pull requests": "Rechercher des pull requests",
@@ -4406,7 +4405,6 @@
"Memory & Diagnostics": "Mémoire et diagnostics",
"Over budget ({budget})": "Budget dépassé ({budget})",
"dev": "dev",
"{label}. Showing plugin packages available on this machine.": "{label}. Affichage des packages de plugins disponibles sur cette machine.",
"Zoomed into {name}": "Zoom sur {name}",
"Rename directory '{current}' and its usages to:": "Renommer le répertoire '{current}' et ses utilisations en :",
"Rename file '{current}' and its usages to:": "Renommer le fichier '{current}' et ses utilisations en :",
@@ -5548,8 +5546,6 @@
"Unsigned": "Non signé",
"Roll Back to {previousVersion}": "Revenir à {previousVersion}",
"Install Plugin from Package File…": "Installer un plugin depuis un fichier de paquet…",
"Marketplace index URL": "URL de l'index de la marketplace",
"Refresh Marketplace": "Actualiser la marketplace",
"The plugin system is a preview: its API and the plugins may still change.": "Le système de plugins est en préversion : son API et les plugins peuvent encore changer.",
"Linked for development": "Lié pour le développement",
"Marketplace": "Marketplace",
@@ -5782,7 +5778,6 @@
"Weekly limit": "Limite hebdomadaire",
"Weekly Opus limit": "Limite hebdomadaire Opus",
"Weekly Sonnet limit": "Limite hebdomadaire Sonnet",
"Marketplace index URL (https://…/index.json or a folder path)": "URL de l'index de la marketplace (https://…/index.json ou un chemin de dossier)",
"Editor Providers": "Fournisseurs d'éditeurs",
"Syntax errors": "Erreurs de syntaxe",
"Pull Members Up…": "Remonter les membres…",
-5
View File
@@ -1937,7 +1937,6 @@
"Search frames": "프레임 검색",
"Search in comments and strings": "주석 및 문자열에서 검색",
"Search installed plugins": "설치된 플러그인 검색",
"Search local plugins": "로컬 플러그인 검색",
"Search packages": "패키지 검색",
"Search projects": "프로젝트 검색",
"Search pull requests": "풀 리퀘스트 검색",
@@ -4517,7 +4516,6 @@
"Memory & Diagnostics": "메모리 및 진단",
"Over budget ({budget})": "예산 초과({budget})",
"dev": "개발",
"{label}. Showing plugin packages available on this machine.": "{label}. 이 머신에서 사용 가능한 플러그인 패키지를 표시합니다.",
"Zoomed into {name}": "{name} 확대됨",
"Rename directory '{current}' and its usages to:": "디렉터리 '{current}' 및 해당 사용 위치의 새 이름:",
"Rename file '{current}' and its usages to:": "파일 '{current}' 및 해당 사용 위치의 새 이름:",
@@ -5708,14 +5706,11 @@
"Unsigned": "서명되지 않음",
"Roll Back to {previousVersion}": "{previousVersion}(으)로 롤백",
"Install Plugin from Package File…": "패키지 파일에서 플러그인 설치…",
"Marketplace index URL": "마켓플레이스 인덱스 URL",
"Refresh Marketplace": "마켓플레이스 새로 고침",
"Marketplace": "마켓플레이스",
"Could not download plugin": "플러그인을 다운로드할 수 없습니다",
"Rolled back {name} to {version}": "{name}을(를) {version}(으)로 롤백했습니다",
"Could not roll back": "롤백할 수 없습니다",
"Could not open the package": "패키지를 열 수 없습니다",
"Marketplace index URL (https://…/index.json or a folder path)": "마켓플레이스 인덱스 URL(https://…/index.json 또는 폴더 경로)",
"Settings of the plugin; projects can override them.": "플러그인 설정입니다. 프로젝트에서 재정의할 수 있습니다.",
"Menus": "메뉴",
"Shortcuts": "단축키",
+5 -5
View File
@@ -1940,8 +1940,8 @@
"Search frames": "搜索帧",
"Search in comments and strings": "在注释和字符串中搜索",
"Search installed plugins": "搜索已安装的插件",
"Search local plugins": "搜索本地插件",
"Search packages": "搜索软件包",
"Search plugins": "搜索插件",
"Search projects": "搜索项目",
"Search pull requests": "搜索拉取请求",
"Search query": "搜索查询",
@@ -4535,7 +4535,6 @@
"Memory & Diagnostics": "内存与诊断",
"Over budget ({budget})": "超出预算({budget})",
"dev": "开发",
"{label}. Showing plugin packages available on this machine.": "{label}。显示此计算机上可用的插件包。",
"Zoomed into {name}": "已缩放到 {name}",
"Rename directory '{current}' and its usages to:": "将目录“{current}”及其用法重命名为:",
"Rename file '{current}' and its usages to:": "将文件“{current}”及其用法重命名为:",
@@ -5791,14 +5790,15 @@
"1 theme": "1 个主题",
"{count} themes": "{count} 个主题",
"No theme plugins found in JetBrains IDEs on this computer. Choose a theme plugin downloaded from the JetBrains Marketplace (.jar or .zip) or a .theme.json file instead.": "在此计算机上的 JetBrains IDE 中未找到主题插件。请改为选择从 JetBrains Marketplace 下载的主题插件(.jar 或 .zip)或 .theme.json 文件。",
"Marketplace index URL": "插件市场索引 URL",
"Refresh Marketplace": "刷新插件市场",
"Marketplace": "插件市场",
"Plugin Repository…": "插件仓库…",
"Plugin Repository": "插件仓库",
"Marketplace index URL:": "插件市场索引 URL:",
"Leave empty to use the default repository.": "留空则使用默认仓库。",
"Could not download plugin": "无法下载插件",
"Rolled back {name} to {version}": "已将 {name} 回滚到 {version}",
"Could not roll back": "无法回滚",
"Could not open the package": "无法打开插件包",
"Marketplace index URL (https://…/index.json or a folder path)": "插件市场索引 URL(https://…/index.json 或文件夹路径)",
"Settings of the plugin; projects can override them.": "插件的设置;项目可以覆盖。",
"Menus": "菜单",
"Shortcuts": "快捷键",