Skip to content

Commit 66dcc04

Browse files
authored
Merge pull request #27 from wellmaintained/bugfix/prevent-loss-of-newlines
bugfix/prevent-loss-of-newlines
2 parents fc3731d + 8883c2d commit 66dcc04

File tree

3 files changed

+98
-10
lines changed

3 files changed

+98
-10
lines changed

components/tiptap-editor.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,31 @@ export function TipTapEditor({ content, onChange, className = '', minimal = fals
4343
},
4444
heading: {
4545
levels: [1, 2, 3]
46+
},
47+
hardBreak: {
48+
keepMarks: true,
49+
HTMLAttributes: {
50+
class: 'my-2'
51+
}
52+
},
53+
paragraph: {
54+
HTMLAttributes: {
55+
class: 'mb-2'
56+
}
4657
}
4758
}),
4859
Markdown.configure({
4960
html: false,
5061
transformPastedText: true,
51-
transformCopiedText: true
62+
transformCopiedText: true,
63+
breaks: true
5264
})
5365
],
5466
content: rawMarkdown,
5567
onUpdate: ({ editor }) => {
56-
// Use the markdown extension to get markdown content
5768
const markdown = editor.storage.markdown.getMarkdown();
69+
console.log('markdown content:', markdown);
70+
5871
setRawMarkdown(markdown);
5972
onChange(markdown);
6073
},
@@ -102,9 +115,8 @@ export function TipTapEditor({ content, onChange, className = '', minimal = fals
102115
// Toggle between rich text and raw markdown modes
103116
const toggleEditMode = () => {
104117
if (isRawMode && editor) {
105-
// When switching from raw to rich, set the markdown content
106118
editor.commands.clearContent();
107-
editor.commands.insertContent(rawMarkdown);
119+
editor.commands.setContent(rawMarkdown);
108120
}
109121
setIsRawMode(!isRawMode);
110122
};
@@ -211,7 +223,7 @@ export function TipTapEditor({ content, onChange, className = '', minimal = fals
211223

212224
{isRawMode ? (
213225
<Textarea
214-
value={rawMarkdown}
226+
value={rawMarkdown || ''}
215227
onChange={handleRawMarkdownChange}
216228
className={`resize-none border-none rounded-none focus-visible:ring-0 p-4 font-mono text-sm ${
217229
minimal ? 'min-h-0' : 'min-h-[200px]'

components/tiptap-view.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,27 @@ export function TipTapView({ content, className = '' }: TipTapViewProps) {
2121
},
2222
heading: {
2323
levels: [1, 2, 3]
24+
},
25+
hardBreak: {
26+
keepMarks: true,
27+
HTMLAttributes: {
28+
class: 'my-2'
29+
}
30+
},
31+
paragraph: {
32+
HTMLAttributes: {
33+
class: 'mb-2'
34+
}
2435
}
2536
}),
2637
Markdown.configure({
2738
html: false,
2839
transformPastedText: true,
29-
transformCopiedText: true
40+
transformCopiedText: true,
41+
breaks: true
3042
})
3143
],
32-
content,
44+
content: typeof content === 'string' ? (content.startsWith('"') ? JSON.parse(content) : content) : '',
3345
editable: false,
3446
editorProps: {
3547
attributes: {
@@ -41,9 +53,15 @@ export function TipTapView({ content, className = '' }: TipTapViewProps) {
4153
// Update editor content when content prop changes
4254
React.useEffect(() => {
4355
if (editor && content !== undefined) {
44-
// Only update if content actually changed
45-
if (editor.storage.markdown.getMarkdown() !== content) {
46-
editor.commands.setContent(content);
56+
try {
57+
const parsedContent = content.startsWith('"') ? JSON.parse(content) : content;
58+
// Only update if content actually changed
59+
if (editor.storage.markdown.getMarkdown() !== parsedContent) {
60+
editor.commands.setContent(parsedContent);
61+
}
62+
} catch (e) {
63+
console.error('Error parsing content:', e);
64+
editor.commands.setContent('');
4765
}
4866
}
4967
}, [editor, content]);

lib/infrastructure/__tests__/firestoreDecisionsRepository.integration.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,64 @@ describe('FirestoreDecisionsRepository Integration Tests', () => {
403403
}, 20000);
404404
});
405405

406+
describe('newline preservation', () => {
407+
it('should preserve empty lines in markdown content', async () => {
408+
const testName = 'newline-preservation';
409+
const emptyDecision = Decision.createEmptyDecision();
410+
const randomChars = Math.random().toString(36).substring(5, 9);
411+
const projectId = `test-${testName}-${randomChars}`;
412+
const teamId = `team-${testName}-${randomChars}`;
413+
414+
const project = await projectRepository.create(Project.create({
415+
id: projectId,
416+
name: `Test Project ${testName}`,
417+
description: `Temporary project for integration tests ${testName}`,
418+
organisationId: BASE_TEST_SCOPE.organisationId,
419+
}));
420+
421+
projectsToCleanUp.push(project);
422+
423+
const decisionScope = {
424+
organisationId: project.organisationId,
425+
}
426+
427+
// Create a decision with markdown content containing multiple empty lines
428+
const markdownWithEmptyLines = `# Title
429+
430+
This is a paragraph.
431+
432+
433+
This is another paragraph after two empty lines.
434+
435+
* List item 1
436+
437+
* List item 2 after an empty line`;
438+
439+
const decision = await repository.create(
440+
emptyDecision
441+
.with({
442+
title: 'Decision with empty lines',
443+
description: markdownWithEmptyLines,
444+
teamIds: [teamId],
445+
projectIds: [project.id]
446+
})
447+
.withoutId(),
448+
decisionScope
449+
);
450+
451+
decisionsToCleanUp.push(decision);
452+
453+
// Retrieve the decision and check if empty lines are preserved
454+
const retrievedDecision = await repository.getById(decision.id, decisionScope);
455+
456+
expect(retrievedDecision.description).toBe(markdownWithEmptyLines);
457+
458+
// Specifically check for the double newlines
459+
expect(retrievedDecision.description).toContain('paragraph.\n\n\nThis');
460+
expect(retrievedDecision.description).toContain('item 1\n\n* List');
461+
});
462+
});
463+
406464
describe('Relationship Management', () => {
407465
it('should add and remove relationships correctly', async () => {
408466
// eslint-disable-next-line @typescript-eslint/no-unused-vars

0 commit comments

Comments
 (0)