From e8f4cfb631b04307141c4a3f730468f367cf3fd6 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 14 Jul 2026 15:24:43 -0400 Subject: [PATCH] feat: auto-create local workspace mirror for Gitea projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Opening /gitea-workspace auto-creates a local workspace with same name - New Page/New Folder in remote context → saves to local mirror workspace - Sidebar stays focused on remote workspace (🔗 icon, remote tree) - local_ws_id passed to Alpine for API calls - /api/local-workspace/items accepts workspace_id in body --- app/routers/dashboard.py | 45 +++++++++++++++++++++++++++++++++++++--- app/templates/base.html | 27 ++++++++++++++++-------- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 1333b97..ce7c8f1 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -113,10 +113,25 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict: except Exception: pass + # Get local workspace ID for Gitea workspace mirror + local_ws_id = 0 + if gitea_workspace and gitea_owner and gitea_repo: + try: + with get_conn() as conn: + row = conn.execute( + "SELECT id FROM workspaces WHERE owner_id=? AND name=? AND settings_json LIKE ?", + (user["id"], f"{gitea_owner}/{gitea_repo}", "%gitea_repo%") + ).fetchone() + if row: + local_ws_id = row["id"] + except Exception: + pass + return { "workspace_name": ws, "workspace_initial": initial, "active_ws_name": active_ws_name, "workspace_key": f"{gitea_owner}/{gitea_repo}" if gitea_workspace else "", + "local_ws_id": local_ws_id, "workspace_pages": workspace_pages, "gitea_workspace": gitea_workspace, "current_page": "Dashboard", "last_edited": "now", @@ -385,6 +400,27 @@ async def gitea_workspace_page(request: Request): repo = request.query_params.get("repo", "") ws_key = f"{owner}/{repo}" if owner and repo else "" ws_name = ws_key or "Gitea Workspace" + # Auto-create local workspace mirror for storing local files + local_ws_id = None + if ws_key: + with get_conn() as conn: + existing = conn.execute( + "SELECT id, owner_id FROM workspaces WHERE owner_id=? AND name=? AND settings_json LIKE ?", + (user["id"], ws_key, "%gitea_repo%") + ).fetchone() + if existing: + local_ws_id = existing["id"] + else: + c = conn.execute( + "INSERT INTO workspaces (name, owner_id, settings_json) VALUES (?, ?, ?)", + (ws_key, user["id"], json.dumps({"gitea_repo": ws_key, "gitea_owner": owner})) + ) + local_ws_id = c.lastrowid + conn.execute( + "INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)", + (local_ws_id, user["id"], "admin") + ) + conn.commit() ctx = { **sidebar, "user": user, @@ -394,6 +430,7 @@ async def gitea_workspace_page(request: Request): "workspace_initial": repo[0].upper() if repo else "G", "owner": owner, "repo": repo, + "local_ws_id": local_ws_id or 0, } template = env.get_template("gitea_workspace.html") resp = HTMLResponse(content=template.render(**ctx)) @@ -726,14 +763,16 @@ async def create_local_workspace_item(request: Request): name = body.get("name", "Untitled").strip() item_type = body.get("type", "page") parent_id = body.get("parent_id") + explicit_ws_id = body.get("workspace_id") ws = _get_active_workspace(request) - ws_id = ws["id"] if ws else None + ws_id = explicit_ws_id or (ws["id"] if ws else None) + ws_key = ws["workspace_key"] if ws and not explicit_ws_id else "" section = 'Workspace' if item_type == 'folder' else 'Private' with get_conn() as conn: cursor = conn.execute( "INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, parent_id) " - "VALUES ('', ?, ?, '', 'blocks', ?, ?)", - (ws_id, name, section, parent_id), + "VALUES (?, ?, ?, '', 'blocks', ?, ?)", + (ws_key, ws_id, name, section, parent_id) ) conn.commit() pid = cursor.lastrowid diff --git a/app/templates/base.html b/app/templates/base.html index 5dcc1f9..bad01c3 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -576,6 +576,7 @@ pageTitle: '{% block page_title %}FlowDeck{% endblock %}', lastEdited: '{{ last_edited }}', workspaceKey: '{{ workspace_key }}', + localWsId: {{ local_ws_id|default(0) }}, currentView: 'home', contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' }, contextActions: {}, @@ -875,16 +876,16 @@ const name = prompt('File name:'); if (!name || !name.trim()) return; const wk = this.workspaceKey || ''; - // Gitea/remote workspace: create file in the repo + // Gitea/remote workspace: create local file in mirror workspace, keep remote focus if (wk.includes('/')) { - const parts = wk.split('/'); - fetch('/api/gitea/projects/' + parts[0] + '/' + parts[1] + '/file', { - method: 'PUT', + const wsId = this.localWsId || 0; + fetch('/api/local-workspace/items', { + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() }, - body: JSON.stringify({ path: name.trim() + '.md', content: '# ' + name.trim(), message: 'Create via FlowDeck' }) + body: JSON.stringify({ name: name.trim(), type: 'page', workspace_id: wsId }) }) .then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); }) - .then(data => { window.location.reload(); }) + .then(data => { window.location.href = `/pages/${data.id}`; }) .catch(err => { this.toast('Failed: ' + err.message, 'error'); }); return; } @@ -902,13 +903,21 @@ const name = prompt('Folder name:'); if (!name || !name.trim()) return; const wk = this.workspaceKey || ''; - // Gitea workspace: folders aren't a concept — just warn + // Gitea workspace: create folder in local mirror workspace if (wk.includes('/')) { - this.toast('Folders are not supported for remote workspaces. Use local workspace.', 'warning'); + const wsId = this.localWsId || 0; + fetch('/api/local-workspace/items', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() }, + body: JSON.stringify({ name: name.trim(), type: 'folder', workspace_id: wsId }) + }) + .then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); }) + .then(data => { this.toast('Folder created in local workspace', 'success'); }) + .catch(err => { this.toast('Failed: ' + err.message, 'error'); }); return; } + // Local workspace fetch('/api/local-workspace/items', { - method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() }, body: JSON.stringify({ name: name.trim(), type: 'folder' }) })