mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 20:52:56 +01:00
Prettier format pending files
This commit is contained in:
parent
c3b3d72037
commit
34b0d92e8a
11 changed files with 1330 additions and 726 deletions
|
|
@ -1,14 +1,23 @@
|
||||||
const { app, BrowserWindow } = require('electron');
|
const { app, BrowserWindow } = require("electron");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({ width: 1280, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true } });
|
const win = new BrowserWindow({
|
||||||
const startUrl = process.env.SWITCH_URL || 'http://localhost:8080/switch.html';
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: { nodeIntegration: false, contextIsolation: true },
|
||||||
|
});
|
||||||
|
const startUrl =
|
||||||
|
process.env.SWITCH_URL || "http://localhost:8080/switch.html";
|
||||||
win.loadURL(startUrl);
|
win.loadURL(startUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,54 @@
|
||||||
const CACHE = 'switch-cache-v1';
|
const CACHE = "switch-cache-v1";
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
'/',
|
"/",
|
||||||
'/index.html',
|
"/index.html",
|
||||||
'/playground.html',
|
"/playground.html",
|
||||||
'/monarch.html',
|
"/monarch.html",
|
||||||
'/switch.html'
|
"/switch.html",
|
||||||
];
|
];
|
||||||
self.addEventListener('install', (e) => {
|
self.addEventListener("install", (e) => {
|
||||||
e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS)).then(()=>self.skipWaiting()));
|
e.waitUntil(
|
||||||
|
caches
|
||||||
|
.open(CACHE)
|
||||||
|
.then((c) => c.addAll(ASSETS))
|
||||||
|
.then(() => self.skipWaiting())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
self.addEventListener('activate', (e) => {
|
self.addEventListener("activate", (e) => {
|
||||||
e.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim()));
|
e.waitUntil(
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(() => self.clients.claim())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
self.addEventListener('fetch', (e) => {
|
self.addEventListener("fetch", (e) => {
|
||||||
const url = new URL(e.request.url);
|
const url = new URL(e.request.url);
|
||||||
if (url.origin === location.origin) {
|
if (url.origin === location.origin) {
|
||||||
e.respondWith(caches.match(e.request).then(r=> r || fetch(e.request).then(resp=>{
|
e.respondWith(
|
||||||
if (e.request.method==='GET' && resp.ok && resp.type==='basic') {
|
caches.match(e.request).then(
|
||||||
|
(r) =>
|
||||||
|
r ||
|
||||||
|
fetch(e.request)
|
||||||
|
.then((resp) => {
|
||||||
|
if (
|
||||||
|
e.request.method === "GET" &&
|
||||||
|
resp.ok &&
|
||||||
|
resp.type === "basic"
|
||||||
|
) {
|
||||||
const clone = resp.clone();
|
const clone = resp.clone();
|
||||||
caches.open(CACHE).then(c=>c.put(e.request, clone));
|
caches
|
||||||
|
.open(CACHE)
|
||||||
|
.then((c) => c.put(e.request, clone));
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}).catch(()=>caches.match('/index.html'))));
|
})
|
||||||
|
.catch(() => caches.match("/index.html"))
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,10 @@ async function _loadMonaco(setup: IMonacoSetup): Promise<typeof monaco> {
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If loading optional language contributions fails, still resolve the editor to keep app functional.
|
// If loading optional language contributions fails, still resolve the editor to keep app functional.
|
||||||
console.error('Failed to load Monaco language contributions, continuing without them.', e);
|
console.error(
|
||||||
|
"Failed to load Monaco language contributions, continuing without them.",
|
||||||
|
e
|
||||||
|
);
|
||||||
res(monaco);
|
res(monaco);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ elem.className = "root";
|
||||||
document.body.append(elem);
|
document.body.append(elem);
|
||||||
ReactDOM.render(<App />, elem);
|
ReactDOM.render(<App />, elem);
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,83 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor";
|
import { ControlledMonacoEditor } from "../../components/monaco/MonacoEditor";
|
||||||
import "../../switch.scss";
|
import "../../switch.scss";
|
||||||
import { createRepository, listRepositories, listBranches, createBranch } from "../../switch/models";
|
import {
|
||||||
import { pickDirectory, getDirectoryHandle, ensureReadPerm, walk } from "../../switch/fs";
|
createRepository,
|
||||||
import type { RepoRecord, BranchRecord, CommitRecord, IssueRecord } from "../../switch/db";
|
listRepositories,
|
||||||
|
listBranches,
|
||||||
|
createBranch,
|
||||||
|
} from "../../switch/models";
|
||||||
|
import {
|
||||||
|
pickDirectory,
|
||||||
|
getDirectoryHandle,
|
||||||
|
ensureReadPerm,
|
||||||
|
walk,
|
||||||
|
} from "../../switch/fs";
|
||||||
|
import type {
|
||||||
|
RepoRecord,
|
||||||
|
BranchRecord,
|
||||||
|
CommitRecord,
|
||||||
|
IssueRecord,
|
||||||
|
} from "../../switch/db";
|
||||||
import { FileTree } from "./FileTree";
|
import { FileTree } from "./FileTree";
|
||||||
import { getCurrentBranchId, setCurrentBranchId, commitOnBranch, listCommitsReachable, listIssues, createIssue } from "../../switch/logic";
|
import {
|
||||||
|
getCurrentBranchId,
|
||||||
|
setCurrentBranchId,
|
||||||
|
commitOnBranch,
|
||||||
|
listCommitsReachable,
|
||||||
|
listIssues,
|
||||||
|
createIssue,
|
||||||
|
} from "../../switch/logic";
|
||||||
|
|
||||||
interface OpenFile { path: string; content: string; language: string }
|
interface OpenFile {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function SwitchPage() {
|
export function SwitchPage() {
|
||||||
const [repos, setRepos] = React.useState<RepoRecord[]>([]);
|
const [repos, setRepos] = React.useState<RepoRecord[]>([]);
|
||||||
const [activeRepo, setActiveRepo] = React.useState<RepoRecord | undefined>();
|
const [activeRepo, setActiveRepo] = React.useState<
|
||||||
const [tree, setTree] = React.useState<{ path: string; isDir: boolean; children?: any[] }[]>([]);
|
RepoRecord | undefined
|
||||||
|
>();
|
||||||
|
const [tree, setTree] = React.useState<
|
||||||
|
{ path: string; isDir: boolean; children?: any[] }[]
|
||||||
|
>([]);
|
||||||
const [openFile, setOpenFile] = React.useState<OpenFile | undefined>();
|
const [openFile, setOpenFile] = React.useState<OpenFile | undefined>();
|
||||||
const [editorValue, setEditorValue] = React.useState<string>("// SWITCH: Open a folder to get started\n");
|
const [editorValue, setEditorValue] = React.useState<string>(
|
||||||
|
"// SWITCH: Open a folder to get started\n"
|
||||||
|
);
|
||||||
const [branches, setBranches] = React.useState<BranchRecord[]>([]);
|
const [branches, setBranches] = React.useState<BranchRecord[]>([]);
|
||||||
const [currentBranchId, setCurrentBranchIdState] = React.useState<string | undefined>(undefined);
|
const [currentBranchId, setCurrentBranchIdState] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
const [commitMsg, setCommitMsg] = React.useState("");
|
const [commitMsg, setCommitMsg] = React.useState("");
|
||||||
const [history, setHistory] = React.useState<CommitRecord[]>([]);
|
const [history, setHistory] = React.useState<CommitRecord[]>([]);
|
||||||
const [issues, setIssues] = React.useState<IssueRecord[]>([]);
|
const [issues, setIssues] = React.useState<IssueRecord[]>([]);
|
||||||
const [newIssueTitle, setNewIssueTitle] = React.useState("");
|
const [newIssueTitle, setNewIssueTitle] = React.useState("");
|
||||||
const [selectedIssueId, setSelectedIssueId] = React.useState<string | undefined>(undefined);
|
const [selectedIssueId, setSelectedIssueId] = React.useState<
|
||||||
const selectedIssue = issues.find(i=>i.id===selectedIssueId);
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const selectedIssue = issues.find((i) => i.id === selectedIssueId);
|
||||||
const [issueTitle, setIssueTitle] = React.useState("");
|
const [issueTitle, setIssueTitle] = React.useState("");
|
||||||
const [issueBody, setIssueBody] = React.useState("");
|
const [issueBody, setIssueBody] = React.useState("");
|
||||||
const [issueLabels, setIssueLabels] = React.useState<string>("");
|
const [issueLabels, setIssueLabels] = React.useState<string>("");
|
||||||
const [issueStatus, setIssueStatus] = React.useState<"open"|"closed">("open");
|
const [issueStatus, setIssueStatus] = React.useState<"open" | "closed">(
|
||||||
|
"open"
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
refreshRepos();
|
refreshRepos();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => { (async () => {
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
if (!activeRepo) return;
|
if (!activeRepo) return;
|
||||||
const b = await listBranches(activeRepo.id);
|
const b = await listBranches(activeRepo.id);
|
||||||
setBranches(b);
|
setBranches(b);
|
||||||
const cur = await getCurrentBranchId(activeRepo.id) || b.find(x=>x.name===activeRepo.defaultBranch)?.id;
|
const cur =
|
||||||
|
(await getCurrentBranchId(activeRepo.id)) ||
|
||||||
|
b.find((x) => x.name === activeRepo.defaultBranch)?.id;
|
||||||
if (cur) {
|
if (cur) {
|
||||||
setCurrentBranchIdState(cur);
|
setCurrentBranchIdState(cur);
|
||||||
await refreshHistory(activeRepo.id, cur);
|
await refreshHistory(activeRepo.id, cur);
|
||||||
|
|
@ -44,11 +85,12 @@ export function SwitchPage() {
|
||||||
const iss = await listIssues(activeRepo.id);
|
const iss = await listIssues(activeRepo.id);
|
||||||
setIssues(iss);
|
setIssues(iss);
|
||||||
if (iss.length) selectIssue(iss[0].id);
|
if (iss.length) selectIssue(iss[0].id);
|
||||||
})(); }, [activeRepo?.id]);
|
})();
|
||||||
|
}, [activeRepo?.id]);
|
||||||
|
|
||||||
function selectIssue(id: string) {
|
function selectIssue(id: string) {
|
||||||
setSelectedIssueId(id);
|
setSelectedIssueId(id);
|
||||||
const it = issues.find(i=>i.id===id);
|
const it = issues.find((i) => i.id === id);
|
||||||
if (it) {
|
if (it) {
|
||||||
setIssueTitle(it.title);
|
setIssueTitle(it.title);
|
||||||
setIssueBody(it.body);
|
setIssueBody(it.body);
|
||||||
|
|
@ -88,7 +130,12 @@ export function SwitchPage() {
|
||||||
const key = parts.slice(0, i + 1).join("/");
|
const key = parts.slice(0, i + 1).join("/");
|
||||||
const isDir = i < parts.length - 1;
|
const isDir = i < parts.length - 1;
|
||||||
cur.children = cur.children || new Map<string, any>();
|
cur.children = cur.children || new Map<string, any>();
|
||||||
if (!cur.children.has(key)) cur.children.set(key, { path: key, isDir, children: undefined });
|
if (!cur.children.has(key))
|
||||||
|
cur.children.set(key, {
|
||||||
|
path: key,
|
||||||
|
isDir,
|
||||||
|
children: undefined,
|
||||||
|
});
|
||||||
cur = cur.children.get(key);
|
cur = cur.children.get(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +143,13 @@ export function SwitchPage() {
|
||||||
if (!node.children) return [];
|
if (!node.children) return [];
|
||||||
const arr = Array.from(node.children.values());
|
const arr = Array.from(node.children.values());
|
||||||
for (const n of arr) if (n.isDir) n.children = toArray(n);
|
for (const n of arr) if (n.isDir) n.children = toArray(n);
|
||||||
arr.sort((a, b) => (a.isDir === b.isDir ? a.path.localeCompare(b.path) : a.isDir ? -1 : 1));
|
arr.sort((a, b) =>
|
||||||
|
a.isDir === b.isDir
|
||||||
|
? a.path.localeCompare(b.path)
|
||||||
|
: a.isDir
|
||||||
|
? -1
|
||||||
|
: 1
|
||||||
|
);
|
||||||
return arr;
|
return arr;
|
||||||
};
|
};
|
||||||
setTree(toArray(root));
|
setTree(toArray(root));
|
||||||
|
|
@ -121,7 +174,7 @@ export function SwitchPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshHistory(repoId: string, branchId: string) {
|
async function refreshHistory(repoId: string, branchId: string) {
|
||||||
const b = branches.find(x=>x.id===branchId);
|
const b = branches.find((x) => x.id === branchId);
|
||||||
const list = await listCommitsReachable(repoId, b?.headCommitId);
|
const list = await listCommitsReachable(repoId, b?.headCommitId);
|
||||||
setHistory(list);
|
setHistory(list);
|
||||||
}
|
}
|
||||||
|
|
@ -129,22 +182,61 @@ export function SwitchPage() {
|
||||||
return (
|
return (
|
||||||
<div className="switch-shell">
|
<div className="switch-shell">
|
||||||
<div className="switch-activity-bar" aria-label="Activity Bar">
|
<div className="switch-activity-bar" aria-label="Activity Bar">
|
||||||
<button className="switch-activity-item" title="Explorer" aria-label="Explorer">📁</button>
|
<button
|
||||||
<button className="switch-activity-item" title="Search" aria-label="Search">🔎</button>
|
className="switch-activity-item"
|
||||||
<button className="switch-activity-item" title="Source Control" aria-label="Source Control">🔀</button>
|
title="Explorer"
|
||||||
<button className="switch-activity-item" title="Issues" aria-label="Issues">🏷️</button>
|
aria-label="Explorer"
|
||||||
|
>
|
||||||
|
📁
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-activity-item"
|
||||||
|
title="Search"
|
||||||
|
aria-label="Search"
|
||||||
|
>
|
||||||
|
🔎
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-activity-item"
|
||||||
|
title="Source Control"
|
||||||
|
aria-label="Source Control"
|
||||||
|
>
|
||||||
|
🔀
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-activity-item"
|
||||||
|
title="Issues"
|
||||||
|
aria-label="Issues"
|
||||||
|
>
|
||||||
|
🏷️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="switch-main">
|
<div className="switch-main">
|
||||||
<aside className="switch-sidebar" aria-label="Sidebar">
|
<aside className="switch-sidebar" aria-label="Sidebar">
|
||||||
<div className="switch-pane">
|
<div className="switch-pane">
|
||||||
<div className="switch-pane-header">Repositories</div>
|
<div className="switch-pane-header">Repositories</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<button className="switch-secondary-btn" onClick={onOpenFolder}>Open Folder</button>
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onOpenFolder}
|
||||||
|
>
|
||||||
|
Open Folder
|
||||||
|
</button>
|
||||||
<div>
|
<div>
|
||||||
{repos.map((r) => (
|
{repos.map((r) => (
|
||||||
<div key={r.id}>
|
<div key={r.id}>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="repo" onChange={() => onSelectRepo(r.id)} checked={activeRepo?.id === r.id} /> {r.name}
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="repo"
|
||||||
|
onChange={() =>
|
||||||
|
onSelectRepo(r.id)
|
||||||
|
}
|
||||||
|
checked={
|
||||||
|
activeRepo?.id === r.id
|
||||||
|
}
|
||||||
|
/>{" "}
|
||||||
|
{r.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -155,21 +247,116 @@ export function SwitchPage() {
|
||||||
<div className="switch-pane-header">Branches</div>
|
<div className="switch-pane-header">Branches</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<select value={currentBranchId || ""} onChange={async (e)=>{ if (!activeRepo) return; const id=e.target.value; setCurrentBranchIdState(id); await setCurrentBranchId(activeRepo.id, id); await refreshHistory(activeRepo.id, id); }}>
|
<select
|
||||||
<option value="" disabled>— Select branch —</option>
|
value={currentBranchId || ""}
|
||||||
{branches.map(b=> (<option key={b.id} value={b.id}>{b.name}</option>))}
|
onChange={async (e) => {
|
||||||
|
if (!activeRepo) return;
|
||||||
|
const id = e.target.value;
|
||||||
|
setCurrentBranchIdState(id);
|
||||||
|
await setCurrentBranchId(
|
||||||
|
activeRepo.id,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
await refreshHistory(activeRepo.id, id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
— Select branch —
|
||||||
|
</option>
|
||||||
|
{branches.map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button className="switch-secondary-btn" onClick={async ()=>{ if (!activeRepo) return; const name=prompt("New branch name:", "feature"); if (!name) return; const from = branches.find(b=>b.id===currentBranchId)?.headCommitId; const b = await createBranch(activeRepo.id, name, from); const list=await listBranches(activeRepo.id); setBranches(list); setCurrentBranchIdState(b.id); await setCurrentBranchId(activeRepo.id, b.id); await refreshHistory(activeRepo.id, b.id); }}>New Branch</button>
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!activeRepo) return;
|
||||||
|
const name = prompt(
|
||||||
|
"New branch name:",
|
||||||
|
"feature"
|
||||||
|
);
|
||||||
|
if (!name) return;
|
||||||
|
const from = branches.find(
|
||||||
|
(b) => b.id === currentBranchId
|
||||||
|
)?.headCommitId;
|
||||||
|
const b = await createBranch(
|
||||||
|
activeRepo.id,
|
||||||
|
name,
|
||||||
|
from
|
||||||
|
);
|
||||||
|
const list = await listBranches(
|
||||||
|
activeRepo.id
|
||||||
|
);
|
||||||
|
setBranches(list);
|
||||||
|
setCurrentBranchIdState(b.id);
|
||||||
|
await setCurrentBranchId(
|
||||||
|
activeRepo.id,
|
||||||
|
b.id
|
||||||
|
);
|
||||||
|
await refreshHistory(
|
||||||
|
activeRepo.id,
|
||||||
|
b.id
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Branch
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input placeholder="Commit message" value={commitMsg} onChange={(e)=>setCommitMsg(e.target.value)} />
|
<input
|
||||||
<button className="switch-secondary-btn" onClick={async ()=>{ if (!activeRepo||!currentBranchId) return; await commitOnBranch(activeRepo.id, currentBranchId, commitMsg || "chore: update"); setCommitMsg(""); await refreshHistory(activeRepo.id, currentBranchId); }}>Commit</button>
|
placeholder="Commit message"
|
||||||
|
value={commitMsg}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCommitMsg(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!activeRepo || !currentBranchId)
|
||||||
|
return;
|
||||||
|
await commitOnBranch(
|
||||||
|
activeRepo.id,
|
||||||
|
currentBranchId,
|
||||||
|
commitMsg || "chore: update"
|
||||||
|
);
|
||||||
|
setCommitMsg("");
|
||||||
|
await refreshHistory(
|
||||||
|
activeRepo.id,
|
||||||
|
currentBranchId
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Commit
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<div className="switch-pane-header">History</div>
|
<div className="switch-pane-header">
|
||||||
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
History
|
||||||
{history.map(c => (<li key={c.id}>• {new Date(c.timestamp).toLocaleString()} — {c.message}</li>))}
|
</div>
|
||||||
{history.length===0 && <li className="switch-empty">No commits</li>}
|
<ul
|
||||||
|
style={{
|
||||||
|
listStyle: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{history.map((c) => (
|
||||||
|
<li key={c.id}>
|
||||||
|
•{" "}
|
||||||
|
{new Date(
|
||||||
|
c.timestamp
|
||||||
|
).toLocaleString()}{" "}
|
||||||
|
— {c.message}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{history.length === 0 && (
|
||||||
|
<li className="switch-empty">
|
||||||
|
No commits
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -178,13 +365,43 @@ export function SwitchPage() {
|
||||||
<div className="switch-pane-header">Templates</div>
|
<div className="switch-pane-header">Templates</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<button className="switch-secondary-btn" onClick={onSaveRepoAsTemplate} disabled={!activeRepo}>Save Current Repo as Template</button>
|
<button
|
||||||
<button className="switch-secondary-btn" onClick={onCreateRepoFromTemplate}>New Repo from Template</button>
|
className="switch-secondary-btn"
|
||||||
|
onClick={onSaveRepoAsTemplate}
|
||||||
|
disabled={!activeRepo}
|
||||||
|
>
|
||||||
|
Save Current Repo as Template
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onCreateRepoFromTemplate}
|
||||||
|
>
|
||||||
|
New Repo from Template
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: '#9da5b4', margin: '6px 0' }}>Issue Templates</div>
|
<div
|
||||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
style={{
|
||||||
<button className="switch-secondary-btn" onClick={onCreateIssueTemplate}>New Issue Template</button>
|
fontSize: 12,
|
||||||
|
color: "#9da5b4",
|
||||||
|
margin: "6px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Issue Templates
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 6,
|
||||||
|
marginBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onCreateIssueTemplate}
|
||||||
|
>
|
||||||
|
New Issue Template
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,12 +409,26 @@ export function SwitchPage() {
|
||||||
<div className="switch-pane">
|
<div className="switch-pane">
|
||||||
<div className="switch-pane-header">Cloud Sync</div>
|
<div className="switch-pane-header">Cloud Sync</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div style={{ marginBottom: 8, color: '#9da5b4' }}>
|
<div style={{ marginBottom: 8, color: "#9da5b4" }}>
|
||||||
Supabase storage bucket "switch" is required (public read/write via RLS). URL and anon key are read from env.
|
Supabase storage bucket "switch" is required
|
||||||
|
(public read/write via RLS). URL and anon key
|
||||||
|
are read from env.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button className="switch-secondary-btn" onClick={onPushToCloud} disabled={!activeRepo}>Push</button>
|
<button
|
||||||
<button className="switch-secondary-btn" onClick={onPullFromCloud} disabled={!activeRepo}>Pull</button>
|
className="switch-secondary-btn"
|
||||||
|
onClick={onPushToCloud}
|
||||||
|
disabled={!activeRepo}
|
||||||
|
>
|
||||||
|
Push
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onPullFromCloud}
|
||||||
|
disabled={!activeRepo}
|
||||||
|
>
|
||||||
|
Pull
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,9 +436,24 @@ export function SwitchPage() {
|
||||||
<div className="switch-pane-header">Explorer</div>
|
<div className="switch-pane-header">Explorer</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<button className="switch-secondary-btn" onClick={onNewFile}>New File</button>
|
<button
|
||||||
<button className="switch-secondary-btn" onClick={onNewFolder}>New Folder</button>
|
className="switch-secondary-btn"
|
||||||
<button className="switch-secondary-btn" onClick={onRefresh}>Refresh</button>
|
onClick={onNewFile}
|
||||||
|
>
|
||||||
|
New File
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onNewFolder}
|
||||||
|
>
|
||||||
|
New Folder
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onRefresh}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{tree.length === 0 ? (
|
{tree.length === 0 ? (
|
||||||
<div className="switch-empty">No files</div>
|
<div className="switch-empty">No files</div>
|
||||||
|
|
@ -220,33 +466,128 @@ export function SwitchPage() {
|
||||||
<div className="switch-pane-header">Issues</div>
|
<div className="switch-pane-header">Issues</div>
|
||||||
<div className="switch-pane-body">
|
<div className="switch-pane-body">
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
<input placeholder="New issue title" value={newIssueTitle} onChange={(e)=>setNewIssueTitle(e.target.value)} />
|
<input
|
||||||
<button className="switch-secondary-btn" onClick={async ()=>{ if(!activeRepo||!newIssueTitle) return; await createIssue(activeRepo.id, { title: newIssueTitle, branchId: currentBranchId, filePath: openFile?.path }); setNewIssueTitle(""); setIssues(await listIssues(activeRepo.id)); }}>Add</button>
|
placeholder="New issue title"
|
||||||
|
value={newIssueTitle}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewIssueTitle(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!activeRepo || !newIssueTitle)
|
||||||
|
return;
|
||||||
|
await createIssue(activeRepo.id, {
|
||||||
|
title: newIssueTitle,
|
||||||
|
branchId: currentBranchId,
|
||||||
|
filePath: openFile?.path,
|
||||||
|
});
|
||||||
|
setNewIssueTitle("");
|
||||||
|
setIssues(
|
||||||
|
await listIssues(activeRepo.id)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul style={{ listStyle: "none", padding: 0, marginTop: 8 }}>
|
<ul
|
||||||
{issues.map(i => (
|
style={{
|
||||||
<li key={i.id} onClick={()=>selectIssue(i.id)} style={{ cursor: "pointer", padding: "2px 0", color: selectedIssueId===i.id? "#fff": undefined }}>
|
listStyle: "none",
|
||||||
#{i.id.slice(-5)} {i.title} <span style={{ color: "#858585" }}>({i.status})</span>
|
padding: 0,
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{issues.map((i) => (
|
||||||
|
<li
|
||||||
|
key={i.id}
|
||||||
|
onClick={() => selectIssue(i.id)}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "2px 0",
|
||||||
|
color:
|
||||||
|
selectedIssueId === i.id
|
||||||
|
? "#fff"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{i.id.slice(-5)} {i.title}{" "}
|
||||||
|
<span style={{ color: "#858585" }}>
|
||||||
|
({i.status})
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{issues.length===0 && <li className="switch-empty">No issues</li>}
|
{issues.length === 0 && (
|
||||||
|
<li className="switch-empty">No issues</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{selectedIssue && (
|
{selectedIssue && (
|
||||||
<div style={{ borderTop: "1px solid #2d2d2d", marginTop: 8, paddingTop: 8 }}>
|
<div
|
||||||
<div style={{ marginBottom: 6 }}>Edit Issue</div>
|
style={{
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
borderTop: "1px solid #2d2d2d",
|
||||||
<input placeholder="Title" value={issueTitle} onChange={e=>setIssueTitle(e.target.value)} />
|
marginTop: 8,
|
||||||
<select value={issueStatus} onChange={(e)=>setIssueStatus(e.target.value as any)}>
|
paddingTop: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 6 }}>
|
||||||
|
Edit Issue
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
placeholder="Title"
|
||||||
|
value={issueTitle}
|
||||||
|
onChange={(e) =>
|
||||||
|
setIssueTitle(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={issueStatus}
|
||||||
|
onChange={(e) =>
|
||||||
|
setIssueStatus(
|
||||||
|
e.target.value as any
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<option value="open">open</option>
|
<option value="open">open</option>
|
||||||
<option value="closed">closed</option>
|
<option value="closed">
|
||||||
|
closed
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input placeholder="labels (comma separated)" value={issueLabels} onChange={e=>setIssueLabels(e.target.value)} />
|
<input
|
||||||
|
placeholder="labels (comma separated)"
|
||||||
|
value={issueLabels}
|
||||||
|
onChange={(e) =>
|
||||||
|
setIssueLabels(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div style={{ height: 160 }}>
|
<div style={{ height: 160 }}>
|
||||||
<ControlledMonacoEditor value={issueBody} onDidValueChange={setIssueBody} language="markdown" theme="vs-dark" />
|
<ControlledMonacoEditor
|
||||||
|
value={issueBody}
|
||||||
|
onDidValueChange={setIssueBody}
|
||||||
|
language="markdown"
|
||||||
|
theme="vs-dark"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button className="switch-secondary-btn" onClick={onSaveIssue}>Save</button>
|
<button
|
||||||
<button className="switch-secondary-btn" onClick={onDeleteIssue}>Delete</button>
|
className="switch-secondary-btn"
|
||||||
|
onClick={onSaveIssue}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onDeleteIssue}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -256,22 +597,55 @@ export function SwitchPage() {
|
||||||
</aside>
|
</aside>
|
||||||
<section className="switch-editor" aria-label="Editor">
|
<section className="switch-editor" aria-label="Editor">
|
||||||
<div className="switch-tabbar">
|
<div className="switch-tabbar">
|
||||||
<div className="switch-tab active">{openFile?.path || "welcome.txt"}</div>
|
<div className="switch-tab active">
|
||||||
|
{openFile?.path || "welcome.txt"}
|
||||||
|
</div>
|
||||||
<div style={{ marginLeft: "auto" }}>
|
<div style={{ marginLeft: "auto" }}>
|
||||||
<button className="switch-secondary-btn" onClick={onSave} disabled={!openFile}>Save</button>
|
<button
|
||||||
<button className="switch-secondary-btn" onClick={onSaveAs} disabled={!activeRepo}>Save As</button>
|
className="switch-secondary-btn"
|
||||||
<button className="switch-secondary-btn" onClick={onRename} disabled={!openFile}>Rename</button>
|
onClick={onSave}
|
||||||
<button className="switch-secondary-btn" onClick={onDelete} disabled={!openFile}>Delete</button>
|
disabled={!openFile}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onSaveAs}
|
||||||
|
disabled={!activeRepo}
|
||||||
|
>
|
||||||
|
Save As
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onRename}
|
||||||
|
disabled={!openFile}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="switch-secondary-btn"
|
||||||
|
onClick={onDelete}
|
||||||
|
disabled={!openFile}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="switch-editor-inner">
|
<div className="switch-editor-inner">
|
||||||
<ControlledMonacoEditor value={editorValue} onDidValueChange={setEditorValue} language={openFile?.language || "plaintext"} theme="vs-dark" />
|
<ControlledMonacoEditor
|
||||||
|
value={editorValue}
|
||||||
|
onDidValueChange={setEditorValue}
|
||||||
|
language={openFile?.language || "plaintext"}
|
||||||
|
theme="vs-dark"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<footer className="switch-statusbar" aria-label="Status Bar">
|
<footer className="switch-statusbar" aria-label="Status Bar">
|
||||||
<div className="switch-status-item">SWITCH</div>
|
<div className="switch-status-item">SWITCH</div>
|
||||||
<div className="switch-status-item">{openFile?.language || "Plain Text"}</div>
|
<div className="switch-status-item">
|
||||||
|
{openFile?.language || "Plain Text"}
|
||||||
|
</div>
|
||||||
<div className="switch-status-item">UTF-8</div>
|
<div className="switch-status-item">UTF-8</div>
|
||||||
<div className="switch-status-item">LF</div>
|
<div className="switch-status-item">LF</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -305,17 +679,28 @@ export function SwitchPage() {
|
||||||
if (!activeRepo?.fsHandleId || !openFile) return;
|
if (!activeRepo?.fsHandleId || !openFile) return;
|
||||||
const dir = await getDirectoryHandle(activeRepo.fsHandleId);
|
const dir = await getDirectoryHandle(activeRepo.fsHandleId);
|
||||||
if (!dir) return;
|
if (!dir) return;
|
||||||
await (await import("../../switch/fs")).writeFileText(dir, openFile.path, editorValue);
|
await (
|
||||||
|
await import("../../switch/fs")
|
||||||
|
).writeFileText(dir, openFile.path, editorValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSaveAs() {
|
async function onSaveAs() {
|
||||||
if (!activeRepo?.fsHandleId) return;
|
if (!activeRepo?.fsHandleId) return;
|
||||||
const target = prompt("Save As path (relative):", openFile?.path || "untitled.txt");
|
const target = prompt(
|
||||||
|
"Save As path (relative):",
|
||||||
|
openFile?.path || "untitled.txt"
|
||||||
|
);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const dir = await getDirectoryHandle(activeRepo.fsHandleId);
|
const dir = await getDirectoryHandle(activeRepo.fsHandleId);
|
||||||
if (!dir) return;
|
if (!dir) return;
|
||||||
await (await import("../../switch/fs")).writeFileText(dir, target, editorValue);
|
await (
|
||||||
setOpenFile({ path: target, content: editorValue, language: languageFromPath(target) });
|
await import("../../switch/fs")
|
||||||
|
).writeFileText(dir, target, editorValue);
|
||||||
|
setOpenFile({
|
||||||
|
path: target,
|
||||||
|
content: editorValue,
|
||||||
|
language: languageFromPath(target),
|
||||||
|
});
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,16 +716,29 @@ export function SwitchPage() {
|
||||||
async function onCreateRepoFromTemplate() {
|
async function onCreateRepoFromTemplate() {
|
||||||
const { listRepoTemplates } = await import("../../switch/templates");
|
const { listRepoTemplates } = await import("../../switch/templates");
|
||||||
const list = await listRepoTemplates();
|
const list = await listRepoTemplates();
|
||||||
if (!list.length) { alert("No repo templates."); return; }
|
if (!list.length) {
|
||||||
|
alert("No repo templates.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const names = list.map((t, i) => `${i + 1}. ${t.name}`).join("\n");
|
const names = list.map((t, i) => `${i + 1}. ${t.name}`).join("\n");
|
||||||
const idxStr = prompt(`Choose template:\n${names}`, "1");
|
const idxStr = prompt(`Choose template:\n${names}`, "1");
|
||||||
if (!idxStr) return; const idx = parseInt(idxStr, 10) - 1; if (isNaN(idx) || idx<0 || idx>=list.length) return;
|
if (!idxStr) return;
|
||||||
|
const idx = parseInt(idxStr, 10) - 1;
|
||||||
|
if (isNaN(idx) || idx < 0 || idx >= list.length) return;
|
||||||
const tpl = list[idx];
|
const tpl = list[idx];
|
||||||
const dest = await pickDirectory();
|
const dest = await pickDirectory();
|
||||||
const srcHandle = await getDirectoryHandle(tpl.fsHandleId);
|
const srcHandle = await getDirectoryHandle(tpl.fsHandleId);
|
||||||
if (!srcHandle) { alert("Template folder permission required. Re-save the template."); return; }
|
if (!srcHandle) {
|
||||||
await (await import("../../switch/fs")).copyDirectory(srcHandle, dest.handle);
|
alert("Template folder permission required. Re-save the template.");
|
||||||
const repo = await createRepository(dest.handle.name || "workspace", dest.id);
|
return;
|
||||||
|
}
|
||||||
|
await (
|
||||||
|
await import("../../switch/fs")
|
||||||
|
).copyDirectory(srcHandle, dest.handle);
|
||||||
|
const repo = await createRepository(
|
||||||
|
dest.handle.name || "workspace",
|
||||||
|
dest.id
|
||||||
|
);
|
||||||
await refreshRepos();
|
await refreshRepos();
|
||||||
setActiveRepo(repo);
|
setActiveRepo(repo);
|
||||||
await loadTree(repo);
|
await loadTree(repo);
|
||||||
|
|
@ -355,7 +753,11 @@ export function SwitchPage() {
|
||||||
const fs = await import("../../switch/fs");
|
const fs = await import("../../switch/fs");
|
||||||
await fs.writeFileText(dir, next, editorValue);
|
await fs.writeFileText(dir, next, editorValue);
|
||||||
await fs.deleteEntry(dir, openFile.path);
|
await fs.deleteEntry(dir, openFile.path);
|
||||||
setOpenFile({ path: next, content: editorValue, language: languageFromPath(next) });
|
setOpenFile({
|
||||||
|
path: next,
|
||||||
|
content: editorValue,
|
||||||
|
language: languageFromPath(next),
|
||||||
|
});
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,7 +777,10 @@ export function SwitchPage() {
|
||||||
const { updateIssue } = await import("../../switch/logic");
|
const { updateIssue } = await import("../../switch/logic");
|
||||||
selectedIssue.title = issueTitle;
|
selectedIssue.title = issueTitle;
|
||||||
selectedIssue.body = issueBody;
|
selectedIssue.body = issueBody;
|
||||||
selectedIssue.labels = issueLabels.split(",").map(s=>s.trim()).filter(Boolean);
|
selectedIssue.labels = issueLabels
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
selectedIssue.status = issueStatus;
|
selectedIssue.status = issueStatus;
|
||||||
await updateIssue(selectedIssue);
|
await updateIssue(selectedIssue);
|
||||||
setIssues(await listIssues(activeRepo.id));
|
setIssues(await listIssues(activeRepo.id));
|
||||||
|
|
@ -405,25 +810,43 @@ export function SwitchPage() {
|
||||||
try {
|
try {
|
||||||
const { pullRepoFromSupabase } = await import("../../switch/sync");
|
const { pullRepoFromSupabase } = await import("../../switch/sync");
|
||||||
const bundle = await pullRepoFromSupabase(activeRepo.id);
|
const bundle = await pullRepoFromSupabase(activeRepo.id);
|
||||||
if (!bundle) { alert("No bundle found."); return; }
|
if (!bundle) {
|
||||||
|
alert("No bundle found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// For now, just show summary; full import/merge logic can be added as next step
|
// For now, just show summary; full import/merge logic can be added as next step
|
||||||
alert(`Fetched bundle exportedAt=${new Date(bundle.exportedAt).toLocaleString()}\nbranches=${bundle.branches.length}, commits=${bundle.commits.length}, issues=${bundle.issues.length}`);
|
alert(
|
||||||
|
`Fetched bundle exportedAt=${new Date(
|
||||||
|
bundle.exportedAt
|
||||||
|
).toLocaleString()}\nbranches=${
|
||||||
|
bundle.branches.length
|
||||||
|
}, commits=${bundle.commits.length}, issues=${
|
||||||
|
bundle.issues.length
|
||||||
|
}`
|
||||||
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert("Cloud pull failed: " + (e?.message || e));
|
alert("Cloud pull failed: " + (e?.message || e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Promise<File | undefined> {
|
async function getFileByPath(
|
||||||
|
dir: FileSystemDirectoryHandle,
|
||||||
|
path: string
|
||||||
|
): Promise<File | undefined> {
|
||||||
const parts = path.split("/");
|
const parts = path.split("/");
|
||||||
let cur: FileSystemDirectoryHandle = dir;
|
let cur: FileSystemDirectoryHandle = dir;
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const name = parts[i];
|
const name = parts[i];
|
||||||
if (i === parts.length - 1) {
|
if (i === parts.length - 1) {
|
||||||
const fh = await (cur as any).getFileHandle(name).catch(() => undefined) as FileSystemFileHandle | undefined;
|
const fh = (await (cur as any)
|
||||||
|
.getFileHandle(name)
|
||||||
|
.catch(() => undefined)) as FileSystemFileHandle | undefined;
|
||||||
return fh ? fh.getFile() : undefined;
|
return fh ? fh.getFile() : undefined;
|
||||||
} else {
|
} else {
|
||||||
cur = await (cur as any).getDirectoryHandle(name).catch(() => undefined);
|
cur = await (cur as any)
|
||||||
|
.getDirectoryHandle(name)
|
||||||
|
.catch(() => undefined);
|
||||||
if (!cur) return undefined as any;
|
if (!cur) return undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -433,26 +856,46 @@ async function getFileByPath(dir: FileSystemDirectoryHandle, path: string): Prom
|
||||||
function languageFromPath(path: string): string {
|
function languageFromPath(path: string): string {
|
||||||
const ext = path.split(".").pop()?.toLowerCase();
|
const ext = path.split(".").pop()?.toLowerCase();
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case "ts": return "typescript";
|
case "ts":
|
||||||
case "tsx": return "typescript";
|
return "typescript";
|
||||||
case "js": return "javascript";
|
case "tsx":
|
||||||
case "jsx": return "javascript";
|
return "typescript";
|
||||||
case "json": return "json";
|
case "js":
|
||||||
case "css": return "css";
|
return "javascript";
|
||||||
case "scss": return "scss";
|
case "jsx":
|
||||||
case "html": return "html";
|
return "javascript";
|
||||||
case "md": return "markdown";
|
case "json":
|
||||||
case "py": return "python";
|
return "json";
|
||||||
case "rb": return "ruby";
|
case "css":
|
||||||
case "go": return "go";
|
return "css";
|
||||||
case "rs": return "rust";
|
case "scss":
|
||||||
case "java": return "java";
|
return "scss";
|
||||||
case "cs": return "csharp";
|
case "html":
|
||||||
case "cpp": return "cpp";
|
return "html";
|
||||||
|
case "md":
|
||||||
|
return "markdown";
|
||||||
|
case "py":
|
||||||
|
return "python";
|
||||||
|
case "rb":
|
||||||
|
return "ruby";
|
||||||
|
case "go":
|
||||||
|
return "go";
|
||||||
|
case "rs":
|
||||||
|
return "rust";
|
||||||
|
case "java":
|
||||||
|
return "java";
|
||||||
|
case "cs":
|
||||||
|
return "csharp";
|
||||||
|
case "cpp":
|
||||||
|
return "cpp";
|
||||||
case "yml":
|
case "yml":
|
||||||
case "yaml": return "yaml";
|
case "yaml":
|
||||||
case "xml": return "xml";
|
return "yaml";
|
||||||
case "sql": return "sql";
|
case "xml":
|
||||||
default: return "plaintext";
|
return "xml";
|
||||||
|
case "sql":
|
||||||
|
return "sql";
|
||||||
|
default:
|
||||||
|
return "plaintext";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
export type StoreName = "repos" | "branches" | "commits" | "issues" | "fsHandles" | "settings";
|
export type StoreName =
|
||||||
|
| "repos"
|
||||||
|
| "branches"
|
||||||
|
| "commits"
|
||||||
|
| "issues"
|
||||||
|
| "fsHandles"
|
||||||
|
| "settings";
|
||||||
|
|
||||||
const DB_NAME = "switch-db";
|
const DB_NAME = "switch-db";
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 2;
|
||||||
|
|
@ -68,12 +74,18 @@ export async function openDB(): Promise<IDBDatabase> {
|
||||||
store.createIndex("by_name", "name", { unique: false });
|
store.createIndex("by_name", "name", { unique: false });
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains("branches")) {
|
if (!db.objectStoreNames.contains("branches")) {
|
||||||
const store = db.createObjectStore("branches", { keyPath: "id" });
|
const store = db.createObjectStore("branches", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
store.createIndex("by_repo", "repoId", { unique: false });
|
store.createIndex("by_repo", "repoId", { unique: false });
|
||||||
store.createIndex("by_repo_name", ["repoId", "name"], { unique: true });
|
store.createIndex("by_repo_name", ["repoId", "name"], {
|
||||||
|
unique: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains("commits")) {
|
if (!db.objectStoreNames.contains("commits")) {
|
||||||
const store = db.createObjectStore("commits", { keyPath: "id" });
|
const store = db.createObjectStore("commits", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
store.createIndex("by_repo", "repoId", { unique: false });
|
store.createIndex("by_repo", "repoId", { unique: false });
|
||||||
}
|
}
|
||||||
if (!db.objectStoreNames.contains("issues")) {
|
if (!db.objectStoreNames.contains("issues")) {
|
||||||
|
|
@ -98,7 +110,11 @@ export async function openDB(): Promise<IDBDatabase> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tx<T>(storeNames: StoreName[], mode: IDBTransactionMode, fn: (tx: IDBTransaction) => Promise<T>): Promise<T> {
|
export async function tx<T>(
|
||||||
|
storeNames: StoreName[],
|
||||||
|
mode: IDBTransactionMode,
|
||||||
|
fn: (tx: IDBTransaction) => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeNames, mode);
|
const transaction = db.transaction(storeNames, mode);
|
||||||
|
|
@ -124,7 +140,10 @@ export async function put<T>(store: StoreName, value: T): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get<T>(store: StoreName, key: IDBValidKey): Promise<T | undefined> {
|
export async function get<T>(
|
||||||
|
store: StoreName,
|
||||||
|
key: IDBValidKey
|
||||||
|
): Promise<T | undefined> {
|
||||||
return tx([store], "readonly", async (t) => {
|
return tx([store], "readonly", async (t) => {
|
||||||
return requestAsPromise<T | undefined>(t.objectStore(store).get(key));
|
return requestAsPromise<T | undefined>(t.objectStore(store).get(key));
|
||||||
});
|
});
|
||||||
|
|
@ -137,7 +156,11 @@ export async function del(store: StoreName, key: IDBValidKey): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllByIndex<T>(store: StoreName, index: string, query: IDBValidKey | IDBKeyRange): Promise<T[]> {
|
export async function getAllByIndex<T>(
|
||||||
|
store: StoreName,
|
||||||
|
index: string,
|
||||||
|
query: IDBValidKey | IDBKeyRange
|
||||||
|
): Promise<T[]> {
|
||||||
return tx([store], "readonly", async (t) => {
|
return tx([store], "readonly", async (t) => {
|
||||||
const idx = t.objectStore(store).index(index);
|
const idx = t.objectStore(store).index(index);
|
||||||
return requestAsPromise<T[]>(idx.getAll(query));
|
return requestAsPromise<T[]>(idx.getAll(query));
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,94 @@
|
||||||
import { put, get, type FsHandleRecord } from "./db";
|
import { put, get, type FsHandleRecord } from "./db";
|
||||||
import { nanoid } from "./uid";
|
import { nanoid } from "./uid";
|
||||||
|
|
||||||
export async function pickDirectory(): Promise<{ id: string; handle: FileSystemDirectoryHandle }> {
|
export async function pickDirectory(): Promise<{
|
||||||
|
id: string;
|
||||||
|
handle: FileSystemDirectoryHandle;
|
||||||
|
}> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const handle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker();
|
const handle: FileSystemDirectoryHandle = await (
|
||||||
|
window as any
|
||||||
|
).showDirectoryPicker();
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const rec: FsHandleRecord = { id, handle };
|
const rec: FsHandleRecord = { id, handle };
|
||||||
await put("fsHandles", rec);
|
await put("fsHandles", rec);
|
||||||
return { id, handle };
|
return { id, handle };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDirectoryHandle(id: string): Promise<FileSystemDirectoryHandle | undefined> {
|
export async function getDirectoryHandle(
|
||||||
|
id: string
|
||||||
|
): Promise<FileSystemDirectoryHandle | undefined> {
|
||||||
const rec = await get<FsHandleRecord>("fsHandles", id);
|
const rec = await get<FsHandleRecord>("fsHandles", id);
|
||||||
return rec?.handle;
|
return rec?.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureReadPerm(dir: FileSystemDirectoryHandle): Promise<boolean> {
|
export async function ensureReadPerm(
|
||||||
|
dir: FileSystemDirectoryHandle
|
||||||
|
): Promise<boolean> {
|
||||||
const perm = await (dir as any).queryPermission?.({ mode: "read" });
|
const perm = await (dir as any).queryPermission?.({ mode: "read" });
|
||||||
if (perm === "granted") return true;
|
if (perm === "granted") return true;
|
||||||
const req = await (dir as any).requestPermission?.({ mode: "read" });
|
const req = await (dir as any).requestPermission?.({ mode: "read" });
|
||||||
return req === "granted";
|
return req === "granted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureWritePerm(dir: FileSystemDirectoryHandle): Promise<boolean> {
|
export async function ensureWritePerm(
|
||||||
|
dir: FileSystemDirectoryHandle
|
||||||
|
): Promise<boolean> {
|
||||||
const perm = await (dir as any).queryPermission?.({ mode: "readwrite" });
|
const perm = await (dir as any).queryPermission?.({ mode: "readwrite" });
|
||||||
if (perm === "granted") return true;
|
if (perm === "granted") return true;
|
||||||
const req = await (dir as any).requestPermission?.({ mode: "readwrite" });
|
const req = await (dir as any).requestPermission?.({ mode: "readwrite" });
|
||||||
return req === "granted";
|
return req === "granted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDirectoryHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise<FileSystemDirectoryHandle | undefined> {
|
export async function getDirectoryHandleByPath(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string,
|
||||||
|
create = false
|
||||||
|
): Promise<FileSystemDirectoryHandle | undefined> {
|
||||||
const parts = path.split("/").filter(Boolean);
|
const parts = path.split("/").filter(Boolean);
|
||||||
let cur: FileSystemDirectoryHandle = root;
|
let cur: FileSystemDirectoryHandle = root;
|
||||||
for (const name of parts) {
|
for (const name of parts) {
|
||||||
const next = await (cur as any).getDirectoryHandle(name, { create }).catch(() => undefined);
|
const next = await (cur as any)
|
||||||
|
.getDirectoryHandle(name, { create })
|
||||||
|
.catch(() => undefined);
|
||||||
if (!next) return undefined;
|
if (!next) return undefined;
|
||||||
cur = next;
|
cur = next;
|
||||||
}
|
}
|
||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileHandleByPath(root: FileSystemDirectoryHandle, path: string, create = false): Promise<FileSystemFileHandle | undefined> {
|
export async function getFileHandleByPath(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string,
|
||||||
|
create = false
|
||||||
|
): Promise<FileSystemFileHandle | undefined> {
|
||||||
const parts = path.split("/");
|
const parts = path.split("/");
|
||||||
const dirPath = parts.slice(0, -1).join("/");
|
const dirPath = parts.slice(0, -1).join("/");
|
||||||
const fileName = parts[parts.length - 1];
|
const fileName = parts[parts.length - 1];
|
||||||
const dir = dirPath ? await getDirectoryHandleByPath(root, dirPath, create) : root;
|
const dir = dirPath
|
||||||
|
? await getDirectoryHandleByPath(root, dirPath, create)
|
||||||
|
: root;
|
||||||
if (!dir) return undefined;
|
if (!dir) return undefined;
|
||||||
return (dir as any).getFileHandle(fileName, { create }).catch(() => undefined);
|
return (dir as any)
|
||||||
|
.getFileHandle(fileName, { create })
|
||||||
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readFileText(root: FileSystemDirectoryHandle, path: string): Promise<string | undefined> {
|
export async function readFileText(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
const fh = await getFileHandleByPath(root, path);
|
const fh = await getFileHandleByPath(root, path);
|
||||||
if (!fh) return undefined;
|
if (!fh) return undefined;
|
||||||
const file = await fh.getFile();
|
const file = await fh.getFile();
|
||||||
return file.text();
|
return file.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeFileText(root: FileSystemDirectoryHandle, path: string, content: string): Promise<void> {
|
export async function writeFileText(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string,
|
||||||
|
content: string
|
||||||
|
): Promise<void> {
|
||||||
const dirPerm = await ensureWritePerm(root);
|
const dirPerm = await ensureWritePerm(root);
|
||||||
if (!dirPerm) throw new Error("No write permission");
|
if (!dirPerm) throw new Error("No write permission");
|
||||||
const fh = await getFileHandleByPath(root, path, true);
|
const fh = await getFileHandleByPath(root, path, true);
|
||||||
|
|
@ -66,13 +98,20 @@ export async function writeFileText(root: FileSystemDirectoryHandle, path: strin
|
||||||
await w.close();
|
await w.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDirectory(root: FileSystemDirectoryHandle, path: string): Promise<void> {
|
export async function createDirectory(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string
|
||||||
|
): Promise<void> {
|
||||||
const ok = await ensureWritePerm(root);
|
const ok = await ensureWritePerm(root);
|
||||||
if (!ok) throw new Error("No write permission");
|
if (!ok) throw new Error("No write permission");
|
||||||
await getDirectoryHandleByPath(root, path, true);
|
await getDirectoryHandleByPath(root, path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntry(root: FileSystemDirectoryHandle, path: string, recursive = false): Promise<void> {
|
export async function deleteEntry(
|
||||||
|
root: FileSystemDirectoryHandle,
|
||||||
|
path: string,
|
||||||
|
recursive = false
|
||||||
|
): Promise<void> {
|
||||||
const parts = path.split("/");
|
const parts = path.split("/");
|
||||||
const dirPath = parts.slice(0, -1).join("/");
|
const dirPath = parts.slice(0, -1).join("/");
|
||||||
const name = parts[parts.length - 1];
|
const name = parts[parts.length - 1];
|
||||||
|
|
@ -81,7 +120,10 @@ export async function deleteEntry(root: FileSystemDirectoryHandle, path: string,
|
||||||
await (dir as any).removeEntry(name, { recursive }).catch(() => undefined);
|
await (dir as any).removeEntry(name, { recursive }).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): AsyncGenerator<{ path: string; file: File }>{
|
export async function* walk(
|
||||||
|
dir: FileSystemDirectoryHandle,
|
||||||
|
pathPrefix = ""
|
||||||
|
): AsyncGenerator<{ path: string; file: File }> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for await (const [name, entry] of (dir as any).entries()) {
|
for await (const [name, entry] of (dir as any).entries()) {
|
||||||
const p = pathPrefix ? `${pathPrefix}/${name}` : name;
|
const p = pathPrefix ? `${pathPrefix}/${name}` : name;
|
||||||
|
|
@ -94,17 +136,24 @@ export async function* walk(dir: FileSystemDirectoryHandle, pathPrefix = ""): As
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyDirectory(src: FileSystemDirectoryHandle, dest: FileSystemDirectoryHandle): Promise<void> {
|
export async function copyDirectory(
|
||||||
|
src: FileSystemDirectoryHandle,
|
||||||
|
dest: FileSystemDirectoryHandle
|
||||||
|
): Promise<void> {
|
||||||
const ok = await ensureWritePerm(dest);
|
const ok = await ensureWritePerm(dest);
|
||||||
if (!ok) throw new Error("No write permission on destination");
|
if (!ok) throw new Error("No write permission on destination");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for await (const [name, entry] of (src as any).entries()) {
|
for await (const [name, entry] of (src as any).entries()) {
|
||||||
if (entry.kind === "directory") {
|
if (entry.kind === "directory") {
|
||||||
const sub = await (dest as any).getDirectoryHandle(name, { create: true });
|
const sub = await (dest as any).getDirectoryHandle(name, {
|
||||||
|
create: true,
|
||||||
|
});
|
||||||
await copyDirectory(entry as FileSystemDirectoryHandle, sub);
|
await copyDirectory(entry as FileSystemDirectoryHandle, sub);
|
||||||
} else {
|
} else {
|
||||||
const file = await (entry as FileSystemFileHandle).getFile();
|
const file = await (entry as FileSystemFileHandle).getFile();
|
||||||
const fh = await (dest as any).getFileHandle(name, { create: true });
|
const fh = await (dest as any).getFileHandle(name, {
|
||||||
|
create: true,
|
||||||
|
});
|
||||||
const w = await (fh as any).createWritable();
|
const w = await (fh as any).createWritable();
|
||||||
await w.write(await file.arrayBuffer());
|
await w.write(await file.arrayBuffer());
|
||||||
await w.close();
|
await w.close();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
const url = process.env.SUPABASE_URL as string;
|
const url = process.env.SUPABASE_URL as string;
|
||||||
const key = process.env.SUPABASE_ANON_KEY as string;
|
const key = process.env.SUPABASE_ANON_KEY as string;
|
||||||
export const supabase: SupabaseClient | undefined = url && key ? createClient(url, key) : undefined;
|
export const supabase: SupabaseClient | undefined =
|
||||||
|
url && key ? createClient(url, key) : undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { getAll, getAllByIndex, type RepoRecord, type BranchRecord, type CommitRecord, type IssueRecord } from './db';
|
import {
|
||||||
import { supabase } from './supabase';
|
getAll,
|
||||||
|
getAllByIndex,
|
||||||
|
type RepoRecord,
|
||||||
|
type BranchRecord,
|
||||||
|
type CommitRecord,
|
||||||
|
type IssueRecord,
|
||||||
|
} from "./db";
|
||||||
|
import { supabase } from "./supabase";
|
||||||
|
|
||||||
export interface ExportBundle {
|
export interface ExportBundle {
|
||||||
repo: RepoRecord;
|
repo: RepoRecord;
|
||||||
|
|
@ -10,27 +17,47 @@ export interface ExportBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportRepoBundle(repoId: string): Promise<ExportBundle> {
|
export async function exportRepoBundle(repoId: string): Promise<ExportBundle> {
|
||||||
const repos = await getAll<RepoRecord>('repos');
|
const repos = await getAll<RepoRecord>("repos");
|
||||||
const repo = repos.find(r=>r.id===repoId)!;
|
const repo = repos.find((r) => r.id === repoId)!;
|
||||||
const branches = await getAllByIndex<BranchRecord>('branches','by_repo', repoId);
|
const branches = await getAllByIndex<BranchRecord>(
|
||||||
const commits = await getAllByIndex<CommitRecord>('commits','by_repo', repoId);
|
"branches",
|
||||||
const issues = await getAllByIndex<IssueRecord>('issues','by_repo', repoId);
|
"by_repo",
|
||||||
|
repoId
|
||||||
|
);
|
||||||
|
const commits = await getAllByIndex<CommitRecord>(
|
||||||
|
"commits",
|
||||||
|
"by_repo",
|
||||||
|
repoId
|
||||||
|
);
|
||||||
|
const issues = await getAllByIndex<IssueRecord>(
|
||||||
|
"issues",
|
||||||
|
"by_repo",
|
||||||
|
repoId
|
||||||
|
);
|
||||||
return { repo, branches, commits, issues, exportedAt: Date.now() };
|
return { repo, branches, commits, issues, exportedAt: Date.now() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushRepoToSupabase(repoId: string): Promise<void> {
|
export async function pushRepoToSupabase(repoId: string): Promise<void> {
|
||||||
if (!supabase) throw new Error('Supabase not configured');
|
if (!supabase) throw new Error("Supabase not configured");
|
||||||
const bundle = await exportRepoBundle(repoId);
|
const bundle = await exportRepoBundle(repoId);
|
||||||
const path = `repos/${repoId}.json`;
|
const path = `repos/${repoId}.json`;
|
||||||
const data = new Blob([JSON.stringify(bundle)], { type: 'application/json' });
|
const data = new Blob([JSON.stringify(bundle)], {
|
||||||
const { error } = await supabase.storage.from('switch').upload(path, data, { upsert: true });
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const { error } = await supabase.storage
|
||||||
|
.from("switch")
|
||||||
|
.upload(path, data, { upsert: true });
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pullRepoFromSupabase(repoId: string): Promise<ExportBundle | undefined> {
|
export async function pullRepoFromSupabase(
|
||||||
if (!supabase) throw new Error('Supabase not configured');
|
repoId: string
|
||||||
|
): Promise<ExportBundle | undefined> {
|
||||||
|
if (!supabase) throw new Error("Supabase not configured");
|
||||||
const path = `repos/${repoId}.json`;
|
const path = `repos/${repoId}.json`;
|
||||||
const { data, error } = await supabase.storage.from('switch').download(path);
|
const { data, error } = await supabase.storage
|
||||||
|
.from("switch")
|
||||||
|
.download(path);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
const text = await data.text();
|
const text = await data.text();
|
||||||
return JSON.parse(text) as ExportBundle;
|
return JSON.parse(text) as ExportBundle;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
import { get, put, del, getAll, type RepoTemplateRecord, type IssueTemplateRecord } from "./db";
|
import {
|
||||||
|
get,
|
||||||
|
put,
|
||||||
|
del,
|
||||||
|
getAll,
|
||||||
|
type RepoTemplateRecord,
|
||||||
|
type IssueTemplateRecord,
|
||||||
|
} from "./db";
|
||||||
import { nanoid } from "./uid";
|
import { nanoid } from "./uid";
|
||||||
|
|
||||||
export async function createRepoTemplate(name: string, fsHandleId: string): Promise<RepoTemplateRecord> {
|
export async function createRepoTemplate(
|
||||||
|
name: string,
|
||||||
|
fsHandleId: string
|
||||||
|
): Promise<RepoTemplateRecord> {
|
||||||
const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId };
|
const rec: RepoTemplateRecord = { id: nanoid(), name, fsHandleId };
|
||||||
await put("repoTemplates" as any, rec as any);
|
await put("repoTemplates" as any, rec as any);
|
||||||
return rec;
|
return rec;
|
||||||
|
|
@ -13,7 +23,12 @@ export async function deleteRepoTemplate(id: string): Promise<void> {
|
||||||
await del("repoTemplates" as any, id);
|
await del("repoTemplates" as any, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createIssueTemplate(data: { name: string; title: string; body: string; labels: string[] }): Promise<IssueTemplateRecord> {
|
export async function createIssueTemplate(data: {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
labels: string[];
|
||||||
|
}): Promise<IssueTemplateRecord> {
|
||||||
const rec: IssueTemplateRecord = { id: nanoid(), ...data };
|
const rec: IssueTemplateRecord = { id: nanoid(), ...data };
|
||||||
await put("issueTemplates" as any, rec as any);
|
await put("issueTemplates" as any, rec as any);
|
||||||
return rec;
|
return rec;
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@ module.exports = {
|
||||||
"process.env": {
|
"process.env": {
|
||||||
YEAR: JSON.stringify(new Date().getFullYear()),
|
YEAR: JSON.stringify(new Date().getFullYear()),
|
||||||
SUPABASE_URL: JSON.stringify(process.env.SUPABASE_URL || ""),
|
SUPABASE_URL: JSON.stringify(process.env.SUPABASE_URL || ""),
|
||||||
SUPABASE_ANON_KEY: JSON.stringify(process.env.SUPABASE_ANON_KEY || ""),
|
SUPABASE_ANON_KEY: JSON.stringify(
|
||||||
|
process.env.SUPABASE_ANON_KEY || ""
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
|
|
@ -119,7 +121,11 @@ module.exports = {
|
||||||
}),
|
}),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: "./typedoc/dist", to: "./typedoc/", noErrorOnMissing: true },
|
{
|
||||||
|
from: "./typedoc/dist",
|
||||||
|
to: "./typedoc/",
|
||||||
|
noErrorOnMissing: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue