mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
feat(plugins): link plugin pages to the README at the pinned release commit (#36527)
This commit is contained in:
@@ -8,7 +8,10 @@ import { DetailSection } from './PluginDetailSections'
|
||||
import { formatPluginReleaseDate } from '../presentation/plugin-release-date'
|
||||
import { usePluginRollbackVersion } from '../state/use-plugin-rollback-version'
|
||||
import { openAppHttpLink } from '@/lib/app-http-link-open'
|
||||
import { pluginRepositoryPageUrl } from '../../../../../shared/plugins/catalog/plugin-repository-link'
|
||||
import {
|
||||
pluginReadmePageUrl,
|
||||
pluginRepositoryPageUrl
|
||||
} from '../../../../../shared/plugins/catalog/plugin-repository-link'
|
||||
|
||||
function InfoRow({
|
||||
label,
|
||||
@@ -33,7 +36,7 @@ function InfoRow({
|
||||
)
|
||||
}
|
||||
|
||||
function RepositoryLink({ url }: { url: string | null }) {
|
||||
function ExternalLink({ url, label }: { url: string | null; label: string }) {
|
||||
if (!url) {
|
||||
return null
|
||||
}
|
||||
@@ -44,7 +47,7 @@ function RepositoryLink({ url }: { url: string | null }) {
|
||||
className="-ml-2"
|
||||
onClick={(event) => openAppHttpLink(url, { event })}
|
||||
>
|
||||
{translate('auto.components.settings.PluginDetailPage.repository', 'Repository')}
|
||||
{label}
|
||||
<ArrowUpRight />
|
||||
</Button>
|
||||
)
|
||||
@@ -69,6 +72,13 @@ export function PluginDetailInfo({
|
||||
const sourceUrl =
|
||||
listing?.source.url ?? (plugin?.source?.kind === 'git' ? plugin.source.reference : null)
|
||||
const repository = sourceUrl ? pluginRepositoryPageUrl(sourceUrl) : null
|
||||
const readme = listing?.release
|
||||
? pluginReadmePageUrl({
|
||||
gitUrl: listing.source.url,
|
||||
commit: listing.release.commit,
|
||||
path: listing.source.path
|
||||
})
|
||||
: null
|
||||
|
||||
return (
|
||||
<aside className="mt-6">
|
||||
@@ -129,7 +139,18 @@ export function PluginDetailInfo({
|
||||
/>
|
||||
</dl>
|
||||
<div className="mt-2 flex flex-col items-start">
|
||||
<RepositoryLink url={repository} />
|
||||
<ExternalLink
|
||||
url={repository}
|
||||
label={translate('auto.components.settings.PluginDetailPage.repository', 'Repository')}
|
||||
/>
|
||||
<ExternalLink
|
||||
url={readme}
|
||||
label={translate(
|
||||
'auto.components.settings.PluginDetailInfo.readme',
|
||||
'Read the README on {{value0}}',
|
||||
{ value0: readme?.startsWith('https://gitlab.com/') ? 'GitLab' : 'GitHub' }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -194,6 +194,13 @@ describe('PluginDetailPage', () => {
|
||||
)
|
||||
await act(async () => repository?.click())
|
||||
expect(window.api.shell.openUrl).toHaveBeenCalledWith('https://github.com/acme/icons')
|
||||
const readme = Array.from(container.querySelectorAll('button')).find(
|
||||
(button) => button.textContent === 'Read the README on GitHub'
|
||||
)
|
||||
await act(async () => readme?.click())
|
||||
expect(window.api.shell.openUrl).toHaveBeenCalledWith(
|
||||
`https://github.com/acme/icons/tree/${'d'.repeat(40)}#readme`
|
||||
)
|
||||
})
|
||||
|
||||
it('never claims no access when the plugin could not be read, and retries', async () => {
|
||||
|
||||
@@ -13218,7 +13218,8 @@
|
||||
"license": "License",
|
||||
"whatsNew": "What’s new",
|
||||
"hashCopied": "Content hash copied",
|
||||
"hashCopyFailed": "Couldn't copy the hash."
|
||||
"hashCopyFailed": "Couldn't copy the hash.",
|
||||
"readme": "Read the README on {{value0}}"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "No plugins match these filters",
|
||||
|
||||
@@ -12827,7 +12827,8 @@
|
||||
"license": "Licencia",
|
||||
"whatsNew": "Novedades",
|
||||
"hashCopied": "Hash de contenido copiado",
|
||||
"hashCopyFailed": "No se pudo copiar el hash."
|
||||
"hashCopyFailed": "No se pudo copiar el hash.",
|
||||
"readme": "Leer el README en {{value0}}"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "Ningún plugin coincide con estos filtros",
|
||||
|
||||
@@ -13051,7 +13051,8 @@
|
||||
"license": "Licence",
|
||||
"whatsNew": "Nouveautés",
|
||||
"hashCopied": "Empreinte du contenu copiée",
|
||||
"hashCopyFailed": "Impossible de copier l’empreinte."
|
||||
"hashCopyFailed": "Impossible de copier l’empreinte.",
|
||||
"readme": "Lire le README sur {{value0}}"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "Aucun plugin ne correspond à ces filtres",
|
||||
|
||||
@@ -12539,7 +12539,8 @@
|
||||
"license": "ライセンス",
|
||||
"whatsNew": "新機能",
|
||||
"hashCopied": "コンテンツハッシュをコピーしました",
|
||||
"hashCopyFailed": "ハッシュをコピーできませんでした。"
|
||||
"hashCopyFailed": "ハッシュをコピーできませんでした。",
|
||||
"readme": "{{value0}} で README を読む"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "これらのフィルターに一致するプラグインはありません",
|
||||
|
||||
@@ -12539,7 +12539,8 @@
|
||||
"license": "라이선스",
|
||||
"whatsNew": "새로운 기능",
|
||||
"hashCopied": "콘텐츠 해시를 복사했습니다",
|
||||
"hashCopyFailed": "해시를 복사할 수 없습니다."
|
||||
"hashCopyFailed": "해시를 복사할 수 없습니다.",
|
||||
"readme": "{{value0}}에서 README 읽기"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "이 필터와 일치하는 플러그인이 없습니다",
|
||||
|
||||
@@ -12572,7 +12572,8 @@
|
||||
"license": "许可证",
|
||||
"whatsNew": "新功能",
|
||||
"hashCopied": "已复制内容哈希",
|
||||
"hashCopyFailed": "无法复制哈希。"
|
||||
"hashCopyFailed": "无法复制哈希。",
|
||||
"readme": "在 {{value0}} 上阅读 README"
|
||||
},
|
||||
"PluginListEmptyStates": {
|
||||
"noFilterMatches": "没有与这些筛选条件匹配的插件",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pluginRepositoryPageUrl } from './plugin-repository-link'
|
||||
import { pluginReadmePageUrl, pluginRepositoryPageUrl } from './plugin-repository-link'
|
||||
|
||||
describe('pluginRepositoryPageUrl', () => {
|
||||
it('builds the repository page from the https or ssh install URL', () => {
|
||||
@@ -31,3 +31,31 @@ describe('pluginRepositoryPageUrl', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluginReadmePageUrl', () => {
|
||||
const commit = 'a'.repeat(40)
|
||||
|
||||
it('links the plugin folder at the pinned commit', () => {
|
||||
expect(pluginReadmePageUrl({ gitUrl: 'https://github.com/Acme/Plugins.git', commit })).toBe(
|
||||
`https://github.com/acme/plugins/tree/${commit}#readme`
|
||||
)
|
||||
expect(
|
||||
pluginReadmePageUrl({
|
||||
gitUrl: 'https://github.com/acme/plugins',
|
||||
commit,
|
||||
path: 'themes/gleam'
|
||||
})
|
||||
).toBe(`https://github.com/acme/plugins/tree/${commit}/themes/gleam#readme`)
|
||||
expect(pluginReadmePageUrl({ gitUrl: 'git@gitlab.com:acme/tools/notes.git', commit })).toBe(
|
||||
`https://gitlab.com/acme/tools/notes/-/tree/${commit}#readme`
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses unpinned commits, odd paths and unlinkable hosts', () => {
|
||||
const gitUrl = 'https://github.com/acme/plugins'
|
||||
expect(pluginReadmePageUrl({ gitUrl, commit: 'main' })).toBeNull()
|
||||
expect(pluginReadmePageUrl({ gitUrl, commit, path: 'a/../b' })).toBeNull()
|
||||
expect(pluginReadmePageUrl({ gitUrl, commit, path: 'a/%2e%2e' })).toBeNull()
|
||||
expect(pluginReadmePageUrl({ gitUrl: 'https://evil.example/acme/x', commit })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,3 +17,24 @@ export function pluginRepositoryPageUrl(gitUrl: string): string | null {
|
||||
}
|
||||
return `https://${identity.host}/${segments.join('/')}`
|
||||
}
|
||||
|
||||
const COMMIT_RE = /^[0-9a-f]{40}$/
|
||||
|
||||
/** The plugin's folder at the exact commit the catalog pinned, where the host renders its
|
||||
* README; built like `pluginRepositoryPageUrl`, so a listing cannot point it elsewhere. */
|
||||
export function pluginReadmePageUrl(source: {
|
||||
gitUrl: string
|
||||
commit: string
|
||||
path?: string
|
||||
}): string | null {
|
||||
const repository = pluginRepositoryPageUrl(source.gitUrl)
|
||||
if (!repository || !COMMIT_RE.test(source.commit)) {
|
||||
return null
|
||||
}
|
||||
const pathSegments = source.path ? source.path.split('/') : []
|
||||
if (!pathSegments.every((segment) => PATH_SEGMENT_RE.test(segment))) {
|
||||
return null
|
||||
}
|
||||
const tree = repository.startsWith('https://gitlab.com/') ? '-/tree' : 'tree'
|
||||
return `${[repository, tree, source.commit, ...pathSegments].join('/')}#readme`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user