From ed92c867ed045ce1594d49fbb36546dcfbf4afbd Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Wed, 14 Oct 2026 18:15:57 +0800 Subject: [PATCH] feat(plugin-market): name the plugin and version in a Discover install A Discover install passes the entry's localized title and version with its spec. Every install dialog screen names the plugin and version, and shows the spec, such as a pinned git+https URL, below in smaller monospace text. --- .../client/ui-plugin-manager/README.i18n.yaml | 4 +- packages/client/ui-plugin-manager/README.md | 2 +- .../client/ui-plugin-manager/README.zh.md | 2 +- .../src/client/PluginManagerPage.module.css | 9 +++ .../src/client/PluginManagerPage.tsx | 61 +++++++++++++------ .../ui-plugin-manager/src/client/index.ts | 2 +- .../src/client/manager-store.ts | 24 +++++--- .../src/client/slot-contract.ts | 21 +++++-- .../tests/components.client.spec.tsx | 15 +++-- .../tests/manager-store.client.spec.ts | 8 +-- .../client/ui-plugin-market/README.i18n.yaml | 4 +- packages/client/ui-plugin-market/README.md | 2 +- packages/client/ui-plugin-market/README.zh.md | 2 +- .../src/client/DiscoverSection.module.css | 2 +- .../src/client/DiscoverSection.tsx | 5 +- .../tests/discover.client.spec.tsx | 4 +- .../src/client/slot-catalog.ts | 24 ++++---- 17 files changed, 125 insertions(+), 66 deletions(-) diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index 496a456c70..dbb4836fb5 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -33,8 +33,8 @@ en: 5266d86638cc5cd7 zh: 398d710df9806cea /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/page-tabs: - en: d9eb9fdbccc39b3b - zh: e2f2fa1857a07678 + en: 76e0908903ab8fde + zh: ce6ae1cdd6aea971 /deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation: en: 203168fc87e939bd zh: 72d0ccd2c54f3f44 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 9fe0a6b19f..21d56214a4 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -98,7 +98,7 @@ A row's page exists only while a `plugins.row.config` entry names the row, so a ### Page tabs -Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install(spec, registry?)` and `openPackage(name)`. `install` is for a spec the person already confirmed in the tab: the install dialog opens at its check screen with that spec, without the spec form, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here. +Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install({ spec, registry?, title, version })` and `openPackage(name)`. `install` is for a version the person already confirmed in the tab: the install dialog opens at its check screen, without the spec form, names the plugin with `title` and `version` above the spec, which it shows in smaller monospace text, keeps that name on the progress and result screens, with the installed version once done, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here. ----- diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index 7c1310b406..2d4e6f72ca 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -98,7 +98,7 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({ ### 页面标签页 -其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install(spec, registry?)` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的 spec:安装对话框以该 spec 在检查界面打开,不显示 spec 表单;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。 +其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的版本:安装对话框在检查界面打开,不显示 spec 表单,在以较小等宽字显示的 spec 上方用 `title` 和 `version` 标明插件,并在进度和结果界面保持该名称,完成后显示已安装的版本;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。 ----- diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css index 36ddce9866..efb3fb04f7 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -1017,6 +1017,15 @@ label.registryOption { overflow-wrap: anywhere; } +.subjectSpec { + margin: 0; + font-family: var(--ds-font-family-code); + font-size: 12px; + line-height: 18px; + color: var(--dsw-alias-label-tertiary); + overflow-wrap: anywhere; +} + .subjectDesc, .subjectMeta { margin: 0; diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index fb4b0f9459..77be2faf9f 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -928,20 +928,45 @@ function failureText(failure: InstallState['failure'], t: Translate, install?: P return failure.reason === '' ? t('installFailureGeneric') : failure.reason } -/** The package the install is about: its name, one-liner, and version, as the Host read them before installing. */ -function SubjectCard({ subject, t }: { readonly subject: InstallSubject; readonly t: Translate }): ReactNode { - const title = subject.name ?? subject.spec - const kindKey = SUBJECT_KIND_KEYS[subject.kind] - const description = subject.description ?? (kindKey === undefined ? undefined : t(kindKey)) +interface SubjectLines { + readonly spec: string + readonly title: string + readonly description?: string | undefined + readonly version?: string | undefined + /** A confirmed version is named by its tab, so the spec follows as reference text. */ + readonly showSpec: boolean +} + +/** The package an install is about: its name, one-liner, version, and, when `showSpec` is set, its spec. */ +function SubjectBlock({ lines, t }: { readonly lines: SubjectLines; readonly t: Translate }): ReactNode { return ( -
-

{title}

- {description === undefined ? null :

{description}

} - {subject.version === undefined ? null :

{t('installVersion', { version: subject.version })}

} +
+

{lines.title}

+ {lines.description === undefined ? null :

{lines.description}

} + {lines.version === undefined ? null :

{t('installVersion', { version: lines.version })}

} + {lines.showSpec ?

{lines.spec}

: null}
) } +/** The checked package as the Host read it before installing; a confirmed version keeps its tab's name and version. */ +function SubjectCard({ subject, confirmed, t }: { + readonly subject: InstallSubject + readonly confirmed: InstallState['confirmed'] + readonly t: Translate +}): ReactNode { + const kindKey = SUBJECT_KIND_KEYS[subject.kind] + return ( + + ) +} + /** Track one install field's composition, including Safari's 10ms post-composition Enter window. */ function useInstallComposition(active: boolean) { const composition = useRef({ active: false, until: 0 }) @@ -1045,9 +1070,9 @@ function InstallDialog({ /> ) } - if ((phase === 'idle' || phase === 'checking') && install.confirmed === true) { - // A confirmed spec has no form to return to: the dialog shows the spec with its check, a refusal with a retry, or, - // after a cancelled run, the spec with Install. + if ((phase === 'idle' || phase === 'checking') && install.confirmed !== undefined) { + // A confirmed spec has no form to return to: the dialog names the plugin and version above the spec, with its + // check, a refusal with a retry, or, after a cancelled run, Install. const checking = phase === 'checking' const problem = install.inputError return ( @@ -1065,7 +1090,7 @@ function InstallDialog({ )} >
-

{install.spec}

+ {checking ?

{t('installChecking')}

: problem === null @@ -1309,19 +1334,19 @@ function InstallDialog({ }) : null // Another registry is worth offering only for a failure the Host laid at the one it asked. - const changeable = phase === 'failed' && !approvable && install.failure?.failedAt === 'registry' && install.confirmed !== true + const changeable = phase === 'failed' && !approvable && install.failure?.failedAt === 'registry' && install.confirmed === undefined const subject = install.subject return (
- {phase === 'done' || (install.confirmed === true && !pending) + {phase === 'done' || (install.confirmed !== undefined && !pending) ? : ( - )} + ) @@ -228,7 +228,7 @@ describe('PluginManagerPage', () => { expect(document.querySelector('[data-plugin-group="bundles"]')).toBeNull() expect(screen.queryByRole('button', { name: en.refresh })).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Discover install' })) - expect(actions.installFrom).toHaveBeenCalledWith('dsh-a@1.0.0', 'https://r.example/') + expect(actions.installFrom).toHaveBeenCalledWith({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' }) fireEvent.click(screen.getByRole('button', { name: 'Discover open' })) expect(navigation.getSnapshot()).toEqual({ tab: 'installed', view: { kind: 'package', name: 'dsh-better-sidebar' } }) expect(document.querySelector('[data-plugin-detail="dsh-better-sidebar"]')).not.toBeNull() @@ -270,10 +270,12 @@ describe('PluginManagerPage', () => { expect(screen.getByRole('heading', { name: en.moreTitle, level: 1 })).toBeTruthy() }) - it('shows a confirmed spec as a check and a refusal with retry, without the spec form or another registry', () => { - const confirmed: InstallState = { ...IDLE_INSTALL, open: true, spec: 'dsh-a@1.0.0', confirmed: true, registries: REGISTRIES } + it('shows a confirmed version by name above its spec, as a check and a refusal with retry, without the spec form or another registry', () => { + const confirmed: InstallState = { ...IDLE_INSTALL, open: true, spec: 'dsh-a@1.0.0', confirmed: { title: 'Alpha', version: '1.0.0' }, registries: REGISTRIES } const { actions, set } = renderTab({ install: { ...confirmed, phase: 'checking' } }) - expect(screen.getByRole('dialog', { name: en.installConfirmedTitle })).toBeTruthy() + const dialog = screen.getByRole('dialog', { name: en.installConfirmedTitle }) + expect([...dialog.querySelectorAll('[data-install-subject] p')].map(line => line.textContent)) + .toEqual(['Alpha', en.installVersion.replace('{version}', '1.0.0'), 'dsh-a@1.0.0']) expect(screen.getByRole('status').textContent).toBe(en.installChecking) expect(screen.queryByRole('textbox')).toBeNull() expect(screen.queryByRole('button', { name: en.retry })).toBeNull() @@ -311,6 +313,9 @@ describe('PluginManagerPage', () => { }) expect(screen.getByText(en.installFailedTitle)).toBeTruthy() expect(screen.queryByRole('button', { name: en.installUseGithubMirror })).toBeNull() + // A Git spec checks without a name or version; the confirmed name and version head the card, above the spec. + expect([...document.querySelectorAll('[data-install-subject] p')].map(line => line.textContent)) + .toEqual(['Alpha', en.installSubjectGit, en.installVersion.replace('{version}', '1.0.0'), 'git+https://github.com/o/r.git#abc']) set({ install: { ...confirmed, phase: 'running', requestId: 'r' as never, diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts index 2944acc91b..c4592690da 100644 --- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts +++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts @@ -589,18 +589,18 @@ describe('PluginManagerController', () => { .mockReturnValueOnce(gate.promise) const { plugins, face, state, controller, started, track } = bench({ installBundle }) await controller.load() - face.installFrom('dsh-new@1.0.0', 'https://r.example/') + face.installFrom({ spec: 'dsh-new@1.0.0', registry: 'https://r.example/', title: 'New', version: '1.0.0' }) expect(track).not.toHaveBeenCalledWith('plugin_add_button_click', {}) - expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0', confirmed: true, registry: { kind: 'custom', url: 'https://r.example/' } }) + expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0', confirmed: { title: 'New', version: '1.0.0' }, registry: { kind: 'custom', url: 'https://r.example/' } }) await started() expect(plugins.inspect).toHaveBeenCalledWith('dsh-new@1.0.0', { registry: 'https://r.example/' }, expect.any(AbortSignal)) face.closeInstall() - face.installFrom('dsh-other@2.0.0') + face.installFrom({ spec: 'dsh-other@2.0.0', title: 'Other', version: '2.0.0' }) expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0' }) gate.resolve(ok({ ...APPLIED, bundle: 'dsh-new' })) await vi.waitFor(() => { expect(state().install.requestId).toBeUndefined() }) face.closeInstall() - face.installFrom('dsh-other@2.0.0') + face.installFrom({ spec: 'dsh-other@2.0.0', title: 'Other', version: '2.0.0' }) expect(state().install).toMatchObject({ open: true, spec: 'dsh-other@2.0.0', registry: { kind: 'offered', registry: null } }) await vi.waitFor(() => { expect(plugins.inspect).toHaveBeenLastCalledWith('dsh-other@2.0.0', { registry: null }, expect.any(AbortSignal)) }) await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml index 2b12760fe5..4223a3e430 100644 --- a/packages/client/ui-plugin-market/README.i18n.yaml +++ b/packages/client/ui-plugin-market/README.i18n.yaml @@ -18,8 +18,8 @@ en: 2690b95e156cc473 zh: 0d9e897ad628f9dd /deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation: - en: cc9e8550a01ea847 - zh: eda18f5680df3e88 + en: c56790cb58083f85 + zh: a3d4ad4bad08e483 /deepseek-ai-dsh-client-ui-plugin-market/model-experience: en: 08d25ab9cbdd7567 zh: 26e6e75c123951ee diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md index 02a88fd003..b89f158b64 100644 --- a/packages/client/ui-plugin-market/README.md +++ b/packages/client/ui-plugin-market/README.md @@ -41,7 +41,7 @@ Reading a market shows card skeletons. A failed read shows a short sentence with ## Understand the implementation -The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install(spec, registry?)` and `openPackage(name)`. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. +The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. ----- diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md index 69b0b32d64..c5d906e802 100644 --- a/packages/client/ui-plugin-market/README.zh.md +++ b/packages/client/ui-plugin-market/README.zh.md @@ -41,7 +41,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- ## 理解实现 -本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install(spec, registry?)` 和 `openPackage(name)`。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 +本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 ----- diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css b/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css index e91a063982..6ad459de5d 100644 --- a/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css +++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css @@ -333,7 +333,7 @@ border-radius: 4px; background: var(--dsw-alias-bg-module-platform); box-decoration-break: clone; - font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-family: var(--ds-font-family-code); font-size: 12px; } diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx index 011b8a119c..0941d101ba 100644 --- a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx +++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx @@ -589,7 +589,10 @@ export function DiscoverSection(props: DiscoverSectionProps): ReactNode { {body} { setConfirming(null) }} - onConfirm={({ version }) => { setConfirming(null); install(version.install.spec, version.install.registry) }} /> + onConfirm={({ entry, version }) => { + setConfirming(null) + install({ ...version.install, title: entryTitle(entry, face), version: version.version }) + }} />
) } diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx index d7b64e9ea1..f0e4796ea0 100644 --- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx +++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx @@ -145,7 +145,7 @@ describe('DiscoverSection', () => { expect(install).not.toHaveBeenCalled() fireEvent.click(card('dsh-a').getByRole('button', { name: 'Install Alpha' })) fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: en.install })) - expect(install).toHaveBeenCalledWith('dsh-a@1.0.0', 'https://r.example/') + expect(install).toHaveBeenCalledWith({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' }) expect(screen.queryByRole('dialog')).toBeNull() for (const [name, label] of [['dsh-b', en.statusInstalled], ['dsh-c', en.statusUpdate], ['dsh-f', en.statusProvided]] as const) { fireEvent.click(card(name).getByRole('button', { name: `View ${name} in Installed` })) @@ -346,7 +346,7 @@ describe('DiscoverSection', () => { const dialog = within(screen.getByRole('dialog')) expect(dialog.getByText('dsh-v@3.0.0-rc.1')).toBeTruthy() fireEvent.click(dialog.getByRole('button', { name: en.install })) - expect(install).toHaveBeenCalledWith('dsh-v@3.0.0-rc.1', undefined) + expect(install).toHaveBeenCalledWith({ spec: 'dsh-v@3.0.0-rc.1', title: 'dsh-v', version: '3.0.0-rc.1' }) }) it('marks exempted versions and offers no version of a bundle DSH provides', () => { diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 399cbc7c98..4145c585ef 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2205,7 +2205,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.add.actions\', () => ctx.slots.register(\n { name: \'plugins.add.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:107', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:118', }, { key: 'plugins.bundle.activation', @@ -2245,7 +2245,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.activation\', () => ctx.slots.register(\n { name: \'plugins.bundle.activation\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:115', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:126', }, { key: 'plugins.bundle.config', @@ -2288,7 +2288,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.config\', () => ctx.slots.register(\n { name: \'plugins.bundle.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:131', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:142', }, { key: 'plugins.detail.actions', @@ -2340,7 +2340,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.actions\', () => ctx.slots.register(\n { name: \'plugins.detail.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:145', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:156', }, { key: 'plugins.detail.badge', @@ -2392,7 +2392,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.badge\', () => ctx.slots.register(\n { name: \'plugins.detail.badge\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:150', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:161', }, { key: 'plugins.detail.section', @@ -2444,7 +2444,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.section\', () => ctx.slots.register(\n { name: \'plugins.detail.section\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:157', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:168', }, { key: 'plugins.item', @@ -2501,7 +2501,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.item\', () => ctx.slots.register(\n { name: \'plugins.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:125', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:136', }, { key: 'plugins.row.config', @@ -2541,7 +2541,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.row.config\', () => ctx.slots.register(\n { name: \'plugins.row.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:139', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:150', }, { key: 'plugins.tab', @@ -2570,9 +2570,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact spec the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form.\n * @param spec - the install spec, such as `name@version`.\n * @param registry - the registry to ask first for this install only; absent keeps the dialog\'s registry choice.\n */\n readonly install: (spec: string, registry?: string) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}', + '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact version the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form, and names the plugin and version above the spec.\n * @param target - the spec, optional registry, and display text.\n */\n readonly install: (target: PluginTabInstall) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}', + ], + ownerPropsReferences: [ + 'PluginTabInstall', ], - ownerPropsReferences: [], standardProps: [ 'useRemoteAccess: (action: RemoteEndpoint) => boolean', 'useResource: UseResource', @@ -2593,7 +2595,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.tab\', () => ctx.slots.register(\n { name: \'plugins.tab\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:113', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:124', }, { key: 'rightbar',