Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eded924
fix: keep download counts consistent across skill pages
yun-zhi-ztl Mar 17, 2026
5e89a4d
fix: stabilize empty search ordering across sorts
yun-zhi-ztl Mar 17, 2026
d6a4831
Merge remote-tracking branch 'origin/main' into feature/project-fixbug
yun-zhi-ztl Mar 17, 2026
55586ef
fix: show disabled-account reason on login redirect
yun-zhi-ztl Mar 17, 2026
4ea280c
fix: mute report input placeholder text
yun-zhi-ztl Mar 17, 2026
caa6093
fix: return skill detail to my skills page
yun-zhi-ztl Mar 17, 2026
4d39163
test: stabilize auth context filter coverage
yun-zhi-ztl Mar 17, 2026
aa6a9ff
Merge remote-tracking branch 'origin/main' into feature/project-fixbug
yun-zhi-ztl Mar 18, 2026
2430e2f
Merge remote-tracking branch 'origin/main' into feature/project-fixbug
yun-zhi-ztl Mar 18, 2026
7dae434
feat(publish): increase single file limit to 10MB
yun-zhi-ztl Mar 18, 2026
e55947d
feat(publish): expand allowed file extensions
yun-zhi-ztl Mar 18, 2026
81edb08
feat(publish): extend secret scanning to new text file types
yun-zhi-ztl Mar 18, 2026
fc53107
feat(publish): add content validation for new file types
yun-zhi-ztl Mar 18, 2026
7e869fa
refactor(publish): inject configurable limits into SkillPackageArchiv…
yun-zhi-ztl Mar 18, 2026
dac7fa1
feat(publish): support zip with single root directory wrapper
yun-zhi-ztl Mar 18, 2026
ce25d19
feat(publish): expand determineContentType for new file types
yun-zhi-ztl Mar 18, 2026
1231820
test(publish): update tests for new upload constraints
yun-zhi-ztl Mar 18, 2026
020a96e
Merge remote-tracking branch 'origin/main' into feature/project-fixbug
yun-zhi-ztl Mar 18, 2026
78bc6ba
fix(web): add REJECTED status label and styling to my-skills page
yun-zhi-ztl Mar 18, 2026
0d0b419
fix: prevent deleting the last remaining version of a skill
yun-zhi-ztl Mar 18, 2026
034b4c0
Merge remote-tracking branch 'origin/main' into feature/project-fixbug
yun-zhi-ztl Mar 18, 2026
fe75ece
test: fix and add tests for last-version deletion guard
yun-zhi-ztl Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/skillhub-app/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=不能举报自己发布的技能
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ public void deleteVersion(Skill skill,
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ void deleteVersion_removesDraftFilesAndBundle() {
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));
Expand Down Expand Up @@ -212,6 +216,21 @@ void deleteVersion_rejectsPublishedVersion() {
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);
Expand Down
1 change: 1 addition & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions web/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
"statusArchived": "已归档",
"statusPendingReview": "审核中",
"statusPublished": "已发布",
"statusRejected": "已拒绝",
"archiveConfirmTitle": "确认归档技能",
"archiveConfirmDescription": "归档后普通用户将无法看到或下载“{{skill}}”,确定继续吗?",
"unarchiveConfirmTitle": "确认恢复技能",
Expand Down
1 change: 1 addition & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions web/src/pages/dashboard/my-skills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export function MySkillsPage() {
if (status === 'PUBLISHED') {
return t('mySkills.statusPublished')
}
if (status === 'REJECTED') {
return t('mySkills.statusRejected')
}
return status
}

Expand All @@ -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'
}

Expand Down
3 changes: 2 additions & 1 deletion web/src/pages/skill-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,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'

Expand Down Expand Up @@ -590,7 +591,7 @@ export function SkillDetailPage() {
</Button>
</>
)}
{skill.canManageLifecycle && canDeleteVersion(version.status) && (
{skill.canManageLifecycle && canDeleteVersion(version.status) && !isLastVersion && (
<Button
size="sm"
variant="outline"
Expand Down
Loading