feat: frontend Gitea — workspaces 2 sections + gitea workspace vue
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

workspaces.html:
- Section 📁 Local Workspaces (existante, avec badges)
- Section 🔗 Gitea Projects (par organisation, lazy load)
- États: loading, not_linked (lien pour login OAuth), error (retry), ok

gitea_workspace.html:
- Arbre fichiers (lazy: un dossier à la fois)
- Vue fichier (lecture seule → bouton Edit)
- Éditeur avec zone de commit (message + historique dropdown)
- Section 🔒 Private Pages (placeholder pour pages FlowDeck locales)
- Bouton Delete fichier

Routing: /gitea-workspace?owner=X&repo=Y

Fallback: token admin serveur si pas de token OAuth utilisateur
This commit is contained in:
2026-07-13 14:58:01 -04:00
parent 00860417a8
commit 46336df21b
4 changed files with 513 additions and 23 deletions
+155 -20
View File
@@ -19,19 +19,33 @@
{% block content %}
<style>
.ws-page{max-width:800px;margin:0 auto;padding:40px 24px;}
.ws-page{max-width:1000px;margin:0 auto;padding:40px 24px;}
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;}
.ws-header h1{font-size:24px;}
.ws-section{margin-bottom:40px;}
.ws-section-title{font-size:14px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;display:flex;align-items:center;gap:8px;}
.ws-section-title .badge{font-size:11px;background:var(--bg-tertiary);color:var(--text-dim);padding:1px 8px;border-radius:10px;}
.ws-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;}
.ws-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px;cursor:pointer;transition:background 150ms;}
.ws-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px;cursor:pointer;transition:background 150ms, border-color 150ms;}
.ws-card:hover{background:var(--bg-tertiary);border-color:var(--border-strong);}
.ws-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
.ws-card-name{font-size:16px;font-weight:500;margin-bottom:4px;}
.ws-card-name{font-size:15px;font-weight:500;margin-bottom:2px;}
.ws-card-sub{font-size:12px;color:var(--text-tertiary);margin-bottom:4px;}
.ws-card-count{font-size:12px;color:var(--text-tertiary);}
.ws-card-actions{display:flex;gap:4px;margin-top:8px;opacity:0;transition:opacity 150ms;}
.ws-card:hover .ws-card-actions{opacity:1;}
/* Gitea org grouping */
.gitea-org{margin-bottom:16px;}
.gitea-org-header{font-size:14px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px;margin-bottom:8px;}
.gitea-org-header img{border-radius:4px;}
.gitea-org-avatar{width:24px;height:24px;border-radius:4px;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;font-size:14px;}
.gitea-empty{padding:24px;text-align:center;color:var(--text-dim);font-size:14px;border:1px dashed var(--border);border-radius:var(--radius);}
.gitea-connect{display:inline-block;margin-top:8px;color:var(--accent);cursor:pointer;font-size:13px;}
.gitea-connect:hover{text-decoration:underline;}
.gitea-loading{color:var(--text-dim);font-size:13px;padding:12px 0;}
.dialog-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:500;}
.dialog-box{background:#1F1F1F;border:1px solid var(--border);border-radius:18px;padding:24px;width:400px;max-width:90vw;}
.dialog-box h3{margin-bottom:16px;}
@@ -47,28 +61,85 @@
.btn-danger:hover{background:rgba(255,80,80,.25);}
.btn-new{border:1px dashed var(--border-strong);background:transparent;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;min-height:120px;}
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
.btn-sm{padding:4px 8px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;}
.btn-sm:hover{background:var(--bg-tertiary);color:var(--text);}
</style>
<div class="ws-page" x-data="workspacesPage()">
<div class="ws-header">
<h1>🏠 Workspaces</h1>
<button class="btn btn-primary" @click="showCreate=true">+ New Workspace</button>
</div>
<div class="ws-grid">
<template x-for="ws in workspaces" :key="ws.id">
<div class="ws-card" :class="{active:ws.id===activeId}" @click="select(ws)">
<div class="ws-card-name" x-text="ws.name"></div>
<div class="ws-card-count" x-text="ws.page_count+' page(s)'"></div>
<div class="ws-card-actions" @click.stop>
<button class="btn btn-secondary" style="padding:4px 8px;font-size:11px" @click="renameWs(ws)">✏️</button>
<button class="btn btn-danger" style="padding:4px 8px;font-size:11px" @click="deleteWs(ws)">🗑</button>
<!-- ── Local Workspaces ── -->
<div class="ws-section">
<div class="ws-section-title">📁 My Workspaces <span class="badge" x-text="workspaces.length"></span></div>
<div class="ws-grid">
<template x-for="ws in workspaces" :key="'local-'+ws.id">
<div class="ws-card" :class="{active:ws.id===activeId}" @click="selectLocal(ws)">
<div class="ws-card-name" x-text="ws.name"></div>
<div class="ws-card-count" x-text="ws.page_count+' page(s)'"></div>
<div class="ws-card-actions" @click.stop>
<button class="btn-sm" @click="renameWs(ws)">✏️</button>
<button class="btn-sm" style="color:var(--danger);border-color:rgba(255,80,80,.3)" @click="deleteWs(ws)">🗑</button>
</div>
</div>
</template>
<div class="ws-card btn-new" @click="showCreate=true">
<span style="font-size:32px">+</span>
</div>
</div>
</div>
<!-- ── Gitea Projects ── -->
<div class="ws-section">
<div class="ws-section-title">🔗 Gitea Projects <span class="badge" x-text="giteaTotal"></span></div>
<!-- Not linked -->
<div class="gitea-empty" x-show="giteaStatus==='not_linked'">
<div>Connect your Gitea account to see your projects.</div>
<a class="gitea-connect" href="/auth/login?provider=gitea">🔗 Login with Gitea</a>
</div>
<!-- Loading -->
<div class="gitea-loading" x-show="giteaStatus==='loading'">Loading Gitea projects…</div>
<!-- Error -->
<div class="gitea-empty" x-show="giteaStatus==='error'">
<div>⚠ Could not load Gitea projects.</div>
<button class="gitea-connect" @click="loadGitea()">Retry</button>
</div>
<!-- Projects (grouped by org) -->
<template x-for="group in giteaGroups" :key="group.org">
<div class="gitea-org">
<div class="gitea-org-header">
<img x-show="group.avatar" :src="group.avatar" class="gitea-org-avatar" alt="">
<span class="gitea-org-avatar" x-show="!group.avatar" x-text="group.org.charAt(0).toUpperCase()"></span>
<span x-text="group.org"></span>
</div>
<div class="ws-grid">
<template x-for="proj in group.projects" :key="proj.id">
<div class="ws-card" @click="openGitea(proj)">
<div class="ws-card-name" x-text="proj.name"></div>
<div class="ws-card-sub">
<span x-text="proj.owner?.login || group.org"></span>
<span style="margin:0 4px">/</span>
<span x-text="proj.name"></span>
</div>
<div class="ws-card-count">
<span x-show="proj.private" style="margin-right:6px">🔒</span>
<span x-text="proj.description || ''" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;"></span>
</div>
</div>
</template>
</div>
</div>
</template>
<div class="ws-card btn-new" @click="showCreate=true">
<span style="font-size:32px">+</span>
<!-- No projects in orgs -->
<div class="gitea-empty" x-show="giteaStatus==='ok' && giteaTotal==0">
No Gitea projects found.
</div>
</div>
@@ -96,23 +167,28 @@ function workspacesPage() {
wsName: '',
renameTarget: null,
// Gitea
giteaStatus: 'loading', // loading|ok|not_linked|error
giteaGroups: [],
giteaTotal: 0,
async init() {
await this.load();
await this.loadGitea();
},
async load() {
const r = await fetch('/api/workspaces');
const d = await r.json();
this.workspaces = d.workspaces || [];
this.activeId = d.active_id;
},
async select(ws) {
async selectLocal(ws) {
await fetch(`/api/workspaces/${ws.id}/select`, {method:'POST'});
if (ws.page_count > 0) {
window.location = '/local-workspace';
} else {
window.location = '/local-workspace';
}
window.location = '/local-workspace';
},
async doCreate() {
if (!this.wsName.trim()) return;
await fetch('/api/workspaces', {
@@ -124,11 +200,13 @@ function workspacesPage() {
this.showCreate = false;
await this.load();
},
renameWs(ws) {
this.renameTarget = ws;
this.wsName = ws.name;
this.showRename = true;
},
async doRename() {
if (!this.wsName.trim()||!this.renameTarget) return;
await fetch(`/api/workspaces/${this.renameTarget.id}`, {
@@ -141,10 +219,67 @@ function workspacesPage() {
this.renameTarget = null;
await this.load();
},
async deleteWs(ws) {
if (!confirm(`Delete workspace "${ws.name}" and all its pages?`)) return;
await fetch(`/api/workspaces/${ws.id}`, {method:'DELETE'});
await this.load();
},
// ── Gitea ──
async loadGitea() {
this.giteaStatus = 'loading';
try {
// Get orgs
const orgsRes = await fetch('/api/gitea/orgs');
if (orgsRes.status === 401) { this.giteaStatus = 'not_linked'; return; }
if (!orgsRes.ok) { this.giteaStatus = 'error'; return; }
const orgsData = await orgsRes.json();
const orgs = orgsData.orgs || [];
// Fetch repos: user repos + org repos
const groups = [];
// User repos (no org filter)
try {
const userRes = await fetch('/api/gitea/projects');
if (userRes.ok) {
const d = await userRes.json();
const repos = d.projects || [];
if (repos.length) {
groups.push({org: 'Personal', avatar: '', projects: repos.map(r => ({...r, owner: r.owner || {login: '~'}}))});
}
}
} catch(e) {}
// Org repos
for (const org of orgs) {
try {
const res = await fetch('/api/gitea/projects?org=' + encodeURIComponent(org.username || org.login || org.name));
if (res.ok) {
const d = await res.json();
if (d.projects && d.projects.length) {
groups.push({org: org.username || org.login || org.name, avatar: org.avatar_url || '', projects: d.projects});
}
}
} catch(e) {}
}
this.giteaGroups = groups;
this.giteaTotal = groups.reduce((sum, g) => sum + g.projects.length, 0);
this.giteaStatus = 'ok';
} catch(e) {
this.giteaStatus = 'error';
}
},
openGitea(proj) {
const owner = proj.owner?.login || proj.owner?.username || proj.full_name?.split('/')[0] || '';
const repo = proj.name;
if (owner && repo) {
window.location = `/gitea-workspace?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
}
}
};
}