Add Source and Copy buttons to note markdown modal (#21)

* Initial plan

* Add Source and Copy buttons to ModalMarkdown for note source code view

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* feat: add healthcheck do native image

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>
Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
This commit is contained in:
Copilot
2026-02-27 19:13:53 -03:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> RMCampos rmcampos
parent bec73e04dd
commit 553eb3c59d
14 changed files with 243 additions and 17 deletions
@@ -1,6 +1,6 @@
import React from 'react';
import { describe, vi, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, vi, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import ModalMarkdown from '../../components/ModalMarkdown';
describe('ModalMarkdown Component', () => {
@@ -11,6 +11,14 @@ describe('ModalMarkdown Component', () => {
onHide: vi.fn(),
};
beforeEach(() => {
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});
});
it('should render the modal with the correct title and markdown text', () => {
render(<ModalMarkdown {...props} />);
@@ -36,4 +44,64 @@ describe('ModalMarkdown Component', () => {
expect(screen.getByTestId('modal-header-title').innerHTML).toBe('No title');
});
it('should show Source and Copy buttons', () => {
render(<ModalMarkdown {...props} />);
expect(screen.getByTestId('modal-source-button')).toBeDefined();
expect(screen.getByTestId('modal-copy-button')).toBeDefined();
});
it('should toggle to source view when Source button is clicked', () => {
render(<ModalMarkdown {...props} />);
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
expect(screen.getByTestId('markdown-source-view').textContent).toBe('# Test Markdown');
});
it('should toggle back to rendered view when Source button is clicked again', () => {
render(<ModalMarkdown {...props} />);
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
});
it('should call clipboard writeText with markdownText when Copy button is clicked', async () => {
render(<ModalMarkdown {...props} />);
await act(async () => {
fireEvent.click(screen.getByTestId('modal-copy-button'));
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('# Test Markdown');
});
it('should show "Copied!" text after Copy button is clicked', async () => {
render(<ModalMarkdown {...props} />);
await act(async () => {
fireEvent.click(screen.getByTestId('modal-copy-button'));
});
await waitFor(() => {
expect(screen.getByTestId('modal-copy-button').textContent).toBe('Copied!');
});
});
it('should reset source view state when modal is closed', () => {
render(<ModalMarkdown {...props} />);
fireEvent.click(screen.getByTestId('modal-source-button'));
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
fireEvent.click(screen.getByText('Close'));
expect(props.onHide).toHaveBeenCalled();
});
});
+47 -5
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';
import Markdown from 'react-markdown';
@@ -23,11 +23,31 @@ type Props = {
* @returns {React.ReactNode} the Markdown component rendered.
*/
const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
const [showSource, setShowSource] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const handleToggleSource = () => setShowSource(prev => !prev);
const handleCopy = () => {
navigator.clipboard.writeText(props.markdownText).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(() => {
// clipboard write failed silently; no state change
});
};
const handleHide = () => {
setShowSource(false);
setCopied(false);
props.onHide();
};
return props.show
? (
<Modal
show={props.show}
onHide={props.onHide}
onHide={handleHide}
backdrop="static"
keyboard={false}
size="xl"
@@ -42,12 +62,34 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
</Modal.Title>
</Modal.Header>
<Modal.Body className="markdown-modal">
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
{showSource
? (
<pre className="markdown-source" data-testid="markdown-source-view">
{props.markdownText}
</pre>
)
: (
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="outline-secondary" onClick={props.onHide}>
<Modal.Footer className="d-flex flex-wrap gap-2 justify-content-end">
<Button variant="outline-secondary" onClick={handleHide}>
Close
</Button>
<Button
variant={showSource ? 'info' : 'outline-info'}
onClick={handleToggleSource}
data-testid="modal-source-button"
>
Source
</Button>
<Button
variant="outline-primary"
onClick={handleCopy}
data-testid="modal-copy-button"
>
{copied ? 'Copied!' : 'Copy'}
</Button>
</Modal.Footer>
</Modal>
)
@@ -154,3 +154,24 @@
text-decoration: line-through;
}
}
.markdown-source {
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: 13px;
line-height: 1.6;
padding: 1em;
background-color: var(--bs-tertiary-bg);
border-radius: 6px;
overflow: auto;
max-height: 60vh;
}
@media (max-width: 576px) {
.markdown-source {
font-size: 12px;
padding: 0.75em;
max-height: 50vh;
}
}