feat: add plugin to copy issues as Cacoo cards#75
Conversation
Add copyIssueCacooFormat plugin that copies Backlog issues to clipboard in Cacoo card format. Supports both issue detail pages and issue list pages with per-row copy buttons using the Cacoo logo icon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use CSS transform to position the detail page button relative to the existing copy-key button. Add viewBox padding to the Cacoo logo SVG for better visual spacing within the button. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move issue key button injection logic from plugin-specific code into a reusable utility at utils/inject-issue-key-button.ts with its own CSS for positioning. Supports up to 5 buttons with deduplication by key. Remove the now-unnecessary plugin CSS module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the Cacoo copy button from beside the issue link to the cell-hover-actions container so it appears alongside the existing action buttons. Add custom styling (transparent bg, currentColor border, 20×20 size) and block mousedown propagation to prevent parent anchor navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ation Extract StatusBar access into a generic main-world bridge pattern so content scripts in isolated world can call page-context APIs via CustomEvent dispatch. The bridge entrypoint runs in world: MAIN and the utility provides typed dispatchMainWorld() for extensibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… utility Replace separate detail/list injection logic with a single injectIssueButton<T> arrow function that observes both #copyKey-help and .cell-hover-actions, accepting typed options for SVG, tooltip, and click handlers. Simplifies the Cacoo plugin to a single call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Revert the over-abstracted injectIssueButton utility back to direct observeQuerySelector calls in the plugin. Merge button positioning CSS from utils/ into the plugin's index.module.css and remove the now-unused utils/inject-issue-key-button.css. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new “Copy issue as Cacoo card” feature to the Backlog Power Ups extension, enabling users to copy Backlog issues into the clipboard in Cacoo’s cacoo/shape format (with a plain-text fallback), including UI injection on issue detail and list/search pages.
Changes:
- Introduces
copyIssueCacooFormatplugin with buttons on issue detail (#copyKey-help) and issue list rows (.cell-hover-actions) to copy Cacoo-shape JSON + plain text. - Adds a MAIN-world content-script bridge (CustomEvent-based) to display StatusBar feedback despite isolated-world restrictions.
- Adds
injectIssueKeyButtonutility + CSS stacking rules for positioning multiple buttons next to the issue key.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/main-world-bridge.ts | Dispatch helper for isolated-world → MAIN-world actions (StatusBar messaging). |
| entrypoints/main-world-bridge.content/index.ts | MAIN-world listener that calls window.Backlog.StatusBar APIs. |
| utils/inject-issue-key-button.ts | Utility for inserting keyed buttons next to #copyKey-help with dedupe/limit logic. |
| plugins/index.ts | Exports the new copyIssueCacooFormat plugin. |
| plugins/copyIssueCacooFormat/index.ts | Injects UI buttons and wires click handlers to extract issue data and copy to clipboard. |
| plugins/copyIssueCacooFormat/index.module.css | Styles list button + positions stacked issue-key buttons via CSS. |
| plugins/copyIssueCacooFormat/extract-issue-data.ts | Extracts issue data from detail pages and list rows. |
| plugins/copyIssueCacooFormat/build-cacoo-json.ts | Builds Cacoo “card shape” JSON payload (color + height calculation). |
| plugins/copyIssueCacooFormat/copy-to-clipboard.ts | Uses execCommand('copy') + copy event to write custom MIME types. |
| locales/ja.yaml | Adds i18n strings for the new plugin (JA). |
| locales/en.yaml | Adds i18n strings for the new plugin (EN). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <svg role="image" viewBox="-4.5 -6.9 27.9 36.8" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M16.01 22.8995C13.8387 22.8995 12.4668 22.804 10.0092 22.1775C6.50191 21.2945 3.62081 19.421 1.87901 16.8853C-0.67999 13.1503 -0.620291 8.944 2.02811 5.3343C3.79971 2.9119 6.72261 1.18759 10.2419 0.465695C12.6159 -0.0236051 13.9461 -0.0117044 15.9503 0.00619558C16.3142 0.00619558 16.7079 0.0181957 17.1374 0.0181957V5.44169C16.684 5.44169 16.2844 5.4417 15.9145 5.4297C14.0594 5.4178 13.1349 5.4118 11.3215 5.7877C9.11451 6.2412 7.36681 7.2256 6.38851 8.5621C5.11201 10.2924 5.10601 12.0167 6.35271 13.8305C7.34291 15.2684 9.10261 16.3662 11.3335 16.9211C13.6359 17.4998 14.5665 17.4879 17.0479 17.47L17.0896 22.8935C16.6959 22.8935 16.3321 22.8995 16.01 22.8995Z" fill="currentColor"/> | ||
| </svg> |
There was a problem hiding this comment.
role="image" is not a valid ARIA role for SVGs (the correct role is img). This may be ignored by assistive tech and can fail accessibility validation. Use role="img" (and ideally add an accessible name via aria-label/title) or drop the role if not needed.
| document.execCommand("copy"); | ||
| document.removeEventListener("copy", handler); |
There was a problem hiding this comment.
document.execCommand('copy') can throw (or be blocked) and in that case the copy event handler would remain attached because removal happens after the call. Wrap the execCommand call in a try/finally so removeEventListener always runs, and consider handling the boolean return value to surface a failure to the user.
| document.execCommand("copy"); | |
| document.removeEventListener("copy", handler); | |
| try { | |
| const success = document.execCommand("copy"); | |
| if (!success) { | |
| console.warn("Copy command was blocked or did not succeed."); | |
| } | |
| } finally { | |
| document.removeEventListener("copy", handler); | |
| } |
| type: 1, | ||
| to: issue.url, | ||
| startIndex: 0, | ||
| endIndex: issue.key.length - 1, | ||
| }, |
There was a problem hiding this comment.
endIndex: issue.key.length - 1 becomes -1 when issue.key is empty (which can happen because extraction falls back to "" instead of failing). This produces invalid link ranges in the generated Cacoo payload. Add validation (e.g., throw if key/url/summary are missing) or clamp indices so the payload is always valid.
| if (type === "バグ" || priority === "高") return COLOR_RED; | ||
| if (priority === "低") return COLOR_GREEN; |
There was a problem hiding this comment.
resolveColor compares type/priority against Japanese UI labels only ("バグ", "高", "低"). On English Backlog UIs these values will differ, so the color will silently fall back to blue and the feature won't behave as intended. Prefer comparing against language-independent identifiers (if available) or handle both JA/EN label variants.
| if (type === "バグ" || priority === "高") return COLOR_RED; | |
| if (priority === "低") return COLOR_GREEN; | |
| const normalizedType = type.trim().toLowerCase(); | |
| const normalizedPriority = priority.trim().toLowerCase(); | |
| const isBug = | |
| normalizedType === "バグ" || normalizedType === "bug"; | |
| const isHighPriority = | |
| normalizedPriority === "高" || normalizedPriority === "high"; | |
| const isLowPriority = | |
| normalizedPriority === "低" || normalizedPriority === "low"; | |
| if (isBug || isHighPriority) return COLOR_RED; | |
| if (isLowPriority) return COLOR_GREEN; |
| return { | ||
| key, | ||
| summary, | ||
| assignee: "", | ||
| dueDate: null, | ||
| type: "", | ||
| priority: "", | ||
| url, |
There was a problem hiding this comment.
extractIssueDataFromRow always returns empty type and priority, so cards copied from list/search pages will always use the default color (blue) regardless of the issue's actual type/priority. If dynamic coloring is expected for list pages too, extract these fields from the row (or fetch the issue JSON/HTML) before building the Cacoo payload.
| const EVENT_PREFIX = "backlog-power-ups:main-world"; | ||
|
|
There was a problem hiding this comment.
EVENT_PREFIX is duplicated here and in entrypoints/main-world-bridge.content/index.ts. If one side changes, the bridge silently breaks. Consider exporting the prefix (or a small helper like getMainWorldEventName(action)) from a single shared module and importing it from both places.
Summary
cacoo/shapeMIME type and plain text fallback)#copyKey-help) and issue list pages (.cell-hover-actions)world: "MAIN") for StatusBar feedback via CustomEvent, bypassing isolated world CSP restrictionsinjectIssueKeyButtonutility for detail page button positioning with CSS-based nth-child stackingLimitations
Captures
🤖 Generated with Claude Code