Sync YAML frontmatter with share and bookmark actions
This commit is contained in:
+44
-1
@@ -994,6 +994,26 @@ async def api_toggle_bookmark(req: BookmarkToggleRequest, current_user=Depends(r
|
|||||||
raise HTTPException(status_code=403, detail="Access denied to vault")
|
raise HTTPException(status_code=403, detail="Access denied to vault")
|
||||||
|
|
||||||
is_now_bookmarked = toggle_bookmark(username, req.vault, req.path, req.title)
|
is_now_bookmarked = toggle_bookmark(username, req.vault, req.path, req.title)
|
||||||
|
|
||||||
|
# Update the file's YAML frontmatter: favoris: true/false
|
||||||
|
vault_data = get_vault_data(req.vault)
|
||||||
|
if vault_data:
|
||||||
|
file_path = _resolve_safe_path(Path(vault_data["path"]), req.path)
|
||||||
|
if file_path.exists() and file_path.suffix == ".md":
|
||||||
|
try:
|
||||||
|
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
post = frontmatter.loads(raw)
|
||||||
|
if is_now_bookmarked:
|
||||||
|
post.metadata["favoris"] = True
|
||||||
|
elif "favoris" in post.metadata:
|
||||||
|
del post.metadata["favoris"]
|
||||||
|
new_raw = frontmatter.dumps(post)
|
||||||
|
_backup_file(file_path, req.vault, req.path)
|
||||||
|
file_path.write_text(new_raw, encoding="utf-8")
|
||||||
|
await update_single_file(req.vault, str(file_path))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to update favoris metadata on {req.vault}/{req.path}: {e}")
|
||||||
|
|
||||||
return {"bookmarked": is_now_bookmarked}
|
return {"bookmarked": is_now_bookmarked}
|
||||||
|
|
||||||
|
|
||||||
@@ -2721,13 +2741,36 @@ async def api_share_create(
|
|||||||
body: dict = Body(...),
|
body: dict = Body(...),
|
||||||
current_user=Depends(require_auth),
|
current_user=Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Create a public share link for a document."""
|
"""Create a public share link for a document.
|
||||||
|
|
||||||
|
Also sets ``publish: true`` in the file's YAML frontmatter so the
|
||||||
|
frontend can visually indicate the file is publicly shared.
|
||||||
|
"""
|
||||||
if not check_vault_access(vault_name, current_user):
|
if not check_vault_access(vault_name, current_user):
|
||||||
raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'")
|
raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'")
|
||||||
path = body.get("path", "")
|
path = body.get("path", "")
|
||||||
expires = body.get("expires_in_hours")
|
expires = body.get("expires_in_hours")
|
||||||
share = create_share(vault_name, path, current_user["username"], expires)
|
share = create_share(vault_name, path, current_user["username"], expires)
|
||||||
share["url"] = f"/s/{share['token']}"
|
share["url"] = f"/s/{share['token']}"
|
||||||
|
|
||||||
|
# Set publish: true in the file's frontmatter
|
||||||
|
vault_data = get_vault_data(vault_name)
|
||||||
|
if vault_data:
|
||||||
|
file_path = _resolve_safe_path(Path(vault_data["path"]), path)
|
||||||
|
if file_path.exists() and file_path.suffix == ".md":
|
||||||
|
try:
|
||||||
|
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
post = frontmatter.loads(raw)
|
||||||
|
if not post.metadata.get("publish"):
|
||||||
|
post.metadata["publish"] = True
|
||||||
|
new_raw = frontmatter.dumps(post)
|
||||||
|
_backup_file(file_path, vault_name, path)
|
||||||
|
file_path.write_text(new_raw, encoding="utf-8")
|
||||||
|
await update_single_file(vault_name, str(file_path))
|
||||||
|
logger.info(f"Set publish:true on {vault_name}/{path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to set publish metadata on {vault_name}/{path}: {e}")
|
||||||
|
|
||||||
return share
|
return share
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+89
-2
@@ -3269,8 +3269,18 @@
|
|||||||
RightSidebarManager.toggle();
|
RightSidebarManager.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Share button
|
// Share button — check if already shared
|
||||||
const shareBtn = el("button", { class: "btn-action", title: "Partager ce document" }, [icon("share-2", 14), document.createTextNode("Partager")]);
|
const shareBtn = el("button", { class: "btn-action btn-share", title: "Partager ce document" }, [icon("share-2", 14), document.createTextNode("Partager")]);
|
||||||
|
// Check if already shared and color the button
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const shares = await api("/api/shares");
|
||||||
|
if (shares.some(s => s.vault === data.vault && s.path === data.path)) {
|
||||||
|
shareBtn.classList.add("shared");
|
||||||
|
shareBtn.title = "Document partagé — cliquer pour gérer";
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
})();
|
||||||
shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
|
shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
|
||||||
|
|
||||||
// Bookmark button
|
// Bookmark button
|
||||||
@@ -3399,6 +3409,59 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Dashboard Shared Widget ──
|
||||||
|
const DashboardSharedWidget = {
|
||||||
|
async load() {
|
||||||
|
const grid = document.getElementById("dashboard-shared-grid");
|
||||||
|
const empty = document.getElementById("dashboard-shared-empty");
|
||||||
|
if (!grid) return;
|
||||||
|
try {
|
||||||
|
const shares = await api("/api/shares");
|
||||||
|
if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
|
||||||
|
if (empty) empty.style.display = "none";
|
||||||
|
grid.innerHTML = shares.map(s => `
|
||||||
|
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
|
||||||
|
<div class="shared-card-header">
|
||||||
|
<i data-lucide="file-text" style="width:14px;height:14px"></i>
|
||||||
|
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
|
||||||
|
<span class="shared-card-vault">${escapeHtml(s.vault)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="shared-card-meta">
|
||||||
|
<span>${s.access_count || 0} vue(s)</span>
|
||||||
|
${s.expires_at ? `<span>Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}</span>` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="shared-card-actions">
|
||||||
|
<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">📋 Copier</button>
|
||||||
|
<button class="shared-open-btn">📂 Ouvrir</button>
|
||||||
|
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
lucide.createIcons();
|
||||||
|
grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const url = b.dataset.url;
|
||||||
|
try { await navigator.clipboard.writeText(url); } catch { const ta = document.createElement("textarea"); ta.value=url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
|
||||||
|
showToast("Lien copié !", "success");
|
||||||
|
}));
|
||||||
|
grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const card = b.closest(".shared-card");
|
||||||
|
if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||||
|
}));
|
||||||
|
grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
|
||||||
|
showToast("Partage révoqué", "success");
|
||||||
|
this.load();
|
||||||
|
}));
|
||||||
|
grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
|
||||||
|
TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||||
|
}));
|
||||||
|
} catch (err) { if (empty) empty.style.display = ""; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Dashboard Conflicts Widget ──
|
// ── Dashboard Conflicts Widget ──
|
||||||
const DashboardConflictsWidget = {
|
const DashboardConflictsWidget = {
|
||||||
async load() {
|
async load() {
|
||||||
@@ -4586,7 +4649,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
div.querySelector(".share-copy-btn").addEventListener("click", async () => {
|
div.querySelector(".share-copy-btn").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback for non-HTTPS contexts
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px";
|
||||||
|
document.body.appendChild(ta); ta.select(); document.execCommand("copy");
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
showToast("Lien copié !", "success");
|
showToast("Lien copié !", "success");
|
||||||
div.remove();
|
div.remove();
|
||||||
});
|
});
|
||||||
@@ -5813,6 +5884,9 @@
|
|||||||
<button class="dashboard-tab" data-tab="recent">
|
<button class="dashboard-tab" data-tab="recent">
|
||||||
<i data-lucide="clock" style="width:14px;height:14px"></i> Récents
|
<i data-lucide="clock" style="width:14px;height:14px"></i> Récents
|
||||||
</button>
|
</button>
|
||||||
|
<button class="dashboard-tab" data-tab="shared">
|
||||||
|
<i data-lucide="share-2" style="width:14px;height:14px"></i> Partagés
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Panel -->
|
<!-- Stats Panel -->
|
||||||
@@ -5856,6 +5930,16 @@
|
|||||||
<p>Ouvrez un fichier pour le voir apparaître ici</p>
|
<p>Ouvrez un fichier pour le voir apparaître ici</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared Panel -->
|
||||||
|
<div id="dashboard-panel-shared" class="dashboard-panel">
|
||||||
|
<div id="dashboard-shared-grid" class="dashboard-recent-grid"></div>
|
||||||
|
<div id="dashboard-shared-empty" class="dashboard-recent-empty">
|
||||||
|
<i data-lucide="share-2"></i>
|
||||||
|
<span>Aucun document partagé</span>
|
||||||
|
<p>Partagez un document pour le voir apparaître ici</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Re-initialize widgets and dashboard tabs
|
// Re-initialize widgets and dashboard tabs
|
||||||
@@ -5889,6 +5973,9 @@
|
|||||||
if (typeof DashboardBookmarkWidget !== "undefined") {
|
if (typeof DashboardBookmarkWidget !== "undefined") {
|
||||||
DashboardBookmarkWidget.load(selectedContextVault);
|
DashboardBookmarkWidget.load(selectedContextVault);
|
||||||
}
|
}
|
||||||
|
if (typeof DashboardSharedWidget !== "undefined") {
|
||||||
|
DashboardSharedWidget.load();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
|
|||||||
@@ -382,6 +382,9 @@
|
|||||||
<button class="dashboard-tab" data-tab="recent">
|
<button class="dashboard-tab" data-tab="recent">
|
||||||
<i data-lucide="clock" style="width:14px;height:14px"></i> Récents
|
<i data-lucide="clock" style="width:14px;height:14px"></i> Récents
|
||||||
</button>
|
</button>
|
||||||
|
<button class="dashboard-tab" data-tab="shared">
|
||||||
|
<i data-lucide="share-2" style="width:14px;height:14px"></i> Partagés
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Panel -->
|
<!-- Stats Panel -->
|
||||||
@@ -425,6 +428,16 @@
|
|||||||
<p>Ouvrez un fichier pour le voir apparaître ici</p>
|
<p>Ouvrez un fichier pour le voir apparaître ici</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared Panel -->
|
||||||
|
<div id="dashboard-panel-shared" class="dashboard-panel">
|
||||||
|
<div id="dashboard-shared-grid" class="dashboard-recent-grid"></div>
|
||||||
|
<div id="dashboard-shared-empty" class="dashboard-recent-empty">
|
||||||
|
<i data-lucide="share-2"></i>
|
||||||
|
<span>Aucun document partagé</span>
|
||||||
|
<p>Partagez un document pour le voir apparaître ici</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div><!-- /content-wrapper -->
|
</div><!-- /content-wrapper -->
|
||||||
|
|||||||
+55
-4
@@ -5830,11 +5830,62 @@ body.popup-mode .content-area {
|
|||||||
.search-replace-btn.destructive { border-color: var(--text-error); color: var(--text-error); }
|
.search-replace-btn.destructive { border-color: var(--text-error); color: var(--text-error); }
|
||||||
|
|
||||||
/* ── Active search result highlight ── */
|
/* ── Active search result highlight ── */
|
||||||
.search-result-item.search-result-active {
|
.btn-share.shared {
|
||||||
background: var(--accent-bg, rgba(99, 102, 241, 0.08));
|
color: #059669;
|
||||||
border-left: 3px solid var(--accent);
|
border-color: #059669;
|
||||||
padding-left: calc(var(--result-padding, 12px) - 3px);
|
background: rgba(5, 150, 105, 0.08);
|
||||||
}
|
}
|
||||||
|
.btn-share.shared:hover {
|
||||||
|
background: rgba(5, 150, 105, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Shared Cards ── */
|
||||||
|
.shared-card {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--bg-card, var(--bg-secondary));
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.shared-card:hover { background: var(--bg-hover); }
|
||||||
|
.shared-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.shared-card-title { font-weight: 500; font-size: 0.85rem; }
|
||||||
|
.shared-card-vault {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-bg, rgba(99,102,241,0.1));
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.shared-card-meta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.shared-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.shared-card-actions button {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.shared-card-actions button:hover { background: var(--bg-hover); }
|
||||||
|
|
||||||
/* ── Dashboard Tab Navigation ── */
|
/* ── Dashboard Tab Navigation ── */
|
||||||
.dashboard-tabs {
|
.dashboard-tabs {
|
||||||
|
|||||||
Reference in New Issue
Block a user