feat: Add 'Copy Markdown' button (#13083)

This pull request adds a "Copy Markdown" button to issue comments.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13083
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Dylan Hackworth 2026-06-25 15:40:50 +02:00 committed by Gusted
commit 8ca338754d
4 changed files with 50 additions and 2 deletions

View file

@ -62,6 +62,7 @@
"repo.issues.filter_mention.hint": "Filter by mentioned user",
"repo.issues.filter_modified.hint": "Filter by last modified date",
"repo.issues.filter_sort.hint_with_placeholder": "Sort by: %s",
"repo.issues.context.copy_markdown": "Copy Markdown",
"issues.updated": "updated %s",
"issues.filters.labels.exclude": "Exclude label",
"issues.filters.labels.unexclude": "Clear exclusion",

View file

@ -10,6 +10,9 @@
{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
{{end}}
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
{{if .item.Content}}
<div class="item context js-aria-clickable" data-clipboard-target="#{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.copy_markdown"}}</div>
{{end}}
{{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}}
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-content" data-author="{{.item.Poster.Name}}" data-reference-url="{{$referenceUrl}}" data-context="{{.ctxData.Repository.Link}}">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
{{if not .ctxData.UnitIssuesGlobalDisabled}}

View file

@ -4,6 +4,7 @@
// @watch start
// templates/repo/home.tmpl
// templates/repo/diff/box.tmpl
// templates/repo/issue/view_content/context_menu.tmpl
// web_src/js/features/clipboard.js
// @watch end
@ -26,6 +27,47 @@ test('copy src file path to clipboard', async ({page}) => {
await screenshot(page, page.getByText('Copied'), 50);
});
test('copy issue content to clipboard', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
await page.click('#issue-1 .comment-container .context-menu');
await page.locator('#issue-1 .comment-container .menu').getByText('Copy Markdown').click();
await expect(async () => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toBe('content for the first issue');
}).toPass();
});
test('copy comment content copies the original markdown', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
await page.click('#issuecomment-1001 .comment-container .context-menu');
await page.locator('#issuecomment-1001 .comment-container .menu').getByText('Copy Markdown').click();
await expect(async () => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('## Lorem Ipsum');
expect(clipboardText).toContain('**I am not appealed**');
expect(clipboardText).toContain('`feature`');
}).toPass();
});
test('copy pull request content to clipboard', async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/5');
expect(response?.status()).toBe(200);
await page.click('#issue-11 .comment-container .context-menu');
await page.locator('#issue-11 .comment-container .menu').getByText('Copy Markdown').click();
await expect(async () => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toBe('content for the a pull request');
}).toPass();
});
test('copy diff file path to clipboard', async ({page}) => {
const response = await page.goto('/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/README.md');
expect(response?.status()).toBe(200);

View file

@ -6,7 +6,8 @@ const {copy_success, copy_error} = window.config.i18n;
// Enable clipboard copy from HTML attributes. These properties are supported:
// - data-clipboard-text: Direct text to copy
// - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
// - data-clipboard-target: Holds a selector for an element whose content is copied. For
// <input> and <textarea> the value is copied, for any other element its text content.
// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
export function initGlobalCopyToClipboardListener() {
document.addEventListener('click', async (e) => {
@ -17,7 +18,8 @@ export function initGlobalCopyToClipboardListener() {
let text = target.getAttribute('data-clipboard-text');
if (!text) {
text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
const source = document.querySelector(target.getAttribute('data-clipboard-target'));
text = source?.value ?? source?.textContent;
}
if (text && target.getAttribute('data-clipboard-text-type') === 'url') {