Files
flowdeck/app/templates/gitea_workspace.html
T
bruno 46336df21b
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
feat: frontend Gitea — workspaces 2 sections + gitea workspace vue
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
2026-07-13 14:58:01 -04:00

341 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block page_title %}Gitea Workspace{% 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">
<a href="/workspaces" class="breadcrumb" style="color:rgba(255,255,255,.5);text-decoration:none;">Home</a>
<span class="page-title-topbar" x-text="owner + '/' + repo"></span>
</div>
<div class="topbar-right">
<button class="page-action-btn" @click="refresh()" title="Refresh">🔄</button>
<button class="page-action-btn" onclick="window.location='/accounts/settings'" title="Settings">⚙</button>
</div>
</header>
{% endblock %}
{% block content %}
<style>
.gw-layout{display:flex;height:calc(100vh - 48px);}
.gw-tree{width:280px;min-width:280px;background:var(--bg-sidebar);border-right:1px solid var(--border);overflow-y:auto;padding:12px 0;}
.gw-tree-item{display:flex;align-items:center;gap:6px;padding:4px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.gw-tree-item:hover{background:var(--bg-tertiary);color:var(--text);}
.gw-tree-item.selected{background:var(--accent);color:#fff;}
.gw-tree-icon{font-size:14px;flex-shrink:0;}
.gw-tree-indent{padding-left:32px !important;}
.gw-tree-indent2{padding-left:48px !important;}
.gw-tree-section-title{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;padding:8px 16px 4px;margin-top:8px;border-top:1px solid var(--border);}
.gw-main{flex:1;display:flex;flex-direction:column;overflow:hidden;}
.gw-content{padding:24px;overflow-y:auto;flex:1;}
.gw-content h3{font-size:16px;margin-bottom:12px;color:var(--text-dim);}
.gw-content pre{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:16px;overflow-x:auto;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:70vh;overflow-y:auto;}
.gw-content .empty-state{text-align:center;padding:60px 20px;color:var(--text-dim);}
.gw-content .empty-state h2{font-size:18px;margin-bottom:8px;color:var(--text);}
.gw-content textarea{width:100%;min-height:50vh;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;padding:12px;font-family:monospace;resize:vertical;outline:none;}
.gw-content textarea:focus{border-color:var(--accent);}
.gw-commit-bar{display:flex;align-items:center;gap:8px;padding:12px 24px;background:var(--bg-secondary);border-top:1px solid var(--border);flex-wrap:wrap;}
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
.gw-commit-bar input:focus{border-color:var(--accent);}
.gw-file-toolbar{display:flex;align-items:center;gap:8px;padding:8px 24px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:13px;color:var(--text-dim);}
.gw-file-toolbar strong{color:var(--text);margin-right:12px;}
.btn-primary{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;background:var(--accent);color:#fff;}
.btn-primary:hover{background:var(--accent-hover);}
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
.btn-danger:hover{background:rgba(255,80,80,.25);}
.btn{border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;font-weight:500;border:1px solid var(--border);background:transparent;color:var(--text-dim);}
.btn:hover{background:var(--bg-tertiary);color:var(--text);}
.loading-spinner{text-align:center;padding:20px;color:var(--text-dim);}
</style>
<div class="gw-layout" x-data="giteaWorkspace()">
<!-- Tree sidebar -->
<div class="gw-tree">
<div class="gw-tree-section-title">📁 GITEA REPO</div>
<!-- Loading -->
<div class="loading-spinner" x-show="loading">Loading…</div>
<!-- Tree items (recursive rendering) -->
<template x-for="item in treeItems" :key="item.path">
<div>
<div class="gw-tree-item"
:class="{selected: item.path === selectedPath}"
:style="'padding-left:' + (16 + item.depth * 16) + 'px'"
@click="item.type==='folder' ? toggleFolder(item) : openFile(item)">
<span class="gw-tree-icon" x-text="item.type==='folder' ? (item.expanded ? '📂' : '📁') : '📄'"></span>
<span x-text="item.name" style="overflow:hidden;text-overflow:ellipsis;"></span>
</div>
<!-- children -->
<template x-if="item.expanded">
<template x-for="child in item.children" :key="child.path">
<div class="gw-tree-item"
:class="{selected: child.path === selectedPath}"
:style="'padding-left:' + (16 + child.depth * 16) + 'px'"
@click="child.type==='folder' ? toggleFolder(child) : openFile(child)">
<span class="gw-tree-icon" x-text="child.type==='folder' ? (child.expanded ? '📂' : '📁') : '📄'"></span>
<span x-text="child.name" style="overflow:hidden;text-overflow:ellipsis;"></span>
</div>
</template>
</template>
</div>
</template>
<!-- Private section -->
<div class="gw-tree-section-title">🔒 PRIVATE PAGES</div>
<div class="gw-tree-item" @click="selectedPath='--private--'; showEditor=false; showFile=false;">
<span class="gw-tree-icon">📝</span>
<span>My notes</span>
</div>
<div class="gw-tree-item" @click="newPrivatePage()" style="color:var(--accent);">
<span class="gw-tree-icon">➕</span>
<span>New page</span>
</div>
</div>
<!-- Main content -->
<div class="gw-main">
<!-- Private pages view -->
<template x-if="selectedPath==='--private--'">
<div class="gw-content">
<template x-if="privatePages.length === 0">
<div class="empty-state">
<h2>🔒 Private Pages</h2>
<p>Create FlowDeck pages attached to this Gitea project. These are stored locally, not in the repo.</p>
<button class="btn btn-primary" @click="newPrivatePage()" style="margin-top:12px;">+ New Page</button>
</div>
</template>
<template x-for="pp in privatePages" :key="pp.id">
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:8px;cursor:pointer;"
@click="openPrivatePage(pp)">
<div style="font-weight:500;margin-bottom:4px;" x-text="pp.title"></div>
<div style="font-size:12px;color:var(--text-dim);" x-text="pp.updated_at ? 'Updated ' + new Date(pp.updated_at).toLocaleDateString() : ''"></div>
</div>
</template>
</div>
</template>
<!-- File view -->
<template x-if="showFile && !showEditor">
<div>
<div class="gw-file-toolbar">
<strong x-text="currentFile?.name || ''"></strong>
<span x-text="formatSize(currentFile?.size || 0)"></span>
<span style="flex:1;"></span>
<button class="btn" @click="openEditor()">✏️ Edit</button>
<button class="btn btn-danger" @click="deleteCurrentFile()">🗑 Delete</button>
</div>
<div class="gw-content">
<pre x-text="fileContent"></pre>
</div>
</div>
</template>
<!-- Editor view -->
<template x-if="showEditor">
<div>
<div class="gw-file-toolbar">
<strong>Editing: <span x-text="currentFile?.name || 'new file'"></span></strong>
<span style="flex:1;"></span>
<button class="btn" @click="showEditor=false">Cancel</button>
</div>
<div class="gw-content">
<textarea x-model="editContent"></textarea>
</div>
<div class="gw-commit-bar">
<input type="text" x-model="commitMessage" placeholder="Commit message" list="commit-history">
<datalist id="commit-history">
<template x-for="msg in commitHistory" :key="msg">
<option :value="msg">
</template>
</datalist>
<button class="btn-primary" @click="saveFile()">💾 Commit</button>
</div>
</div>
</template>
<!-- Empty state -->
<template x-if="!showFile && !showEditor && selectedPath!=='--private--'">
<div class="gw-content">
<div class="empty-state">
<h2>📁 <span x-text="owner + '/' + repo"></span></h2>
<p>Select a file from the tree to view it, or create a private page.</p>
</div>
</div>
</template>
</div>
</div>
<script>
function giteaWorkspace() {
const params = new URLSearchParams(window.location.search);
const owner = params.get('owner') || '';
const repo = params.get('repo') || '';
return {
owner, repo,
treeItems: [],
selectedPath: '',
currentFile: null,
fileContent: '',
showFile: false,
showEditor: false,
editContent: '',
commitMessage: 'Update via FlowDeck',
commitHistory: JSON.parse(localStorage.getItem('fd_commit_msgs') || '[]'),
loading: true,
privatePages: [],
async init() {
await this.loadRoot();
await this.loadPrivatePages();
},
async refresh() {
this.treeItems = [];
this.loading = true;
await this.loadRoot();
},
async loadRoot() {
this.loading = true;
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/tree`);
if (r.ok) {
const d = await r.json();
this.treeItems = (d.tree || []).map(item => ({
...item,
depth: 0,
expanded: false,
children: [],
}));
}
} catch(e) {}
this.loading = false;
},
async toggleFolder(item) {
if (item.expanded) {
item.expanded = false;
return;
}
if (item.children.length > 0) {
item.expanded = true;
return;
}
// Lazy load
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/tree?path=${encodeURIComponent(item.path)}`);
if (r.ok) {
const d = await r.json();
item.children = (d.tree || []).map(child => ({
...child,
depth: item.depth + 1,
expanded: false,
children: [],
}));
}
item.expanded = true;
} catch(e) {}
},
async openFile(item) {
this.selectedPath = item.path;
this.currentFile = item;
this.showEditor = false;
this.showFile = true;
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/file?path=${encodeURIComponent(item.path)}`);
if (r.ok) {
const d = await r.json();
this.fileContent = d.content || '';
} else {
this.fileContent = '[Error loading file]';
}
} catch(e) {
this.fileContent = '[Error loading file]';
}
},
openEditor() {
this.editContent = this.fileContent;
this.showEditor = true;
},
async saveFile() {
if (!this.currentFile) return;
const path = this.currentFile.path;
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/file`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
path, content: this.editContent,
message: this.commitMessage,
sha: this.currentFile.sha,
})
});
if (r.ok) {
this.fileContent = this.editContent;
this.showEditor = false;
// Save commit message history
if (!this.commitHistory.includes(this.commitMessage)) {
this.commitHistory.unshift(this.commitMessage);
if (this.commitHistory.length > 10) this.commitHistory.pop();
localStorage.setItem('fd_commit_msgs', JSON.stringify(this.commitHistory));
}
} else {
const d = await r.json();
alert('Failed to save: ' + (d.error || 'Unknown error'));
}
} catch(e) {
alert('Network error');
}
},
async deleteCurrentFile() {
if (!this.currentFile) return;
if (!confirm(`Delete ${this.currentFile.path} from ${this.owner}/${this.repo}?`)) return;
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/file?path=${encodeURIComponent(this.currentFile.path)}&sha=${encodeURIComponent(this.currentFile.sha)}&message=${encodeURIComponent('Delete via FlowDeck')}`, {
method: 'DELETE',
});
if (r.ok) {
this.showFile = false;
this.selectedPath = '';
await this.refresh();
}
} catch(e) {}
},
formatSize(bytes) {
if (!bytes) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/1048576).toFixed(1) + ' MB';
},
// ── Private Pages ──
async loadPrivatePages() {
// Pages stored in FlowDeck DB, linked to this Gitea project
// For now, store them as local workspace pages with gitea_owner/gitea_repo
// TODO: Create a dedicated API endpoint for gitea private pages
},
newPrivatePage() {
// TODO: Create a new FlowDeck page linked to this Gitea project
alert('Private pages will be available in a future update. Stay tuned!');
},
openPrivatePage(pp) {
// TODO: Open FlowDeck page editor
},
};
}
</script>
{% endblock %}