mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): avoid reusing unrelated html artifact groups
This commit is contained in:
parent
361c18a414
commit
4ecdb4a15c
2 changed files with 89 additions and 20 deletions
|
|
@ -1144,22 +1144,26 @@ export function ProjectView({
|
|||
// keeping index.html stable for multi-page HTML artifacts.
|
||||
const currentProjectFiles = projectFilesSnapshot ?? projectFilesRef.current;
|
||||
const existing = new Set(currentProjectFiles.map((f) => f.name));
|
||||
const canOverwriteExistingEntry = canOverwriteHtmlArtifactEntry({
|
||||
baseName,
|
||||
ext,
|
||||
projectFiles: currentProjectFiles,
|
||||
savedArtifactName: savedArtifactRef.current,
|
||||
artifactIdentifier: art.identifier,
|
||||
});
|
||||
const fileName = resolveHtmlArtifactFileName({
|
||||
baseName,
|
||||
ext,
|
||||
existingFileNames: existing,
|
||||
savedArtifactName: savedArtifactRef.current,
|
||||
canOverwriteExistingEntry: canOverwriteHtmlArtifactEntry({
|
||||
baseName,
|
||||
ext,
|
||||
projectFiles: currentProjectFiles,
|
||||
savedArtifactName: savedArtifactRef.current,
|
||||
artifactIdentifier: art.identifier,
|
||||
}),
|
||||
canOverwriteExistingEntry,
|
||||
});
|
||||
const artifactGroupIdentifier =
|
||||
ext === '.html'
|
||||
? htmlArtifactGroupIdentifierFor(art, currentProjectFiles, fileName)
|
||||
? htmlArtifactGroupIdentifierFor(art, currentProjectFiles, fileName, {
|
||||
canReuseExistingGroup:
|
||||
canOverwriteExistingEntry || savedArtifactRef.current === fileName,
|
||||
})
|
||||
: undefined;
|
||||
const html =
|
||||
ext === '.html'
|
||||
|
|
@ -4640,23 +4644,40 @@ function htmlArtifactGroupIdentifierFor(
|
|||
art: Artifact,
|
||||
projectFiles: ProjectFile[],
|
||||
fileName: string,
|
||||
options: { canReuseExistingGroup: boolean },
|
||||
): string {
|
||||
const identifier = normalizeArtifactGroupSegment(art.identifier);
|
||||
if (identifier) {
|
||||
const existingGroup = projectFiles
|
||||
.filter((file) => {
|
||||
return normalizeArtifactGroupSegment(file.artifactManifest?.metadata?.identifier) === identifier;
|
||||
})
|
||||
.sort((a, b) => b.mtime - a.mtime)
|
||||
.map((file) => file.artifactManifest?.metadata?.artifactGroupIdentifier)
|
||||
.find((value): value is string => {
|
||||
return typeof value === 'string' && normalizeArtifactGroupSegment(value).length > 0;
|
||||
});
|
||||
if (existingGroup) return existingGroup;
|
||||
const existingFile = projectFiles.find((file) => file.name === fileName || file.path === fileName);
|
||||
const existingFileGroup = existingArtifactGroupIdentifier(
|
||||
existingFile?.artifactManifest?.metadata?.artifactGroupIdentifier,
|
||||
);
|
||||
if (options.canReuseExistingGroup && existingFileGroup) {
|
||||
return existingFileGroup;
|
||||
}
|
||||
|
||||
if (options.canReuseExistingGroup) {
|
||||
const identifier = normalizeArtifactGroupSegment(art.identifier);
|
||||
if (identifier) {
|
||||
const existingGroup = projectFiles
|
||||
.filter((file) => {
|
||||
return normalizeArtifactGroupSegment(file.artifactManifest?.metadata?.identifier) === identifier;
|
||||
})
|
||||
.sort((a, b) => b.mtime - a.mtime)
|
||||
.map((file) => file.artifactManifest?.metadata?.artifactGroupIdentifier)
|
||||
.find((value): value is string => {
|
||||
return typeof value === 'string' && normalizeArtifactGroupSegment(value).length > 0;
|
||||
});
|
||||
if (existingGroup) return existingGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return `html-artifact:${fileName}`;
|
||||
}
|
||||
|
||||
function existingArtifactGroupIdentifier(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return normalizeArtifactGroupSegment(value).length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function normalizeArtifactGroupSegment(value: unknown): string {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -643,6 +643,54 @@ describe('ProjectView API empty response handling', () => {
|
|||
expect(content).toContain('href="about-2.html"');
|
||||
});
|
||||
|
||||
it('starts a fresh group when a new index artifact is relocated away from an existing owner', async () => {
|
||||
mockedFetchProjectFiles.mockResolvedValue([
|
||||
htmlProjectFile('index.html', 10, {
|
||||
artifactIdentifier: 'Index',
|
||||
artifactGroupIdentifier: 'site-a',
|
||||
}),
|
||||
htmlProjectFile('about.html', 20, {
|
||||
artifactIdentifier: 'about',
|
||||
artifactGroupIdentifier: 'site-a',
|
||||
}),
|
||||
htmlProjectFile('about-2.html', 40, {
|
||||
artifactIdentifier: 'about',
|
||||
artifactGroupIdentifier: 'site-a',
|
||||
}),
|
||||
] as never);
|
||||
mockedWriteProjectTextFile.mockResolvedValue(htmlProjectFile('index-2.html', 50) as never);
|
||||
const artifact =
|
||||
'<artifact identifier="index" type="text/html" title="Multipage Site">' +
|
||||
'<!doctype html><html><head><title>Home</title></head><body>' +
|
||||
'<main><h1>Home</h1><a href="about.html">About</a></main>' +
|
||||
'</body></html>' +
|
||||
'</artifact>';
|
||||
mockedStreamMessage.mockImplementation(async (
|
||||
_cfg: AppConfig,
|
||||
_system: string,
|
||||
_history: ChatMessage[],
|
||||
_signal: AbortSignal,
|
||||
handlers: StreamHandlers,
|
||||
) => {
|
||||
handlers.onDelta(artifact);
|
||||
handlers.onDone('');
|
||||
});
|
||||
renderProjectView();
|
||||
|
||||
await sendTestPrompt();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedWriteProjectTextFile).toHaveBeenCalled();
|
||||
});
|
||||
const [, fileName, content, options] = mockedWriteProjectTextFile.mock.calls.at(-1) ?? [];
|
||||
expect(fileName).toBe('index-2.html');
|
||||
expect(content).toContain('href="about.html"');
|
||||
expect(content).not.toContain('href="about-2.html"');
|
||||
expect(options?.artifactManifest?.metadata?.artifactGroupIdentifier).toBe(
|
||||
'html-artifact:index-2.html',
|
||||
);
|
||||
});
|
||||
|
||||
it('rewrites child links for legacy multipage artifacts without group metadata', async () => {
|
||||
mockedFetchProjectFiles.mockResolvedValue([
|
||||
htmlProjectFile('index.html', 10, { artifactIdentifier: 'index' }),
|
||||
|
|
|
|||
Loading…
Reference in a new issue