mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
Merge pull request #302 from iflytek/fix/rerelease-precheck-warnings
fix(rerelease): support precheck warning confirmation flow
This commit is contained in:
commit
2cfd4a730b
11 changed files with 257 additions and 19 deletions
|
|
@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotBlank;
|
|||
|
||||
public record SkillVersionRereleaseRequest(
|
||||
@NotBlank(message = "{validation.required}")
|
||||
String targetVersion
|
||||
String targetVersion,
|
||||
boolean confirmWarnings
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ public class SkillLifecycleAppService {
|
|||
skillVersion.getVersion(),
|
||||
targetVersion,
|
||||
userId,
|
||||
normalizeRoles(userNamespaceRoles)
|
||||
normalizeRoles(userNamespaceRoles),
|
||||
request.confirmWarnings()
|
||||
);
|
||||
auditLogService.record(
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -212,7 +212,8 @@ class SkillLifecycleControllerTest {
|
|||
eq("1.2.3"),
|
||||
eq("1.2.4"),
|
||||
eq("usr_1"),
|
||||
anyMap()))
|
||||
anyMap(),
|
||||
eq(false)))
|
||||
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
|
||||
|
||||
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
|
||||
|
|
@ -279,7 +280,8 @@ class SkillLifecycleControllerTest {
|
|||
eq("1.2.3"),
|
||||
eq("1.2.4"),
|
||||
eq("usr_1"),
|
||||
anyMap()))
|
||||
anyMap(),
|
||||
eq(false)))
|
||||
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
|
||||
|
||||
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
|
||||
|
|
@ -299,7 +301,44 @@ class SkillLifecycleControllerTest {
|
|||
eq("1.2.3"),
|
||||
eq("1.2.4"),
|
||||
eq("usr_1"),
|
||||
anyMap());
|
||||
anyMap(),
|
||||
eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rereleaseVersion_passesConfirmWarningsToService() throws Exception {
|
||||
Namespace namespace = new Namespace("global", "Global", "owner");
|
||||
setNamespaceId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, "demo-skill", "owner", SkillVisibility.PUBLIC);
|
||||
setSkillId(skill, 1L);
|
||||
SkillVersion newVersion = new SkillVersion(1L, "1.2.4", "owner");
|
||||
setSkillVersionId(newVersion, 3L);
|
||||
newVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
|
||||
given(skillSlugResolutionService.resolve(1L, "demo-skill", "usr_1", SkillSlugResolutionService.Preference.CURRENT_USER))
|
||||
.willReturn(skill);
|
||||
SkillVersion sourceVersion = new SkillVersion(1L, "1.2.3", "owner");
|
||||
setSkillVersionId(sourceVersion, 2L);
|
||||
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.3")).willReturn(java.util.Optional.of(sourceVersion));
|
||||
given(skillPublishService.rereleasePublishedVersion(
|
||||
eq(1L), eq("1.2.3"), eq("1.2.4"), eq("usr_1"), anyMap(), eq(true)))
|
||||
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
|
||||
|
||||
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
|
||||
.requestAttr("userId", "usr_1")
|
||||
.requestAttr("userNsRoles", java.util.Map.of(1L, NamespaceRole.ADMIN))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetVersion\":\"1.2.4\",\"confirmWarnings\":true}")
|
||||
.with(user("usr_1"))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.action").value("RERELEASE_VERSION"));
|
||||
|
||||
verify(skillPublishService).rereleasePublishedVersion(
|
||||
eq(1L), eq("1.2.3"), eq("1.2.4"), eq("usr_1"), anyMap(), eq(true));
|
||||
}
|
||||
|
||||
private Skill skillWithStatus(Skill skill, com.iflytek.skillhub.domain.skill.SkillStatus status) {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,8 @@ public class SkillPublishService {
|
|||
String sourceVersion,
|
||||
String targetVersion,
|
||||
String publisherId,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
boolean confirmWarnings) {
|
||||
Skill skill = skillRepository.findById(skillId)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillId));
|
||||
assertCanManageLifecycle(skill, publisherId, userNamespaceRoles);
|
||||
|
|
@ -181,7 +182,7 @@ public class SkillPublishService {
|
|||
publisherId,
|
||||
skill.getVisibility(),
|
||||
Set.of(),
|
||||
false, // confirmWarnings=false: no warnings to confirm for rerelease
|
||||
confirmWarnings, // confirmWarnings: honour caller's choice for rerelease
|
||||
false, // forceAutoPublish=false: respect visibility rules
|
||||
true
|
||||
);
|
||||
|
|
|
|||
|
|
@ -855,7 +855,8 @@ class SkillPublishServiceTest {
|
|||
"1.2.3",
|
||||
"1.2.4",
|
||||
publisherId,
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals("1.2.4", result.version().getVersion());
|
||||
|
|
@ -892,7 +893,8 @@ class SkillPublishServiceTest {
|
|||
"1.2.3",
|
||||
"1.2.4",
|
||||
publisherId,
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -953,7 +955,8 @@ class SkillPublishServiceTest {
|
|||
"1.2.3",
|
||||
"1.2.4",
|
||||
publisherId,
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals("1.2.4", result.version().getVersion());
|
||||
|
|
@ -966,6 +969,100 @@ class SkillPublishServiceTest {
|
|||
assertEquals(30L, skill.getLatestVersionId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRereleasePublishedVersion_ShouldRequireConfirmationWhenWarningsExist() throws Exception {
|
||||
String publisherId = "user-100";
|
||||
Skill skill = new Skill(1L, "demo-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 11L);
|
||||
skill.setDisplayName("Demo Skill");
|
||||
skill.setSummary("Original summary");
|
||||
Namespace namespace = new Namespace("global", "Global", "owner");
|
||||
setId(namespace, 1L);
|
||||
|
||||
SkillVersion sourceVersion = new SkillVersion(skill.getId(), "1.2.3", publisherId);
|
||||
setId(sourceVersion, 21L);
|
||||
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
sourceVersion.setPublishedAt(Instant.parse("2026-03-15T10:00:00Z"));
|
||||
|
||||
String sourceSkillMd = "---\nname: Demo Skill\ndescription: Original summary\nversion: 1.2.3\n---\nHello world";
|
||||
SkillFile skillMdFile = new SkillFile(sourceVersion.getId(), "SKILL.md", (long) sourceSkillMd.getBytes(StandardCharsets.UTF_8).length, "text/markdown", "hash1", "skills/11/21/SKILL.md");
|
||||
SkillMetadata rereleaseMetadata = new SkillMetadata(
|
||||
"Demo Skill", "Original summary", "1.2.4", "Hello world",
|
||||
Map.of("name", "Demo Skill", "description", "Original summary", "version", "1.2.4"));
|
||||
|
||||
when(skillRepository.findById(skill.getId())).thenReturn(Optional.of(skill));
|
||||
when(namespaceRepository.findById(skill.getNamespaceId())).thenReturn(Optional.of(namespace));
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.3")).thenReturn(Optional.of(sourceVersion));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.4")).thenReturn(Optional.empty());
|
||||
when(skillFileRepository.findByVersionId(sourceVersion.getId())).thenReturn(List.of(skillMdFile));
|
||||
when(objectStorageService.getObject(skillMdFile.getStorageKey())).thenReturn(new java.io.ByteArrayInputStream(sourceSkillMd.getBytes(StandardCharsets.UTF_8)));
|
||||
when(skillPackageValidator.validate(anyList())).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(anyString())).thenReturn(rereleaseMetadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.warn(List.of(
|
||||
"SKILL.md line 5 contains a value that looks like a secret or token.")));
|
||||
|
||||
DomainBadRequestException exception = assertThrows(DomainBadRequestException.class, () -> service.rereleasePublishedVersion(
|
||||
skill.getId(), "1.2.3", "1.2.4", publisherId,
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
|
||||
false
|
||||
));
|
||||
|
||||
assertEquals("error.skill.publish.precheck.confirmRequired", exception.messageCode());
|
||||
assertTrue(String.valueOf(exception.messageArgs()[0]).contains("looks like a secret or token"));
|
||||
verify(skillVersionRepository, never()).save(any(SkillVersion.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRereleasePublishedVersion_ShouldSucceedWhenWarningsConfirmed() throws Exception {
|
||||
String publisherId = "user-100";
|
||||
Skill skill = new Skill(1L, "demo-skill", publisherId, SkillVisibility.PUBLIC);
|
||||
setId(skill, 11L);
|
||||
skill.setDisplayName("Demo Skill");
|
||||
skill.setSummary("Original summary");
|
||||
Namespace namespace = new Namespace("global", "Global", "owner");
|
||||
setId(namespace, 1L);
|
||||
|
||||
SkillVersion sourceVersion = new SkillVersion(skill.getId(), "1.2.3", publisherId);
|
||||
setId(sourceVersion, 21L);
|
||||
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
sourceVersion.setPublishedAt(Instant.parse("2026-03-15T10:00:00Z"));
|
||||
|
||||
String sourceSkillMd = "---\nname: Demo Skill\ndescription: Original summary\nversion: 1.2.3\n---\nHello world";
|
||||
SkillFile skillMdFile = new SkillFile(sourceVersion.getId(), "SKILL.md", (long) sourceSkillMd.getBytes(StandardCharsets.UTF_8).length, "text/markdown", "hash1", "skills/11/21/SKILL.md");
|
||||
SkillMetadata rereleaseMetadata = new SkillMetadata(
|
||||
"Demo Skill", "Original summary", "1.2.4", "Hello world",
|
||||
Map.of("name", "Demo Skill", "description", "Original summary", "version", "1.2.4"));
|
||||
|
||||
when(skillRepository.findById(skill.getId())).thenReturn(Optional.of(skill));
|
||||
when(namespaceRepository.findById(skill.getNamespaceId())).thenReturn(Optional.of(namespace));
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.3")).thenReturn(Optional.of(sourceVersion));
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.4")).thenReturn(Optional.empty());
|
||||
when(skillFileRepository.findByVersionId(sourceVersion.getId())).thenReturn(List.of(skillMdFile));
|
||||
when(objectStorageService.getObject(skillMdFile.getStorageKey())).thenReturn(new java.io.ByteArrayInputStream(sourceSkillMd.getBytes(StandardCharsets.UTF_8)));
|
||||
when(skillPackageValidator.validate(anyList())).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(anyString())).thenReturn(rereleaseMetadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.warn(List.of(
|
||||
"SKILL.md line 5 contains a value that looks like a secret or token.")));
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
|
||||
SkillVersion saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) { setId(saved, 30L); }
|
||||
return saved;
|
||||
});
|
||||
when(skillRepository.save(any())).thenReturn(skill);
|
||||
|
||||
SkillPublishService.PublishResult result = service.rereleasePublishedVersion(
|
||||
skill.getId(), "1.2.3", "1.2.4", publisherId,
|
||||
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
|
||||
true // confirmWarnings = true → should bypass warning and succeed
|
||||
);
|
||||
|
||||
assertEquals("1.2.4", result.version().getVersion());
|
||||
assertEquals(SkillVersionStatus.PENDING_REVIEW, result.version().getStatus());
|
||||
verify(skillVersionRepository, atLeastOnce()).save(any(SkillVersion.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
|
|
@ -999,6 +1096,39 @@ class SkillPublishServiceTest {
|
|||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldRejectWithPrivateConflictWhenOtherOwnerHasPrivatePublishedSkill() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
String publisherId = "user-200";
|
||||
String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 1.0.0\n---\nBody";
|
||||
|
||||
PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown");
|
||||
List<PackageEntry> entries = List.of(skillMd);
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
|
||||
setId(namespace, 1L);
|
||||
NamespaceMember member = mock(NamespaceMember.class);
|
||||
SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of());
|
||||
|
||||
Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PRIVATE);
|
||||
setId(existingSkill, 1L);
|
||||
SkillVersion publishedVersion = new SkillVersion(1L, "0.1.0", "user-100");
|
||||
publishedVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
|
||||
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
|
||||
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
|
||||
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
|
||||
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(publishedVersion));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries(
|
||||
namespaceSlug, entries, publisherId, SkillVisibility.PRIVATE, Set.of()
|
||||
));
|
||||
assertEquals("error.skill.publish.nameConflict.private", ex.messageCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishFromEntries_ShouldAllowWhenOtherOwnerHasNonPublishedSkill() throws Exception {
|
||||
String namespaceSlug = "test-ns";
|
||||
|
|
|
|||
|
|
@ -488,14 +488,14 @@ export const skillLifecycleApi = {
|
|||
})
|
||||
},
|
||||
|
||||
async rereleaseVersion(namespace: string, slug: string, version: string, targetVersion: string): Promise<void> {
|
||||
async rereleaseVersion(namespace: string, slug: string, version: string, targetVersion: string, confirmWarnings = false): Promise<void> {
|
||||
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
|
||||
await fetchJson<void>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/rerelease`, {
|
||||
method: 'POST',
|
||||
headers: await ensureCsrfHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({ targetVersion }),
|
||||
body: JSON.stringify({ targetVersion, confirmWarnings }),
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
1
web/src/api/generated/schema.d.ts
vendored
1
web/src/api/generated/schema.d.ts
vendored
|
|
@ -3237,6 +3237,7 @@ export interface components {
|
|||
};
|
||||
SkillVersionRereleaseRequest: {
|
||||
targetVersion: string;
|
||||
confirmWarnings?: boolean;
|
||||
};
|
||||
SkillReportSubmitRequest: {
|
||||
reason?: string;
|
||||
|
|
|
|||
|
|
@ -911,6 +911,9 @@
|
|||
"rereleaseSuccessTitle": "Version re-released",
|
||||
"rereleaseSuccessDescription": "Created v{{target}} from v{{source}}.",
|
||||
"rereleaseErrorTitle": "Failed to re-release version",
|
||||
"rereleaseWarningTitle": "Pre-publish warning",
|
||||
"rereleaseWarningDescription": "We found the following risk reminders. If you understand them and still want to proceed, you can continue re-releasing.",
|
||||
"rereleaseWarningConfirm": "Continue re-releasing",
|
||||
"yankVersion": "Yank Current Version",
|
||||
"promoteToGlobal": "Promote to Global",
|
||||
"promotionSectionTitle": "Promote to Global",
|
||||
|
|
|
|||
|
|
@ -912,6 +912,9 @@
|
|||
"rereleaseSuccessTitle": "版本已重新发布",
|
||||
"rereleaseSuccessDescription": "已基于 v{{source}} 创建新版本 v{{target}}。",
|
||||
"rereleaseErrorTitle": "重新发布版本失败",
|
||||
"rereleaseWarningTitle": "发布前风险提醒",
|
||||
"rereleaseWarningDescription": "检测到以下风险项。若你确认这些内容可以接受,仍可继续重新发布。",
|
||||
"rereleaseWarningConfirm": "继续重新发布",
|
||||
"yankVersion": "撤回当前版本",
|
||||
"promoteToGlobal": "申请提升到全局",
|
||||
"promotionSectionTitle": "提升到全局",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate, useRouterState, useSearch } from '@tanstack/react-router'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { ArrowLeft, ArrowUpCircle, ChevronDown, ChevronUp, Clock, Folder, RefreshCw, ShieldCheck, Terminal, User } from 'lucide-react'
|
||||
import { ArrowLeft, ArrowUpCircle, ChevronDown, ChevronUp, Clock, Folder, Globe, Lock, RefreshCw, ShieldCheck, Terminal, User, Users } from 'lucide-react'
|
||||
import { MarkdownRenderer } from '@/features/skill/markdown-renderer'
|
||||
import { FileTree } from '@/features/skill/file-tree'
|
||||
import { FilePreviewDialog } from '@/features/skill/file-preview-dialog'
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
shouldCollapseOverview,
|
||||
} from '@/features/skill/overview-collapse'
|
||||
import { resolveSkillActionErrorTitle } from '@/features/skill/skill-action-error'
|
||||
import { isPrecheckConfirmationMessage, extractPrecheckWarnings } from '@/features/publish/publish-error-utils'
|
||||
import { clearDeletedSkillQueries, isDeleteSlugConfirmationValid, resolveDeletedSkillReturnTo } from '@/features/skill/skill-delete-flow'
|
||||
import { isSkillDetailQueriesEnabled } from './skill-detail-query'
|
||||
import { RatingInput } from '@/features/social/rating-input'
|
||||
|
|
@ -116,6 +117,8 @@ export function SkillDetailPage() {
|
|||
const [withdrawVersionTarget, setWithdrawVersionTarget] = useState<string | null>(null)
|
||||
const [rereleaseTarget, setRereleaseTarget] = useState<string | null>(null)
|
||||
const [targetVersionInput, setTargetVersionInput] = useState('')
|
||||
const [rereleaseWarnings, setRereleaseWarnings] = useState<string[]>([])
|
||||
const [rereleaseWarningDialogOpen, setRereleaseWarningDialogOpen] = useState(false)
|
||||
const [diffSourceVersion, setDiffSourceVersion] = useState<string | null>(null)
|
||||
const [confirmPublishTarget, setConfirmPublishTarget] = useState<string | null>(null)
|
||||
const [submitReviewTarget, setSubmitReviewTarget] = useState<string | null>(null)
|
||||
|
|
@ -575,7 +578,7 @@ export function SkillDetailPage() {
|
|||
setTargetVersionInput(suggestNextVersion(version))
|
||||
}
|
||||
|
||||
const handleRereleaseVersion = async () => {
|
||||
const handleRereleaseVersion = async (confirmWarnings = false) => {
|
||||
if (!rereleaseTarget || !targetVersionInput.trim()) {
|
||||
return
|
||||
}
|
||||
|
|
@ -585,6 +588,7 @@ export function SkillDetailPage() {
|
|||
slug,
|
||||
version: rereleaseTarget,
|
||||
targetVersion: targetVersionInput.trim(),
|
||||
confirmWarnings,
|
||||
})
|
||||
toast.success(
|
||||
t('skillDetail.rereleaseSuccessTitle'),
|
||||
|
|
@ -592,7 +596,15 @@ export function SkillDetailPage() {
|
|||
)
|
||||
setRereleaseTarget(null)
|
||||
setTargetVersionInput('')
|
||||
setRereleaseWarnings([])
|
||||
setRereleaseWarningDialogOpen(false)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && isPrecheckConfirmationMessage(error.serverMessage)) {
|
||||
const warnings = extractPrecheckWarnings(error.serverMessage)
|
||||
setRereleaseWarnings(warnings)
|
||||
setRereleaseWarningDialogOpen(true)
|
||||
return
|
||||
}
|
||||
toast.error(t('skillDetail.rereleaseErrorTitle'), error instanceof Error ? error.message : '')
|
||||
throw error
|
||||
}
|
||||
|
|
@ -710,10 +722,31 @@ export function SkillDetailPage() {
|
|||
<div className="flex items-center gap-3 mb-1">
|
||||
<NamespaceBadge type="GLOBAL" name={namespace} />
|
||||
{skill.status && (
|
||||
<span className="badge-soft badge-soft-blue">
|
||||
<span className={cn(
|
||||
'badge-soft',
|
||||
skill.status === 'ACTIVE' && 'badge-soft-green',
|
||||
skill.status === 'ARCHIVED' && 'bg-secondary text-muted-foreground',
|
||||
skill.status === 'HIDDEN' && 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
|
||||
!['ACTIVE', 'ARCHIVED', 'HIDDEN'].includes(skill.status) && 'badge-soft-blue',
|
||||
)}>
|
||||
{resolveSkillStatusLabel(skill.status)}
|
||||
</span>
|
||||
)}
|
||||
{skill.visibility && (
|
||||
<span className={cn(
|
||||
'badge-soft inline-flex items-center gap-1',
|
||||
skill.visibility === 'PUBLIC' && 'badge-soft-green',
|
||||
skill.visibility === 'PRIVATE' && 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
||||
skill.visibility === 'NAMESPACE_ONLY' && 'badge-soft-blue',
|
||||
)}>
|
||||
{skill.visibility === 'PUBLIC' && <Globe className="h-3 w-3" />}
|
||||
{skill.visibility === 'PRIVATE' && <Lock className="h-3 w-3" />}
|
||||
{skill.visibility === 'NAMESPACE_ONLY' && <Users className="h-3 w-3" />}
|
||||
{skill.visibility === 'PUBLIC' && t('publish.visibilityOptions.public')}
|
||||
{skill.visibility === 'PRIVATE' && t('publish.visibilityOptions.private')}
|
||||
{skill.visibility === 'NAMESPACE_ONLY' && t('publish.visibilityOptions.namespaceOnly')}
|
||||
</span>
|
||||
)}
|
||||
{isReviewFlowPending && (
|
||||
<span className="badge-soft" style={{ background: '#fef3c7', color: '#92400e' }}>
|
||||
{t('skillDetail.versionStatusPendingReview')}
|
||||
|
|
@ -957,7 +990,7 @@ export function SkillDetailPage() {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-center py-8">{t('skillDetail.noVersions')}</div>
|
||||
<Card className="p-8 text-muted-foreground text-center">{t('skillDetail.noVersions')}</Card>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
|
@ -1396,6 +1429,8 @@ export function SkillDetailPage() {
|
|||
if (!open) {
|
||||
setRereleaseTarget(null)
|
||||
setTargetVersionInput('')
|
||||
setRereleaseWarnings([])
|
||||
setRereleaseWarningDialogOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1422,13 +1457,34 @@ export function SkillDetailPage() {
|
|||
<Button variant="outline" onClick={() => setRereleaseTarget(null)}>
|
||||
{t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleRereleaseVersion} disabled={rereleaseVersionMutation.isPending || !targetVersionInput.trim()}>
|
||||
<Button onClick={() => handleRereleaseVersion()} disabled={rereleaseVersionMutation.isPending || !targetVersionInput.trim()}>
|
||||
{rereleaseVersionMutation.isPending ? t('skillDetail.processing') : t('skillDetail.rereleaseVersion')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={rereleaseWarningDialogOpen}
|
||||
onOpenChange={setRereleaseWarningDialogOpen}
|
||||
title={t('skillDetail.rereleaseWarningTitle')}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<p>{t('skillDetail.rereleaseWarningDescription')}</p>
|
||||
<ul className="list-disc space-y-1 pl-5 text-sm">
|
||||
{rereleaseWarnings.map((warning, index) => (
|
||||
<li key={index}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
confirmText={t('skillDetail.rereleaseWarningConfirm')}
|
||||
onConfirm={() => {
|
||||
setRereleaseWarningDialogOpen(false)
|
||||
handleRereleaseVersion(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!confirmPublishTarget}
|
||||
onOpenChange={(open) => {
|
||||
|
|
|
|||
|
|
@ -206,8 +206,11 @@ export function useRereleaseSkillVersion() {
|
|||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ namespace, slug, version, targetVersion }: { namespace: string; slug: string; version: string; targetVersion: string }) =>
|
||||
skillLifecycleApi.rereleaseVersion(namespace, slug, version, targetVersion),
|
||||
mutationFn: ({ namespace, slug, version, targetVersion, confirmWarnings }: { namespace: string; slug: string; version: string; targetVersion: string; confirmWarnings?: boolean }) =>
|
||||
skillLifecycleApi.rereleaseVersion(namespace, slug, version, targetVersion, confirmWarnings),
|
||||
meta: {
|
||||
skipGlobalErrorHandler: true,
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug] })
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue