feat: auto-create local workspace mirror for Gitea projects
- 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
This commit is contained in:
@@ -113,10 +113,25 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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 {
|
return {
|
||||||
"workspace_name": ws, "workspace_initial": initial,
|
"workspace_name": ws, "workspace_initial": initial,
|
||||||
"active_ws_name": active_ws_name,
|
"active_ws_name": active_ws_name,
|
||||||
"workspace_key": f"{gitea_owner}/{gitea_repo}" if gitea_workspace else "",
|
"workspace_key": f"{gitea_owner}/{gitea_repo}" if gitea_workspace else "",
|
||||||
|
"local_ws_id": local_ws_id,
|
||||||
"workspace_pages": workspace_pages,
|
"workspace_pages": workspace_pages,
|
||||||
"gitea_workspace": gitea_workspace,
|
"gitea_workspace": gitea_workspace,
|
||||||
"current_page": "Dashboard", "last_edited": "now",
|
"current_page": "Dashboard", "last_edited": "now",
|
||||||
@@ -385,6 +400,27 @@ async def gitea_workspace_page(request: Request):
|
|||||||
repo = request.query_params.get("repo", "")
|
repo = request.query_params.get("repo", "")
|
||||||
ws_key = f"{owner}/{repo}" if owner and repo else ""
|
ws_key = f"{owner}/{repo}" if owner and repo else ""
|
||||||
ws_name = ws_key or "Gitea Workspace"
|
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 = {
|
ctx = {
|
||||||
**sidebar,
|
**sidebar,
|
||||||
"user": user,
|
"user": user,
|
||||||
@@ -394,6 +430,7 @@ async def gitea_workspace_page(request: Request):
|
|||||||
"workspace_initial": repo[0].upper() if repo else "G",
|
"workspace_initial": repo[0].upper() if repo else "G",
|
||||||
"owner": owner,
|
"owner": owner,
|
||||||
"repo": repo,
|
"repo": repo,
|
||||||
|
"local_ws_id": local_ws_id or 0,
|
||||||
}
|
}
|
||||||
template = env.get_template("gitea_workspace.html")
|
template = env.get_template("gitea_workspace.html")
|
||||||
resp = HTMLResponse(content=template.render(**ctx))
|
resp = HTMLResponse(content=template.render(**ctx))
|
||||||
@@ -726,14 +763,16 @@ async def create_local_workspace_item(request: Request):
|
|||||||
name = body.get("name", "Untitled").strip()
|
name = body.get("name", "Untitled").strip()
|
||||||
item_type = body.get("type", "page")
|
item_type = body.get("type", "page")
|
||||||
parent_id = body.get("parent_id")
|
parent_id = body.get("parent_id")
|
||||||
|
explicit_ws_id = body.get("workspace_id")
|
||||||
ws = _get_active_workspace(request)
|
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'
|
section = 'Workspace' if item_type == 'folder' else 'Private'
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, parent_id) "
|
"INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, parent_id) "
|
||||||
"VALUES ('', ?, ?, '', 'blocks', ?, ?)",
|
"VALUES (?, ?, ?, '', 'blocks', ?, ?)",
|
||||||
(ws_id, name, section, parent_id),
|
(ws_key, ws_id, name, section, parent_id)
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
pid = cursor.lastrowid
|
pid = cursor.lastrowid
|
||||||
|
|||||||
+18
-9
@@ -576,6 +576,7 @@
|
|||||||
pageTitle: '{% block page_title %}FlowDeck{% endblock %}',
|
pageTitle: '{% block page_title %}FlowDeck{% endblock %}',
|
||||||
lastEdited: '{{ last_edited }}',
|
lastEdited: '{{ last_edited }}',
|
||||||
workspaceKey: '{{ workspace_key }}',
|
workspaceKey: '{{ workspace_key }}',
|
||||||
|
localWsId: {{ local_ws_id|default(0) }},
|
||||||
currentView: 'home',
|
currentView: 'home',
|
||||||
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
||||||
contextActions: {},
|
contextActions: {},
|
||||||
@@ -875,16 +876,16 @@
|
|||||||
const name = prompt('File name:');
|
const name = prompt('File name:');
|
||||||
if (!name || !name.trim()) return;
|
if (!name || !name.trim()) return;
|
||||||
const wk = this.workspaceKey || '';
|
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('/')) {
|
if (wk.includes('/')) {
|
||||||
const parts = wk.split('/');
|
const wsId = this.localWsId || 0;
|
||||||
fetch('/api/gitea/projects/' + parts[0] + '/' + parts[1] + '/file', {
|
fetch('/api/local-workspace/items', {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
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(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'); });
|
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -902,13 +903,21 @@
|
|||||||
const name = prompt('Folder name:');
|
const name = prompt('Folder name:');
|
||||||
if (!name || !name.trim()) return;
|
if (!name || !name.trim()) return;
|
||||||
const wk = this.workspaceKey || '';
|
const wk = this.workspaceKey || '';
|
||||||
// Gitea workspace: folders aren't a concept — just warn
|
// Gitea workspace: create folder in local mirror workspace
|
||||||
if (wk.includes('/')) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
// Local workspace
|
||||||
fetch('/api/local-workspace/items', {
|
fetch('/api/local-workspace/items', {
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||||
body: JSON.stringify({ name: name.trim(), type: 'folder' })
|
body: JSON.stringify({ name: name.trim(), type: 'folder' })
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user