v5.5.0 Partage v2 : fenêtre Share rechargée, sidebar « Par moi »/« Avec moi », Library dir=made/received, badge 👥 + indicateurs
- Correctif : la fenêtre Share rechargait ses partages à chaque ouverture (before: liste vide après refresh) - Sidebar « Shared » scindée en sous-groupes « Par moi » (partages nominatifs + liens) et « Avec moi » (partages reçus), sans doublons - Bibliothèque : filtre « Tous / Par moi / Avec moi » sur l'onglet Shared ; /api/library/shared?dir=made|received|all + share_dir par élément - Document : bouton barre affiche « 👥 Shared ▾ » quand la page est partagée (membre, lien ou publiée), recalculé à la volée - Workspace local : emoji 👥 juste avant le nom des fichiers partagés (is_shared exposé par /api/local-workspace/tree) - Tests +6 (tests/test_sharing.py) : direction made/received/all, fallback dir invalide, rendu pageIsShared ; suite 325 verte (+3 PDF pré-existants)
This commit is contained in:
@@ -1,5 +1,32 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v5.5.0 (2026-09-08) — Partage : réorganisation & indicateurs
|
||||||
|
|
||||||
|
> Vue complète du partage : sidebar « Shared » scindée en « Par moi » /
|
||||||
|
> « Avec moi », onglet Shared de la bibliothèque filtrable, fenêtre Share
|
||||||
|
> rechargée à l'ouverture (bug de refresh), et emoji 👥 sur les fichiers
|
||||||
|
> partagés du workspace local.
|
||||||
|
|
||||||
|
- **Correctif fenêtre Share** — ouverture de la boîte de dialogue
|
||||||
|
(`toggleShareOpen`) au lieu de laisser la liste vide après un refresh ;
|
||||||
|
les partages sont rechargés (`loadShares`) à chaque ouverture.
|
||||||
|
- **Sidebar Shared** — la section affiche deux sous-groupes : « Par moi »
|
||||||
|
(partages nominatifs créés + pages en « lien ») et « Avec moi » (partages
|
||||||
|
reçus), chacun avec son propre état vide (les pages déjà dans « Par moi »
|
||||||
|
sont exclues de « Avec moi » pour éviter les doublons).
|
||||||
|
- **Bibliothèque** — onglet Shared doté d'un filtre « Tous / Par moi /
|
||||||
|
Avec moi » ; l'endpoint `GET /api/library/shared` accepte `dir=made|received|all`
|
||||||
|
et renseigne `share_dir` sur chaque élément.
|
||||||
|
- **Indicateur de partage** — la barre du document affiche « 👥 Shared »
|
||||||
|
au lieu de « 🔒 Share » dès que la page est partagée (membre, lien ou
|
||||||
|
publiée) ; état recalculé à la volée après chaque action de partage.
|
||||||
|
- **Workspace local** — les fichiers partagés affichent l'emoji 👥 juste
|
||||||
|
avant leur nom dans l'arborescence (`is_shared` exposé par
|
||||||
|
`/api/local-workspace/tree`).
|
||||||
|
- **Tests** (tests/test_sharing.py +6) : direction made/received/all de la
|
||||||
|
bibliothèque, fallback dir invalide, et rendu `pageIsShared` sur la page.
|
||||||
|
Suite complète 325 verte (+3 PDF pré-existants).
|
||||||
|
|
||||||
## v5.4.1 (2026-09-08) — Partage de page : correctif permissions + autocomplete membres
|
## v5.4.1 (2026-09-08) — Partage de page : correctif permissions + autocomplete membres
|
||||||
|
|
||||||
> Le partage à un membre existant échouait avec « Invalid permission. Use view,
|
> Le partage à un membre existant échouait avec « Invalid permission. Use view,
|
||||||
|
|||||||
+1
-1
@@ -636,4 +636,4 @@ Quality DB views, Agent IA Palette → Realtime + S
|
|||||||
DB avancée, redo, drag&drop,
|
DB avancée, redo, drag&drop,
|
||||||
Calendrier, AI duplicate)
|
Calendrier, AI duplicate)
|
||||||
|
|
||||||
*Dernière mise à jour: 2026-09-08 — v5.13.0 Realtime livré (WebSocket gateway, présence, curseurs live, merge LWW + fallback polling, 12 tests) puis v5.10.0 Interactions de bloc livré (drag&drop multi, undo/redo, duplicate, menu ⋮, en-têtes de tableau, sync realtime, 9 tests, suite 307 verte) ; reste v5.4 → v6.0*
|
*Dernière mise à jour: 2026-09-08 — v5.13.0 Realtime livré (WebSocket gateway, présence, curseurs live, merge LWW + fallback polling, 12 tests) puis v5.10.0 Interactions de bloc livré (drag&drop multi, undo/redo, duplicate, menu ⋮, en-têtes de tableau, sync realtime, 9 tests, suite 307 verte) puis v5.5.0 (repo) Partage v2 livré (fenêtre Share rechargée, sidebar « Par moi »/« Avec moi », Library dir=made/received, badge 👥 Shared, 👥 workspace local, 6 tests, suite 325 verte) ; reste v5.4 → v6.0*
|
||||||
|
|||||||
+2
-2
@@ -53,7 +53,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
from app.services.automations import automation_scheduler
|
from app.services.automations import automation_scheduler
|
||||||
automation_task = asyncio.create_task(automation_scheduler())
|
automation_task = asyncio.create_task(automation_scheduler())
|
||||||
|
|
||||||
logger.info("FlowDeck v5.4.1 started on port %d", settings.app_port)
|
logger.info("FlowDeck v5.5.0 started on port %d", settings.app_port)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
@@ -71,7 +71,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="5.4.1",
|
version="5.5.0",
|
||||||
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
+41
-13
@@ -338,20 +338,36 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
"children": [],
|
"children": [],
|
||||||
})
|
})
|
||||||
|
|
||||||
# Load shared pages (anyone with link or published)
|
# Load shared pages: made (page_shares created by me + link-mode pages) and
|
||||||
|
# received (page_shares toward me), plus published pages.
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
shared_rows = conn.execute(
|
made_nominal = conn.execute(
|
||||||
"SELECT id, title, workspace, share_mode, published FROM pages "
|
"SELECT DISTINCT p.id, p.title, p.workspace, p.updated_at FROM page_shares s "
|
||||||
"WHERE share_mode='anyone' OR published=1 ORDER BY updated_at DESC LIMIT 20"
|
"JOIN pages p ON p.id=s.page_id "
|
||||||
|
"WHERE s.created_by=? AND p.deleted_at IS NULL",
|
||||||
|
(uid,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
shared_pages = []
|
made_link = conn.execute(
|
||||||
published_pages = []
|
"SELECT id, title, workspace, updated_at FROM pages "
|
||||||
for r in shared_rows:
|
"WHERE share_mode='anyone' AND published=0 AND deleted_at IS NULL"
|
||||||
page_entry = {
|
).fetchall()
|
||||||
|
published_rows = conn.execute(
|
||||||
|
"SELECT id, title, workspace, updated_at FROM pages "
|
||||||
|
"WHERE published=1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 20"
|
||||||
|
).fetchall()
|
||||||
|
received_rows = conn.execute(
|
||||||
|
"SELECT DISTINCT p.id, p.title, p.workspace, p.updated_at FROM page_shares s "
|
||||||
|
"JOIN pages p ON p.id=s.page_id "
|
||||||
|
"WHERE s.shared_with_user_id=? AND p.deleted_at IS NULL",
|
||||||
|
(uid,),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
def _shared_entry(r, icon):
|
||||||
|
return {
|
||||||
"id": f"page/{r['id']}",
|
"id": f"page/{r['id']}",
|
||||||
"db_id": r["id"],
|
"db_id": r["id"],
|
||||||
"name": r["title"] or "New page",
|
"name": r["title"] or "New page",
|
||||||
"icon": "🌐" if r["published"] else "🔗",
|
"icon": icon,
|
||||||
"url": f"/pages/{r['id']}",
|
"url": f"/pages/{r['id']}",
|
||||||
"active": False,
|
"active": False,
|
||||||
"indent": 0,
|
"indent": 0,
|
||||||
@@ -359,10 +375,17 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
"has_children": False,
|
"has_children": False,
|
||||||
"children": [],
|
"children": [],
|
||||||
}
|
}
|
||||||
if r["published"]:
|
|
||||||
published_pages.append(page_entry)
|
made_map = {}
|
||||||
else:
|
for r in (*made_nominal, *made_link):
|
||||||
shared_pages.append(page_entry)
|
made_map.setdefault(r["id"], r)
|
||||||
|
made_sorted = sorted(made_map.values(), key=lambda r: r["updated_at"] or "", reverse=True)[:20]
|
||||||
|
shared_made_pages = [_shared_entry(r, "link") for r in made_sorted]
|
||||||
|
received_sorted = [r for r in received_rows if r["id"] not in made_map]
|
||||||
|
received_sorted = sorted(received_sorted, key=lambda r: r["updated_at"] or "", reverse=True)[:20]
|
||||||
|
shared_received_pages = [_shared_entry(r, "users") for r in received_sorted]
|
||||||
|
published_pages = [_shared_entry(r, "globe") for r in published_rows]
|
||||||
|
shared_pages = [_shared_entry(r, "link") for r in made_sorted]
|
||||||
|
|
||||||
# Auth method & OAuth badge data
|
# Auth method & OAuth badge data
|
||||||
auth_method = "local"
|
auth_method = "local"
|
||||||
@@ -396,6 +419,8 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
"current_page": repo or "Dashboard", "last_edited": "now",
|
"current_page": repo or "Dashboard", "last_edited": "now",
|
||||||
"recent_pages": recent, "private_pages": private_items,
|
"recent_pages": recent, "private_pages": private_items,
|
||||||
"favorite_pages": favorites, "shared_pages": shared_pages,
|
"favorite_pages": favorites, "shared_pages": shared_pages,
|
||||||
|
"shared_made_pages": shared_made_pages,
|
||||||
|
"shared_received_pages": shared_received_pages,
|
||||||
"published_pages": published_pages,
|
"published_pages": published_pages,
|
||||||
"user": user,
|
"user": user,
|
||||||
"auth_method": auth_method,
|
"auth_method": auth_method,
|
||||||
@@ -542,6 +567,8 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
|
|||||||
sidebar["favorite_pages"] = []
|
sidebar["favorite_pages"] = []
|
||||||
sidebar["private_pages"] = [p for p in all_pages if p.get("source") == "🔒 Private"]
|
sidebar["private_pages"] = [p for p in all_pages if p.get("source") == "🔒 Private"]
|
||||||
sidebar["shared_pages"] = []
|
sidebar["shared_pages"] = []
|
||||||
|
sidebar["shared_made_pages"] = []
|
||||||
|
sidebar["shared_received_pages"] = []
|
||||||
template = env.get_template("library.html")
|
template = env.get_template("library.html")
|
||||||
return template.render(**sidebar)
|
return template.render(**sidebar)
|
||||||
|
|
||||||
@@ -1016,6 +1043,7 @@ async def view_page(request: Request, page_id: int):
|
|||||||
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_share_mode": page.get("share_mode", "private"),
|
||||||
"page_published": bool(page.get("published", 0)),
|
"page_published": bool(page.get("published", 0)),
|
||||||
|
"page_is_shared": bool(page.get("is_shared", 0)) or page.get("share_mode", "private") != "private" or bool(page.get("published", 0)),
|
||||||
"page_data": page_data,
|
"page_data": page_data,
|
||||||
"breadcrumb_items": nav_crumbs,
|
"breadcrumb_items": nav_crumbs,
|
||||||
"nav_workspace_id": page.get("workspace_id") or 0,
|
"nav_workspace_id": page.get("workspace_id") or 0,
|
||||||
|
|||||||
@@ -222,6 +222,8 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
|||||||
"private_pages": private_pages,
|
"private_pages": private_pages,
|
||||||
"favorite_pages": [],
|
"favorite_pages": [],
|
||||||
"shared_pages": [],
|
"shared_pages": [],
|
||||||
|
"shared_made_pages": [],
|
||||||
|
"shared_received_pages": [],
|
||||||
"published_pages": [],
|
"published_pages": [],
|
||||||
"user": user,
|
"user": user,
|
||||||
"avatar_url": avatar_url,
|
"avatar_url": avatar_url,
|
||||||
@@ -384,6 +386,7 @@ async def view_page_root(request: Request, page_id: int):
|
|||||||
"page_favorited": fav is not None,
|
"page_favorited": fav is not None,
|
||||||
"page_share_mode": page.get("share_mode", "private"),
|
"page_share_mode": page.get("share_mode", "private"),
|
||||||
"page_published": bool(page.get("published", 0)),
|
"page_published": bool(page.get("published", 0)),
|
||||||
|
"page_is_shared": bool(page.get("is_shared", 0)) or page.get("share_mode", "private") != "private" or bool(page.get("published", 0)),
|
||||||
"page_data": page_data,
|
"page_data": page_data,
|
||||||
"collection_data": collection_data,
|
"collection_data": collection_data,
|
||||||
"breadcrumb_items": nav_crumbs,
|
"breadcrumb_items": nav_crumbs,
|
||||||
@@ -1021,7 +1024,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
|||||||
if parent_id is None:
|
if parent_id is None:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id, title, parent_section, content_format, content, "
|
"SELECT id, title, parent_section, content_format, content, "
|
||||||
"created_at, updated_at FROM pages "
|
"is_shared, created_at, updated_at FROM pages "
|
||||||
"WHERE parent_id IS NULL AND workspace_id=? AND deleted_at IS NULL "
|
"WHERE parent_id IS NULL AND workspace_id=? AND deleted_at IS NULL "
|
||||||
"ORDER BY created_at DESC",
|
"ORDER BY created_at DESC",
|
||||||
(ws_id,),
|
(ws_id,),
|
||||||
@@ -1029,7 +1032,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
|||||||
else:
|
else:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id, title, parent_section, content_format, content, "
|
"SELECT id, title, parent_section, content_format, content, "
|
||||||
"created_at, updated_at FROM pages "
|
"is_shared, created_at, updated_at FROM pages "
|
||||||
"WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL "
|
"WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL "
|
||||||
"ORDER BY created_at DESC",
|
"ORDER BY created_at DESC",
|
||||||
(parent_id, ws_id),
|
(parent_id, ws_id),
|
||||||
@@ -1089,6 +1092,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
|||||||
"updated_at": r["updated_at"],
|
"updated_at": r["updated_at"],
|
||||||
"author": author,
|
"author": author,
|
||||||
"tags": tags_map.get(r["id"], []),
|
"tags": tags_map.get(r["id"], []),
|
||||||
|
"is_shared": bool(r["is_shared"]),
|
||||||
})
|
})
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|||||||
+39
-1
@@ -84,6 +84,7 @@ def _build_item(db_row: dict, uid: int = 1) -> dict:
|
|||||||
"url": url,
|
"url": url,
|
||||||
"content_format": content_format,
|
"content_format": content_format,
|
||||||
"favorited": bool(db_row.get("favorited", 0)),
|
"favorited": bool(db_row.get("favorited", 0)),
|
||||||
|
"share_mode": db_row.get("share_mode", "private"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -197,13 +198,45 @@ async def library_shared(
|
|||||||
request: Request,
|
request: Request,
|
||||||
source_type: str = Query(default="all"),
|
source_type: str = Query(default="all"),
|
||||||
tree: int = Query(default=0),
|
tree: int = Query(default=0),
|
||||||
|
dir: str = Query(default="all"),
|
||||||
):
|
):
|
||||||
if source_type not in SOURCE_TYPES:
|
if source_type not in SOURCE_TYPES:
|
||||||
source_type = "all"
|
source_type = "all"
|
||||||
|
if dir not in ("made", "received", "all"):
|
||||||
|
dir = "all"
|
||||||
|
|
||||||
uid = _get_user_id(request)
|
uid = _get_user_id(request)
|
||||||
query = f"{BASE_SELECT} FROM pages p WHERE p.share_mode != 'private' AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
|
||||||
|
# Page ids the user shares toward others (nominal page_shares) or receives
|
||||||
|
with get_conn() as conn:
|
||||||
|
made_rows = conn.execute(
|
||||||
|
"SELECT DISTINCT s.page_id FROM page_shares s WHERE s.created_by=?",
|
||||||
|
(uid,),
|
||||||
|
).fetchall()
|
||||||
|
recv_rows = conn.execute(
|
||||||
|
"SELECT DISTINCT s.page_id FROM page_shares s WHERE s.shared_with_user_id=?",
|
||||||
|
(uid,),
|
||||||
|
).fetchall()
|
||||||
|
made_ids = {r[0] for r in made_rows}
|
||||||
|
recv_ids = {r[0] for r in recv_rows}
|
||||||
|
|
||||||
|
conds: list[str] = []
|
||||||
params: list = []
|
params: list = []
|
||||||
|
if dir in ("all", "made"):
|
||||||
|
conds.append("p.share_mode != 'private'")
|
||||||
|
if dir in ("all", "received") and recv_ids:
|
||||||
|
conds.append(f"p.id IN ({','.join('?' for _ in recv_ids)})")
|
||||||
|
params.extend(recv_ids)
|
||||||
|
if dir == "made" and made_ids:
|
||||||
|
conds.append(f"p.id IN ({','.join('?' for _ in made_ids)})")
|
||||||
|
params.extend(made_ids)
|
||||||
|
if dir == "received" and not recv_ids:
|
||||||
|
return {"items": []}
|
||||||
|
|
||||||
|
query = (
|
||||||
|
f"{BASE_SELECT} FROM pages p "
|
||||||
|
f"WHERE ({' OR '.join(conds)}) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
||||||
|
)
|
||||||
if tree:
|
if tree:
|
||||||
query += " AND p.parent_id IS NULL"
|
query += " AND p.parent_id IS NULL"
|
||||||
query, params = _apply_source_filter(query, params, source_type)
|
query, params = _apply_source_filter(query, params, source_type)
|
||||||
@@ -213,6 +246,11 @@ async def library_shared(
|
|||||||
rows = conn.execute(query, params).fetchall()
|
rows = conn.execute(query, params).fetchall()
|
||||||
|
|
||||||
items = _rows_to_items(rows, uid)
|
items = _rows_to_items(rows, uid)
|
||||||
|
for it in items:
|
||||||
|
sid = it["id"]
|
||||||
|
is_made = sid in made_ids or it.get("share_mode", "private") != "private"
|
||||||
|
is_recv = sid in recv_ids
|
||||||
|
it["share_dir"] = "both" if (is_made and is_recv) else ("made" if is_made else ("received" if is_recv else ""))
|
||||||
_enrich_children(items)
|
_enrich_children(items)
|
||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
|
|||||||
@@ -806,6 +806,7 @@
|
|||||||
activityOpen:false,pageCreated:'{{ page.get("created_at", "") }}',
|
activityOpen:false,pageCreated:'{{ page.get("created_at", "") }}',
|
||||||
shareTab:'share',favorited:false,
|
shareTab:'share',favorited:false,
|
||||||
pagePublished:{{ page_published | tojson }},
|
pagePublished:{{ page_published | tojson }},
|
||||||
|
pageIsShared:{{ page_is_shared | tojson }},
|
||||||
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
||||||
pubAllowEdit:false,pubAllowComments:true,
|
pubAllowEdit:false,pubAllowComments:true,
|
||||||
pageUrl:window.location.href,publishedUrl:'',
|
pageUrl:window.location.href,publishedUrl:'',
|
||||||
@@ -1062,7 +1063,7 @@ applyAIBlocks(text){
|
|||||||
else{this.activityOpen=false;}
|
else{this.activityOpen=false;}
|
||||||
},
|
},
|
||||||
toggleShareOpen(){
|
toggleShareOpen(){
|
||||||
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);this.loadShares();}
|
||||||
else{this.shareOpen=false;}
|
else{this.shareOpen=false;}
|
||||||
},
|
},
|
||||||
toggleMoreOpen(){
|
toggleMoreOpen(){
|
||||||
@@ -1070,10 +1071,11 @@ applyAIBlocks(text){
|
|||||||
else{this.moreOpen=false;}
|
else{this.moreOpen=false;}
|
||||||
},
|
},
|
||||||
_justOpened:false,
|
_justOpened:false,
|
||||||
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
|
_syncIsShared(){this.pageIsShared=!!(this.pagePublished||this.generalAccess==='anyone'||(this.accessList||[]).length>0);},
|
||||||
|
togglePublish(){this.pagePublished=!this.pagePublished;this._syncIsShared();this.saveShare();},
|
||||||
saveShare(){
|
saveShare(){
|
||||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
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(()=>{});
|
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})}).then(()=>this._syncIsShared()).catch(()=>{});
|
||||||
},
|
},
|
||||||
async copyPageLink(){
|
async copyPageLink(){
|
||||||
console.log('copyPageLink invoked');
|
console.log('copyPageLink invoked');
|
||||||
@@ -1203,7 +1205,8 @@ applyAIBlocks(text){
|
|||||||
var self = this;
|
var self = this;
|
||||||
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
||||||
self.accessList = d.shares || [];
|
self.accessList = d.shares || [];
|
||||||
}).catch(function(){ self.accessList = []; });
|
self._syncIsShared();
|
||||||
|
}).catch(function(){ self.accessList = []; self._syncIsShared(); });
|
||||||
},
|
},
|
||||||
removeShare(sid) {
|
removeShare(sid) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|||||||
+22
-8
@@ -393,14 +393,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-section-items" x-show="sectionsOpen.shared" x-transition>
|
<div class="sidebar-section-items" x-show="sectionsOpen.shared" x-transition>
|
||||||
<ul class="sidebar-items" data-section="shared">
|
<div class="sidebar-subgroup">
|
||||||
{% for page in shared_pages %}
|
<div class="sidebar-subgroup-label">Par moi</div>
|
||||||
{{ render_tree_item(page) }}
|
<ul class="sidebar-items" data-section="shared-made">
|
||||||
{% endfor %}
|
{% for page in shared_made_pages %}
|
||||||
{% if not shared_pages %}
|
{{ render_tree_item(page) }}
|
||||||
<li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("users",16) }}</span><span class="page-name text-dim">No shared pages</span></li>
|
{% endfor %}
|
||||||
{% endif %}
|
{% if not shared_made_pages %}
|
||||||
</ul>
|
<li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("link",16) }}</span><span class="page-name text-dim">Aucun partage par moi</span></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-subgroup">
|
||||||
|
<div class="sidebar-subgroup-label">Avec moi</div>
|
||||||
|
<ul class="sidebar-items" data-section="shared-received">
|
||||||
|
{% for page in shared_received_pages %}
|
||||||
|
{{ render_tree_item(page) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% if not shared_received_pages %}
|
||||||
|
<li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("users",16) }}</span><span class="page-name text-dim">Aucun partage avec moi</span></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,12 @@
|
|||||||
.row-title:hover{text-decoration:underline;}
|
.row-title:hover{text-decoration:underline;}
|
||||||
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
|
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
|
||||||
|
|
||||||
|
/* ── Shared direction sub-filter pils ── */
|
||||||
|
.lib-share-directory{display:flex;align-items:center;gap:6px;padding:4px 16px;}
|
||||||
|
.lib-dir-pill{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:background 0.15s,color 0.15s,border-color 0.15s;}
|
||||||
|
.lib-dir-pill:hover{background:var(--bg-hover);color:var(--text-primary);}
|
||||||
|
.lib-dir-pill.active{background:var(--accent);border-color:var(--accent);color:#fff;}
|
||||||
|
|
||||||
/* ── Hover-only elements (hidden until row hover, or when checked/dragging) ── */
|
/* ── Hover-only elements (hidden until row hover, or when checked/dragging) ── */
|
||||||
.hover-only{opacity:0;transition:opacity 0.12s;flex-shrink:0;}
|
.hover-only{opacity:0;transition:opacity 0.12s;flex-shrink:0;}
|
||||||
.lib-row:hover .hover-only,.lib-row.selected .hover-only{opacity:1;}
|
.lib-row:hover .hover-only,.lib-row.selected .hover-only{opacity:1;}
|
||||||
@@ -247,7 +253,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="lib-toolbar-wrap">
|
<div class="lib-toolbar-wrap">
|
||||||
<button class="lib-icon-btn" :class="{active: viewOpen}" title="View options" @click.stop="viewOpen=!viewOpen; filterOpen=false; sortOpen=false">
|
<button class="lib-icon-btn" :class="{active: viewOpen}" title="View options" @click.stop="viewOpen=!viewOpen; filterOpen=false; sortOpen=false">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/><circle cx="9" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="9" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/><circle cx="9" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="9" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="lib-dropdown" x-show="viewOpen" @click.outside="viewOpen=false" x-transition>
|
<div class="lib-dropdown" x-show="viewOpen" @click.outside="viewOpen=false" x-transition>
|
||||||
<div class="dd-label">Show columns</div>
|
<div class="dd-label">Show columns</div>
|
||||||
@@ -258,6 +264,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared direction sub-filter (Par moi / Avec moi) -->
|
||||||
|
<div class="lib-share-directory" x-show="tab === 'shared'">
|
||||||
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'all' }" @click="setShareDir('all')">Tous</button>
|
||||||
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'made' }" @click="setShareDir('made')">Par moi</button>
|
||||||
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'received' }" @click="setShareDir('received')">Avec moi</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search bar (toggleable) -->
|
<!-- Search bar (toggleable) -->
|
||||||
<div class="lib-search-bar" x-show="searchOpen" x-transition>
|
<div class="lib-search-bar" x-show="searchOpen" x-transition>
|
||||||
<input id="lib-search-input" type="text" placeholder="Search in this list..." x-model="searchQuery" @input="doSearch()">
|
<input id="lib-search-input" type="text" placeholder="Search in this list..." x-model="searchQuery" @input="doSearch()">
|
||||||
@@ -485,6 +498,7 @@ function libraryPage() {
|
|||||||
sortOpen: false,
|
sortOpen: false,
|
||||||
viewOpen: false,
|
viewOpen: false,
|
||||||
sourceFilter: 'all',
|
sourceFilter: 'all',
|
||||||
|
shareDir: 'all',
|
||||||
showAuthor: false,
|
showAuthor: false,
|
||||||
showSource: false,
|
showSource: false,
|
||||||
|
|
||||||
@@ -837,6 +851,7 @@ function libraryPage() {
|
|||||||
if (this.tab === t) return;
|
if (this.tab === t) return;
|
||||||
if (t === 'repository' && !this.isGiteaActive) return;
|
if (t === 'repository' && !this.isGiteaActive) return;
|
||||||
this.tab = t;
|
this.tab = t;
|
||||||
|
this.shareDir = 'all';
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.selected = {};
|
this.selected = {};
|
||||||
this.selectedCount = 0;
|
this.selectedCount = 0;
|
||||||
@@ -875,6 +890,9 @@ function libraryPage() {
|
|||||||
} else if (this.tab !== 'repository' && this.tab !== 'workspace' && this.sourceFilter && this.sourceFilter !== 'all') {
|
} else if (this.tab !== 'repository' && this.tab !== 'workspace' && this.sourceFilter && this.sourceFilter !== 'all') {
|
||||||
url += sep + 'source_type=' + encodeURIComponent(this.sourceFilter); sep = '&';
|
url += sep + 'source_type=' + encodeURIComponent(this.sourceFilter); sep = '&';
|
||||||
}
|
}
|
||||||
|
if (this.tab === 'shared' && this.shareDir && this.shareDir !== 'all') {
|
||||||
|
url += sep + 'dir=' + encodeURIComponent(this.shareDir); sep = '&';
|
||||||
|
}
|
||||||
var r = await fetch(url);
|
var r = await fetch(url);
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
// Backend already computes has_children correctly for each item (children
|
// Backend already computes has_children correctly for each item (children
|
||||||
@@ -899,6 +917,12 @@ function libraryPage() {
|
|||||||
this.loadTab();
|
this.loadTab();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setShareDir(v) {
|
||||||
|
if (this.shareDir === v) return;
|
||||||
|
this.shareDir = v;
|
||||||
|
this.loadTab();
|
||||||
|
},
|
||||||
|
|
||||||
setSort(field) {
|
setSort(field) {
|
||||||
this.sortBy = field;
|
this.sortBy = field;
|
||||||
this.sortOpen = false;
|
this.sortOpen = false;
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
.ws-tree-item .name.file{display:inline-flex;align-items:center;max-width:100%;overflow:hidden;}
|
.ws-tree-item .name.file{display:inline-flex;align-items:center;max-width:100%;overflow:hidden;}
|
||||||
.ws-tree-item .name.file .inline-rename-input{flex:0 1 auto;}
|
.ws-tree-item .name.file .inline-rename-input{flex:0 1 auto;}
|
||||||
.ws-tree-item .name.file .btn-open{flex:0 0 auto;}
|
.ws-tree-item .name.file .btn-open{flex:0 0 auto;}
|
||||||
|
.ws-shared-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;opacity:.9;}
|
||||||
.ws-tree-item .actions{display:none;gap:2px;}
|
.ws-tree-item .actions{display:none;gap:2px;}
|
||||||
.ws-tree-item:hover .actions{display:flex;}
|
.ws-tree-item:hover .actions{display:flex;}
|
||||||
.ws-chevron{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:8px;padding:0;border-radius:3px;transition:transform 150ms,color 100ms;}
|
.ws-chevron{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:8px;padding:0;border-radius:3px;transition:transform 150ms,color 100ms;}
|
||||||
@@ -1210,6 +1211,7 @@ window._wsInitData = (function() {
|
|||||||
+ ' title="Click to rename, double-click to open"'
|
+ ' title="Click to rename, double-click to open"'
|
||||||
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
|
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
|
||||||
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
|
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
|
||||||
|
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
|
||||||
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
|
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
|
||||||
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
|
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
|
||||||
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
|
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||||
{% set page_icon = "file" if page.content_format != 'file' else "paperclip" %}
|
{% set page_icon = "file" if page.content_format != 'file' else "paperclip" %}
|
||||||
{% set page_title = page.title %}
|
{% set page_title = page.title %}
|
||||||
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn" @click="window.E && window.E.toggleComments()" title="Comments"><span class="fd-comment-btn-ico">💬</span><span class="fd-comment-count" x-text="window.E && window.E.commentCount>0 ? window.E.commentCount : \'\'"></span></button><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()">' ~ fd_icon("lock",14) ~ ' Share ▾</button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn" @click="window.E && window.E.toggleComments()" title="Comments"><span class="fd-comment-btn-ico">💬</span><span class="fd-comment-count" x-text="window.E && window.E.commentCount>0 ? window.E.commentCount : \'\'"></span></button><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()"><span x-show="!window.E || !window.E.pageIsShared">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="window.E && window.E.pageIsShared" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
||||||
{% include '_header.html' %}
|
{% include '_header.html' %}
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
{% include "_page_editor_content.html" %}
|
{% include "_page_editor_content.html" %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||||
{% set page_icon = "file" %}
|
{% set page_icon = "file" %}
|
||||||
{% set page_title = page.title %}
|
{% set page_title = page.title %}
|
||||||
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()">' ~ fd_icon("lock",14) ~ ' Share ▾</button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()"><span x-show="!window.E || !window.E.pageIsShared">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="window.E && window.E.pageIsShared" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
||||||
{% include '_header.html' %}
|
{% include '_header.html' %}
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
{% include "_database_table.html" %}
|
{% include "_database_table.html" %}
|
||||||
|
|||||||
@@ -369,6 +369,27 @@ input, select, textarea { font-family: inherit; color: inherit; }
|
|||||||
.sidebar-item.empty-hint:hover { background: none; }
|
.sidebar-item.empty-hint:hover { background: none; }
|
||||||
.sidebar-item.empty-hint .page-name { font-size: 12px; }
|
.sidebar-item.empty-hint .page-name { font-size: 12px; }
|
||||||
|
|
||||||
|
/* Shared subgroups (Par moi / Avec moi) */
|
||||||
|
.sidebar-subgroup {
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
.sidebar-subgroup + .sidebar-subgroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.sidebar-subgroup-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
.sidebar-section {
|
.sidebar-section {
|
||||||
padding: var(--space-xs) var(--space-md);
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
|||||||
+90
-1
@@ -199,4 +199,93 @@ def test_remove_share_unsets_is_shared(client):
|
|||||||
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
is_shared = conn.execute("SELECT is_shared FROM pages WHERE id=?", (pid,)).fetchone()["is_shared"]
|
is_shared = conn.execute("SELECT is_shared FROM pages WHERE id=?", (pid,)).fetchone()["is_shared"]
|
||||||
assert is_shared == 0
|
assert is_shared == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── Library : direction des partages (dir=made|received|all) ──
|
||||||
|
|
||||||
|
|
||||||
|
def _make_page_with(conn, title, share_mode="private", published=0):
|
||||||
|
cur = conn.execute(
|
||||||
|
"INSERT INTO pages (workspace, title, content, content_format, parent_section, share_mode, published) "
|
||||||
|
"VALUES ('Private', ?, '[]', 'blocks', 'Private', ?, ?)",
|
||||||
|
(title, share_mode, published),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_shared_dir_made_includes_nominal_and_link(client):
|
||||||
|
_auth(client, user_id=1, login="owner")
|
||||||
|
with get_conn_ctx() as conn:
|
||||||
|
a = _make_page_with(conn, "Shared A")
|
||||||
|
b = _make_page_with(conn, "Link B", share_mode="anyone")
|
||||||
|
_make_page_with(conn, "Private C")
|
||||||
|
|
||||||
|
r = client.post(f"/api/pages/{a}/share", json={"user_id": 2, "permission": "edit"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
made = client.get("/api/library/shared?dir=made").json()["items"]
|
||||||
|
ids = [it["id"] for it in made]
|
||||||
|
assert a in ids, "page shared to a user should appear in made"
|
||||||
|
assert b in ids, "link-mode page should appear in made"
|
||||||
|
assert all(it["share_dir"] == "made" for it in made if it["id"] in (a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_shared_dir_received(client):
|
||||||
|
_auth(client, user_id=1, login="owner")
|
||||||
|
with get_conn_ctx() as conn:
|
||||||
|
a = _make_page_with(conn, "Received A")
|
||||||
|
r = client.post(f"/api/pages/{a}/share", json={"user_id": 2, "permission": "view"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
_auth(client, user_id=2, login="alice")
|
||||||
|
recv = client.get("/api/library/shared?dir=received").json()["items"]
|
||||||
|
ids = [it["id"] for it in recv]
|
||||||
|
assert a in ids
|
||||||
|
item = next(it for it in recv if it["id"] == a)
|
||||||
|
assert item["share_dir"] == "received"
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_shared_dir_all_is_union(client):
|
||||||
|
_auth(client, user_id=1, login="owner")
|
||||||
|
with get_conn_ctx() as conn:
|
||||||
|
a = _make_page_with(conn, "Union A")
|
||||||
|
client.post(f"/api/pages/{a}/share", json={"user_id": 2, "permission": "comment"})
|
||||||
|
|
||||||
|
_auth(client, user_id=2, login="alice")
|
||||||
|
all_items = client.get("/api/library/shared").json()["items"]
|
||||||
|
ids = [it["id"] for it in all_items]
|
||||||
|
assert a in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_shared_invalid_dir_falls_back_to_all(client):
|
||||||
|
_auth(client, user_id=1, login="owner")
|
||||||
|
with get_conn_ctx() as conn:
|
||||||
|
a = _make_page_with(conn, "Fallback A", share_mode="anyone")
|
||||||
|
r = client.get("/api/library/shared?dir=bogus")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert a in [it["id"] for it in r.json()["items"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_shared_received_none(client):
|
||||||
|
_auth(client, user_id=2, login="alice")
|
||||||
|
r = client.get("/api/library/shared?dir=received")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["items"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_page_editor_renders_page_is_shared(client):
|
||||||
|
_auth(client, user_id=1, login="owner")
|
||||||
|
with get_conn_ctx() as conn:
|
||||||
|
a = _make_page_with(conn, "Marked A")
|
||||||
|
client.post(f"/api/pages/{a}/share", json={"user_id": 2, "permission": "view"})
|
||||||
|
|
||||||
|
r = client.get(f"/pages/{a}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "pageIsShared:true" in r.text
|
||||||
|
|
||||||
|
|
||||||
|
def get_conn_ctx():
|
||||||
|
from app.db import get_conn
|
||||||
|
return get_conn()
|
||||||
Reference in New Issue
Block a user