mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix: skill upload constraints, rejected label, and last-version guard (#82)
* fix: keep download counts consistent across skill pages * fix: stabilize empty search ordering across sorts * fix: show disabled-account reason on login redirect * fix: mute report input placeholder text * fix: return skill detail to my skills page * test: stabilize auth context filter coverage * feat(publish): increase single file limit to 10MB * feat(publish): expand allowed file extensions * feat(publish): extend secret scanning to new text file types * feat(publish): add content validation for new file types * refactor(publish): inject configurable limits into SkillPackageArchiveExtractor * feat(publish): support zip with single root directory wrapper * feat(publish): expand determineContentType for new file types * test(publish): update tests for new upload constraints * fix(web): add REJECTED status label and styling to my-skills page The resolveStatusLabel and resolveStatusClassName functions were missing the REJECTED case, causing rejected skills to show raw status string with no color styling. * fix: prevent deleting the last remaining version of a skill Backend: added version count check in SkillGovernanceService.deleteVersion() Frontend: hide delete button when only one version remains * test: fix and add tests for last-version deletion guard - Fix deleteVersion_removesDraftFilesAndBundle: mock findBySkillId to return 2 versions so the new guard doesn't block the happy path - Add deleteVersion_rejectsLastRemainingVersion: verify that deleting the only remaining version is rejected with the correct error code ---------
This commit is contained in:
parent
b7fd95459c
commit
6b2d8ce7ef
9 changed files with 37 additions and 1 deletions
|
|
@ -94,6 +94,7 @@ error.skill.version.exists=Version already exists: {0}
|
|||
error.skill.version.notFound=Version not found: {0}
|
||||
error.skill.version.notPublished=Version is not published: {0}
|
||||
error.skill.version.delete.unsupported=Only DRAFT or REJECTED versions can be deleted: {0}
|
||||
error.skill.version.delete.lastVersion=Cannot delete the last remaining version: {0}
|
||||
error.skill.report.reason.required=Please provide a report reason
|
||||
error.skill.report.unavailable=This skill cannot be reported right now: {0}
|
||||
error.skill.report.self=You cannot report your own skill
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ error.skill.version.exists=版本已存在:{0}
|
|||
error.skill.version.notFound=未找到版本:{0}
|
||||
error.skill.version.notPublished=版本未发布:{0}
|
||||
error.skill.version.delete.unsupported=只有 DRAFT 或 REJECTED 版本可以删除:{0}
|
||||
error.skill.version.delete.lastVersion=无法删除最后一个版本:{0}
|
||||
error.skill.report.reason.required=请填写举报原因
|
||||
error.skill.report.unavailable=当前无法举报该技能:{0}
|
||||
error.skill.report.self=不能举报自己发布的技能
|
||||
|
|
|
|||
|
|
@ -141,6 +141,11 @@ public class SkillGovernanceService {
|
|||
throw new DomainBadRequestException("error.skill.version.delete.unsupported", version.getVersion());
|
||||
}
|
||||
|
||||
long versionCount = skillVersionRepository.findBySkillId(skill.getId()).size();
|
||||
if (versionCount <= 1) {
|
||||
throw new DomainBadRequestException("error.skill.version.delete.lastVersion", version.getVersion());
|
||||
}
|
||||
|
||||
List<SkillFile> files = skillFileRepository.findByVersionId(version.getId());
|
||||
if (!files.isEmpty()) {
|
||||
objectStorageService.deleteObjects(files.stream().map(SkillFile::getStorageKey).toList());
|
||||
|
|
|
|||
|
|
@ -181,6 +181,10 @@ class SkillGovernanceServiceTest {
|
|||
SkillVersion version = new SkillVersion(2L, "1.0.0", "owner");
|
||||
setField(version, "id", 2L);
|
||||
version.setStatus(SkillVersionStatus.DRAFT);
|
||||
SkillVersion otherVersion = new SkillVersion(2L, "2.0.0", "owner");
|
||||
setField(otherVersion, "id", 3L);
|
||||
otherVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
given(skillVersionRepository.findBySkillId(1L)).willReturn(java.util.List.of(version, otherVersion));
|
||||
SkillFile readme = new SkillFile(version.getId(), "README.md", 10L, "text/markdown", "sha1", "skills/demo/readme");
|
||||
SkillFile icon = new SkillFile(version.getId(), "icon.png", 20L, "image/png", "sha2", "skills/demo/icon");
|
||||
given(skillFileRepository.findByVersionId(version.getId())).willReturn(java.util.List.of(readme, icon));
|
||||
|
|
@ -212,6 +216,21 @@ class SkillGovernanceServiceTest {
|
|||
verify(objectStorageService, never()).deleteObject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteVersion_rejectsLastRemainingVersion() {
|
||||
Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC);
|
||||
setField(skill, "id", 1L);
|
||||
SkillVersion version = new SkillVersion(2L, "1.0.0", "owner");
|
||||
setField(version, "id", 2L);
|
||||
version.setStatus(SkillVersionStatus.DRAFT);
|
||||
given(skillVersionRepository.findBySkillId(1L)).willReturn(java.util.List.of(version));
|
||||
|
||||
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class,
|
||||
() -> service.deleteVersion(skill, version, "owner", Map.of(), "127.0.0.1", "JUnit"));
|
||||
assertThat(ex.messageCode()).isEqualTo("error.skill.version.delete.lastVersion");
|
||||
|
||||
verify(skillVersionRepository, never()).delete(any());
|
||||
}
|
||||
private void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"statusArchived": "Archived",
|
||||
"statusPendingReview": "Pending Review",
|
||||
"statusPublished": "Published",
|
||||
"statusRejected": "Rejected",
|
||||
"archiveConfirmTitle": "Archive skill",
|
||||
"archiveConfirmDescription": "After archiving, regular users will no longer be able to view or download \"{{skill}}\". Continue?",
|
||||
"unarchiveConfirmTitle": "Restore skill",
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"statusArchived": "已归档",
|
||||
"statusPendingReview": "审核中",
|
||||
"statusPublished": "已发布",
|
||||
"statusRejected": "已拒绝",
|
||||
"archiveConfirmTitle": "确认归档技能",
|
||||
"archiveConfirmDescription": "归档后普通用户将无法看到或下载“{{skill}}”,确定继续吗?",
|
||||
"unarchiveConfirmTitle": "确认恢复技能",
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@
|
|||
.status-pill--published { background: #16a34a; }
|
||||
.status-pill--review { background: #ea580c; }
|
||||
.status-pill--archived { background: #6b7280; }
|
||||
.status-pill--rejected { background: #dc2626; }
|
||||
|
||||
/* ─── Role pill ─── */
|
||||
.role-pill {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export function MySkillsPage() {
|
|||
if (status === 'PUBLISHED') {
|
||||
return t('mySkills.statusPublished')
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return t('mySkills.statusRejected')
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +74,9 @@ export function MySkillsPage() {
|
|||
if (status === 'PUBLISHED') {
|
||||
return 'status-pill status-pill--published'
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return 'status-pill status-pill--rejected'
|
||||
}
|
||||
return 'status-pill'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ export function SkillDetailPage() {
|
|||
}
|
||||
|
||||
const canDeleteVersion = (status?: string) => status === 'DRAFT' || status === 'REJECTED'
|
||||
const isLastVersion = versions?.length === 1
|
||||
const canWithdrawVersion = (status?: string) => status === 'PENDING_REVIEW'
|
||||
const canRereleaseVersion = (status?: string) => status === 'PUBLISHED'
|
||||
|
||||
|
|
@ -687,7 +688,7 @@ export function SkillDetailPage() {
|
|||
</Button>
|
||||
</>
|
||||
)}
|
||||
{skill.canManageLifecycle && canDeleteVersion(version.status) && (
|
||||
{skill.canManageLifecycle && canDeleteVersion(version.status) && !isLastVersion && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue