mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-18 23:25:13 +00:00
Update skill modal ZIP download (#1015)
Make the skill modal download button reuse the existing skill ZIP behavior so it downloads the full skill bundle instead of only the current file. Extract the ZIP creation into a shared utility and reuse it from the skills and hooks pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
showToast,
|
showToast,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
|
downloadZipBundle,
|
||||||
shareFile,
|
shareFile,
|
||||||
getResourceType,
|
getResourceType,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
@@ -46,6 +47,7 @@ interface SkillFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SkillItem extends ResourceItem {
|
interface SkillItem extends ResourceItem {
|
||||||
|
id: string;
|
||||||
skillFile: string;
|
skillFile: string;
|
||||||
files: SkillFile[];
|
files: SkillFile[];
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,10 @@ interface SkillsData {
|
|||||||
|
|
||||||
let skillsCache: SkillsData | null | undefined;
|
let skillsCache: SkillsData | null | undefined;
|
||||||
|
|
||||||
|
function getSkillDownloadName(skill: SkillItem): string {
|
||||||
|
return skill.id || skill.path.split("/").pop() || "skill";
|
||||||
|
}
|
||||||
|
|
||||||
const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
|
const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
|
||||||
agent: "agents.json",
|
agent: "agents.json",
|
||||||
instruction: "instructions.json",
|
instruction: "instructions.json",
|
||||||
@@ -524,6 +530,24 @@ export function setupModal(): void {
|
|||||||
|
|
||||||
downloadBtn?.addEventListener("click", async () => {
|
downloadBtn?.addEventListener("click", async () => {
|
||||||
if (currentFilePath) {
|
if (currentFilePath) {
|
||||||
|
if (currentFileType === "skill") {
|
||||||
|
const skill = await getSkillItemByFilePath(currentFilePath);
|
||||||
|
if (!skill || skill.files.length === 0) {
|
||||||
|
showToast("No files found for this skill.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadZipBundle(getSkillDownloadName(skill), skill.files);
|
||||||
|
showToast("Download started!", "success");
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Download failed";
|
||||||
|
showToast(message, "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const success = await downloadFile(currentFilePath);
|
const success = await downloadFile(currentFilePath);
|
||||||
showToast(
|
showToast(
|
||||||
success ? "Download started!" : "Download failed",
|
success ? "Download started!" : "Download failed",
|
||||||
@@ -868,6 +892,12 @@ export async function openFileModal(
|
|||||||
// Show copy/download buttons for regular files
|
// Show copy/download buttons for regular files
|
||||||
if (copyBtn) copyBtn.style.display = "inline-flex";
|
if (copyBtn) copyBtn.style.display = "inline-flex";
|
||||||
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
type === "skill" ? "Download skill as ZIP" : "Download file"
|
||||||
|
);
|
||||||
|
}
|
||||||
renderPlainText("Loading...");
|
renderPlainText("Loading...");
|
||||||
hideSkillFileSwitcher();
|
hideSkillFileSwitcher();
|
||||||
updateViewButtons();
|
updateViewButtons();
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
|
|||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
debounce,
|
||||||
getRawGitHubUrl,
|
|
||||||
showToast,
|
showToast,
|
||||||
loadJSZip,
|
downloadZipBundle,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { setupModal, openFileModal } from "../modal";
|
import { setupModal, openFileModal } from "../modal";
|
||||||
import {
|
import {
|
||||||
@@ -157,42 +156,7 @@ async function downloadHook(
|
|||||||
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const JSZip = await loadJSZip();
|
await downloadZipBundle(hook.id, files);
|
||||||
const zip = new JSZip();
|
|
||||||
const folder = zip.folder(hook.id);
|
|
||||||
|
|
||||||
const fetchPromises = files.map(async (file) => {
|
|
||||||
const url = getRawGitHubUrl(file.path);
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) return null;
|
|
||||||
const content = await response.text();
|
|
||||||
return { name: file.name, content };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(fetchPromises);
|
|
||||||
let addedFiles = 0;
|
|
||||||
for (const result of results) {
|
|
||||||
if (result && folder) {
|
|
||||||
folder.file(result.name, result.content);
|
|
||||||
addedFiles++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedFiles === 0) throw new Error("Failed to fetch any files");
|
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = downloadUrl;
|
|
||||||
link.download = `${hook.id}.zip`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
|
|
||||||
btn.innerHTML =
|
btn.innerHTML =
|
||||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
|
|||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
debounce,
|
||||||
getRawGitHubUrl,
|
|
||||||
showToast,
|
showToast,
|
||||||
loadJSZip,
|
downloadZipBundle,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { setupModal, openFileModal } from "../modal";
|
import { setupModal, openFileModal } from "../modal";
|
||||||
import {
|
import {
|
||||||
@@ -137,42 +136,7 @@ async function downloadSkill(
|
|||||||
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const JSZip = await loadJSZip();
|
await downloadZipBundle(skill.id, skill.files);
|
||||||
const zip = new JSZip();
|
|
||||||
const folder = zip.folder(skill.id);
|
|
||||||
|
|
||||||
const fetchPromises = skill.files.map(async (file) => {
|
|
||||||
const url = getRawGitHubUrl(file.path);
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) return null;
|
|
||||||
const content = await response.text();
|
|
||||||
return { name: file.name, content };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(fetchPromises);
|
|
||||||
let addedFiles = 0;
|
|
||||||
for (const result of results) {
|
|
||||||
if (result && folder) {
|
|
||||||
folder.file(result.name, result.content);
|
|
||||||
addedFiles++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedFiles === 0) throw new Error("Failed to fetch any files");
|
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = downloadUrl;
|
|
||||||
link.download = `${skill.id}.zip`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
|
|
||||||
btn.innerHTML =
|
btn.innerHTML =
|
||||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||||
|
|||||||
@@ -70,6 +70,66 @@ export async function loadJSZip() {
|
|||||||
return JSZip;
|
return JSZip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ZipDownloadFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerBlobDownload(blob: Blob, filename: string): void {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadZipBundle(
|
||||||
|
bundleName: string,
|
||||||
|
files: ZipDownloadFile[]
|
||||||
|
): Promise<void> {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error("No files found for this download.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const JSZip = await loadJSZip();
|
||||||
|
const zip = new JSZip();
|
||||||
|
const folder = zip.folder(bundleName);
|
||||||
|
|
||||||
|
const fetchPromises = files.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(getRawGitHubUrl(file.path));
|
||||||
|
if (!response.ok) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
content: await response.text(),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(fetchPromises);
|
||||||
|
let addedFiles = 0;
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
if (result && folder) {
|
||||||
|
folder.file(result.name, result.content);
|
||||||
|
addedFiles++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedFiles === 0) {
|
||||||
|
throw new Error("Failed to fetch any files");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
|
triggerBlobDownload(blob, `${bundleName}.zip`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch raw file content from GitHub
|
* Fetch raw file content from GitHub
|
||||||
*/
|
*/
|
||||||
@@ -156,15 +216,7 @@ export async function downloadFile(filePath: string): Promise<boolean> {
|
|||||||
const filename = filePath.split("/").pop() || "file.md";
|
const filename = filePath.split("/").pop() || "file.md";
|
||||||
|
|
||||||
const blob = new Blob([content], { type: "text/markdown" });
|
const blob = new Blob([content], { type: "text/markdown" });
|
||||||
const url = URL.createObjectURL(blob);
|
triggerBlobDownload(blob, filename);
|
||||||
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user