Fix new template disappearing after save

- Update TemplateManage save flows to use the current in-memory template list instead of stale localStorage as the source of truth.

- Merge current templates back into the compatibility cache so newly created backend templates are not dropped on content save.

- Add an accessible label for the new-template button to support reliable E2E coverage.

- Add template list merge unit tests covering stale-cache replacement and newly created templates.

- Add Playwright coverage for creating a template, saving template content, and confirming it remains visible and persisted.

- Update feature, progress, testing, and AGENTS documentation for the template management fix.
This commit is contained in:
2026-05-02 03:42:41 +08:00
parent b346b7e194
commit d61ee4f83a
8 changed files with 96 additions and 20 deletions

View File

@@ -11,6 +11,7 @@ import { createTemplate, deleteTemplateFromApi, listTemplates, updateTemplate }
import { getFieldLibrary, updateFieldLibrary } from '../api/library';
import { deleteFileResource, listFiles, uploadFileResource } from '../api/files';
import { isLocalFallbackEnabled } from '../config/runtime';
import { mergeTemplatesById } from '../utils/templateList';
export default function TemplateManage() {
const navigate = useNavigate();
@@ -475,13 +476,13 @@ export default function TemplateManage() {
if (cleanContent !== editorRef.current.innerHTML) {
editorRef.current.innerHTML = cleanContent;
}
const allTemplates = storage.get<Template[]>('templates', []);
const updated = allTemplates.map(t =>
const cachedTemplates = storage.get<Template[]>('templates', []);
const updatedTemplates = templates.map(t =>
t.id === currentTemplateId ? { ...t, content: cleanContent, updatedAt: new Date().toISOString() } : t
);
const currentUpdated = updated.find(t => t.id === currentTemplateId);
setTemplates(prevTemplates => prevTemplates.map(t => updated.find(u => u.id === t.id) || t));
storage.set('templates', updated);
const currentUpdated = updatedTemplates.find(t => t.id === currentTemplateId);
setTemplates(updatedTemplates);
storage.set('templates', mergeTemplatesById(cachedTemplates, updatedTemplates));
if (currentUpdated) {
void updateTemplate(currentTemplateId, currentUpdated).catch(() => {});
}
@@ -680,16 +681,16 @@ export default function TemplateManage() {
if (cleanContent !== editorRef.current.innerHTML) {
editorRef.current.innerHTML = cleanContent;
}
const allTemplates = storage.get<Template[]>('templates', []);
const updated = allTemplates.map(t => {
const cachedTemplates = storage.get<Template[]>('templates', []);
const updatedTemplates = templates.map(t => {
if (t.id === currentTemplateId) {
return { ...t, content: cleanContent, updatedAt: new Date().toISOString() };
}
return t;
});
const currentUpdated = updated.find(t => t.id === currentTemplateId);
setTemplates(updated.filter(t => templates.some(x => x.id === t.id)));
storage.set('templates', updated);
const currentUpdated = updatedTemplates.find(t => t.id === currentTemplateId);
setTemplates(updatedTemplates);
storage.set('templates', mergeTemplatesById(cachedTemplates, updatedTemplates));
if (currentUpdated) {
void updateTemplate(currentTemplateId, currentUpdated).catch(() => {});
}
@@ -818,10 +819,10 @@ export default function TemplateManage() {
const handleModalSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const allTemplates = storage.get<Template[]>('templates', []);
const cachedTemplates = storage.get<Template[]>('templates', []);
if (isEditing) {
let editedTemplate: Template | undefined;
let updated = allTemplates.map(t => {
let updatedTemplates = templates.map(t => {
if (t.id === currentTemplateId) {
editedTemplate = { ...t, name: formData.name, desc: formData.desc };
return editedTemplate;
@@ -831,7 +832,7 @@ export default function TemplateManage() {
if (editedTemplate && currentTemplateId) {
try {
const apiTemplate = await updateTemplate(currentTemplateId, editedTemplate);
updated = updated.map(t => t.id === currentTemplateId ? apiTemplate : t);
updatedTemplates = updatedTemplates.map(t => t.id === currentTemplateId ? apiTemplate : t);
} catch {
if (!isLocalFallbackEnabled()) {
alert('保存模板失败:后端服务不可用');
@@ -839,8 +840,8 @@ export default function TemplateManage() {
}
}
}
setTemplates(updated.filter(t => templates.some(x => x.id === t.id)));
if (isLocalFallbackEnabled()) storage.set('templates', updated);
setTemplates(updatedTemplates);
storage.set('templates', mergeTemplatesById(cachedTemplates, updatedTemplates));
} else {
let newTpl: Template = {
id: 'tpl_' + Date.now(),
@@ -861,9 +862,9 @@ export default function TemplateManage() {
return;
}
}
const updated = [...allTemplates, newTpl];
setTemplates([...templates, newTpl]);
if (isLocalFallbackEnabled()) storage.set('templates', updated);
const updatedTemplates = [...templates, newTpl];
setTemplates(updatedTemplates);
storage.set('templates', mergeTemplatesById(cachedTemplates, updatedTemplates));
setCurrentTemplateId(newTpl.id);
if (importedContent?.fields && importedContent.fields.length > 0) {
setFormFields(importedContent.fields);
@@ -927,6 +928,8 @@ export default function TemplateManage() {
<button
onClick={handleAddTemplate}
className="w-8 h-8 bg-accent text-white rounded-lg flex items-center justify-center hover:bg-blue-700 transition-colors shadow-sm"
title="新增模板"
aria-label="新增模板"
>
<Plus size={16} />
</button>

View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { Template } from '../types';
import { mergeTemplatesById } from './templateList';
const template = (id: string, name: string): Template => ({
id,
name,
desc: '',
content: `<p>${name}</p>`,
createdAt: '2026-05-02T00:00:00.000Z',
author: 'admin',
scope: 'department',
department: '外科',
});
describe('templateList utilities', () => {
it('keeps newly created templates when merging with stale cached templates', () => {
const cached = [template('tpl_old', '旧模板')];
const current = [template('tpl_old', '旧模板'), template('tpl_new', '新模板')];
expect(mergeTemplatesById(cached, current).map((item) => item.id)).toEqual([
'tpl_old',
'tpl_new',
]);
});
it('lets newer template state replace stale cache entries', () => {
const cached = [template('tpl_old', '旧名称')];
const current = [template('tpl_old', '新名称')];
expect(mergeTemplatesById(cached, current)[0].name).toBe('新名称');
});
});

11
src/utils/templateList.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Template } from '../types';
export const mergeTemplatesById = (...templateGroups: Template[][]) => {
const merged = new Map<string, Template>();
for (const group of templateGroups) {
for (const template of group) {
merged.set(template.id, template);
}
}
return Array.from(merged.values());
};