feat: filtering in sidebars
This commit is contained in:
@@ -45,6 +45,25 @@
|
||||
"render.video.vimeoTitle": "Vimeo-Video",
|
||||
"render.video.youtubeTitle": "YouTube-Video",
|
||||
"sidebar.chat.yesterday": "Gestern",
|
||||
"sidebar.tags": "Schlagwörter",
|
||||
"sidebar.categories": "Kategorien",
|
||||
"sidebar.clearTags": "Tags löschen",
|
||||
"sidebar.clearCategories": "Kategorien löschen",
|
||||
"sidebar.noPostsYet": "Noch keine Beiträge",
|
||||
"sidebar.noPagesYet": "Noch keine Seiten",
|
||||
"sidebar.noMediaYet": "Noch keine Medien",
|
||||
"sidebar.search": "Suchen",
|
||||
"sidebar.searchPostsPlaceholder": "Beiträge durchsuchen...",
|
||||
"sidebar.searchPagesPlaceholder": "Seiten durchsuchen...",
|
||||
"sidebar.searchMediaPlaceholder": "Medien durchsuchen...",
|
||||
"sidebar.toggleFilters": "Filter umschalten",
|
||||
"sidebar.results": "%{count} Ergebnisse",
|
||||
"sidebar.resultsFor": "%{count} Ergebnisse für \"%{query}\"",
|
||||
"sidebar.clearFilters": "Filter löschen",
|
||||
"sidebar.noMatchingPosts": "Keine passenden Beiträge",
|
||||
"sidebar.loadMore": "Mehr laden (%{loaded} von %{total})",
|
||||
"sidebar.loading": "Lädt...",
|
||||
"sidebar.noMediaFiles": "Keine Mediendateien",
|
||||
"%{count} media": "%{count} Medien",
|
||||
"%{count} posts": "%{count} Beiträge",
|
||||
"2 langs": "2 Sprachen",
|
||||
|
||||
@@ -45,6 +45,25 @@
|
||||
"render.video.vimeoTitle": "Vimeo video",
|
||||
"render.video.youtubeTitle": "YouTube video",
|
||||
"sidebar.chat.yesterday": "Yesterday",
|
||||
"sidebar.tags": "Tags",
|
||||
"sidebar.categories": "Categories",
|
||||
"sidebar.clearTags": "Clear tags",
|
||||
"sidebar.clearCategories": "Clear categories",
|
||||
"sidebar.noPostsYet": "No posts yet",
|
||||
"sidebar.noPagesYet": "No pages yet",
|
||||
"sidebar.noMediaYet": "No media yet",
|
||||
"sidebar.search": "Search",
|
||||
"sidebar.searchPostsPlaceholder": "Search posts...",
|
||||
"sidebar.searchPagesPlaceholder": "Search pages...",
|
||||
"sidebar.searchMediaPlaceholder": "Search media...",
|
||||
"sidebar.toggleFilters": "Toggle Filters",
|
||||
"sidebar.results": "%{count} results",
|
||||
"sidebar.resultsFor": "%{count} results for \"%{query}\"",
|
||||
"sidebar.clearFilters": "Clear filters",
|
||||
"sidebar.noMatchingPosts": "No matching posts",
|
||||
"sidebar.loadMore": "Load more (%{loaded} of %{total})",
|
||||
"sidebar.loading": "Loading...",
|
||||
"sidebar.noMediaFiles": "No media files",
|
||||
"%{count} media": "%{count} media",
|
||||
"%{count} posts": "%{count} posts",
|
||||
"2 langs": "2 langs",
|
||||
|
||||
@@ -45,6 +45,25 @@
|
||||
"render.video.vimeoTitle": "Vídeo de Vimeo",
|
||||
"render.video.youtubeTitle": "Vídeo de YouTube",
|
||||
"sidebar.chat.yesterday": "Ayer",
|
||||
"sidebar.tags": "Etiquetas",
|
||||
"sidebar.categories": "Categorías",
|
||||
"sidebar.clearTags": "Limpiar etiquetas",
|
||||
"sidebar.clearCategories": "Limpiar categorías",
|
||||
"sidebar.noPostsYet": "Aún no hay entradas",
|
||||
"sidebar.noPagesYet": "Aún no hay páginas",
|
||||
"sidebar.noMediaYet": "Aún no hay medios",
|
||||
"sidebar.search": "Buscar",
|
||||
"sidebar.searchPostsPlaceholder": "Buscar entradas...",
|
||||
"sidebar.searchPagesPlaceholder": "Buscar páginas...",
|
||||
"sidebar.searchMediaPlaceholder": "Buscar medios...",
|
||||
"sidebar.toggleFilters": "Alternar filtros",
|
||||
"sidebar.results": "%{count} resultados",
|
||||
"sidebar.resultsFor": "%{count} resultados para \"%{query}\"",
|
||||
"sidebar.clearFilters": "Limpiar filtros",
|
||||
"sidebar.noMatchingPosts": "No hay entradas coincidentes",
|
||||
"sidebar.loadMore": "Cargar más (%{loaded} de %{total})",
|
||||
"sidebar.loading": "Cargando...",
|
||||
"sidebar.noMediaFiles": "No hay archivos multimedia",
|
||||
"%{count} media": "%{count} medios",
|
||||
"%{count} posts": "%{count} publicaciones",
|
||||
"2 langs": "2 idiomas",
|
||||
|
||||
@@ -45,6 +45,25 @@
|
||||
"render.video.vimeoTitle": "Vidéo Vimeo",
|
||||
"render.video.youtubeTitle": "Vidéo YouTube",
|
||||
"sidebar.chat.yesterday": "Hier",
|
||||
"sidebar.tags": "Étiquettes",
|
||||
"sidebar.categories": "Catégories",
|
||||
"sidebar.clearTags": "Effacer les étiquettes",
|
||||
"sidebar.clearCategories": "Effacer les catégories",
|
||||
"sidebar.noPostsYet": "Aucun article pour le moment",
|
||||
"sidebar.noPagesYet": "Aucune page pour le moment",
|
||||
"sidebar.noMediaYet": "Aucun média pour le moment",
|
||||
"sidebar.search": "Rechercher",
|
||||
"sidebar.searchPostsPlaceholder": "Rechercher des articles...",
|
||||
"sidebar.searchPagesPlaceholder": "Rechercher des pages...",
|
||||
"sidebar.searchMediaPlaceholder": "Rechercher des médias...",
|
||||
"sidebar.toggleFilters": "Afficher/masquer les filtres",
|
||||
"sidebar.results": "%{count} résultats",
|
||||
"sidebar.resultsFor": "%{count} résultats pour \"%{query}\"",
|
||||
"sidebar.clearFilters": "Effacer les filtres",
|
||||
"sidebar.noMatchingPosts": "Aucun article correspondant",
|
||||
"sidebar.loadMore": "Charger plus (%{loaded} sur %{total})",
|
||||
"sidebar.loading": "Chargement...",
|
||||
"sidebar.noMediaFiles": "Aucun fichier média",
|
||||
"%{count} media": "%{count} médias",
|
||||
"%{count} posts": "%{count} articles",
|
||||
"2 langs": "2 langues",
|
||||
|
||||
@@ -45,6 +45,25 @@
|
||||
"render.video.vimeoTitle": "Video Vimeo",
|
||||
"render.video.youtubeTitle": "Video YouTube",
|
||||
"sidebar.chat.yesterday": "Ieri",
|
||||
"sidebar.tags": "Tag",
|
||||
"sidebar.categories": "Categorie",
|
||||
"sidebar.clearTags": "Cancella tag",
|
||||
"sidebar.clearCategories": "Cancella categorie",
|
||||
"sidebar.noPostsYet": "Nessun post",
|
||||
"sidebar.noPagesYet": "Nessuna pagina",
|
||||
"sidebar.noMediaYet": "Nessun media",
|
||||
"sidebar.search": "Cerca",
|
||||
"sidebar.searchPostsPlaceholder": "Cerca post...",
|
||||
"sidebar.searchPagesPlaceholder": "Cerca pagine...",
|
||||
"sidebar.searchMediaPlaceholder": "Cerca media...",
|
||||
"sidebar.toggleFilters": "Mostra/nascondi filtri",
|
||||
"sidebar.results": "%{count} risultati",
|
||||
"sidebar.resultsFor": "%{count} risultati per \"%{query}\"",
|
||||
"sidebar.clearFilters": "Cancella filtri",
|
||||
"sidebar.noMatchingPosts": "Nessun post corrispondente",
|
||||
"sidebar.loadMore": "Carica altro (%{loaded} di %{total})",
|
||||
"sidebar.loading": "Caricamento...",
|
||||
"sidebar.noMediaFiles": "Nessun file multimediale",
|
||||
"%{count} media": "%{count} media",
|
||||
"%{count} posts": "%{count} post",
|
||||
"2 langs": "2 lingue",
|
||||
|
||||
195
priv/ui/app.css
195
priv/ui/app.css
@@ -1563,6 +1563,201 @@ button {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sidebar-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-action:hover,
|
||||
.sidebar-action.active {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 6px;
|
||||
padding: 10px 12px 0;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.search-box button,
|
||||
.clear-filter,
|
||||
.filter-status button,
|
||||
.load-more-button,
|
||||
.calendar-year-header,
|
||||
.calendar-month,
|
||||
.filter-header,
|
||||
.filter-chip {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-box button,
|
||||
.clear-search {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.search-box button:hover,
|
||||
.clear-search:hover,
|
||||
.clear-filter:hover,
|
||||
.load-more-button:hover,
|
||||
.calendar-year-header:hover,
|
||||
.calendar-month:hover,
|
||||
.filter-header:hover,
|
||||
.filter-chip:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.calendar-view,
|
||||
.filter-panel,
|
||||
.filter-status,
|
||||
.sidebar-load-more {
|
||||
padding: 10px 12px 0;
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
width: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.calendar-years,
|
||||
.calendar-months,
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.calendar-year-header,
|
||||
.calendar-month {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.calendar-year-header.selected,
|
||||
.calendar-month.selected,
|
||||
.filter-chip.active {
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.year-count,
|
||||
.month-count,
|
||||
.sidebar-section-count {
|
||||
margin-left: auto;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.month-label,
|
||||
.year-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-months {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.filter-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-status button,
|
||||
.load-more-button {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.sidebar-load-more {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.load-more-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
464
priv/ui/app.js
464
priv/ui/app.js
@@ -14,6 +14,8 @@ const state = {
|
||||
session: hydrateSession(clone(bootstrap.session)),
|
||||
status: clone(bootstrap.status),
|
||||
projects: normalizeProjects(bootstrap.projects),
|
||||
sidebarContent: clone(bootstrap.content.sidebar),
|
||||
sidebarFilters: hydrateSidebarFilters(bootstrap.content.sidebar),
|
||||
projectMenuOpen: false,
|
||||
taskStatus: normalizeTaskStatus(bootstrap.task_status),
|
||||
handledTaskResults: {},
|
||||
@@ -135,6 +137,7 @@ function renderActivityButton(view) {
|
||||
function renderSidebar() {
|
||||
const view = currentSidebarView();
|
||||
const data = currentSidebarData();
|
||||
const filterState = currentSidebarFilterState(view.id);
|
||||
|
||||
root.querySelector(".sidebar").innerHTML = `
|
||||
<div class="sidebar-header">
|
||||
@@ -142,10 +145,187 @@ function renderSidebar() {
|
||||
<strong>${escapeHtml(tText(data.title))}</strong>
|
||||
<span class="sidebar-subtitle">${escapeHtml(tText(data.subtitle))}</span>
|
||||
</div>
|
||||
${data.filters?.enabled ? `
|
||||
<div class="sidebar-actions">
|
||||
<button class="sidebar-action ${filterState.showFilters ? "active" : ""}" data-sidebar-toggle-filters="${escapeHtmlAttribute(view.id)}" type="button" title="${escapeHtmlAttribute(t(data.filters.toggle_filters_label))}">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M6 12v-1h4v1H6zM4 8v-1h8v1H4zm-2-4v-1h12v1H2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
${renderSidebarSearchBox(data, view, filterState)}
|
||||
${renderSidebarFilterPanel(data, view, filterState)}
|
||||
${renderSidebarFilterStatus(data, view, filterState)}
|
||||
<div class="sidebar-content sidebar-body">
|
||||
${renderSidebarBody(data, view)}
|
||||
</div>
|
||||
${renderSidebarLoadMore(data, view)}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarSearchBox(data, view, filterState) {
|
||||
if (!data.filters?.enabled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
<form class="search-box" data-sidebar-search="${escapeHtmlAttribute(view.id)}">
|
||||
<input
|
||||
type="text"
|
||||
value="${escapeHtmlAttribute(filterState.search || "") }"
|
||||
placeholder="${escapeHtmlAttribute(t(data.filters.search_placeholder))}"
|
||||
data-sidebar-search-input="${escapeHtmlAttribute(view.id)}"
|
||||
>
|
||||
<button type="submit" title="${escapeHtmlAttribute(t("sidebar.search"))}">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M15.7 14.3l-4.2-4.2c-.2-.2-.5-.3-.8-.3.9-1.1 1.5-2.5 1.5-4C12.2 2.6 9.6 0 6.4 0S.6 2.6.6 5.8s2.6 5.8 5.8 5.8c1.5 0 2.9-.5 4-1.4 0 .3.1.6.3.8l4.2 4.2c.2.2.5.3.7.3s.5-.1.7-.3c.4-.4.4-1 0-1.4zm-9.3-4c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
${filterState.search ? `<button type="button" class="clear-search" data-sidebar-clear-search="${escapeHtmlAttribute(view.id)}" title="${escapeHtmlAttribute(t("sidebar.clearFilters"))}">✕</button>` : ""}
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarFilterPanel(data, view, filterState) {
|
||||
if (!data.filters?.enabled || !filterState.showFilters) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
${renderSidebarArchiveFilter(data, view, filterState)}
|
||||
${renderSidebarFilterChips(data, view, filterState)}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarArchiveFilter(data, view, filterState) {
|
||||
const entries = Array.isArray(data.filters?.year_month_counts) ? data.filters.year_month_counts : [];
|
||||
const years = groupSidebarYearMonths(entries);
|
||||
|
||||
return `
|
||||
<div class="calendar-view">
|
||||
<button class="calendar-header collapsible-header ${filterState.archiveCollapsed ? "collapsed" : "expanded"}" data-sidebar-toggle-collapse="${escapeHtmlAttribute(view.id)}:archive" type="button">
|
||||
<span class="collapse-icon">${filterState.archiveCollapsed ? "▶" : "▼"}</span>
|
||||
<span>${escapeHtml(t(data.filters.archive_label))}</span>
|
||||
</button>
|
||||
${(filterState.year || filterState.month) ? `<button class="clear-filter" data-sidebar-clear-date="${escapeHtmlAttribute(view.id)}" type="button" title="${escapeHtmlAttribute(t(data.filters.clear_filters_label))}">✕</button>` : ""}
|
||||
${filterState.archiveCollapsed ? "" : `
|
||||
<div class="calendar-years">
|
||||
${years
|
||||
.map(
|
||||
(yearEntry) => `
|
||||
<div class="calendar-year">
|
||||
<button class="calendar-year-header ${filterState.year === yearEntry.year && !filterState.month ? "selected" : ""}" data-sidebar-year="${escapeHtmlAttribute(view.id)}:${yearEntry.year}" type="button">
|
||||
<span class="expand-icon">${filterState.expandedYear === yearEntry.year ? "▼" : "▶"}</span>
|
||||
<span class="year-label">${escapeHtml(String(yearEntry.year))}</span>
|
||||
<span class="year-count">${escapeHtml(String(yearEntry.count))}</span>
|
||||
</button>
|
||||
${filterState.expandedYear === yearEntry.year ? `
|
||||
<div class="calendar-months">
|
||||
${yearEntry.months
|
||||
.map(
|
||||
(monthEntry) => `
|
||||
<button class="calendar-month ${filterState.year === yearEntry.year && filterState.month === monthEntry.month ? "selected" : ""}" data-sidebar-month="${escapeHtmlAttribute(view.id)}:${yearEntry.year}:${monthEntry.month}" type="button">
|
||||
<span class="month-label">${escapeHtml(formatDashboardMonth(yearEntry.year, monthEntry.month))}</span>
|
||||
<span class="month-count">${escapeHtml(String(monthEntry.count))}</span>
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarFilterChips(data, view, filterState) {
|
||||
const tags = Array.isArray(data.filters?.available_tags) ? data.filters.available_tags : [];
|
||||
const categories = Array.isArray(data.filters?.available_categories) ? data.filters.available_categories : [];
|
||||
|
||||
return `
|
||||
<div class="filter-panel">
|
||||
${tags.length ? `
|
||||
<div class="filter-section">
|
||||
<button class="filter-header collapsible-header ${filterState.tagsCollapsed ? "collapsed" : "expanded"}" data-sidebar-toggle-collapse="${escapeHtmlAttribute(view.id)}:tags" type="button">
|
||||
<span class="collapse-icon">${filterState.tagsCollapsed ? "▶" : "▼"}</span>
|
||||
<span>${escapeHtml(t(data.filters.tags_label))}</span>
|
||||
</button>
|
||||
${filterState.tagsCollapsed ? "" : `
|
||||
<div class="filter-chips">
|
||||
${tags
|
||||
.map(
|
||||
(tag) => `
|
||||
<button class="filter-chip ${filterState.tags.includes(tag) ? "active" : ""}" data-sidebar-tag="${escapeHtmlAttribute(view.id)}:${escapeHtmlAttribute(tag)}" type="button">
|
||||
${escapeHtml(tag)}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : ""}
|
||||
${categories.length ? `
|
||||
<div class="filter-section">
|
||||
<button class="filter-header collapsible-header ${filterState.categoriesCollapsed ? "collapsed" : "expanded"}" data-sidebar-toggle-collapse="${escapeHtmlAttribute(view.id)}:categories" type="button">
|
||||
<span class="collapse-icon">${filterState.categoriesCollapsed ? "▶" : "▼"}</span>
|
||||
<span>${escapeHtml(t(data.filters.categories_label))}</span>
|
||||
</button>
|
||||
${filterState.categoriesCollapsed ? "" : `
|
||||
<div class="filter-chips">
|
||||
${categories
|
||||
.map(
|
||||
(category) => `
|
||||
<button class="filter-chip ${filterState.categories.includes(category) ? "active" : ""}" data-sidebar-category="${escapeHtmlAttribute(view.id)}:${escapeHtmlAttribute(category)}" type="button">
|
||||
${escapeHtml(category)}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarFilterStatus(data, view, filterState) {
|
||||
if (!data.filters?.enabled || !data.filters.has_active_filters) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const count = Number(data.filters.total_count) || 0;
|
||||
const label = filterState.search
|
||||
? t(data.filters.results_for_label, { count, query: filterState.search })
|
||||
: t(data.filters.results_label, { count });
|
||||
|
||||
return `
|
||||
<div class="filter-status">
|
||||
<span>${escapeHtml(label)}</span>
|
||||
<button data-sidebar-clear-filters="${escapeHtmlAttribute(view.id)}" type="button">${escapeHtml(t(data.filters.clear_filters_label))}</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebarLoadMore(data, view) {
|
||||
if (!data.filters?.enabled || !data.filters.has_more) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="sidebar-load-more">
|
||||
<button class="load-more-button" data-sidebar-load-more="${escapeHtmlAttribute(view.id)}" type="button">
|
||||
${escapeHtml(t("sidebar.loadMore", { loaded: data.filters.loaded_count || 0, total: data.filters.total_count || 0 }))}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -340,6 +520,113 @@ function renderSidebarEmpty(message) {
|
||||
`;
|
||||
}
|
||||
|
||||
function groupSidebarYearMonths(entries) {
|
||||
const years = new Map();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const year = Number(entry.year);
|
||||
const month = Number(entry.month);
|
||||
const count = Number(entry.count) || 0;
|
||||
|
||||
if (!years.has(year)) {
|
||||
years.set(year, { year, count: 0, months: [] });
|
||||
}
|
||||
|
||||
const yearEntry = years.get(year);
|
||||
yearEntry.count += count;
|
||||
yearEntry.months.push({ month, count });
|
||||
});
|
||||
|
||||
return Array.from(years.values())
|
||||
.map((yearEntry) => ({
|
||||
...yearEntry,
|
||||
months: yearEntry.months.sort((left, right) => right.month - left.month),
|
||||
}))
|
||||
.sort((left, right) => right.year - left.year);
|
||||
}
|
||||
|
||||
function hydrateSidebarFilters(sidebarContent) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(sidebarContent || {}).map(([viewId, data]) => [viewId, defaultSidebarFilterState(viewId, data)])
|
||||
);
|
||||
}
|
||||
|
||||
function defaultSidebarFilterState(viewId, data) {
|
||||
const selected = data?.filters?.selected || {};
|
||||
|
||||
return {
|
||||
search: selected.search || "",
|
||||
year: selected.year || null,
|
||||
month: selected.month || null,
|
||||
tags: Array.isArray(selected.tags) ? [...selected.tags] : [],
|
||||
categories: Array.isArray(selected.categories) ? [...selected.categories] : [],
|
||||
showFilters: false,
|
||||
archiveCollapsed: true,
|
||||
tagsCollapsed: true,
|
||||
categoriesCollapsed: true,
|
||||
expandedYear: selected.year || null,
|
||||
displayLimit: data?.filters?.display_limit || data?.filters?.max_items || 500,
|
||||
};
|
||||
}
|
||||
|
||||
function currentSidebarFilterState(viewId) {
|
||||
if (!state.sidebarFilters[viewId]) {
|
||||
state.sidebarFilters[viewId] = defaultSidebarFilterState(viewId, state.sidebarContent[viewId]);
|
||||
}
|
||||
|
||||
return state.sidebarFilters[viewId];
|
||||
}
|
||||
|
||||
function applySidebarPostFilters(viewId) {
|
||||
void refreshSidebarView(viewId);
|
||||
}
|
||||
|
||||
function applySidebarMediaFilters(viewId) {
|
||||
void refreshSidebarView(viewId);
|
||||
}
|
||||
|
||||
async function refreshSidebarView(viewId) {
|
||||
try {
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
const response = await fetch("/api/sidebar", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
view: viewId,
|
||||
filters: {
|
||||
search: filterState.search,
|
||||
year: filterState.year,
|
||||
month: filterState.month,
|
||||
tags: filterState.tags,
|
||||
categories: filterState.categories,
|
||||
display_limit: filterState.displayLimit,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload.status !== "ok") {
|
||||
return;
|
||||
}
|
||||
|
||||
state.sidebarContent[viewId] = payload.data;
|
||||
state.sidebarFilters[viewId] = {
|
||||
...filterState,
|
||||
displayLimit: payload.data?.filters?.display_limit || filterState.displayLimit,
|
||||
};
|
||||
render();
|
||||
} catch (_error) {
|
||||
// Keep the shell usable if sidebar filtering is temporarily unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const tabs = state.session.tabs;
|
||||
const node = root.querySelector(".tab-bar");
|
||||
@@ -794,6 +1081,171 @@ function bindEvents() {
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-toggle-filters]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const viewId = button.dataset.sidebarToggleFilters;
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.showFilters = !filterState.showFilters;
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("form[data-sidebar-search]").forEach((form) => {
|
||||
form.onsubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const viewId = form.dataset.sidebarSearch;
|
||||
const input = form.querySelector("input[data-sidebar-search-input]");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.search = input?.value?.trim() || "";
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-clear-search]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const viewId = button.dataset.sidebarClearSearch;
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.search = "";
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-toggle-collapse]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const [viewId, section] = button.dataset.sidebarToggleCollapse.split(":");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
|
||||
if (section === "archive") {
|
||||
filterState.archiveCollapsed = !filterState.archiveCollapsed;
|
||||
}
|
||||
|
||||
if (section === "tags") {
|
||||
filterState.tagsCollapsed = !filterState.tagsCollapsed;
|
||||
}
|
||||
|
||||
if (section === "categories") {
|
||||
filterState.categoriesCollapsed = !filterState.categoriesCollapsed;
|
||||
}
|
||||
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-year]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const [viewId, year] = button.dataset.sidebarYear.split(":");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
const nextYear = Number.parseInt(year, 10);
|
||||
filterState.expandedYear = filterState.expandedYear === nextYear ? null : nextYear;
|
||||
filterState.year = nextYear;
|
||||
filterState.month = null;
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-month]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const [viewId, year, month] = button.dataset.sidebarMonth.split(":");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.year = Number.parseInt(year, 10);
|
||||
filterState.month = Number.parseInt(month, 10);
|
||||
filterState.expandedYear = filterState.year;
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-clear-date]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const viewId = button.dataset.sidebarClearDate;
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.year = null;
|
||||
filterState.month = null;
|
||||
filterState.expandedYear = null;
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-tag]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const [viewId, tag] = button.dataset.sidebarTag.split(":");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.tags = toggleSidebarFilterValue(filterState.tags, tag);
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-category]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const [viewId, category] = button.dataset.sidebarCategory.split(":");
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.categories = toggleSidebarFilterValue(filterState.categories, category);
|
||||
filterState.displayLimit = state.sidebarContent[viewId]?.filters?.max_items || filterState.displayLimit;
|
||||
applySidebarPostFilters(viewId);
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-clear-filters]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const viewId = button.dataset.sidebarClearFilters;
|
||||
const existing = currentSidebarFilterState(viewId);
|
||||
state.sidebarFilters[viewId] = {
|
||||
...defaultSidebarFilterState(viewId, state.sidebarContent[viewId]),
|
||||
showFilters: existing.showFilters,
|
||||
archiveCollapsed: existing.archiveCollapsed,
|
||||
tagsCollapsed: existing.tagsCollapsed,
|
||||
categoriesCollapsed: existing.categoriesCollapsed,
|
||||
};
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-sidebar-load-more]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const viewId = button.dataset.sidebarLoadMore;
|
||||
const filterState = currentSidebarFilterState(viewId);
|
||||
filterState.displayLimit += state.sidebarContent[viewId]?.filters?.max_items || 500;
|
||||
if (viewId === "media") {
|
||||
applySidebarMediaFilters(viewId);
|
||||
} else {
|
||||
applySidebarPostFilters(viewId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-open-tab]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
openTab(button.dataset.openRoute, button.dataset.openTab, button.dataset.openTitle, true);
|
||||
@@ -1383,7 +1835,7 @@ function activeItem() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = Object.values(bootstrap.content.sidebar).flatMap(flattenSidebarItems);
|
||||
const items = Object.values(state.sidebarContent).flatMap(flattenSidebarItems);
|
||||
return items.find((item) => item.route === tab.type && tabIdForItem(item, item.route) === tab.id) || null;
|
||||
}
|
||||
|
||||
@@ -1414,7 +1866,7 @@ function currentSidebarView() {
|
||||
}
|
||||
|
||||
function currentSidebarData() {
|
||||
return bootstrap.content.sidebar[state.session.active_view] || bootstrap.content.sidebar[bootstrap.registry.default_sidebar_view];
|
||||
return state.sidebarContent[state.session.active_view] || state.sidebarContent[bootstrap.registry.default_sidebar_view];
|
||||
}
|
||||
|
||||
function currentTabRef() {
|
||||
@@ -1615,6 +2067,12 @@ function tabIdForItem(item, route) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function toggleSidebarFilterValue(values, value) {
|
||||
return values.includes(value)
|
||||
? values.filter((entry) => entry !== value)
|
||||
: [...values, value];
|
||||
}
|
||||
|
||||
function sidebarViews() {
|
||||
return bootstrap.registry.sidebar_views;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user