fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
+324
-7
@@ -13,6 +13,11 @@
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/design-tokens.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/components.css?v={{ asset_version }}">
|
||||
<!-- My Tasks + page « /db » : lies dans le SHELL et non dans la zone
|
||||
swappée. Un <link> à l'intérieur de `.main-wrapper` peut être retiré par
|
||||
htmx lors d'une navigation partielle : la page revenait alors sans
|
||||
aucune feuille de style (composants blancs en thème sombre). -->
|
||||
<link rel="stylesheet" href="/static/css/my_tasks.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
|
||||
<style>
|
||||
/* ── Mobile responsive (v4.0.2) ── */
|
||||
@@ -241,24 +246,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation icons row -->
|
||||
<div class="sidebar-nav-row">
|
||||
<a href="{% if local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn" :class="{ active: currentView === 'home' }" title="Home">
|
||||
<!-- Navigation tabs row — collapsible pills: the active tab expands with
|
||||
its label, the others collapse to their icon. Selecting a tab replaces
|
||||
the whole sidebar content (Home tree / Chat history / Meetings / Inbox). -->
|
||||
<div class="sidebar-nav-row" role="tablist" aria-label="Sidebar sections">
|
||||
<a href="{% if local_ws_id %}/local-workspace?ws={{ local_ws_id }}{% elif local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn nav-tab-btn" data-tab="home" :class="{ active: sidebarTab === 'home' }" @click="setSidebarTab('home')" :aria-selected="sidebarTab === 'home'" role="tab" title="Home">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||
Home
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'home' ? 'display:inline-block' : 'display:none'">Home</span>
|
||||
</a>
|
||||
<button class="nav-icon-btn" title="Chat (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.open()">
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="chat" :class="{ active: sidebarTab === 'chat' }" @click="setSidebarTab('chat')" :aria-selected="sidebarTab === 'chat'" role="tab" title="Chat (Ctrl+J)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'chat' ? 'display:inline-block' : 'display:none'">Chat</span>
|
||||
<span class="nav-tab-badge" x-show="chatHistory.length" x-cloak x-text="chatHistory.length" :style="sidebarTab === 'chat' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button class="nav-icon-btn" title="Inbox">
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="meetings" :class="{ active: sidebarTab === 'meetings' }" @click="setSidebarTab('meetings')" :aria-selected="sidebarTab === 'meetings'" role="tab" title="Meetings">
|
||||
<svg width="18" height="18" 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>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'meetings' ? 'display:inline-block' : 'display:none'">Meeting</span>
|
||||
<span class="nav-tab-badge" x-show="meetingToday.length" x-cloak x-text="meetingToday.length" :style="sidebarTab === 'meetings' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="inbox" :class="{ active: sidebarTab === 'inbox' }" @click="setSidebarTab('inbox')" :aria-selected="sidebarTab === 'inbox'" role="tab" title="Inbox">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/></svg>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'inbox' ? 'display:inline-block' : 'display:none'">Inbox</span>
|
||||
<span class="nav-tab-badge nav-tab-badge-accent" x-show="inboxUnread > 0" x-cloak x-text="inboxUnread > 99 ? '99+' : inboxUnread" :style="sidebarTab === 'inbox' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
|
||||
<button type="button" class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
|
||||
<svg width="18" height="18" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-scroll">
|
||||
|
||||
<!-- ── TAB: Home (workspace tree, recents, favorites, libraries…) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'home'" x-cloak>
|
||||
<!-- Workspace / Private -->
|
||||
<div class="sidebar-section" x-show="isSectionVisible('workspace')" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
||||
<div class="sidebar-section-header" @click="toggleSection('workspace')" @contextmenu.prevent="openSectionMenu($event, 'workspace')">
|
||||
@@ -571,6 +590,123 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div><!-- /sidebar-tab-panel home -->
|
||||
|
||||
<!-- ── TAB: Chat (agent shortcuts + conversation history) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'chat'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Chat</span>
|
||||
<button type="button" class="sb-panel-action" @click="chatNewConversation()" title="New conversation">{{ fd_icon("plus",14) }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Circular agent shortcuts -->
|
||||
<div class="sb-agent-row" x-show="agentList.length">
|
||||
<template x-for="a in agentList.slice(0, 8)" :key="a.id">
|
||||
<button type="button" class="sb-agent-avatar" :title="a.name" @click="agentOpen(a.id)">
|
||||
<span x-text="a.icon || '\u{1F916}'"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup-label">Past 30 days</div>
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="c in chatRecent" :key="c.id">
|
||||
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
|
||||
<span class="page-name" x-text="c.title || 'New conversation'"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!chatLoading && !chatRecent.length">
|
||||
<span class="page-name text-dim">No recent conversations</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-subgroup" x-show="chatOlder.length">
|
||||
<div class="sidebar-subgroup-label">Older</div>
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="c in chatOlder" :key="c.id">
|
||||
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
|
||||
<span class="page-name" x-text="c.title || 'New conversation'"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-item empty-hint" x-show="chatLoading" style="padding:8px;">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TAB: Meeting (upcoming events + quick actions) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'meetings'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Meeting</span>
|
||||
<button type="button" class="sb-panel-action" @click="loadMeetings(true)" title="Refresh">{{ fd_icon("refresh",14) }}</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="sb-quick-actions">
|
||||
<button type="button" class="sb-quick-btn" @click="meetingTranscribe()">{{ fd_icon("upload",14) }} Transcribe a podcast</button>
|
||||
<button type="button" class="sb-quick-btn" @click="meetingNote()">{{ fd_icon("sparkles",14) }} New AI meeting note</button>
|
||||
<input type="file" x-ref="meetingAudio" accept="audio/*" class="sb-hidden-input" @change="meetingUpload($event)">
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + 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>
|
||||
</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>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ── TAB: Inbox (notifications) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'inbox'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Inbox <span class="sb-panel-count" x-show="inboxUnread > 0" x-text="inboxUnread"></span></span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button type="button" class="sb-panel-action" x-show="inboxUnread > 0" @click="inboxMarkAllRead()" title="Mark all read">{{ fd_icon("check-square",14) }}</button>
|
||||
<button type="button" class="sb-panel-action" @click="inboxFilter = (inboxFilter === 'all' ? 'unread' : 'all')"
|
||||
:title="inboxFilter === 'all' ? 'Show unread only' : 'Show all'" x-text="inboxFilter === 'all' ? 'All' : 'Unread'"></button>
|
||||
<button type="button" class="sb-panel-action" @click="loadInbox()" title="Refresh">{{ fd_icon("refresh",14) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="n in inboxVisible" :key="n.id">
|
||||
<li class="sidebar-item" :class="{ 'sb-item-unread': !n.is_read }" @click="inboxOpen(n)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("bell",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.message"></span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!inboxLoading && !inboxVisible.length" style="flex-direction:column;align-items:center;gap:4px;padding:22px 8px;">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text-secondary);">Inbox Zero</span>
|
||||
<span class="page-name text-dim">You’re all caught up</span>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="inboxLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="sidebar-footer">
|
||||
<div style="display:flex; gap:6px;">
|
||||
@@ -1128,6 +1264,15 @@
|
||||
Alpine.data('appState', appState);
|
||||
});
|
||||
|
||||
/** Parse a SQL/ISO timestamp ("2026-10-04 12:00:00") into a ms epoch.
|
||||
Shared by the sidebar Chat tab (grouping by recency). */
|
||||
window.fdTime = function(value) {
|
||||
if (!value) return 0;
|
||||
var t = new Date(String(value).replace(' ', 'T'));
|
||||
if (isNaN(t.getTime())) t = new Date(value);
|
||||
return isNaN(t.getTime()) ? 0 : t.getTime();
|
||||
};
|
||||
|
||||
function appState() {
|
||||
return {
|
||||
init() {
|
||||
@@ -1135,6 +1280,8 @@
|
||||
this.loadSidebarConfig();
|
||||
this.loadAgents();
|
||||
this.loadTeamspaces();
|
||||
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
|
||||
this.loadInbox();
|
||||
this.initSidebarWidth();
|
||||
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
|
||||
this.startClipAutoRefresh();
|
||||
@@ -1240,6 +1387,176 @@
|
||||
giteaRepo: '{{ gitea_repo|default("") }}',
|
||||
giteaTree: [], // loaded client-side for sidebar
|
||||
currentView: 'home',
|
||||
|
||||
// ── Collapsible sidebar tabs (Home / Chat / Meeting / Inbox) ──
|
||||
// The active tab renders as a pill with its label, the others collapse
|
||||
// to an icon; selecting one replaces the whole sidebar content.
|
||||
sidebarTab: (function() {
|
||||
try {
|
||||
var saved = localStorage.getItem('fd_sidebar_tab');
|
||||
return ['home', 'chat', 'meetings', 'inbox'].indexOf(saved) >= 0 ? saved : 'home';
|
||||
} catch (e) { return 'home'; }
|
||||
})(),
|
||||
setSidebarTab(tab) {
|
||||
this.sidebarTab = tab;
|
||||
this.currentView = tab;
|
||||
try { localStorage.setItem('fd_sidebar_tab', tab); } catch (e) {}
|
||||
if (tab === 'chat') this.loadChatHistory();
|
||||
else if (tab === 'meetings') this.loadMeetings();
|
||||
else if (tab === 'inbox') this.loadInbox();
|
||||
},
|
||||
|
||||
// ── Chat tab ──
|
||||
chatHistory: [],
|
||||
chatLoading: false,
|
||||
get chatRecent() {
|
||||
var cut = Date.now() - 30 * 86400000;
|
||||
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) >= cut; });
|
||||
},
|
||||
get chatOlder() {
|
||||
var cut = Date.now() - 30 * 86400000;
|
||||
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) < cut; });
|
||||
},
|
||||
loadChatHistory() {
|
||||
var self = this;
|
||||
this.chatLoading = true;
|
||||
return fetch('/api/agent/conversations', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
self.chatHistory = (d && d.conversations) || [];
|
||||
self.chatLoading = false;
|
||||
})
|
||||
.catch(function() { self.chatLoading = false; });
|
||||
},
|
||||
chatOpenConversation(id) {
|
||||
if (window.fdAgent && window.fdAgent.openConversation) window.fdAgent.openConversation(id);
|
||||
else this.agentOpen();
|
||||
},
|
||||
chatNewConversation() {
|
||||
if (window.fdAgent && window.fdAgent.open) window.fdAgent.open();
|
||||
else this.agentNew();
|
||||
},
|
||||
|
||||
// ── Meeting tab ──
|
||||
meetings: [],
|
||||
meetingsLoading: false,
|
||||
meetingFilter: 'upcoming',
|
||||
get meetingVisible() {
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today; });
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today; });
|
||||
},
|
||||
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
|
||||
loadMeetings(force) {
|
||||
var self = this;
|
||||
if (!force && this.meetings.length) return Promise.resolve();
|
||||
this.meetingsLoading = true;
|
||||
return fetch('/api/v2/meetings/upcoming?days=30', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { events: [] }; })
|
||||
.then(function(d) {
|
||||
self.meetings = (d && d.events) || [];
|
||||
self.meetingsLoading = false;
|
||||
})
|
||||
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
|
||||
},
|
||||
meetingOpen(ev) {
|
||||
var target = ev.url || ('/db/' + ev.collection_id);
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
meetingTranscribe() {
|
||||
var input = this.$refs && this.$refs.meetingAudio;
|
||||
if (input) input.click();
|
||||
},
|
||||
meetingNote() {
|
||||
if (window.fdAgent && window.fdAgent.ask) {
|
||||
window.fdAgent.ask('/meeting-note');
|
||||
} else if (window.fdAgent && window.fdAgent.open) {
|
||||
window.fdAgent.open();
|
||||
}
|
||||
},
|
||||
meetingUpload(ev) {
|
||||
var self = this;
|
||||
var input = ev.target;
|
||||
var file = input && input.files && input.files[0];
|
||||
if (!file) return;
|
||||
var title = 'Podcast ' + new Date().toISOString().slice(0, 10);
|
||||
// A transcript row is attached to a page: create the meeting page first,
|
||||
// then upload the audio against it.
|
||||
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'); return r.json(); })
|
||||
.then(function(page) {
|
||||
var fd = new FormData();
|
||||
fd.append('audio', file);
|
||||
fd.append('page_id', String(page.id));
|
||||
return fetch('/api/v2/meetings/transcribe', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: fd
|
||||
}).then(function(r) {
|
||||
if (!r.ok) throw new Error('transcribe');
|
||||
return r.json();
|
||||
}).then(function() { return page; });
|
||||
})
|
||||
.then(function(page) {
|
||||
self.toast('Audio uploaded — transcription attached to "' + title + '"', 'success');
|
||||
window.location.href = '/pages/' + page.id;
|
||||
})
|
||||
.catch(function() { self.toast('Upload failed', 'error'); })
|
||||
.then(function() { if (input) input.value = ''; });
|
||||
},
|
||||
|
||||
// ── Inbox tab ──
|
||||
inboxItems: [],
|
||||
inboxUnread: 0,
|
||||
inboxLoading: false,
|
||||
inboxFilter: 'all',
|
||||
get inboxVisible() {
|
||||
if (this.inboxFilter === 'unread') {
|
||||
return this.inboxItems.filter(function(n) { return !n.is_read; });
|
||||
}
|
||||
return this.inboxItems;
|
||||
},
|
||||
loadInbox() {
|
||||
var self = this;
|
||||
this.inboxLoading = true;
|
||||
return fetch('/api/notifications?limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { notifications: [], unread: 0 }; })
|
||||
.then(function(d) {
|
||||
self.inboxItems = (d && d.notifications) || [];
|
||||
self.inboxUnread = (d && d.unread) || 0;
|
||||
self.inboxLoading = false;
|
||||
})
|
||||
.catch(function() { self.inboxLoading = false; });
|
||||
},
|
||||
inboxOpen(n) {
|
||||
if (!n.is_read) {
|
||||
var self = this;
|
||||
fetch('/api/notifications/read', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({ id: n.id })
|
||||
}).then(function() { return self.loadInbox(); });
|
||||
}
|
||||
if (n.url) window.location.href = n.url;
|
||||
},
|
||||
inboxMarkAllRead() {
|
||||
var self = this;
|
||||
fetch('/api/notifications/read', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({})
|
||||
}).then(function() { return self.loadInbox(); });
|
||||
},
|
||||
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
||||
contextActions: {},
|
||||
showNewPageMenu: false,
|
||||
|
||||
Reference in New Issue
Block a user