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:
committed by
GitHub
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
RMCampos
rmcampos
parent
bec73e04dd
commit
553eb3c59d
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user