mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
feat(ui): point help new-content indicator at the updated tabs and elements
- Replace timestamp comparison (help_last_viewed vs a hardcoded date) with a content-version marker (data-help-content-version) read from the rendered modal markup, so badge state always reflects the content actually served - Only mark content as viewed when the modal is opened while it contains new content; opening a stale pre-upgrade page no longer suppresses the badge after a refresh - Flag the Replay Tutorial button itself with a 'New' chip (hidden by default, one-time glow animation) and scroll it into view when revealed; tab-level dots now mark getting-started and shortcuts instead of documentation - Translate help.newContentBadge into all 9 locales, reusing the established help.documentation.newBadge renderings - Add HelpManager content-version unit tests (12 cases)
This commit is contained in:
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "Einstellungen & Konfiguration",
|
||||
"extensions": "Erweiterungen",
|
||||
"newBadge": "NEU"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NEU"
|
||||
},
|
||||
"update": {
|
||||
"title": "Nach Updates suchen",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "Settings & Configuration",
|
||||
"extensions": "Extensions",
|
||||
"newBadge": "NEW"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "New"
|
||||
},
|
||||
"update": {
|
||||
"title": "Check for Updates",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "Configuración",
|
||||
"extensions": "Extensiones",
|
||||
"newBadge": "NUEVO"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NUEVO"
|
||||
},
|
||||
"update": {
|
||||
"title": "Comprobar actualizaciones",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "Paramètres & Configuration",
|
||||
"extensions": "Extensions",
|
||||
"newBadge": "NOUVEAU"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NOUVEAU"
|
||||
},
|
||||
"update": {
|
||||
"title": "Vérifier les mises à jour",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "הגדרות ותצורה",
|
||||
"extensions": "הרחבות",
|
||||
"newBadge": "חדש"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "חדש"
|
||||
},
|
||||
"update": {
|
||||
"title": "בדוק עדכונים",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "設定&構成",
|
||||
"extensions": "拡張機能",
|
||||
"newBadge": "新着"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新着"
|
||||
},
|
||||
"update": {
|
||||
"title": "更新確認",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "설정 & 구성",
|
||||
"extensions": "확장",
|
||||
"newBadge": "신규"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "신규"
|
||||
},
|
||||
"update": {
|
||||
"title": "업데이트 확인",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "Настройки и конфигурация",
|
||||
"extensions": "Расширения",
|
||||
"newBadge": "НОВОЕ"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "НОВОЕ"
|
||||
},
|
||||
"update": {
|
||||
"title": "Проверить обновления",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "设置与配置",
|
||||
"extensions": "扩展",
|
||||
"newBadge": "新"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新"
|
||||
},
|
||||
"update": {
|
||||
"title": "检查更新",
|
||||
|
||||
+2
-1
@@ -2028,7 +2028,8 @@
|
||||
"settings": "設定與配置",
|
||||
"extensions": "擴充功能",
|
||||
"newBadge": "新"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新"
|
||||
},
|
||||
"update": {
|
||||
"title": "檢查更新",
|
||||
|
||||
@@ -167,6 +167,29 @@
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* Replay Tutorial button: badge hidden until the button is flagged as new content */
|
||||
.replay-tutorial-btn .new-content-badge {
|
||||
display: none;
|
||||
background-color: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.replay-tutorial-btn.has-new-content .new-content-badge {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* One-time attention pulse when the button is flagged as new content */
|
||||
@keyframes new-content-glow {
|
||||
0% { box-shadow: 0 0 0 0 oklch(from var(--lora-accent) l c h / 55%); }
|
||||
100% { box-shadow: 0 0 0 16px transparent; }
|
||||
}
|
||||
|
||||
.replay-tutorial-btn.has-new-content {
|
||||
animation: new-content-glow 1.2s ease-out 3;
|
||||
}
|
||||
|
||||
/* Update video list styles */
|
||||
.video-list {
|
||||
display: flex;
|
||||
|
||||
@@ -6,19 +6,11 @@ import { onboardingManager } from './OnboardingManager.js';
|
||||
*/
|
||||
export class HelpManager {
|
||||
constructor() {
|
||||
this.lastViewedTimestamp = getStorageItem('help_last_viewed', 0);
|
||||
this.latestContentTimestamp = new Date('2025-10-11').getTime(); // Will be updated from server or config
|
||||
// Version of the help content the user has seen. Compared against the
|
||||
// data-help-content-version marker rendered into the help modal markup,
|
||||
// so badge state is always derived from the content actually served.
|
||||
this.viewedContentVersion = getStorageItem('help_viewed_content_version', null);
|
||||
this.isInitialized = false;
|
||||
|
||||
// Default latest content data - could be fetched from server
|
||||
this.latestVideoData = {
|
||||
timestamp: new Date('2024-06-09').getTime(), // Default timestamp
|
||||
walkthrough: {
|
||||
id: 'hvKw31YpE-U',
|
||||
title: 'Getting Started with LoRA Manager'
|
||||
},
|
||||
playlistUpdated: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,9 +27,6 @@ export class HelpManager {
|
||||
// Check if we need to show the badge
|
||||
this.updateHelpBadge();
|
||||
|
||||
// Fetch latest video data (could be implemented to fetch from remote source)
|
||||
this.fetchLatestVideoData();
|
||||
|
||||
this.isInitialized = true;
|
||||
return this;
|
||||
}
|
||||
@@ -122,58 +111,81 @@ export class HelpManager {
|
||||
*/
|
||||
openHelpModal(tabId) {
|
||||
// Use modalManager to open the help modal
|
||||
if (window.modalManager) {
|
||||
window.modalManager.toggleModal('helpModal');
|
||||
if (!window.modalManager) return;
|
||||
|
||||
if (tabId) {
|
||||
this.activateHelpTab(tabId);
|
||||
}
|
||||
|
||||
// Add visual indicator to Documentation tab if there's new content
|
||||
this.updateDocumentationTabIndicator();
|
||||
|
||||
// Update the last viewed timestamp
|
||||
this.markContentAsViewed();
|
||||
|
||||
// Hide the badge
|
||||
this.hideHelpBadge();
|
||||
const hadNewContent = this.hasNewContent();
|
||||
|
||||
window.modalManager.toggleModal('helpModal');
|
||||
|
||||
if (tabId) {
|
||||
this.activateHelpTab(tabId);
|
||||
}
|
||||
|
||||
// Only acknowledge the content as viewed when the user opened the
|
||||
// modal while it actually contained new content. Opening a stale
|
||||
// (pre-upgrade) page must not suppress the badge after a refresh.
|
||||
if (hadNewContent) {
|
||||
this.updateNewContentTabIndicators();
|
||||
this.markContentAsViewed();
|
||||
}
|
||||
|
||||
// Hide the badge
|
||||
this.hideHelpBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual indicator to Documentation tab for new content
|
||||
* Add visual indicator to tabs that received new content
|
||||
*/
|
||||
updateDocumentationTabIndicator() {
|
||||
const docTab = document.querySelector('.tab-btn[data-tab="documentation"]');
|
||||
if (docTab && this.hasNewContent()) {
|
||||
docTab.classList.add('has-new-content');
|
||||
updateNewContentTabIndicators() {
|
||||
if (!this.hasNewContent()) return;
|
||||
|
||||
// Tabs updated in the 2026-09-03 discoverability release:
|
||||
// getting-started (Replay Tutorial button) and shortcuts (new cheat-sheet tab)
|
||||
const NEW_CONTENT_TABS = ['getting-started', 'shortcuts'];
|
||||
NEW_CONTENT_TABS.forEach(tabId => {
|
||||
const tab = document.querySelector(`.help-tabs .tab-btn[data-tab="${tabId}"]`);
|
||||
if (tab) {
|
||||
tab.classList.add('has-new-content');
|
||||
}
|
||||
});
|
||||
|
||||
// Point the indicator at the specific new element inside the
|
||||
// Getting Started tab, and scroll it into view so it is not lost
|
||||
// below the fold of the modal body.
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
if (replayBtn) {
|
||||
replayBtn.classList.add('has-new-content');
|
||||
const gettingStartedActive = document.querySelector('#getting-started.tab-pane.active');
|
||||
if (gettingStartedActive && typeof replayBtn.scrollIntoView === 'function') {
|
||||
replayBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark content as viewed by saving current timestamp
|
||||
* Mark content as viewed by persisting the version rendered in the DOM.
|
||||
* No-op when the served markup carries no version marker (stale assets),
|
||||
* so viewing old content never suppresses the badge for new content.
|
||||
*/
|
||||
markContentAsViewed() {
|
||||
this.lastViewedTimestamp = Date.now();
|
||||
setStorageItem('help_last_viewed', this.lastViewedTimestamp);
|
||||
const currentVersion = this.getCurrentContentVersion();
|
||||
if (!currentVersion) return;
|
||||
|
||||
this.viewedContentVersion = currentVersion;
|
||||
setStorageItem('help_viewed_content_version', this.viewedContentVersion);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch latest video data (could be implemented to actually fetch from a remote source)
|
||||
* Read the help content version from the rendered modal markup
|
||||
* @returns {string|null} Version marker, or null if the served markup has none
|
||||
*/
|
||||
fetchLatestVideoData() {
|
||||
// In a real implementation, you'd fetch this from your server
|
||||
// For now, we'll just use the hardcoded data from constructor
|
||||
|
||||
// Update the timestamp with the latest data
|
||||
this.latestContentTimestamp = Math.max(this.latestContentTimestamp, this.latestVideoData.timestamp);
|
||||
|
||||
// Check again if we need to show the badge with this new data
|
||||
this.updateHelpBadge();
|
||||
getCurrentContentVersion() {
|
||||
const marker = document.querySelector('[data-help-content-version]');
|
||||
return marker ? marker.getAttribute('data-help-content-version') : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update help badge visibility based on timestamps
|
||||
* Update help badge visibility based on viewed vs. served content version
|
||||
*/
|
||||
updateHelpBadge() {
|
||||
if (this.hasNewContent()) {
|
||||
@@ -182,13 +194,13 @@ export class HelpManager {
|
||||
this.hideHelpBadge();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's new content the user hasn't seen
|
||||
* Check if the served help content is newer than what the user has viewed
|
||||
*/
|
||||
hasNewContent() {
|
||||
// If user has never viewed the help, or the content is newer than last viewed
|
||||
return this.lastViewedTimestamp === 0 || this.latestContentTimestamp > this.lastViewedTimestamp;
|
||||
const currentVersion = this.getCurrentContentVersion();
|
||||
return Boolean(currentVersion) && currentVersion !== this.viewedContentVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- Help Modal -->
|
||||
<div id="helpModal" class="modal">
|
||||
<div id="helpModal" class="modal" data-help-content-version="2026-09-03">
|
||||
<div class="modal-content help-modal">
|
||||
<button class="close" onclick="modalManager.closeModal('helpModal')">×</button>
|
||||
<div class="help-header">
|
||||
@@ -44,6 +44,7 @@
|
||||
<button id="replayTutorialBtn" class="replay-tutorial-btn">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
<span>{{ t('help.gettingStarted.replayTutorial') }}</span>
|
||||
<span class="new-content-badge">{{ t('help.newContentBadge') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
||||
|
||||
const HELP_MANAGER_MODULE = new URL('../../../static/js/managers/HelpManager.js', import.meta.url).pathname;
|
||||
const VIEWED_KEY = 'lora_manager_help_viewed_content_version';
|
||||
|
||||
function setupDom({ versionMarker = null } = {}) {
|
||||
const markerAttr = versionMarker ? ` data-help-content-version="${versionMarker}"` : '';
|
||||
document.body.innerHTML = `
|
||||
<div class="help-toggle" id="helpToggleBtn">
|
||||
<span class="update-badge"></span>
|
||||
</div>
|
||||
<div id="helpModal" class="modal"${markerAttr}>
|
||||
<div class="help-tabs">
|
||||
<button class="tab-btn" data-tab="getting-started"></button>
|
||||
<button class="tab-btn" data-tab="shortcuts"></button>
|
||||
</div>
|
||||
<div class="tab-pane active" id="getting-started">
|
||||
<button id="replayTutorialBtn" class="replay-tutorial-btn"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('HelpManager content-version badge logic', () => {
|
||||
let HelpManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ HelpManager } = await import(HELP_MANAGER_MODULE));
|
||||
});
|
||||
|
||||
function badgeIsVisible() {
|
||||
return document.querySelector('#helpToggleBtn .update-badge').classList.contains('visible');
|
||||
}
|
||||
|
||||
it('has no new content when the served markup carries no version marker', () => {
|
||||
setupDom({ versionMarker: null });
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('has new content when a version marker exists and nothing has been viewed yet', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(true);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('has no new content once the stored viewed version matches the marker', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2026-09-03');
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('has new content again when the marker moves to a newer version', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2025-10-11');
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(true);
|
||||
});
|
||||
|
||||
it('markContentAsViewed persists the DOM marker version', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.markContentAsViewed();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBe('2026-09-03');
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
});
|
||||
|
||||
it('markContentAsViewed is a no-op without a version marker (stale assets)', () => {
|
||||
setupDom({ versionMarker: null });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.markContentAsViewed();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('opening the help modal without new content does not mark it as viewed', () => {
|
||||
// Regression test: on a stale (pre-upgrade) page the user may open the
|
||||
// help modal before refreshing; that must not suppress the badge for
|
||||
// the new content they have not seen yet.
|
||||
setupDom({ versionMarker: null });
|
||||
window.modalManager = { toggleModal: vi.fn() };
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.openHelpModal();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBeNull();
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('opening the help modal with new content marks it as viewed and hides the badge', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
window.modalManager = { toggleModal: vi.fn() };
|
||||
const manager = new HelpManager();
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(true);
|
||||
|
||||
manager.openHelpModal();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBe('2026-09-03');
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('adds new-content indicators to the getting-started and shortcuts tabs', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(document.querySelector('.help-tabs .tab-btn[data-tab="getting-started"]').classList.contains('has-new-content')).toBe(true);
|
||||
expect(document.querySelector('.help-tabs .tab-btn[data-tab="shortcuts"]').classList.contains('has-new-content')).toBe(true);
|
||||
});
|
||||
|
||||
it('flags the Replay Tutorial button and scrolls it into view', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
replayBtn.scrollIntoView = vi.fn();
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(replayBtn.classList.contains('has-new-content')).toBe(true);
|
||||
expect(replayBtn.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
|
||||
it('does not flag the Replay Tutorial button when the content is not new', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2026-09-03');
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(document.getElementById('replayTutorialBtn').classList.contains('has-new-content')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not scroll the Replay Tutorial button when the getting-started tab is inactive', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
document.getElementById('getting-started').classList.remove('active');
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
replayBtn.scrollIntoView = vi.fn();
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(replayBtn.scrollIntoView).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user