fix: share persistence + clipboard fallback + auto-save
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- DB: pages.share_mode (private/invited/anyone) + published (bool)
- API: POST /board/api/share/{id} saves share settings to DB
- view_page: passes page_share_mode + page_published to template
- page_editor: inits share state from server, auto-saves on change
- Clipboard: fallback to textarea for HTTP (navigator.clipboard blocked)
- Toast: showToast() with proper timeout clearing
This commit is contained in:
2026-07-10 12:03:32 -04:00
parent c75cdab134
commit 6033a0b1e4
3 changed files with 53 additions and 9 deletions
+8
View File
@@ -296,6 +296,14 @@ def init_db():
conn.execute("ALTER TABLE pages ADD COLUMN deleted_at TEXT")
except sqlite3.OperationalError:
pass
try:
conn.execute("ALTER TABLE pages ADD COLUMN share_mode TEXT DEFAULT 'private'")
except sqlite3.OperationalError:
pass
try:
conn.execute("ALTER TABLE pages ADD COLUMN published INTEGER DEFAULT 0")
except sqlite3.OperationalError:
pass
conn.commit()
+21 -1
View File
@@ -383,6 +383,24 @@ async def remove_favorite(request: Request, page_id: int):
conn.commit()
return {"status": "removed", "page_id": page_id}
# ═══════════ Share API ═══════════
@router.post("/api/share/{page_id:int}")
async def update_share(request: Request, page_id: int):
"""Save share settings for a page."""
import json
body = await request.json()
mode = body.get("mode", "private")
published = body.get("published", False)
with get_conn() as conn:
conn.execute(
"UPDATE pages SET share_mode=?, published=? WHERE id=?",
(mode, 1 if published else 0, page_id),
)
conn.commit()
return {"status": "ok", "share_mode": mode, "published": published}
# ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════
@router.get("/api/trash")
@@ -732,7 +750,9 @@ async def view_page(request: Request, page_id: int):
fav = conn.execute(
"SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)
).fetchone()
ctx = {**sidebar, "page": page, "page_favorited": fav is not None}
ctx = {**sidebar, "page": page, "page_favorited": fav is not None,
"page_share_mode": page.get("share_mode", "private"),
"page_published": bool(page.get("published", 0))}
template = env.get_template("page_editor.html")
return template.render(**ctx)
+24 -8
View File
@@ -334,8 +334,9 @@ function editor(pid,title,fmt,raw,favorited){
pid,pageTitle:title,blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
fmt:{open:false,top:0,left:0,idx:-1},
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,
shareTab:'share',favorited:!!favorited,pagePublished:false,
generalAccess:'invited',publicPerm:'viewer',
shareTab:'share',favorited:!!favorited,
pagePublished:{{ page_published | tojson }},
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
pubAllowEdit:false,pubAllowComments:true,
pageUrl:window.location.href,publishedUrl:'',
inviteEmail:'',invitePermission:'editor',accessList:[],
@@ -374,15 +375,30 @@ function editor(pid,title,fmt,raw,favorited){
const m=this.favorited?'DELETE':'POST';
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;}).catch(()=>{});
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
saveShare(){
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).catch(()=>{});
},
copyPageLink(){
navigator.clipboard.writeText(this.pageUrl).then(()=>{
this.toastMsg='Link copied to clipboard';
this.toastVisible=true;
setTimeout(()=>{this.toastVisible=false;},2500);
}).catch(()=>{});
const url = this.pageUrl;
const fallback = () => {
const ta = document.createElement('textarea');
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select();
document.execCommand('copy'); document.body.removeChild(ta);
};
try {
navigator.clipboard.writeText(url).then(() => {
this.showToast('Link copied to clipboard');
}).catch(() => { fallback(); this.showToast('Link copied to clipboard'); });
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
},
showToast(msg){
this.toastMsg = msg; this.toastVisible = true;
clearTimeout(this._toastTimer);
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
},
togglePublish(){this.pagePublished=!this.pagePublished;},
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
exportPage(){this.moreOpen=false;},