fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
@@ -181,6 +181,16 @@ document.addEventListener('alpine:init', function () {
|
||||
return String(c.id) === id ? Object.assign({}, c, { label: d.title || 'Untitled' }) : c;
|
||||
});
|
||||
});
|
||||
// Navigation in-place (ex. /local-workspace qui descend dans ses
|
||||
// dossiers sans recharger) : le chemin du header est recalé sur la
|
||||
// pile de dossiers courante. Même contrat que le rendu serveur.
|
||||
window.addEventListener('flowdeck:breadcrumb-changed', function (e) {
|
||||
var d = e && e.detail;
|
||||
if (!d || !d.crumbs || !d.crumbs.length) return;
|
||||
self.crumbs = d.crumbs.map(function (c, i) { c.origIndex = i; return c; });
|
||||
if (d.workspace_id) self.wsId = d.workspace_id;
|
||||
self.closeAll();
|
||||
});
|
||||
},
|
||||
|
||||
get display() {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script type="application/json" id="page-data" nonce="{{ csp_nonce() }}">
|
||||
{{ page_data | tojson }}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
|
||||
<script data-cfasync="false" src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/page_editor_scripts.js?v={{ asset_version }}"></script>
|
||||
<link rel="stylesheet" href="/static/css/meeting.css?v={{ asset_version }}">
|
||||
<script data-cfasync="false" src="/static/js/meeting_block.js?v={{ asset_version }}"></script>
|
||||
|
||||
+221
-25
@@ -361,13 +361,28 @@
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
{% if has_active_workspace %}
|
||||
<button class="section-action-btn" title="Add" @click.stop="fdCreatePage()">+</button>
|
||||
<button class="section-action-btn" title="New meeting note" @click.stop="fdCreateMeeting()">+</button>
|
||||
{% endif %}
|
||||
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'meetings')">⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section-items" x-show="sectionsOpen.meetings" x-transition>
|
||||
<ul class="sidebar-items"><li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span><span class="page-name text-dim">No meetings</span></li></ul>
|
||||
<ul class="sidebar-items" id="sidebar-meetings-items">
|
||||
<template x-for="n in meetingNotes" :key="'mn-' + n.id">
|
||||
<li class="sidebar-item" @click="meetingNoteOpen(n)"
|
||||
@contextmenu.prevent="showWsContext($event, n.id, n.title, false)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
|
||||
<span style="flex:1;min-width:0;">
|
||||
<span class="page-name" style="display:block;" x-text="n.title"></span>
|
||||
<span class="sb-item-sub" x-text="n.has_summary ? 'Summary ready' : (n.status && n.status !== 'idle' ? n.status : 'Ready to record')"></span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!meetingNotes.length">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span>
|
||||
<span class="page-name text-dim">No meetings</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -662,6 +677,10 @@
|
||||
<div class="sb-filter-row">
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'upcoming' }" @click="meetingFilter = 'upcoming'">Upcoming</button>
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'today' }" @click="meetingFilter = 'today'">Today</button>
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'past' }" @click="meetingFilter = 'past'; loadMeetingHistory()">Past</button>
|
||||
</div>
|
||||
<div class="sb-search-row">
|
||||
<input type="search" class="sb-search-input" placeholder="Search meetings…" x-model="meetingSearch" @input="if (meetingFilter === 'past') loadMeetingHistory()" aria-label="Search meetings">
|
||||
</div>
|
||||
|
||||
<div class="sb-quick-actions">
|
||||
@@ -671,15 +690,18 @@
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + ev.id">
|
||||
<template x-for="ev in meetingVisible" :key="(ev.collection_id || 'h') + '-' + ev.id">
|
||||
<li class="sidebar-item" @click="meetingOpen(ev)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
|
||||
<span class="page-name" x-text="ev.title"></span>
|
||||
<span class="sb-item-meta" x-text="ev.today ? 'Today' : ev.date"></span>
|
||||
<span style="flex:1;min-width:0;">
|
||||
<span class="page-name" style="display:block;" x-text="ev.title"></span>
|
||||
<span class="sb-item-sub" x-text="meetingCta(ev)"></span>
|
||||
</span>
|
||||
<span class="sb-item-meta" x-text="ev.today ? 'Today' : (ev.date || '')"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!meetingsLoading && !meetingVisible.length">
|
||||
<span class="page-name text-dim" x-text="meetingFilter === 'today' ? 'Nothing scheduled today' : 'No upcoming meetings'"></span>
|
||||
<span class="page-name text-dim" x-text="meetingEmptyHint()"></span>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
@@ -719,18 +741,35 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Footer : bouton "New" + popover "Create new" (vers le haut) -->
|
||||
<div class="sidebar-footer">
|
||||
<div style="display:flex; gap:6px;">
|
||||
<button class="sidebar-footer-btn" style="flex:1; justify-content:center; background:var(--bg-hover); border-radius:20px;">
|
||||
<span style="margin-right:4px;">{{ fd_icon("message-square",14) }}</span>New chat
|
||||
<span class="shortcut">Ctrl+O</span>
|
||||
<div class="fd-new-wrap" @click.outside="closeNewMenu()">
|
||||
<div class="fd-new-popover" x-show="newMenuOpen" x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 translate-y-1"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
role="menu" aria-label="Create new">
|
||||
<div class="fd-new-pop-header">Create new</div>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('page')">
|
||||
<span>{{ fd_icon("file",16) }}</span><span>Page</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('chat')">
|
||||
<span>{{ fd_icon("message-square",16) }}</span><span>Chat</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('meeting')">
|
||||
<span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="17" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg></span><span>AI Meeting Notes</span>
|
||||
</button>
|
||||
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('database')">
|
||||
<span>{{ fd_icon("grid",16) }}</span><span>Database</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="fd-new-btn" :class="{ open: newMenuOpen }"
|
||||
@click="toggleNewMenu()" :aria-expanded="newMenuOpen" title="Create new">
|
||||
<span>{{ fd_icon("edit",16) }}</span><span>New</span>
|
||||
</button>
|
||||
{% if has_active_workspace %}
|
||||
<button class="sidebar-footer-btn" style="width:36px; justify-content:center; background:var(--bg-hover); border-radius:8px;" onclick="window.FlowDeck.createPage()" title="New page">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /sidebar-scroll -->
|
||||
@@ -899,6 +938,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu-section">
|
||||
<div class="context-menu-item" @click="wsCtxAction('open')">
|
||||
<span class="menu-icon">{{ fd_icon("external-link",14) }}</span> Open
|
||||
</div>
|
||||
<div class="context-menu-item" @click="wsCtxAction('rename')">
|
||||
<span class="menu-icon">{{ fd_icon("edit",14) }}</span> Rename
|
||||
</div>
|
||||
@@ -1294,6 +1336,8 @@
|
||||
this.loadTeamspaces();
|
||||
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
|
||||
this.loadInbox();
|
||||
// v7.1.1 : notes de réunion (section Meetings, pas Workspace).
|
||||
this.loadMeetingNotes();
|
||||
this.initSidebarWidth();
|
||||
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
|
||||
this.startClipAutoRefresh();
|
||||
@@ -1310,7 +1354,7 @@
|
||||
});
|
||||
document.addEventListener('flowdeck:workspace-changed', () => {
|
||||
clearTimeout(self._clipRefreshTimer);
|
||||
self._clipRefreshTimer = setTimeout(() => self.refreshSidebarTree(), 400);
|
||||
self._clipRefreshTimer = setTimeout(() => { self.refreshSidebarTree(); self.loadMeetingNotes(); }, 400);
|
||||
});
|
||||
// Also refresh when tab becomes visible (user returns to FlowDeck after clipping)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -1451,15 +1495,37 @@
|
||||
|
||||
// ── Meeting tab ──
|
||||
meetings: [],
|
||||
meetingPast: [],
|
||||
meetingsLoading: false,
|
||||
meetingFilter: 'upcoming',
|
||||
meetingSearch: '',
|
||||
meetingHistoryLoaded: false,
|
||||
get meetingVisible() {
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today; });
|
||||
var q = (this.meetingSearch || '').trim().toLowerCase();
|
||||
function matches(e) {
|
||||
if (!q) return true;
|
||||
return ((e.title || '') + ' ' + (e.date || '')).toLowerCase().indexOf(q) >= 0;
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today; });
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today && matches(e); });
|
||||
}
|
||||
if (this.meetingFilter === 'past') {
|
||||
return this.meetingPast.filter(matches);
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today && matches(e); });
|
||||
},
|
||||
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
|
||||
meetingEmptyHint() {
|
||||
if (this.meetingFilter === 'today') return 'Nothing scheduled today';
|
||||
if (this.meetingFilter === 'past') return 'No past meeting notes yet';
|
||||
return 'No upcoming meetings';
|
||||
},
|
||||
meetingCta(ev) {
|
||||
if (ev.status === 'done' || ev.summary) return 'View summary';
|
||||
if (ev.today) return 'Join & transcribe';
|
||||
if (ev.page_id) return 'Open note';
|
||||
return 'Prepare';
|
||||
},
|
||||
loadMeetings(force) {
|
||||
var self = this;
|
||||
if (!force && this.meetings.length) return Promise.resolve();
|
||||
@@ -1472,8 +1538,38 @@
|
||||
})
|
||||
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
|
||||
},
|
||||
loadMeetingHistory() {
|
||||
var self = this;
|
||||
this.meetingsLoading = true;
|
||||
var q = encodeURIComponent((this.meetingSearch || '').trim());
|
||||
return fetch('/api/v2/meetings/history?query=' + q + '&limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { meetings: [] }; })
|
||||
.then(function(d) {
|
||||
self.meetingPast = (d && d.meetings) || [];
|
||||
self.meetingHistoryLoaded = true;
|
||||
self.meetingsLoading = false;
|
||||
})
|
||||
.catch(function() { self.meetingPast = []; self.meetingsLoading = false; });
|
||||
},
|
||||
meetingOpen(ev) {
|
||||
var target = ev.url || ('/db/' + ev.collection_id);
|
||||
var target = ev.url || (ev.page_id ? ('/pages/' + ev.page_id) : ('/db/' + ev.collection_id));
|
||||
if (ev.today) {
|
||||
try { this.toast('Opening today\u2019s meeting — press Start transcribing in the note.'); } catch (e) {}
|
||||
}
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
// ── v7.1.1 : notes avec bloc AI Meeting Notes → section Meetings ──
|
||||
meetingNotes: [],
|
||||
loadMeetingNotes() {
|
||||
var self = this;
|
||||
return fetch('/api/v2/meetings/notes?limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { notes: [] }; })
|
||||
.then(function(d) { self.meetingNotes = (d && d.notes) || []; })
|
||||
.catch(function() { self.meetingNotes = []; });
|
||||
},
|
||||
meetingNoteOpen(n) {
|
||||
var target = '/pages/' + n.id;
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
@@ -1614,6 +1710,15 @@
|
||||
this.wsCtx.visible = false;
|
||||
if (!id) return;
|
||||
switch(action) {
|
||||
case 'open':
|
||||
if (this.wsCtx.isFolder) {
|
||||
if (typeof this.navigateToFolder === 'function') this.navigateToFolder(id);
|
||||
} else {
|
||||
var target = '/pages/' + id;
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
}
|
||||
break;
|
||||
case 'new-file':
|
||||
this.newPageInFolder(id);
|
||||
break;
|
||||
@@ -1632,6 +1737,7 @@
|
||||
if (r.ok) {
|
||||
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(id, newName.trim());
|
||||
self.refreshSidebarTree();
|
||||
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
|
||||
document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed'));
|
||||
if (window._wsData && window._wsData._reloadAfterAction) {
|
||||
window._wsData._reloadAfterAction();
|
||||
@@ -1644,6 +1750,7 @@
|
||||
if (!id) break;
|
||||
if (!confirm('Delete "' + name + '"?')) break;
|
||||
this.deleteWorkspacePage(id, true);
|
||||
if (typeof this.loadMeetingNotes === 'function') this.loadMeetingNotes();
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1685,20 +1792,58 @@
|
||||
});
|
||||
},
|
||||
|
||||
// ── Sections collapsible ──
|
||||
// ── Sections collapsible (accordéon v7.70 : une seule ouverte à la fois) ──
|
||||
sectionsOpen: (function() {
|
||||
var def = { recents: true, private: true, meetings: true, favorites: true, agents: true, teamspaces: true, shared: true, published: true, apps: true, workspace: true, gitea: true };
|
||||
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
|
||||
var def = { recents: false, private: false, meetings: false, favorites: false, agents: false, teamspaces: false, shared: false, published: false, apps: false, workspace: true, gitea: false };
|
||||
try {
|
||||
var saved = JSON.parse(localStorage.getItem('fd_sections') || '{}');
|
||||
return Object.assign(def, saved);
|
||||
var merged = Object.assign({}, def, saved);
|
||||
// Normalise : si plusieurs sections ouvertes (ancien état), ne garder que la première.
|
||||
var first = null;
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
if (merged[order[i]]) { first = order[i]; break; }
|
||||
}
|
||||
if (first) {
|
||||
for (var j = 0; j < order.length; j++) merged[order[j]] = (order[j] === first);
|
||||
} else {
|
||||
merged.workspace = true;
|
||||
}
|
||||
return merged;
|
||||
} catch(e) { return def; }
|
||||
})(),
|
||||
toggleSection(key) {
|
||||
this.sectionsOpen[key] = !this.sectionsOpen[key];
|
||||
var isOpen = !!this.sectionsOpen[key];
|
||||
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
|
||||
if (isOpen) {
|
||||
this.sectionsOpen[key] = false;
|
||||
} else {
|
||||
for (var i = 0; i < order.length; i++) this.sectionsOpen[order[i]] = false;
|
||||
this.sectionsOpen[key] = true;
|
||||
}
|
||||
this.sectionsOpen = Object.assign({}, this.sectionsOpen);
|
||||
try { localStorage.setItem('fd_sections', JSON.stringify(this.sectionsOpen)); }
|
||||
catch(e) {}
|
||||
},
|
||||
// ── Menu "New" (popover "Create new" vers le haut) ──
|
||||
newMenuOpen: false,
|
||||
toggleNewMenu() { this.newMenuOpen = !this.newMenuOpen; },
|
||||
closeNewMenu() { this.newMenuOpen = false; },
|
||||
newMenuAction(kind) {
|
||||
this.newMenuOpen = false;
|
||||
if (kind === 'page') {
|
||||
if (window.FlowDeck && window.FlowDeck.createPage) window.FlowDeck.createPage();
|
||||
} else if (kind === 'chat') {
|
||||
if (typeof this.chatNewConversation === 'function') this.chatNewConversation();
|
||||
else if (typeof this.agentNew === 'function') this.agentNew();
|
||||
} else if (kind === 'meeting') {
|
||||
if (typeof this.fdCreateMeeting === 'function') this.fdCreateMeeting();
|
||||
else if (typeof this.meetingNote === 'function') this.meetingNote();
|
||||
} else if (kind === 'database') {
|
||||
if (typeof this.createDatabase === 'function') this.createDatabase();
|
||||
else if (window.showToast) window.showToast('Database creation: use a Gitea repo as a database.', 'info');
|
||||
}
|
||||
},
|
||||
// ── Sidebar customization ──
|
||||
customizing: false,
|
||||
sidebarConfig: {},
|
||||
@@ -1933,6 +2078,50 @@
|
||||
});
|
||||
},
|
||||
fdCreatePage() { window.FlowDeck.createPage(); },
|
||||
fdCreateMeeting() {
|
||||
// v7.1.1 : crée une note de réunion vide avec son bloc
|
||||
// AI Meeting Notes déjà inséré (même action que +, New, /meet).
|
||||
var self = this;
|
||||
var iso = new Date().toISOString().slice(0, 10);
|
||||
var title = 'Meeting @' + iso;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({ name: title, type: 'page' })
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error('page creation (' + r.status + ')'); return r.json(); })
|
||||
.then(function(page) {
|
||||
var block = {
|
||||
id: 'b1_' + Date.now().toString(36), type: 'meeting', content: '',
|
||||
meeting: {
|
||||
v: 2, title: title, date: iso, state: 'idle', mode: 'mic_only',
|
||||
instruction: 'auto', lang: 'fr', consent: null, consentAudio: false,
|
||||
tab: 'notes', channels: [], segments: [], segSeq: 0, notes: '',
|
||||
manual: '', interim: '', startedAt: 0, elapsedBase: 0, pausedMs: 0,
|
||||
steps: [], summaryMd: '', completeness: '', genTitle: '',
|
||||
titleEdited: false, shareHidden: false, bannerHidden: false,
|
||||
transcriptId: null, error: '', remoteSilentWarned: false,
|
||||
menuOpen: false, settingsOpen: false
|
||||
}
|
||||
};
|
||||
// Amorçage du bloc : en cas d'échec la page s'ouvre quand même
|
||||
// (l'utilisateur ajoute le bloc via /meet).
|
||||
return fetch('/board/api/pages/' + page.id + '/blocks', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ title: title, blocks: [block] })
|
||||
}).then(function() { return page; }).catch(function() { return page; });
|
||||
})
|
||||
.then(function(page) {
|
||||
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
|
||||
if (typeof self.refreshSidebarTree === 'function') self.refreshSidebarTree();
|
||||
if (window.fdNavigate) window.fdNavigate('/pages/' + page.id);
|
||||
else window.location.href = '/pages/' + page.id;
|
||||
})
|
||||
.catch(function(err) { self.toast('Meeting note creation failed: ' + err.message, 'error'); });
|
||||
},
|
||||
fdCreateFolder() { window.FlowDeck.showCreateFolderModal(); },
|
||||
fdGwRefresh() { if (window._gwData) window._gwData.refreshTree(); },
|
||||
sidebarSections() {
|
||||
@@ -2296,6 +2485,12 @@
|
||||
const manageLink = ul.querySelector('li:last-child');
|
||||
ul.innerHTML = html;
|
||||
if (manageLink) ul.appendChild(manageLink);
|
||||
// Le HTML injecté porte des directives Alpine (@contextmenu,
|
||||
// x-show des dossiers, drag&drop) : les monter explicitement,
|
||||
// sinon l'arbre perd menu contextuel et toggles après refresh.
|
||||
try {
|
||||
if (window.Alpine && window.Alpine.initTree) window.Alpine.initTree(ul);
|
||||
} catch (e) { console.error('Sidebar Alpine remount failed:', e); }
|
||||
})
|
||||
.catch(e => console.error('Sidebar refresh failed:', e));
|
||||
},
|
||||
@@ -2526,6 +2721,7 @@
|
||||
if (data.contextMenu) data.contextMenu.visible = false;
|
||||
if (data.wsCtx) data.wsCtx.visible = false;
|
||||
if (data.showNewPageMenu) data.showNewPageMenu = false;
|
||||
if (data.newMenuOpen) data.newMenuOpen = false;
|
||||
}
|
||||
}
|
||||
else if (e.ctrlKey && e.key === 's') {
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Prism.js syntax highlighting -->
|
||||
<link rel="stylesheet" href="/static/css/prism.css">
|
||||
<script src="/static/js/prism.min.js"></script>
|
||||
<!-- highlight.js syntax highlighting (~230 file types via FDHighlight helper) -->
|
||||
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
|
||||
<script src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
|
||||
<style>
|
||||
.gw-main{display:flex;flex-direction:column;height:calc(100vh - 48px);overflow:hidden;}
|
||||
.gw-content{padding:24px;overflow-y:auto;flex:1;}
|
||||
@@ -31,7 +33,7 @@
|
||||
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
|
||||
.gw-commit-bar input:focus{border-color:var(--accent);}
|
||||
|
||||
/* Prism theme tweaks for dark UI */
|
||||
/* highlight.js theme tweaks for dark UI */
|
||||
.gw-content pre[class*="language-"]{background:var(--bg-tertiary)!important;border:1px solid var(--border)!important;border-radius:8px!important;padding:16px!important;}
|
||||
</style>
|
||||
|
||||
@@ -62,7 +64,10 @@
|
||||
<div class="skeleton" style="height:15px;width:50%;"></div>
|
||||
</div>
|
||||
<!-- Syntax-highlighted code -->
|
||||
<pre x-show="!fileLoading"><code :class="'language-'+fileLanguage" x-ref="codeBlock" x-text="fileContent"></code></pre>
|
||||
<div class="gw-code-head" x-show="!fileLoading" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<span class="gw-code-lang" x-text="fileLanguageLabel" style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-tertiary);"></span>
|
||||
</div>
|
||||
<pre x-show="!fileLoading"><code class="hljs" :class="'language-'+fileLanguage" :data-hljs-lang="fileLanguage" x-ref="codeBlock" x-text="fileContent"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -60,20 +60,38 @@
|
||||
/* ── Table (div-based for hover effects) ── */
|
||||
.lib-table{width:100%;}
|
||||
.lib-table-header{display:flex;align-items:center;border-bottom:1px solid var(--border);padding:0;}
|
||||
.lib-table-header .th{display:flex;align-items:center;gap:5px;padding:6px 8px;font-size:12px;font-weight:500;color:var(--text-dim);user-select:none;white-space:nowrap;}
|
||||
.lib-table-header .th{display:flex;align-items:center;gap:5px;padding:6px 8px;font-size:12px;font-weight:500;color:var(--text-dim);user-select:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.lib-table-header .th svg{width:13px;height:13px;flex-shrink:0;}
|
||||
|
||||
/* Column widths — Notion-style */
|
||||
.lib-col-name{flex:1;min-width:0;}
|
||||
.lib-col-author{width:150px;flex-shrink:0;display:none;}
|
||||
.lib-col-source{width:120px;flex-shrink:0;display:none;}
|
||||
.lib-col-author{width:150px;flex-shrink:0;}
|
||||
.lib-col-editedby{width:150px;flex-shrink:0;}
|
||||
.lib-col-source{width:120px;flex-shrink:0;}
|
||||
.lib-col-edited{width:110px;flex-shrink:0;text-align:right;justify-content:flex-end;}
|
||||
.lib-table.show-author .lib-col-author{display:flex;}
|
||||
.lib-table.show-source .lib-col-source{display:flex;}
|
||||
.lib-col-visited{width:130px;flex-shrink:0;text-align:right;justify-content:flex-end;}
|
||||
/* Visibilité des colonnes pilotée par Show columns. Ces règles doivent
|
||||
battre .lib-table-header .th et .lib-row .row-cell (spécificité 0,2,0) :
|
||||
avant, seul :not(.show-editedtime) (0,3,0) gagnait, donc seule Last
|
||||
edited time s'affichait/masquait — les autres options semblaient mortes. */
|
||||
.lib-table div.lib-col-author,
|
||||
.lib-table div.lib-col-editedby,
|
||||
.lib-table div.lib-col-source,
|
||||
.lib-table div.lib-col-visited{display:none;}
|
||||
.lib-table.show-author div.lib-col-author,
|
||||
.lib-table.show-editedby div.lib-col-editedby,
|
||||
.lib-table.show-source div.lib-col-source,
|
||||
.lib-table.show-visited div.lib-col-visited{display:flex;}
|
||||
.lib-table:not(.show-editedtime) div.lib-col-edited{display:none;}
|
||||
.row-source{display:inline-flex;align-items:center;gap:5px;}
|
||||
.row-source svg{width:13px;height:13px;flex-shrink:0;color:var(--text-dim);}
|
||||
|
||||
/* ── Row ── */
|
||||
.lib-row{display:flex;align-items:center;height:34px;transition:background 0.1s;position:relative;}
|
||||
.lib-row:hover{background:var(--bg-hover);}
|
||||
/* ── Date section headers (AI Meeting Notes tab) ── */
|
||||
.lib-section-header{display:flex;align-items:center;font-size:12px;font-weight:600;color:var(--text-secondary);padding:10px 8px 4px;user-select:none;}
|
||||
.lib-section-header:first-child{padding-top:2px;}
|
||||
.lib-row.selected{background:rgba(35,131,226,0.08);}
|
||||
.lib-row.drag-over{outline:2px solid var(--accent);outline-offset:-2px;background:rgba(35,131,226,0.12);}
|
||||
.lib-row.dragging{opacity:0.4;}
|
||||
@@ -189,7 +207,7 @@
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media(max-width:768px){
|
||||
.lib-col-author,.lib-col-source{display:none!important;}
|
||||
.lib-col-author,.lib-col-editedby,.lib-col-source,.lib-col-visited{display:none!important;}
|
||||
.peek-overlay{width:100vw;max-width:100vw;}
|
||||
}
|
||||
</style>
|
||||
@@ -212,6 +230,9 @@
|
||||
<button class="lib-tab" :class="{ active: tab === 'recents' }" @click="switchTab('recents')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 16 14"/></svg> Recents
|
||||
</button>
|
||||
<button class="lib-tab" :class="{ active: tab === 'meetings' }" @click="switchTab('meetings')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> AI Meeting Notes
|
||||
</button>
|
||||
<button class="lib-tab" :class="{ active: tab === 'favorites' }" @click="switchTab('favorites')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> Favorites
|
||||
</button>
|
||||
@@ -265,6 +286,9 @@
|
||||
<div class="lib-dropdown" x-show="viewOpen" @click.outside="viewOpen=false" x-transition>
|
||||
<div class="dd-label">Show columns</div>
|
||||
<div class="dd-item" @click="toggleColumn('author')"><span class="check" x-text="showAuthor ? '✓' : ''"></span> Created by</div>
|
||||
<div class="dd-item" @click="toggleColumn('editedby')"><span class="check" x-text="showEditedBy ? '✓' : ''"></span> Last edited by</div>
|
||||
<div class="dd-item" @click="toggleColumn('editedtime')"><span class="check" x-text="showEditedTime ? '✓' : ''"></span> Last edited time</div>
|
||||
<div class="dd-item" @click="toggleColumn('visited')"><span class="check" x-text="showVisited ? '✓' : ''"></span> Last visited time</div>
|
||||
<div class="dd-item" @click="toggleColumn('source')"><span class="check" x-text="showSource ? '✓' : ''"></span> Source</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -351,7 +375,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Table ── -->
|
||||
<div class="lib-table" id="lib-table" :class="{'show-author': showAuthor, 'show-source': showSource}">
|
||||
<div class="lib-table" id="lib-table" :class="{'show-author': showAuthor, 'show-editedby': showEditedBy, 'show-editedtime': showEditedTime, 'show-visited': showVisited, 'show-source': showSource}">
|
||||
<div class="lib-table-header">
|
||||
<div class="th lib-col-name">
|
||||
<input type="checkbox" style="width:14px;height:14px;accent-color:var(--accent);cursor:pointer;margin-left:2px;margin-right:6px;" :checked="allSelected" @click="toggleSelectAll()" title="Select / unselect all">
|
||||
@@ -359,11 +383,16 @@
|
||||
Page name
|
||||
</div>
|
||||
<div class="th lib-col-author">Created by</div>
|
||||
<div class="th lib-col-source">Source</div>
|
||||
<div class="th lib-col-editedby">Last edited by</div>
|
||||
<div class="th lib-col-edited">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 16 14"/></svg>
|
||||
Last edited time
|
||||
</div>
|
||||
<div class="th lib-col-visited">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
Last visited time
|
||||
</div>
|
||||
<div class="th lib-col-source">Source</div>
|
||||
</div>
|
||||
<div class="lib-table-body" id="lib-table-body">
|
||||
</div>
|
||||
|
||||
@@ -166,9 +166,12 @@
|
||||
.bc-edit-go:hover{opacity:.85;}
|
||||
|
||||
/* ── Preserved styles ── */
|
||||
/* NOTE : scopées à .ws-tree (arbre principal) — la variante globale
|
||||
écrasait aussi les items .ws-tree-item de la sidebar (macro partagée),
|
||||
d'où un espacement différent sur /local-workspace. */
|
||||
.ws-tree{list-style:none;margin:0;padding:0;}
|
||||
.ws-tree-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:background 150ms;border:1px solid transparent;margin-bottom:2px;}
|
||||
.ws-tree-item:hover{background:var(--bg-tertiary);border-color:var(--border);}
|
||||
.ws-tree .ws-tree-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:background 150ms;border:1px solid transparent;margin-bottom:2px;}
|
||||
.ws-tree .ws-tree-item:hover{background:var(--bg-tertiary);border-color:var(--border);}
|
||||
.ws-tree-item .icon{font-size:16px;width:20px;text-align:center;flex-shrink:0;}
|
||||
.ws-tree-item .name{flex:1;font-size:14px;color:var(--text);}
|
||||
.ws-tree-item .name.folder{font-weight:500;cursor:pointer;}
|
||||
@@ -179,6 +182,8 @@
|
||||
.ws-tree-item .name.file .inline-rename-input{flex:0 1 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;filter:grayscale(1) brightness(1.6);}
|
||||
.ws-fav-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;color:#f5a623;}
|
||||
.ws-pub-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;}
|
||||
.ws-tree-item .actions{display:none;gap:2px;}
|
||||
.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;}
|
||||
@@ -480,7 +485,11 @@
|
||||
.preview-leave-end{transform:translateX(100%);opacity:0;}
|
||||
</style>
|
||||
|
||||
<script type="application/json" id="lw-config" nonce="{{ csp_nonce() }}">{{ {"current_folder_id": current_folder_id, "workspace_id": workspace_id} | tojson }}</script>
|
||||
<script type="application/json" id="lw-config" nonce="{{ csp_nonce() }}">{{ {"current_folder_id": current_folder_id, "workspace_id": workspace_id, "workspace_name": workspace_name} | tojson }}</script>
|
||||
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
|
||||
<script data-cfasync="false" src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/local_workspace.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">console.log('FLOWDECK v2.4.6 — ws-split about to render, _wsInitData exists:', typeof _wsInitData !== 'undefined');</script>
|
||||
|
||||
@@ -528,7 +537,6 @@
|
||||
<a href="/workspaces" class="btn-icon nav-arrow" title="All workspaces">{{ fd_icon("home",14) }}</a>
|
||||
<button class="btn-icon nav-arrow" @click="goBack()" title="Back" :disabled="!canGoBack">{{ fd_icon("chevron-left",14) }}</button>
|
||||
<button class="btn-icon nav-arrow" @click="goForward()" title="Forward" :disabled="!canGoForward">{{ fd_icon("chevron-right",14) }}</button>
|
||||
{% if breadcrumbs %}
|
||||
<div class="unified-breadcrumb">
|
||||
<span class="bc-icon">{{ fd_icon("folder",13) }}</span>
|
||||
<a href="/local-workspace" @click.prevent="navigateToRoot()" class="bc-root">{{ workspace_name }}</a>
|
||||
@@ -549,7 +557,6 @@
|
||||
</div>
|
||||
<button class="bc-edit-trigger" x-show="!editingPath" @click="startPathEdit()" title="Edit path">{{ fd_icon("edit",11) }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ws-toolbar" style="margin-left:auto;">
|
||||
<!-- View dropdown -->
|
||||
<div class="lib-toolbar-wrap" style="position:relative;">
|
||||
@@ -729,6 +736,8 @@
|
||||
<span class="name folder" @click.stop="navigateToFolder(node.id)"
|
||||
@dblclick.prevent.stop="startInlineRename(node, $event)">
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span x-show="node.favorited" class="ws-fav-badge" title="Favorite">★</span>
|
||||
<span x-show="node.published" class="ws-pub-badge" title="Published to web">🌐</span>
|
||||
<span x-show="renamingId !== node.id" x-text="node.name"></span>
|
||||
<span class="child-count" x-show="node.child_count > 0 && renamingId !== node.id"
|
||||
x-text="'(' + node.child_count + ')'"></span>
|
||||
@@ -743,6 +752,8 @@
|
||||
@mouseleave="hidePreview()"
|
||||
title="Click to rename, double-click to open">
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span x-show="node.favorited" class="ws-fav-badge" title="Favorite">★</span>
|
||||
<span x-show="node.published" class="ws-pub-badge" title="Published to web">🌐</span>
|
||||
<span x-show="renamingId !== node.id" x-text="node.name"></span>
|
||||
<input x-show="renamingId === node.id" class="inline-rename-input" x-model="renamingName"
|
||||
@keydown.enter="commitRename(node)" @keydown.escape="cancelRename()"
|
||||
|
||||
@@ -173,8 +173,17 @@
|
||||
</footer>
|
||||
|
||||
<script src="/static/js/katex.min.js?v=0.16.11"></script>
|
||||
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
|
||||
<script src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// hljs — syntax-highlight code blocks (~230 file types via FDHighlight).
|
||||
try {
|
||||
if (window.FDHighlight) FDHighlight.highlightAllIn(document.querySelector('.pub-content') || document);
|
||||
else if (window.hljs) document.querySelectorAll('.pub-content pre code').forEach(function (el) { try { window.hljs.highlightElement(el); } catch (e) {} });
|
||||
} catch (e) {}
|
||||
if (window.katex) {
|
||||
document.querySelectorAll('div[data-katex]').forEach(function (el) {
|
||||
var tex = el.getAttribute('data-katex') || '';
|
||||
|
||||
Reference in New Issue
Block a user