1. Tab user repos: fetch username via /auth/user → affiche 'bruno' au lieu de 'Personal' 2. Sidebar: ws_cookie='gitea:owner:repo' → _sidebar_data() fetch le tree depuis Gitea API 3. base.html: data-gitea-path/type/sha sur les items workspace pour les projets Gitea 4. gitea_workspace.html: refonte sans tree panel → navigation via sidebar - Clic fichier → ouvre dans le contenu (lecture + edit + commit) - Clic dossier → lazy-load les enfants dans le sidebar 5. openGitea(): set cookie flowdeck_workspace avant navigation
345 lines
15 KiB
HTML
345 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_title %}Workspaces{% endblock %}
|
|
{% block page_icon %}🏠{% endblock %}
|
|
|
|
{% block topbar %}
|
|
<header class="topbar">
|
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
</button>
|
|
<div class="topbar-left">
|
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
|
|
<span class="page-title-topbar">Workspaces</span>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<button class="page-action-btn" onclick="window.location='/accounts/settings'" title="Settings">⚙</button>
|
|
</div>
|
|
</header>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.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, 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: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;}
|
|
.dialog-input{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;margin-bottom:12px;outline:none;}
|
|
.dialog-input:focus{border-color:var(--accent);}
|
|
.dialog-actions{display:flex;gap:8px;justify-content:flex-end;}
|
|
.btn{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
|
|
.btn-primary{background:var(--accent);color:#fff;}
|
|
.btn-primary:hover{background:var(--accent-hover);}
|
|
.btn-secondary{background:var(--bg-tertiary);color:var(--text);}
|
|
.btn-secondary:hover{background:var(--bg-hover);}
|
|
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
|
|
.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);}
|
|
/* Org tabs */
|
|
.org-tab{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:13px;color:var(--text-dim);background:transparent;border:1px solid var(--border);cursor:pointer;white-space:nowrap;transition:all 100ms;}
|
|
.org-tab:hover{background:var(--bg-tertiary);color:var(--text);}
|
|
.org-tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
|
.org-tab-badge{font-size:10px;padding:0 5px;border-radius:8px;background:rgba(255,255,255,.15);}
|
|
.search-input:focus{border-color:var(--accent);}
|
|
</style>
|
|
|
|
<div class="ws-page" x-data="workspacesPage()">
|
|
|
|
<div class="ws-header">
|
|
<h1>🏠 Workspaces</h1>
|
|
</div>
|
|
|
|
<!-- ── 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">🔗 Connect Gitea</a>
|
|
<span style="margin:0 8px;color:var(--text-dim);">or</span>
|
|
<a class="gitea-connect" href="/auth/login?provider=gitea">📝 Register 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 (tabs by org + search) -->
|
|
<div x-show="giteaStatus==='ok' && giteaGroups.length > 0">
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
|
|
<!-- Org tabs -->
|
|
<div style="display:flex;gap:4px;overflow-x:auto;flex:1;min-width:0;">
|
|
<button class="org-tab" :class="{active:activeOrg==='all'}" @click="activeOrg='all'">
|
|
All <span class="org-tab-badge" x-text="giteaTotal"></span>
|
|
</button>
|
|
<template x-for="g in giteaGroups" :key="g.org">
|
|
<button class="org-tab" :class="{active:activeOrg===g.org}" @click="activeOrg=g.org">
|
|
<span x-text="g.org"></span>
|
|
<span class="org-tab-badge" x-text="g.projects.length"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<!-- Search -->
|
|
<input type="text" class="search-input" x-model="giteaSearch" placeholder="🔍 Filter projects…" style="width:200px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;">
|
|
</div>
|
|
|
|
<!-- Project cards (filtered) -->
|
|
<template x-for="group in filteredGroups" :key="group.org">
|
|
<div class="gitea-org">
|
|
<div class="gitea-org-header">
|
|
<span class="gitea-org-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="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 || proj.language || ''" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="gitea-empty" x-show="filteredGroups.length===0" style="margin-top:12px;">
|
|
No projects match your search.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No projects at all -->
|
|
<div class="gitea-empty" x-show="giteaStatus==='ok' && giteaTotal==0 && giteaGroups.length===0">
|
|
No Gitea projects found.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Rename dialog -->
|
|
<div class="dialog-overlay" x-show="showCreate||showRename" @click.away="showCreate=false;showRename=false">
|
|
<div class="dialog-box">
|
|
<h3 x-text="showRename?'Rename Workspace':'New Workspace'"></h3>
|
|
<input class="dialog-input" x-model="wsName" placeholder="Workspace name" @keydown.enter="showRename?doRename():doCreate()">
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" @click="showCreate=false;showRename=false">Cancel</button>
|
|
<button class="btn btn-primary" @click="showRename?doRename():doCreate()" x-text="showRename?'Rename':'Create'"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function workspacesPage() {
|
|
return {
|
|
workspaces: [],
|
|
activeId: null,
|
|
showCreate: false,
|
|
showRename: false,
|
|
wsName: '',
|
|
renameTarget: null,
|
|
|
|
// Gitea
|
|
giteaStatus: 'loading', // loading|ok|not_linked|error
|
|
giteaGroups: [],
|
|
giteaTotal: 0,
|
|
activeOrg: 'all',
|
|
giteaSearch: '',
|
|
|
|
get filteredGroups() {
|
|
var self = this;
|
|
var q = (this.giteaSearch || '').toLowerCase();
|
|
var groups = this.activeOrg === 'all' ? this.giteaGroups : this.giteaGroups.filter(function(g) { return g.org === self.activeOrg; });
|
|
if (!q) return groups;
|
|
return groups.map(function(g) {
|
|
return {org: g.org, avatar: g.avatar, projects: g.projects.filter(function(p) {
|
|
return p.name.toLowerCase().includes(q) || (p.description||'').toLowerCase().includes(q);
|
|
})};
|
|
}).filter(function(g) { return g.projects.length > 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 selectLocal(ws) {
|
|
await fetch(`/api/workspaces/${ws.id}/select`, {method:'POST'});
|
|
window.location = '/local-workspace';
|
|
},
|
|
|
|
async doCreate() {
|
|
if (!this.wsName.trim()) return;
|
|
await fetch('/api/workspaces', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:this.wsName.trim()})
|
|
});
|
|
this.wsName = '';
|
|
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}`, {
|
|
method:'PUT',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:this.wsName.trim()})
|
|
});
|
|
this.wsName = '';
|
|
this.showRename = false;
|
|
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 = [];
|
|
|
|
// Get username for the "personal" tab
|
|
let myLogin = 'Me';
|
|
try {
|
|
const meRes = await fetch('/auth/user');
|
|
if (meRes.ok) {
|
|
const meData = await meRes.json();
|
|
myLogin = meData.user?.login || 'Me';
|
|
if (myLogin.includes('_')) myLogin = myLogin.split('_').pop(); // strip gitea_ prefix if present
|
|
}
|
|
} catch(e) {}
|
|
|
|
// 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: myLogin, avatar: '', projects: repos.map(r => ({...r, owner: r.owner || {login: myLogin}}))});
|
|
}
|
|
}
|
|
} 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) {
|
|
// Set workspace cookie so sidebar shows tree
|
|
document.cookie = 'flowdeck_workspace=gitea:' + owner + ':' + repo + ';path=/;SameSite=Lax';
|
|
window.location = `/gitea-workspace?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|