mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
Merge pull request #390 from iflytek/worktree-skill-version-compare-v2
feat(skill): add version compare page with unified diff
This commit is contained in:
commit
76db91dcb4
24 changed files with 1825 additions and 10 deletions
|
|
@ -14,6 +14,10 @@ import com.iflytek.skillhub.dto.ResolveVersionResponse;
|
|||
import com.iflytek.skillhub.dto.SkillDetailResponse;
|
||||
import com.iflytek.skillhub.dto.SkillFileResponse;
|
||||
import com.iflytek.skillhub.dto.SkillLifecycleVersionResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionCompareFileResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionCompareHunkResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionCompareLineResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionCompareResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionDetailResponse;
|
||||
import com.iflytek.skillhub.dto.SkillVersionResponse;
|
||||
import com.iflytek.skillhub.metrics.SkillHubMetrics;
|
||||
|
|
@ -174,6 +178,27 @@ public class SkillController extends BaseApiController {
|
|||
return ok("response.success.read", response);
|
||||
}
|
||||
|
||||
@GetMapping("/{namespace}/{slug}/versions/compare")
|
||||
public ApiResponse<SkillVersionCompareResponse> compareVersions(
|
||||
@PathVariable String namespace,
|
||||
@PathVariable String slug,
|
||||
@RequestParam("from") String from,
|
||||
@RequestParam("to") String to,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
|
||||
SkillQueryService.SkillVersionCompareDTO compare = skillQueryService.compareVersions(
|
||||
namespace,
|
||||
slug,
|
||||
from,
|
||||
to,
|
||||
userId,
|
||||
userNsRoles != null ? userNsRoles : Map.of()
|
||||
);
|
||||
|
||||
return ok("response.success.read", toCompareResponse(compare));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists packaged files for a concrete version after visibility checks have
|
||||
* been applied.
|
||||
|
|
@ -421,4 +446,51 @@ public class SkillController extends BaseApiController {
|
|||
}
|
||||
return new SkillLifecycleVersionResponse(projection.id(), projection.version(), projection.status());
|
||||
}
|
||||
|
||||
private SkillVersionCompareResponse toCompareResponse(SkillQueryService.SkillVersionCompareDTO compare) {
|
||||
return new SkillVersionCompareResponse(
|
||||
compare.from(),
|
||||
compare.to(),
|
||||
new SkillVersionCompareResponse.SkillVersionCompareSummaryResponse(
|
||||
compare.summary().totalFiles(),
|
||||
compare.summary().addedFiles(),
|
||||
compare.summary().modifiedFiles(),
|
||||
compare.summary().removedFiles(),
|
||||
compare.summary().addedLines(),
|
||||
compare.summary().removedLines()
|
||||
),
|
||||
compare.files().stream().map(this::toCompareFileResponse).toList()
|
||||
);
|
||||
}
|
||||
|
||||
private SkillVersionCompareFileResponse toCompareFileResponse(SkillQueryService.SkillVersionCompareFileDTO file) {
|
||||
return new SkillVersionCompareFileResponse(
|
||||
file.path(),
|
||||
file.changeType(),
|
||||
file.oldSize(),
|
||||
file.newSize(),
|
||||
file.binary(),
|
||||
file.truncated(),
|
||||
file.hunks().stream().map(this::toCompareHunkResponse).toList()
|
||||
);
|
||||
}
|
||||
|
||||
private SkillVersionCompareHunkResponse toCompareHunkResponse(SkillQueryService.SkillVersionCompareHunkDTO hunk) {
|
||||
return new SkillVersionCompareHunkResponse(
|
||||
hunk.oldStart(),
|
||||
hunk.oldLines(),
|
||||
hunk.newStart(),
|
||||
hunk.newLines(),
|
||||
hunk.lines().stream().map(this::toCompareLineResponse).toList()
|
||||
);
|
||||
}
|
||||
|
||||
private SkillVersionCompareLineResponse toCompareLineResponse(SkillQueryService.SkillVersionCompareLineDTO line) {
|
||||
return new SkillVersionCompareLineResponse(
|
||||
line.type(),
|
||||
line.content(),
|
||||
line.oldLineNumber(),
|
||||
line.newLineNumber()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SkillVersionCompareFileResponse(
|
||||
String path,
|
||||
String changeType,
|
||||
Long oldSize,
|
||||
Long newSize,
|
||||
boolean binary,
|
||||
boolean truncated,
|
||||
List<SkillVersionCompareHunkResponse> hunks
|
||||
) {}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SkillVersionCompareHunkResponse(
|
||||
int oldStart,
|
||||
int oldLines,
|
||||
int newStart,
|
||||
int newLines,
|
||||
List<SkillVersionCompareLineResponse> lines
|
||||
) {}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
public record SkillVersionCompareLineResponse(
|
||||
String type,
|
||||
String content,
|
||||
Integer oldLineNumber,
|
||||
Integer newLineNumber
|
||||
) {}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SkillVersionCompareResponse(
|
||||
String from,
|
||||
String to,
|
||||
SkillVersionCompareSummaryResponse summary,
|
||||
List<SkillVersionCompareFileResponse> files
|
||||
) {
|
||||
public record SkillVersionCompareSummaryResponse(
|
||||
int totalFiles,
|
||||
int addedFiles,
|
||||
int modifiedFiles,
|
||||
int removedFiles,
|
||||
int addedLines,
|
||||
int removedLines
|
||||
) {}
|
||||
}
|
||||
|
|
@ -106,6 +106,7 @@ error.skill.version.notFound=Version not found: {0}
|
|||
error.skill.version.notPublished=Version is not published: {0}
|
||||
error.skill.version.delete.unsupported=Only DRAFT, UPLOADED, REJECTED, or SCAN_FAILED versions can be deleted: {0}
|
||||
error.skill.version.delete.lastVersion=Cannot delete the last remaining version: {0}
|
||||
error.skill.version.compare.same=Cannot compare a version with itself
|
||||
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
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ error.skill.version.notFound=未找到版本:{0}
|
|||
error.skill.version.notPublished=版本未发布:{0}
|
||||
error.skill.version.delete.unsupported=只有 DRAFT、UPLOADED、REJECTED 或 SCAN_FAILED 版本可以删除:{0}
|
||||
error.skill.version.delete.lastVersion=无法删除最后一个版本:{0}
|
||||
error.skill.version.compare.same=无法对同一版本进行对比
|
||||
error.skill.report.reason.required=请填写举报原因
|
||||
error.skill.report.unavailable=当前无法举报该技能:{0}
|
||||
error.skill.report.self=不能举报自己发布的技能
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller;
|
|||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.SkillFile;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersion;
|
||||
|
|
@ -239,4 +240,65 @@ class SkillControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].downloadAvailable").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareVersionsShouldReturnStructuredDiffEnvelope() throws Exception {
|
||||
when(skillQueryService.compareVersions(
|
||||
eq("team"),
|
||||
eq("demo"),
|
||||
eq("1.0.0"),
|
||||
eq("1.1.0"),
|
||||
eq((String) null),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.thenReturn(new SkillQueryService.SkillVersionCompareDTO(
|
||||
"1.0.0",
|
||||
"1.1.0",
|
||||
new SkillQueryService.SkillVersionCompareSummaryDTO(1, 0, 1, 0, 2, 1),
|
||||
List.of(new SkillQueryService.SkillVersionCompareFileDTO(
|
||||
"README.md",
|
||||
"MODIFIED",
|
||||
10L,
|
||||
12L,
|
||||
false,
|
||||
false,
|
||||
List.of(new SkillQueryService.SkillVersionCompareHunkDTO(
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
List.of(
|
||||
new SkillQueryService.SkillVersionCompareLineDTO("CONTEXT", "# Demo", 1, 1),
|
||||
new SkillQueryService.SkillVersionCompareLineDTO("DELETE", "old", 2, null),
|
||||
new SkillQueryService.SkillVersionCompareLineDTO("ADD", "new", null, 2)
|
||||
)))))));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/team/demo/versions/compare")
|
||||
.param("from", "1.0.0")
|
||||
.param("to", "1.1.0"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.from").value("1.0.0"))
|
||||
.andExpect(jsonPath("$.data.to").value("1.1.0"))
|
||||
.andExpect(jsonPath("$.data.summary.totalFiles").value(1))
|
||||
.andExpect(jsonPath("$.data.files[0].path").value("README.md"))
|
||||
.andExpect(jsonPath("$.data.files[0].hunks[0].lines[1].type").value("DELETE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareVersionsShouldRejectSameVersion() throws Exception {
|
||||
when(skillQueryService.compareVersions(
|
||||
eq("team"),
|
||||
eq("demo"),
|
||||
eq("1.0.0"),
|
||||
eq("1.0.0"),
|
||||
eq((String) null),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.thenThrow(new DomainBadRequestException("error.skill.version.compare.same"));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/team/demo/versions/compare")
|
||||
.param("from", "1.0.0")
|
||||
.param("to", "1.0.0"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(400));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@
|
|||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.java-diff-utils</groupId>
|
||||
<artifactId>java-diff-utils</artifactId>
|
||||
<version>4.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import com.iflytek.skillhub.domain.skill.*;
|
|||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.patch.AbstractDelta;
|
||||
import com.github.difflib.patch.Chunk;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
|
@ -24,6 +28,7 @@ import java.io.UncheckedIOException;
|
|||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
|
|
@ -31,6 +36,7 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
|
@ -124,6 +130,47 @@ public class SkillQueryService {
|
|||
String manifestJson
|
||||
) {}
|
||||
|
||||
public record SkillVersionCompareDTO(
|
||||
String from,
|
||||
String to,
|
||||
SkillVersionCompareSummaryDTO summary,
|
||||
List<SkillVersionCompareFileDTO> files
|
||||
) {}
|
||||
|
||||
public record SkillVersionCompareSummaryDTO(
|
||||
int totalFiles,
|
||||
int addedFiles,
|
||||
int modifiedFiles,
|
||||
int removedFiles,
|
||||
int addedLines,
|
||||
int removedLines
|
||||
) {}
|
||||
|
||||
public record SkillVersionCompareFileDTO(
|
||||
String path,
|
||||
String changeType,
|
||||
Long oldSize,
|
||||
Long newSize,
|
||||
boolean binary,
|
||||
boolean truncated,
|
||||
List<SkillVersionCompareHunkDTO> hunks
|
||||
) {}
|
||||
|
||||
public record SkillVersionCompareHunkDTO(
|
||||
int oldStart,
|
||||
int oldLines,
|
||||
int newStart,
|
||||
int newLines,
|
||||
List<SkillVersionCompareLineDTO> lines
|
||||
) {}
|
||||
|
||||
public record SkillVersionCompareLineDTO(
|
||||
String type,
|
||||
String content,
|
||||
Integer oldLineNumber,
|
||||
Integer newLineNumber
|
||||
) {}
|
||||
|
||||
public record ResolvedVersionDTO(
|
||||
Long skillId,
|
||||
String namespace,
|
||||
|
|
@ -268,6 +315,70 @@ public class SkillQueryService {
|
|||
);
|
||||
}
|
||||
|
||||
public SkillVersionCompareDTO compareVersions(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String fromVersion,
|
||||
String toVersion,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (Objects.equals(fromVersion, toVersion)) {
|
||||
throw new DomainBadRequestException("error.skill.version.compare.same");
|
||||
}
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion from = findVersion(skill, fromVersion);
|
||||
SkillVersion to = findVersion(skill, toVersion);
|
||||
assertPreviewAccessible(skill, from, fromVersion, currentUserId, userNsRoles);
|
||||
assertPreviewAccessible(skill, to, toVersion, currentUserId, userNsRoles);
|
||||
|
||||
Map<String, SkillFile> fromFiles = availableFiles(from.getId()).stream()
|
||||
.collect(Collectors.toMap(SkillFile::getFilePath, file -> file));
|
||||
Map<String, SkillFile> toFiles = availableFiles(to.getId()).stream()
|
||||
.collect(Collectors.toMap(SkillFile::getFilePath, file -> file));
|
||||
|
||||
List<SkillVersionCompareFileDTO> files = new TreeSet<String>() {{
|
||||
addAll(fromFiles.keySet());
|
||||
addAll(toFiles.keySet());
|
||||
}}.stream()
|
||||
.map(path -> buildCompareFile(fromFiles.get(path), toFiles.get(path)))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
int addedFiles = 0;
|
||||
int modifiedFiles = 0;
|
||||
int removedFiles = 0;
|
||||
int addedLines = 0;
|
||||
int removedLines = 0;
|
||||
for (SkillVersionCompareFileDTO file : files) {
|
||||
if ("ADDED".equals(file.changeType())) {
|
||||
addedFiles++;
|
||||
} else if ("REMOVED".equals(file.changeType())) {
|
||||
removedFiles++;
|
||||
} else {
|
||||
modifiedFiles++;
|
||||
}
|
||||
for (SkillVersionCompareHunkDTO hunk : file.hunks()) {
|
||||
for (SkillVersionCompareLineDTO line : hunk.lines()) {
|
||||
if ("ADD".equals(line.type())) {
|
||||
addedLines++;
|
||||
} else if ("DELETE".equals(line.type())) {
|
||||
removedLines++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SkillVersionCompareDTO(
|
||||
fromVersion,
|
||||
toVersion,
|
||||
new SkillVersionCompareSummaryDTO(files.size(), addedFiles, modifiedFiles, removedFiles, addedLines, removedLines),
|
||||
files);
|
||||
}
|
||||
|
||||
public List<SkillFile> listFiles(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
|
|
@ -491,6 +602,81 @@ public class SkillQueryService {
|
|||
SkillSlugResolutionService.Preference.CURRENT_USER);
|
||||
}
|
||||
|
||||
private static final long COMPARE_MAX_FILE_BYTES = 1024 * 1024;
|
||||
private static final int COMPARE_MAX_LINES = 5000;
|
||||
private static final Set<String> BINARY_FILE_EXTENSIONS = Set.of(
|
||||
".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot",
|
||||
".zip", ".tar", ".gz", ".jar", ".war", ".class", ".so", ".dll", ".exe", ".pdf"
|
||||
);
|
||||
|
||||
private SkillVersionCompareFileDTO buildCompareFile(SkillFile fromFile, SkillFile toFile) {
|
||||
if (fromFile != null && toFile != null && Objects.equals(fromFile.getSha256(), toFile.getSha256())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = fromFile != null ? fromFile.getFilePath() : Objects.requireNonNull(toFile).getFilePath();
|
||||
String changeType = fromFile == null ? "ADDED" : toFile == null ? "REMOVED" : "MODIFIED";
|
||||
Long oldSize = fromFile != null ? fromFile.getFileSize() : null;
|
||||
Long newSize = toFile != null ? toFile.getFileSize() : null;
|
||||
boolean isBinary = isBinaryFile(path);
|
||||
if (isBinary) {
|
||||
return new SkillVersionCompareFileDTO(path, changeType, oldSize, newSize, true, false, List.of());
|
||||
}
|
||||
|
||||
String oldContent = fromFile != null ? readTextContent(fromFile) : "";
|
||||
String newContent = toFile != null ? readTextContent(toFile) : "";
|
||||
List<String> oldLines = splitLines(oldContent);
|
||||
List<String> newLines = splitLines(newContent);
|
||||
boolean isTruncated = (oldSize != null && oldSize > COMPARE_MAX_FILE_BYTES)
|
||||
|| (newSize != null && newSize > COMPARE_MAX_FILE_BYTES)
|
||||
|| oldLines.size() > COMPARE_MAX_LINES
|
||||
|| newLines.size() > COMPARE_MAX_LINES;
|
||||
if (isTruncated) {
|
||||
return new SkillVersionCompareFileDTO(path, changeType, oldSize, newSize, false, true, List.of());
|
||||
}
|
||||
|
||||
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
|
||||
List<SkillVersionCompareHunkDTO> hunks = patch.getDeltas().stream()
|
||||
.map(this::toCompareHunk)
|
||||
.toList();
|
||||
return new SkillVersionCompareFileDTO(path, changeType, oldSize, newSize, false, false, hunks);
|
||||
}
|
||||
|
||||
private SkillVersionCompareHunkDTO toCompareHunk(AbstractDelta<String> delta) {
|
||||
Chunk<String> source = delta.getSource();
|
||||
Chunk<String> target = delta.getTarget();
|
||||
List<SkillVersionCompareLineDTO> lines = new java.util.ArrayList<>();
|
||||
|
||||
int oldLine = source.getPosition() + 1;
|
||||
for (String line : source.getLines()) {
|
||||
lines.add(new SkillVersionCompareLineDTO("DELETE", line, oldLine++, null));
|
||||
}
|
||||
|
||||
int newLine = target.getPosition() + 1;
|
||||
for (String line : target.getLines()) {
|
||||
lines.add(new SkillVersionCompareLineDTO("ADD", line, null, newLine++));
|
||||
}
|
||||
|
||||
return new SkillVersionCompareHunkDTO(
|
||||
source.getPosition() + 1,
|
||||
source.size(),
|
||||
target.getPosition() + 1,
|
||||
target.size(),
|
||||
lines);
|
||||
}
|
||||
|
||||
private List<String> splitLines(String content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.asList(content.split("\\R", -1));
|
||||
}
|
||||
|
||||
private boolean isBinaryFile(String path) {
|
||||
String lowerCasePath = path.toLowerCase(java.util.Locale.ROOT);
|
||||
return BINARY_FILE_EXTENSIONS.stream().anyMatch(lowerCasePath::endsWith);
|
||||
}
|
||||
|
||||
private SkillVersion findVersion(Skill skill, String version) {
|
||||
return skillVersionRepository.findBySkillIdAndVersion(skill.getId(), version)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", version));
|
||||
|
|
|
|||
|
|
@ -466,11 +466,26 @@ export class E2eTestDataBuilder {
|
|||
}
|
||||
|
||||
async approveReview(reviewTaskId: number, comment = 'Approved by Playwright E2E'): Promise<void> {
|
||||
await parseEnvelope<ReviewTaskSummary>(
|
||||
await this.request.post(`/api/web/reviews/${reviewTaskId}/approve`, {
|
||||
data: { comment },
|
||||
}),
|
||||
)
|
||||
let lastError: unknown
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
try {
|
||||
await parseEnvelope<ReviewTaskSummary>(
|
||||
await this.request.post(`/api/web/reviews/${reviewTaskId}/approve`, {
|
||||
data: { comment },
|
||||
}),
|
||||
)
|
||||
return
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
const message = error instanceof Error ? error.message : ''
|
||||
const isScanInProgress = message.includes('扫描') || message.toLowerCase().includes('scan is still in progress')
|
||||
if (!isScanInProgress) {
|
||||
throw error
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
throw lastError instanceof Error ? lastError : new Error('approveReview timed out')
|
||||
}
|
||||
|
||||
async searchNamespaceMemberCandidates(slug: string, search: string): Promise<NamespaceCandidate[]> {
|
||||
|
|
|
|||
74
web/e2e/skill-version-compare.spec.ts
Normal file
74
web/e2e/skill-version-compare.spec.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
import { setEnglishLocale } from './helpers/auth-fixtures'
|
||||
import { loginWithCredentials, registerSession } from './helpers/session'
|
||||
import { E2eTestDataBuilder } from './helpers/test-data-builder'
|
||||
|
||||
function getOptionalEnv(name: string): string | undefined {
|
||||
const value = process.env[name]?.trim()
|
||||
return value ? value : undefined
|
||||
}
|
||||
|
||||
function adminCredentials() {
|
||||
return {
|
||||
username: getOptionalEnv('E2E_ADMIN_USERNAME') ?? getOptionalEnv('BOOTSTRAP_ADMIN_USERNAME') ?? 'admin',
|
||||
password: getOptionalEnv('E2E_ADMIN_PASSWORD') ?? getOptionalEnv('BOOTSTRAP_ADMIN_PASSWORD') ?? 'ChangeMe!2026',
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Skill Version Compare (Real API)', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
await setEnglishLocale(page)
|
||||
await registerSession(page, testInfo)
|
||||
})
|
||||
|
||||
test('opens compare page for two published versions and renders unified diff data', async ({ page, browser }, testInfo) => {
|
||||
const builder = new E2eTestDataBuilder(page, testInfo)
|
||||
await builder.init()
|
||||
|
||||
const adminContext = await browser.newContext()
|
||||
const adminPage = await adminContext.newPage()
|
||||
const adminBuilder = new E2eTestDataBuilder(adminPage, testInfo)
|
||||
await loginWithCredentials(adminPage, adminCredentials(), testInfo)
|
||||
await adminBuilder.init()
|
||||
|
||||
try {
|
||||
const namespace = await builder.ensureWritableNamespace()
|
||||
const skillName = `compare-ui-${Date.now().toString(36)}`
|
||||
const v1 = await builder.publishSkill(namespace.slug, {
|
||||
name: skillName,
|
||||
version: '1.0.0',
|
||||
readmeHeading: `${skillName} v1`,
|
||||
})
|
||||
|
||||
const firstReviewTaskId = await adminBuilder.waitForPendingReview(namespace.slug, v1.slug, v1.version)
|
||||
await adminBuilder.approveReview(firstReviewTaskId)
|
||||
|
||||
const rereleaseResponse = await page.context().request.post(
|
||||
`/api/web/skills/${encodeURIComponent(namespace.slug)}/${encodeURIComponent(v1.slug)}/versions/${encodeURIComponent(v1.version)}/rerelease`,
|
||||
{
|
||||
data: {
|
||||
targetVersion: '1.1.0',
|
||||
confirmWarnings: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(rereleaseResponse.ok()).toBe(true)
|
||||
|
||||
const secondReviewTaskId = await adminBuilder.waitForPendingReview(namespace.slug, v1.slug, '1.1.0')
|
||||
await adminBuilder.approveReview(secondReviewTaskId)
|
||||
|
||||
await page.goto(`/space/${encodeURIComponent(namespace.slug)}/${encodeURIComponent(v1.slug)}/compare?from=1.0.0&to=1.1.0`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Version Compare' })).toBeVisible()
|
||||
await expect(page.getByLabel('Base version')).toBeVisible()
|
||||
await expect(page.getByLabel('Head version')).toBeVisible()
|
||||
await expect(page.getByLabel('File list')).toContainText('SKILL.md')
|
||||
await expect(page.getByText('Modified').first()).toBeVisible()
|
||||
await expect(page.getByText('SKILL.md').first()).toBeVisible()
|
||||
} finally {
|
||||
await adminBuilder.cleanup()
|
||||
await adminContext.close()
|
||||
await builder.cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"jsdom": "^29.1.1",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
|
|
|
|||
406
web/pnpm-lock.yaml
generated
406
web/pnpm-lock.yaml
generated
|
|
@ -87,6 +87,9 @@ importers:
|
|||
'@playwright/test':
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@types/mdast':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
|
|
@ -117,6 +120,9 @@ importers:
|
|||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.5
|
||||
version: 0.4.26(eslint@8.57.1)
|
||||
jsdom:
|
||||
specifier: ^29.1.1
|
||||
version: 29.1.1
|
||||
openapi-typescript:
|
||||
specifier: ^7.6.1
|
||||
version: 7.13.0(typescript@5.9.3)
|
||||
|
|
@ -134,7 +140,7 @@ importers:
|
|||
version: 6.4.1(jiti@1.21.7)
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(jiti@1.21.7)
|
||||
version: 3.2.4(@types/debug@4.1.12)(jiti@1.21.7)(jsdom@29.1.1)
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -142,6 +148,21 @@ packages:
|
|||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@asamuzakjp/css-color@5.1.11':
|
||||
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/dom-selector@7.1.1':
|
||||
resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/generational-cache@1.0.1':
|
||||
resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9':
|
||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -229,6 +250,46 @@ packages:
|
|||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||
hasBin: true
|
||||
|
||||
'@csstools/color-helpers@6.0.2':
|
||||
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@csstools/css-calc@3.2.0':
|
||||
resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-color-parser@4.1.0':
|
||||
resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@4.0.0':
|
||||
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.3':
|
||||
resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==}
|
||||
peerDependencies:
|
||||
css-tree: ^3.2.1
|
||||
peerDependenciesMeta:
|
||||
css-tree:
|
||||
optional: true
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0':
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@emotion/babel-plugin@11.13.5':
|
||||
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
|
||||
|
||||
|
|
@ -447,6 +508,15 @@ packages:
|
|||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@exodus/bytes@1.15.0':
|
||||
resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
'@noble/hashes':
|
||||
optional: true
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||
|
||||
|
|
@ -995,6 +1065,28 @@ packages:
|
|||
'@tanstack/store@0.9.2':
|
||||
resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==}
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@testing-library/react@16.3.2':
|
||||
resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': ^10.0.0
|
||||
'@types/react': ^18.0.0 || ^19.0.0
|
||||
'@types/react-dom': ^18.0.0 || ^19.0.0
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@types/aria-query@5.0.4':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
|
|
@ -1173,6 +1265,10 @@ packages:
|
|||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
|
|
@ -1190,6 +1286,9 @@ packages:
|
|||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
array-union@2.1.0:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1224,6 +1323,9 @@ packages:
|
|||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1339,6 +1441,10 @@ packages:
|
|||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -1347,6 +1453,10 @@ packages:
|
|||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
data-urls@7.0.0:
|
||||
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -1356,6 +1466,9 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||
|
||||
|
|
@ -1394,9 +1507,16 @@ packages:
|
|||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
electron-to-chromium@1.5.307:
|
||||
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
|
||||
|
||||
entities@8.0.0:
|
||||
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
|
||||
|
|
@ -1616,6 +1736,10 @@ packages:
|
|||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
html-encoding-sniffer@6.0.0:
|
||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
|
|
@ -1706,6 +1830,9 @@ packages:
|
|||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
isbot@5.1.36:
|
||||
resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1731,6 +1858,15 @@ packages:
|
|||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@29.1.1:
|
||||
resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -1790,6 +1926,10 @@ packages:
|
|||
lowlight@3.3.0:
|
||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||
|
||||
lru-cache@11.3.6:
|
||||
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
|
|
@ -1798,6 +1938,10 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
|
|
@ -1852,6 +1996,9 @@ packages:
|
|||
mdast-util-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||
|
||||
mdn-data@2.27.1:
|
||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||
|
||||
memoize-one@6.0.0:
|
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||
|
||||
|
|
@ -2032,6 +2179,9 @@ packages:
|
|||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parse5@8.0.1:
|
||||
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2142,6 +2292,10 @@ packages:
|
|||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
|
|
@ -2192,6 +2346,9 @@ packages:
|
|||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-markdown@10.1.0:
|
||||
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -2294,6 +2451,10 @@ packages:
|
|||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
|
|
@ -2394,6 +2555,9 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
tailwind-merge@2.6.1:
|
||||
resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==}
|
||||
|
||||
|
|
@ -2440,10 +2604,25 @@ packages:
|
|||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@7.0.30:
|
||||
resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
|
||||
|
||||
tldts@7.0.30:
|
||||
resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@6.0.0:
|
||||
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
|
|
@ -2479,6 +2658,10 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici@7.25.0:
|
||||
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
|
||||
|
|
@ -2623,6 +2806,22 @@ packages:
|
|||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
webidl-conversions@8.0.1:
|
||||
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
whatwg-mimetype@5.0.0:
|
||||
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
whatwg-url@16.0.1:
|
||||
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
@ -2640,6 +2839,13 @@ packages:
|
|||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
|
|
@ -2683,6 +2889,26 @@ snapshots:
|
|||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@asamuzakjp/css-color@5.1.11':
|
||||
dependencies:
|
||||
'@asamuzakjp/generational-cache': 1.0.1
|
||||
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@asamuzakjp/dom-selector@7.1.1':
|
||||
dependencies:
|
||||
'@asamuzakjp/generational-cache': 1.0.1
|
||||
'@asamuzakjp/nwsapi': 2.3.9
|
||||
bidi-js: 1.0.3
|
||||
css-tree: 3.2.1
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
|
||||
'@asamuzakjp/generational-cache@1.0.1': {}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
|
@ -2797,6 +3023,34 @@ snapshots:
|
|||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
dependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@csstools/color-helpers@6.0.2': {}
|
||||
|
||||
'@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 6.0.2
|
||||
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)':
|
||||
optionalDependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@emotion/babel-plugin@11.13.5':
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.28.6
|
||||
|
|
@ -2972,6 +3226,8 @@ snapshots:
|
|||
|
||||
'@eslint/js@8.57.1': {}
|
||||
|
||||
'@exodus/bytes@1.15.0': {}
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
|
@ -3461,6 +3717,29 @@ snapshots:
|
|||
|
||||
'@tanstack/store@0.9.2': {}
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/runtime': 7.28.6
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
dom-accessibility-api: 0.5.16
|
||||
lz-string: 1.5.0
|
||||
picocolors: 1.1.1
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@testing-library/dom': 10.4.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
|
|
@ -3683,6 +3962,8 @@ snapshots:
|
|||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
|
|
@ -3698,6 +3979,10 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
array-union@2.1.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
|
@ -3725,6 +4010,10 @@ snapshots:
|
|||
|
||||
baseline-browser-mapping@2.10.0: {}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
|
|
@ -3837,16 +4126,30 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
data-urls@7.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
debug@4.4.3(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.2
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
|
@ -3877,8 +4180,12 @@ snapshots:
|
|||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
electron-to-chromium@1.5.307: {}
|
||||
|
||||
entities@8.0.0: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
|
|
@ -4162,6 +4469,12 @@ snapshots:
|
|||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
html-encoding-sniffer@6.0.0:
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.15.0
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
|
@ -4238,6 +4551,8 @@ snapshots:
|
|||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
isbot@5.1.36: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
|
@ -4254,6 +4569,32 @@ snapshots:
|
|||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@29.1.1:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 5.1.11
|
||||
'@asamuzakjp/dom-selector': 7.1.1
|
||||
'@bramus/specificity': 2.4.2
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
|
||||
'@exodus/bytes': 1.15.0
|
||||
css-tree: 3.2.1
|
||||
data-urls: 7.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 6.0.0
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.3.6
|
||||
parse5: 8.0.1
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 6.0.1
|
||||
undici: 7.25.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
|
@ -4301,6 +4642,8 @@ snapshots:
|
|||
devlop: 1.1.0
|
||||
highlight.js: 11.11.1
|
||||
|
||||
lru-cache@11.3.6: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
|
@ -4309,6 +4652,8 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
@ -4479,6 +4824,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
mdn-data@2.27.1: {}
|
||||
|
||||
memoize-one@6.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
|
@ -4782,6 +5129,10 @@ snapshots:
|
|||
index-to-position: 1.2.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parse5@8.0.1:
|
||||
dependencies:
|
||||
entities: 8.0.0
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
|
|
@ -4855,6 +5206,12 @@ snapshots:
|
|||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
|
@ -4906,6 +5263,8 @@ snapshots:
|
|||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
|
@ -5070,6 +5429,10 @@ snapshots:
|
|||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
|
@ -5150,6 +5513,8 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tailwind-merge@2.6.1: {}
|
||||
|
||||
tailwindcss@3.4.19:
|
||||
|
|
@ -5209,10 +5574,24 @@ snapshots:
|
|||
|
||||
tinyspy@4.0.4: {}
|
||||
|
||||
tldts-core@7.0.30: {}
|
||||
|
||||
tldts@7.0.30:
|
||||
dependencies:
|
||||
tldts-core: 7.0.30
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
dependencies:
|
||||
tldts: 7.0.30
|
||||
|
||||
tr46@6.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
|
@ -5235,6 +5614,8 @@ snapshots:
|
|||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici@7.25.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
@ -5349,7 +5730,7 @@ snapshots:
|
|||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(jiti@1.21.7):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(jiti@1.21.7)(jsdom@29.1.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
|
|
@ -5376,6 +5757,7 @@ snapshots:
|
|||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
jsdom: 29.1.1
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
|
|
@ -5392,6 +5774,22 @@ snapshots:
|
|||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
webidl-conversions@8.0.1: {}
|
||||
|
||||
whatwg-mimetype@5.0.0: {}
|
||||
|
||||
whatwg-url@16.0.1:
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.15.0
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
|
@ -5405,6 +5803,10 @@ snapshots:
|
|||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
|
|
|||
388
web/src/api/generated/schema.d.ts
vendored
388
web/src/api/generated/schema.d.ts
vendored
|
|
@ -4,6 +4,38 @@
|
|||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/v1/skills/{skillId}/subscription": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["checkSubscribed"];
|
||||
put: operations["subscribeSkill"];
|
||||
post?: never;
|
||||
delete: operations["unsubscribeSkill"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/skills/{skillId}/subscription": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["checkSubscribed_1"];
|
||||
put: operations["subscribeSkill_1"];
|
||||
post?: never;
|
||||
delete: operations["unsubscribeSkill_1"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/skills/{skillId}/star": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -1764,6 +1796,38 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/skills/{namespace}/{slug}/versions/compare": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["compareVersions"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/versions/compare": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["compareVersions_1"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/versions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -2436,6 +2500,38 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/me/subscriptions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listMySubscriptions"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/me/subscriptions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listMySubscriptions_1"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/me/stars": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -3778,6 +3874,7 @@ export interface components {
|
|||
slug?: string;
|
||||
displayName?: string;
|
||||
summary?: string;
|
||||
visibility?: string;
|
||||
status?: string;
|
||||
/** Format: int64 */
|
||||
downloadCount?: number;
|
||||
|
|
@ -3860,6 +3957,65 @@ export interface components {
|
|||
parsedMetadataJson?: string;
|
||||
manifestJson?: string;
|
||||
};
|
||||
ApiResponseSkillVersionCompareResponse: {
|
||||
/** Format: int32 */
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: components["schemas"]["SkillVersionCompareResponse"];
|
||||
/** Format: date-time */
|
||||
timestamp?: string;
|
||||
requestId?: string;
|
||||
};
|
||||
SkillVersionCompareFileResponse: {
|
||||
path?: string;
|
||||
changeType?: string;
|
||||
/** Format: int64 */
|
||||
oldSize?: number;
|
||||
/** Format: int64 */
|
||||
newSize?: number;
|
||||
binary?: boolean;
|
||||
truncated?: boolean;
|
||||
hunks?: components["schemas"]["SkillVersionCompareHunkResponse"][];
|
||||
};
|
||||
SkillVersionCompareHunkResponse: {
|
||||
/** Format: int32 */
|
||||
oldStart?: number;
|
||||
/** Format: int32 */
|
||||
oldLines?: number;
|
||||
/** Format: int32 */
|
||||
newStart?: number;
|
||||
/** Format: int32 */
|
||||
newLines?: number;
|
||||
lines?: components["schemas"]["SkillVersionCompareLineResponse"][];
|
||||
};
|
||||
SkillVersionCompareLineResponse: {
|
||||
type?: string;
|
||||
content?: string;
|
||||
/** Format: int32 */
|
||||
oldLineNumber?: number;
|
||||
/** Format: int32 */
|
||||
newLineNumber?: number;
|
||||
};
|
||||
SkillVersionCompareResponse: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
summary?: components["schemas"]["SkillVersionCompareSummaryResponse"];
|
||||
files?: components["schemas"]["SkillVersionCompareFileResponse"][];
|
||||
};
|
||||
SkillVersionCompareSummaryResponse: {
|
||||
/** Format: int32 */
|
||||
totalFiles?: number;
|
||||
/** Format: int32 */
|
||||
addedFiles?: number;
|
||||
/** Format: int32 */
|
||||
modifiedFiles?: number;
|
||||
/** Format: int32 */
|
||||
removedFiles?: number;
|
||||
/** Format: int32 */
|
||||
addedLines?: number;
|
||||
/** Format: int32 */
|
||||
removedLines?: number;
|
||||
};
|
||||
ApiResponsePageResponseSkillVersionResponse: {
|
||||
/** Format: int32 */
|
||||
code?: number;
|
||||
|
|
@ -3954,6 +4110,8 @@ export interface components {
|
|||
downloadCount?: number;
|
||||
/** Format: int32 */
|
||||
starCount?: number;
|
||||
/** Format: int32 */
|
||||
subscriptionCount?: number;
|
||||
ratingAvg?: number;
|
||||
/** Format: int32 */
|
||||
ratingCount?: number;
|
||||
|
|
@ -4649,6 +4807,138 @@ export interface components {
|
|||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
checkSubscribed: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseBoolean"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
subscribeSkill: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseVoid"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
unsubscribeSkill: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseVoid"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
checkSubscribed_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseBoolean"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
subscribeSkill_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseVoid"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
unsubscribeSkill_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
skillId: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseVoid"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
checkStarred: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -8047,6 +8337,58 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
compareVersions: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillVersionCompareResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
compareVersions_1: {
|
||||
parameters: {
|
||||
query: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillVersionCompareResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
listVersions: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
|
@ -9077,6 +9419,52 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
listMySubscriptions: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponsePageResponseSkillSummaryResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
listMySubscriptions_1: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponsePageResponseSkillSummaryResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
listMyStars: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
|
|
|||
|
|
@ -274,6 +274,47 @@ export interface SkillFile {
|
|||
sha256: string
|
||||
}
|
||||
|
||||
export interface SkillVersionCompareLine {
|
||||
type: 'CONTEXT' | 'ADD' | 'DELETE' | string
|
||||
content: string
|
||||
oldLineNumber: number | null
|
||||
newLineNumber: number | null
|
||||
}
|
||||
|
||||
export interface SkillVersionCompareHunk {
|
||||
oldStart: number
|
||||
oldLines: number
|
||||
newStart: number
|
||||
newLines: number
|
||||
lines: SkillVersionCompareLine[]
|
||||
}
|
||||
|
||||
export interface SkillVersionCompareFile {
|
||||
path: string
|
||||
changeType: 'ADDED' | 'MODIFIED' | 'REMOVED' | string
|
||||
oldSize: number | null
|
||||
newSize: number | null
|
||||
binary: boolean
|
||||
truncated: boolean
|
||||
hunks: SkillVersionCompareHunk[]
|
||||
}
|
||||
|
||||
export interface SkillVersionCompareSummary {
|
||||
totalFiles: number
|
||||
addedFiles: number
|
||||
modifiedFiles: number
|
||||
removedFiles: number
|
||||
addedLines: number
|
||||
removedLines: number
|
||||
}
|
||||
|
||||
export interface SkillVersionCompare {
|
||||
from: string
|
||||
to: string
|
||||
summary: SkillVersionCompareSummary
|
||||
files: SkillVersionCompareFile[]
|
||||
}
|
||||
|
||||
export interface SkillTag {
|
||||
id: number
|
||||
tagName: string
|
||||
|
|
|
|||
|
|
@ -39,4 +39,10 @@ describe('router', () => {
|
|||
// In test environment, flatRoutes may not be populated until router is used
|
||||
expect(router.routeTree).toBeDefined()
|
||||
})
|
||||
|
||||
it('registers the skill version compare route', () => {
|
||||
const children = (router.routeTree.children ?? []) as Array<{ fullPath?: string; path?: string }>
|
||||
const childPaths = children.map((route) => route.fullPath ?? route.path)
|
||||
expect(childPaths).toContain('/space/$namespace/$slug/compare')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const SearchPage = createLazyRouteComponent(() => import('@/pages/search'), 'Sea
|
|||
const TermsOfServicePage = createLazyRouteComponent(() => import('@/pages/terms'), 'TermsOfServicePage')
|
||||
const NamespacePage = createLazyRouteComponent(() => import('@/pages/namespace'), 'NamespacePage')
|
||||
const SkillDetailPage = createLazyRouteComponent(() => import('@/pages/skill-detail'), 'SkillDetailPage')
|
||||
const SkillVersionComparePage = createLazyRouteComponent(() => import('@/pages/skill-version-compare'), 'SkillVersionComparePage')
|
||||
const DashboardPage = createLazyRouteComponent(() => import('@/pages/dashboard'), 'DashboardPage')
|
||||
const MySkillsPage = createLazyRouteComponent(() => import('@/pages/dashboard/my-skills'), 'MySkillsPage')
|
||||
const PublishPage = createLazyRouteComponent(() => import('@/pages/dashboard/publish'), 'PublishPage')
|
||||
|
|
@ -231,6 +232,16 @@ const skillDetailRoute = createRoute({
|
|||
component: SkillDetailPage,
|
||||
})
|
||||
|
||||
const skillVersionCompareRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/space/$namespace/$slug/compare',
|
||||
validateSearch: (search: Record<string, unknown>): { from: string; to: string } => ({
|
||||
from: typeof search.from === 'string' ? search.from : '',
|
||||
to: typeof search.to === 'string' ? search.to : '',
|
||||
}),
|
||||
component: SkillVersionComparePage,
|
||||
})
|
||||
|
||||
const dashboardRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: 'dashboard',
|
||||
|
|
@ -424,6 +435,7 @@ const routeTree = rootRoute.addChildren([
|
|||
termsRoute,
|
||||
namespaceRoute,
|
||||
skillDetailRoute,
|
||||
skillVersionCompareRoute,
|
||||
dashboardRoute,
|
||||
dashboardSkillsRoute,
|
||||
dashboardPublishRoute,
|
||||
|
|
|
|||
|
|
@ -963,6 +963,27 @@
|
|||
"defaultDescription": "A useful skill"
|
||||
}
|
||||
},
|
||||
"skillCompare": {
|
||||
"pageTitle": "Version Compare",
|
||||
"filesChanged": "{{count}} files changed",
|
||||
"searchFiles": "Search files",
|
||||
"errorLoadingCompare": "Failed to load version comparison",
|
||||
"loadDiff": "Load diff",
|
||||
"loading": "Loading...",
|
||||
"totalFiles": "Files",
|
||||
"addedLines": "Added lines",
|
||||
"removedLines": "Removed lines",
|
||||
"baseVersion": "Base version",
|
||||
"headVersion": "Head version",
|
||||
"fileList": "File list",
|
||||
"binaryFile": "Binary file — cannot display diff",
|
||||
"truncatedFile": "Diff output is truncated",
|
||||
"notEnoughPublishedVersions": "Publish at least two versions before comparing.",
|
||||
"noFilesFound": "No matching files found.",
|
||||
"changeTypeAdded": "Added",
|
||||
"changeTypeModified": "Modified",
|
||||
"changeTypeRemoved": "Removed"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Skill Reports",
|
||||
"subtitle": "Handle user-submitted skill reports",
|
||||
|
|
|
|||
|
|
@ -964,6 +964,27 @@
|
|||
"defaultDescription": "实用技能"
|
||||
}
|
||||
},
|
||||
"skillCompare": {
|
||||
"pageTitle": "版本对比",
|
||||
"filesChanged": "{{count}} 个文件变更",
|
||||
"searchFiles": "搜索文件",
|
||||
"errorLoadingCompare": "加载版本对比失败",
|
||||
"loadDiff": "加载差异",
|
||||
"loading": "加载中...",
|
||||
"totalFiles": "文件数",
|
||||
"addedLines": "新增行",
|
||||
"removedLines": "删除行",
|
||||
"baseVersion": "基础版本",
|
||||
"headVersion": "目标版本",
|
||||
"fileList": "文件列表",
|
||||
"binaryFile": "二进制文件,无法展示差异",
|
||||
"truncatedFile": "差异输出已截断",
|
||||
"notEnoughPublishedVersions": "至少发布两个版本后才能进行对比。",
|
||||
"noFilesFound": "未找到匹配文件。",
|
||||
"changeTypeAdded": "新增",
|
||||
"changeTypeModified": "修改",
|
||||
"changeTypeRemoved": "删除"
|
||||
},
|
||||
"reports": {
|
||||
"title": "技能举报",
|
||||
"subtitle": "处理用户提交的技能举报",
|
||||
|
|
|
|||
|
|
@ -618,8 +618,11 @@ export function SkillDetailPage() {
|
|||
toast.error(t('skillDetail.versionCompareUnavailableTitle'), t('skillDetail.versionCompareUnavailableDescription'))
|
||||
return
|
||||
}
|
||||
setDiffSourceVersion(version)
|
||||
setDiffCompareVersion(compareVersion)
|
||||
navigate({
|
||||
to: '/space/$namespace/$slug/compare',
|
||||
params: { namespace, slug },
|
||||
search: { from: version, to: compareVersion },
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmitPromotion = async () => {
|
||||
|
|
|
|||
145
web/src/pages/skill-version-compare.test.tsx
Normal file
145
web/src/pages/skill-version-compare.test.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/** @vitest-environment jsdom */
|
||||
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const useSkillVersionCompareMock = vi.fn()
|
||||
const useSkillVersionsMock = vi.fn()
|
||||
const navigateMock = vi.fn()
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
useParams: () => ({ namespace: 'global', slug: 'demo-skill' }),
|
||||
useSearch: () => ({ from: '1.0.0', to: '1.1.0' }),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/shared/hooks/use-skill-queries', () => ({
|
||||
useSkillVersionCompare: (...args: unknown[]) => useSkillVersionCompareMock(...args),
|
||||
useSkillVersions: (...args: unknown[]) => useSkillVersionsMock(...args),
|
||||
}))
|
||||
|
||||
import { SkillVersionComparePage } from './skill-version-compare'
|
||||
|
||||
describe('SkillVersionComparePage', () => {
|
||||
beforeEach(() => {
|
||||
navigateMock.mockReset()
|
||||
|
||||
useSkillVersionsMock.mockReturnValue({
|
||||
data: [
|
||||
{ id: 10, version: '1.0.0', status: 'PUBLISHED', changelog: '', fileCount: 1, totalSize: 10, publishedAt: '2026-01-01T00:00:00Z', downloadAvailable: true },
|
||||
{ id: 11, version: '1.1.0', status: 'PUBLISHED', changelog: '', fileCount: 1, totalSize: 12, publishedAt: '2026-01-02T00:00:00Z', downloadAvailable: true },
|
||||
{ id: 12, version: '1.2.0-rc.1', status: 'UPLOADED', changelog: '', fileCount: 2, totalSize: 22, publishedAt: '2026-01-03T00:00:00Z', downloadAvailable: false },
|
||||
],
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
useSkillVersionCompareMock.mockReturnValue({
|
||||
data: {
|
||||
from: '1.0.0',
|
||||
to: '1.1.0',
|
||||
summary: { totalFiles: 2, addedFiles: 1, modifiedFiles: 1, removedFiles: 0, addedLines: 4, removedLines: 2 },
|
||||
files: [
|
||||
{
|
||||
path: 'README.md',
|
||||
changeType: 'MODIFIED',
|
||||
oldSize: 10,
|
||||
newSize: 12,
|
||||
binary: false,
|
||||
truncated: false,
|
||||
hunks: [
|
||||
{
|
||||
oldStart: 1,
|
||||
oldLines: 1,
|
||||
newStart: 1,
|
||||
newLines: 1,
|
||||
lines: [
|
||||
{ type: 'DELETE', content: 'old', oldLineNumber: 1, newLineNumber: null },
|
||||
{ type: 'ADD', content: 'new', oldLineNumber: null, newLineNumber: 1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'src/index.ts',
|
||||
changeType: 'ADDED',
|
||||
oldSize: null,
|
||||
newSize: 50,
|
||||
binary: false,
|
||||
truncated: false,
|
||||
hunks: [
|
||||
{
|
||||
oldStart: 0,
|
||||
oldLines: 0,
|
||||
newStart: 1,
|
||||
newLines: 1,
|
||||
lines: [{ type: 'ADD', content: 'console.log(1)', oldLineNumber: null, newLineNumber: 1 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders compare summary and files', () => {
|
||||
const html = renderToStaticMarkup(<SkillVersionComparePage />)
|
||||
|
||||
expect(html).toContain('skillCompare.totalFiles')
|
||||
expect(html).toContain('+4')
|
||||
expect(html).toContain('-2')
|
||||
expect(html).toContain('README.md')
|
||||
expect(html).toContain('src/index.ts')
|
||||
})
|
||||
|
||||
it('renders search input and version selectors from published versions', () => {
|
||||
const html = renderToStaticMarkup(<SkillVersionComparePage />)
|
||||
|
||||
expect(html).toContain('aria-label="skillCompare.searchFiles"')
|
||||
expect(html).toContain('aria-label="skillCompare.baseVersion"')
|
||||
expect(html).toContain('aria-label="skillCompare.headVersion"')
|
||||
expect(html).toContain('v1.0.0')
|
||||
expect(html).toContain('v1.1.0')
|
||||
expect(html).not.toContain('v1.2.0-rc.1')
|
||||
})
|
||||
|
||||
it('switches active file marker when a file item is clicked', () => {
|
||||
render(<SkillVersionComparePage />)
|
||||
|
||||
const readmeLink = screen.getByRole('link', { name: 'README.md' })
|
||||
const sourceLink = screen.getByRole('link', { name: 'src/index.ts' })
|
||||
|
||||
expect(readmeLink.getAttribute('aria-current')).toBe('true')
|
||||
expect(sourceLink.getAttribute('aria-current')).toBeNull()
|
||||
|
||||
fireEvent.click(sourceLink)
|
||||
|
||||
expect(sourceLink.getAttribute('aria-current')).toBe('true')
|
||||
expect(readmeLink.getAttribute('aria-current')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows an empty state when there are not enough published versions', () => {
|
||||
useSkillVersionsMock.mockReturnValueOnce({
|
||||
data: [
|
||||
{ id: 10, version: '1.0.0', status: 'PUBLISHED', changelog: '', fileCount: 1, totalSize: 10, publishedAt: '2026-01-01T00:00:00Z', downloadAvailable: true },
|
||||
],
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(<SkillVersionComparePage />)
|
||||
|
||||
expect(html).toContain('skillCompare.notEnoughPublishedVersions')
|
||||
})
|
||||
})
|
||||
292
web/src/pages/skill-version-compare.tsx
Normal file
292
web/src/pages/skill-version-compare.tsx
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSkillVersionCompare, useSkillVersions } from '@/shared/hooks/use-skill-queries'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
|
||||
function toDiffSectionId(path: string) {
|
||||
return `diff-${encodeURIComponent(path)}`
|
||||
}
|
||||
|
||||
function getChangeTypeLabel(t: (key: string) => string, changeType: string) {
|
||||
if (changeType === 'ADDED') {
|
||||
return t('skillCompare.changeTypeAdded')
|
||||
}
|
||||
if (changeType === 'REMOVED') {
|
||||
return t('skillCompare.changeTypeRemoved')
|
||||
}
|
||||
return t('skillCompare.changeTypeModified')
|
||||
}
|
||||
|
||||
export function SkillVersionComparePage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { namespace, slug } = useParams({ from: '/space/$namespace/$slug/compare' })
|
||||
const { from, to } = useSearch({ from: '/space/$namespace/$slug/compare' }) as { from: string; to: string }
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [activePath, setActivePath] = useState<string | null>(null)
|
||||
const { data: versions, isLoading: isVersionsLoading } = useSkillVersions(namespace, slug)
|
||||
|
||||
const publishedVersions = useMemo(
|
||||
() => (versions ?? []).filter((version) => version.status === 'PUBLISHED'),
|
||||
[versions]
|
||||
)
|
||||
|
||||
const hasEnoughVersions = publishedVersions.length >= 2
|
||||
const defaultFrom = publishedVersions[1]?.version ?? publishedVersions[0]?.version ?? ''
|
||||
const defaultTo = publishedVersions[0]?.version ?? ''
|
||||
|
||||
const normalizedVersions = useMemo(() => {
|
||||
if (!hasEnoughVersions) {
|
||||
return { from: '', to: '' }
|
||||
}
|
||||
|
||||
const isPublishedVersion = (value: string) => publishedVersions.some((version) => version.version === value)
|
||||
let nextFrom = from && isPublishedVersion(from) ? from : defaultFrom
|
||||
let nextTo = to && isPublishedVersion(to) ? to : defaultTo
|
||||
|
||||
if (nextFrom === nextTo) {
|
||||
const alternative = publishedVersions.find((version) => version.version !== nextFrom)?.version ?? ''
|
||||
if (!from && !to) {
|
||||
nextFrom = defaultFrom
|
||||
nextTo = defaultTo
|
||||
} else if (from && !to) {
|
||||
nextTo = alternative
|
||||
} else if (!from && to) {
|
||||
nextFrom = alternative
|
||||
} else {
|
||||
nextTo = alternative
|
||||
}
|
||||
}
|
||||
|
||||
return { from: nextFrom, to: nextTo }
|
||||
}, [defaultFrom, defaultTo, from, hasEnoughVersions, publishedVersions, to])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasEnoughVersions || !normalizedVersions.from || !normalizedVersions.to) {
|
||||
return
|
||||
}
|
||||
if (from === normalizedVersions.from && to === normalizedVersions.to) {
|
||||
return
|
||||
}
|
||||
navigate({
|
||||
to: '/space/$namespace/$slug/compare',
|
||||
params: { namespace, slug },
|
||||
search: { from: normalizedVersions.from, to: normalizedVersions.to },
|
||||
replace: true,
|
||||
})
|
||||
}, [from, hasEnoughVersions, namespace, navigate, normalizedVersions.from, normalizedVersions.to, slug, to])
|
||||
|
||||
const { data, isLoading: isCompareLoading, error } = useSkillVersionCompare(
|
||||
namespace,
|
||||
slug,
|
||||
normalizedVersions.from,
|
||||
normalizedVersions.to,
|
||||
hasEnoughVersions && !!normalizedVersions.from && !!normalizedVersions.to
|
||||
)
|
||||
|
||||
const filteredFiles = useMemo(() => {
|
||||
const files = data?.files ?? []
|
||||
if (!keyword.trim()) {
|
||||
return files
|
||||
}
|
||||
return files.filter((file) => file.path.toLowerCase().includes(keyword.trim().toLowerCase()))
|
||||
}, [data?.files, keyword])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePath && filteredFiles.length > 0) {
|
||||
setActivePath(filteredFiles[0].path)
|
||||
return
|
||||
}
|
||||
if (activePath && !filteredFiles.some((file) => file.path === activePath)) {
|
||||
setActivePath(filteredFiles[0]?.path ?? null)
|
||||
}
|
||||
}, [activePath, filteredFiles])
|
||||
|
||||
if (isVersionsLoading) {
|
||||
return <div className="py-10 text-sm text-muted-foreground">{t('skillCompare.loading')}</div>
|
||||
}
|
||||
|
||||
if (!hasEnoughVersions) {
|
||||
return <div className="py-10 text-sm text-muted-foreground">{t('skillCompare.notEnoughPublishedVersions')}</div>
|
||||
}
|
||||
|
||||
if (isCompareLoading) {
|
||||
return <div className="py-10 text-sm text-muted-foreground">{t('skillCompare.loading')}</div>
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <div className="py-10 text-sm text-muted-foreground">{t('skillCompare.errorLoadingCompare')}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 px-4 py-6 lg:flex-row">
|
||||
<aside className="w-full shrink-0 space-y-4 rounded-xl border border-border/60 p-4 lg:w-80">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">{t('skillCompare.pageTitle')}</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">v{data.from} → v{data.to}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 rounded-lg border border-border/50 p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{t('skillCompare.totalFiles')}</div>
|
||||
<div className="font-medium text-foreground">{data.summary.totalFiles}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{t('skillCompare.addedLines')}</div>
|
||||
<div className="font-medium text-emerald-600">+{data.summary.addedLines}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{t('skillCompare.removedLines')}</div>
|
||||
<div className="font-medium text-rose-600">-{data.summary.removedLines}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-muted-foreground" htmlFor="compare-from-select">
|
||||
{t('skillCompare.baseVersion')}
|
||||
</label>
|
||||
<Select
|
||||
value={normalizedVersions.from}
|
||||
onValueChange={(value) => {
|
||||
navigate({
|
||||
to: '/space/$namespace/$slug/compare',
|
||||
params: { namespace, slug },
|
||||
search: { from: value, to: normalizedVersions.to },
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="compare-from-select" aria-label={t('skillCompare.baseVersion')}>
|
||||
<SelectValue placeholder={t('skillCompare.baseVersion')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{publishedVersions.map((version) => (
|
||||
<SelectItem key={version.id} value={version.version} disabled={version.version === normalizedVersions.to}>
|
||||
v{version.version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-muted-foreground" htmlFor="compare-to-select">
|
||||
{t('skillCompare.headVersion')}
|
||||
</label>
|
||||
<Select
|
||||
value={normalizedVersions.to}
|
||||
onValueChange={(value) => {
|
||||
navigate({
|
||||
to: '/space/$namespace/$slug/compare',
|
||||
params: { namespace, slug },
|
||||
search: { from: normalizedVersions.from, to: value },
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="compare-to-select" aria-label={t('skillCompare.headVersion')}>
|
||||
<SelectValue placeholder={t('skillCompare.headVersion')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{publishedVersions.map((version) => (
|
||||
<SelectItem key={version.id} value={version.version} disabled={version.version === normalizedVersions.from}>
|
||||
v{version.version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder={t('skillCompare.searchFiles')}
|
||||
aria-label={t('skillCompare.searchFiles')}
|
||||
/>
|
||||
|
||||
<nav className="space-y-1 text-sm" aria-label={t('skillCompare.fileList')}>
|
||||
{filteredFiles.length > 0 ? (
|
||||
filteredFiles.map((file, index) => {
|
||||
const isActive = activePath ? activePath === file.path : index === 0
|
||||
return (
|
||||
<a
|
||||
key={file.path}
|
||||
href={`#${toDiffSectionId(file.path)}`}
|
||||
onClick={() => setActivePath(file.path)}
|
||||
aria-current={isActive ? 'true' : undefined}
|
||||
data-active={isActive ? 'true' : 'false'}
|
||||
className={[
|
||||
'block rounded-md border px-3 py-2 font-mono text-xs',
|
||||
isActive
|
||||
? 'border-primary/50 bg-primary/10 text-foreground'
|
||||
: 'border-border/50 text-muted-foreground hover:text-foreground',
|
||||
].join(' ')}
|
||||
>
|
||||
{file.path}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed border-border/50 px-3 py-2 text-xs text-muted-foreground">
|
||||
{keyword.trim() ? t('skillCompare.noFilesFound') : t('skillCompare.noFilesFound')}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0 flex-1 space-y-6">
|
||||
{filteredFiles.length > 0 ? (
|
||||
filteredFiles.map((file) => (
|
||||
<section key={file.path} id={toDiffSectionId(file.path)} className="rounded-xl border border-border/60 p-4">
|
||||
<header className="mb-3 flex items-center justify-between gap-4">
|
||||
<div className="font-mono text-sm text-foreground">{file.path}</div>
|
||||
<div className="text-xs text-muted-foreground">{getChangeTypeLabel(t, file.changeType)}</div>
|
||||
</header>
|
||||
|
||||
{file.binary ? (
|
||||
<div className="text-sm text-muted-foreground">{t('skillCompare.binaryFile')}</div>
|
||||
) : file.truncated ? (
|
||||
<div className="text-sm text-muted-foreground">{t('skillCompare.truncatedFile')}</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-md border border-border/40 font-mono text-sm">
|
||||
{file.hunks.flatMap((hunk) => hunk.lines).map((line, index) => (
|
||||
<div
|
||||
key={`${file.path}-${index}`}
|
||||
className={[
|
||||
'grid grid-cols-[56px_56px_1fr] gap-0',
|
||||
line.type === 'ADD' ? 'bg-emerald-50 dark:bg-emerald-950/30' : '',
|
||||
line.type === 'DELETE' ? 'bg-rose-50 dark:bg-rose-950/30' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="select-none border-r border-border/30 px-2 py-0.5 text-right text-xs text-muted-foreground">
|
||||
{line.oldLineNumber ?? ''}
|
||||
</span>
|
||||
<span className="select-none border-r border-border/30 px-2 py-0.5 text-right text-xs text-muted-foreground">
|
||||
{line.newLineNumber ?? ''}
|
||||
</span>
|
||||
<span
|
||||
className={[
|
||||
'whitespace-pre overflow-x-auto px-3 py-0.5',
|
||||
line.type === 'ADD' ? 'text-emerald-700 dark:text-emerald-400' : '',
|
||||
line.type === 'DELETE' ? 'text-rose-700 dark:text-rose-400' : '',
|
||||
line.type === 'CONTEXT' ? 'text-foreground' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{line.type === 'ADD' ? '+' : line.type === 'DELETE' ? '-' : ' '}
|
||||
{line.content}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/60 p-8 text-sm text-muted-foreground">
|
||||
{t('skillCompare.noFilesFound')}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import type { SkillSummary, SkillDetail, SkillVersion, SkillVersionDetail, SkillFile, SearchParams, PagedResponse, PublishResult } from '@/api/types'
|
||||
import type { SkillSummary, SkillDetail, SkillVersion, SkillVersionDetail, SkillVersionCompare, SkillFile, SearchParams, PagedResponse, PublishResult } from '@/api/types'
|
||||
import { fetchJson, fetchText, getCsrfHeaders, skillLifecycleApi, WEB_API_PREFIX } from '@/api/client'
|
||||
import { clearDeletedSkillQueries } from '@/features/skill/skill-delete-flow'
|
||||
import { getSkillDetailQueryKey } from './query-keys'
|
||||
|
|
@ -32,6 +32,13 @@ async function getSkillVersionDetail(namespace: string, slug: string, version: s
|
|||
return fetchJson<SkillVersionDetail>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`)
|
||||
}
|
||||
|
||||
async function getSkillVersionCompare(namespace: string, slug: string, from: string, to: string): Promise<SkillVersionCompare> {
|
||||
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
|
||||
return fetchJson<SkillVersionCompare>(
|
||||
`${WEB_API_PREFIX}/skills/${cleanNamespace}/${encodeURIComponent(slug)}/versions/compare?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`
|
||||
)
|
||||
}
|
||||
|
||||
async function getSkillDocumentation(namespace: string, slug: string, version: string, path: string): Promise<string> {
|
||||
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
|
||||
return fetchText(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/file?path=${encodeURIComponent(path)}`)
|
||||
|
|
@ -116,6 +123,14 @@ export function useSkillVersionDetail(namespace: string, slug: string, version?:
|
|||
})
|
||||
}
|
||||
|
||||
export function useSkillVersionCompare(namespace: string, slug: string, from?: string, to?: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: ['skills', namespace, slug, 'versions', 'compare', from, to],
|
||||
queryFn: () => getSkillVersionCompare(namespace, slug, from!, to!),
|
||||
enabled: enabled && !!namespace && !!slug && !!from && !!to,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePublishSkill() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue