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:
@@ -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>
|
||||
|
||||
33
src/utils/templateList.test.ts
Normal file
33
src/utils/templateList.test.ts
Normal 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
11
src/utils/templateList.ts
Normal 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());
|
||||
};
|
||||
Reference in New Issue
Block a user