mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
fix(web-ui): Fix GitHub token warning persistence and improve UX (#154)
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
* fix(config): Add defaults to schemas and fix None value handling
- Updated merge_with_defaults to replace None values with defaults
- Fixed form processing to skip empty optional fields without defaults
- Added script to automatically add defaults to all plugin config schemas
- Added defaults to 89 fields across 10 plugin schemas
- Prevents validation errors from None values in configs
Changes:
- schema_manager.py: Enhanced merge_with_defaults to replace None with defaults
- api_v3.py: Added _SKIP_FIELD sentinel to skip optional fields without defaults
- add_defaults_to_schemas.py: Script to add sensible defaults to schemas
- Plugin schemas: Added defaults for number, boolean, and array fields
* fix(config): Fix save button spinner by checking HTTP status code
- Fixed handleConfigSave to check xhr.status instead of event.detail.successful
- With hx-swap="none", HTMX doesn't set event.detail.successful
- Now properly detects successful saves (status 200-299) and stops spinner
- Improved error message extraction from API responses
- Also fixed handleToggleResponse for consistency
* fix(web-ui): Resolve GitHub token warning persistence after save
- Made checkGitHubAuthStatus() return Promise for proper async handling
- Clear sessionStorage dismissal flag when token is saved
- Add delay before status check to ensure backend token reload
- Wait for status check completion before hiding settings panel
Fixes issue where GitHub token warnings and pop-ups would not
disappear after successfully saving a token in the web UI.
* fix(web-ui): Add token validation and improve GitHub token warning behavior
- Add token validation to backend API endpoint to check if token is valid/expired
- Implement _validate_github_token() method in PluginStoreManager with caching
- Update frontend to show warning only when token is missing or invalid
- Keep settings panel accessible (collapsible) when token is configured
- Collapse settings panel content after successful token save instead of hiding
- Display specific error messages for invalid/expired tokens
- Clear sessionStorage dismissal flag when token becomes valid
Fixes issue where GitHub token warnings and settings panel would not
properly hide/show based on token status. Now validates token validity
and provides better UX with collapsible settings panel.
* fix(web-ui): Fix CSS/display issue for GitHub token warning and settings
- Update all hide/show operations to use both classList and style.display
- Fix checkGitHubAuthStatus() to properly hide/show warning and settings
- Fix dismissGithubWarning() to use both methods
- Fix toggleGithubTokenSettings() with improved state checking
- Fix collapse button handler with improved state checking
- Fix saveGithubToken() to properly show/collapse settings panel
This ensures elements actually hide/show when status changes, matching
the pattern used elsewhere in the codebase (like toggleSection). All
buttons (dismiss, close, collapse) should now work correctly.
* fix(web-ui): Fix GitHub token expand button functionality
- Convert collapse button handler to named function (toggleGithubTokenContent)
- Improve state checking using class, inline style, and computed style
- Re-attach event listener after saving token to ensure it works
- Add console logging for debugging
- Make function globally accessible for better reliability
Fixes issue where expand button didn't work after saving token.
* fix(web-ui): Remove X button and improve GitHub token panel behavior
- Remove X (close) button from GitHub token configuration panel
- Replace toggleGithubTokenSettings() with openGithubTokenSettings() that only opens
- Auto-collapse panel when token is valid (user must click expand to edit)
- Auto-detect token status on page load (no need to click save)
- Simplify saveGithubToken() to rely on checkGitHubAuthStatus() for UI updates
- Ensure expand button works correctly with proper event listener attachment
The panel now remains visible but collapsed when a token is configured,
allowing users to expand it when needed without the ability to completely hide it.
* refactor(web-ui): Improve GitHub token collapse button code quality
- Update comment to reflect actual behavior (prevent parent click handlers)
- Use empty string for display to defer to CSS instead of hard-coding block/none
- Extract duplicate clone-and-attach logic into attachGithubTokenCollapseHandler() helper
- Make helper function globally accessible for reuse in checkGitHubAuthStatus()
Improves maintainability and makes code more future-proof for layout changes.
* fix(web-ui): Fix collapse/expand button by using removeProperty for display
- Use style.removeProperty('display') instead of style.display = ''
- This properly removes inline styles and defers to CSS classes
- Fixes issue where collapse/expand button stopped working after refactor
* fix(web-ui): Make display handling consistent for token collapse
- Use removeProperty('display') consistently in all places
- Fix checkGitHubAuthStatus() to use removeProperty instead of inline style
- Simplify state checking to rely on hidden class with computed style fallback
- Ensures collapse/expand button works correctly by deferring to CSS classes
* fix(web-ui): Fix token collapse button and simplify state detection
- Simplify state checking to rely on hidden class only (element has class='block')
- Only remove inline display style if it exists (check before removing)
- Add console logging to debug handler attachment
- Ensure collapse/expand works by relying on CSS classes
Fixes issues where:
- Collapse button did nothing
- Auto-detection of token status wasn't working
* debug(web-ui): Add extensive debugging for token collapse button
- Add console logs to track function calls and element detection
- Improve state detection to use computed style as fallback
- Add wrapper function for click handler to ensure it's called
- Better error messages to identify why handler might not attach
This will help identify why the collapse button isn't working.
* debug(web-ui): Add comprehensive debugging for GitHub token features
- Add console logs to checkGitHubAuthStatus() to track execution
- Re-attach collapse handler after plugin store is rendered
- Add error stack traces for better debugging
- Ensure handler is attached when content is dynamically loaded
This will help identify why:
- Auto-detection of token status isn't working
- Collapse button isn't functioning
* fix(web-ui): Move checkGitHubAuthStatus before IIFE to fix scope issue
- Move checkGitHubAuthStatus function definition before IIFE starts
- Function was defined after IIFE but called inside it, causing it to be undefined
- Now function is available when called during initialization
- This should fix auto-detection of token status on page load
* debug(web-ui): Add extensive logging to GitHub token functions
- Add logging when checkGitHubAuthStatus is defined
- Add logging when function is called during initialization
- Add logging in attachGithubTokenCollapseHandler
- Add logging in store render callback
- This will help identify why functions aren't executing
* fix(web-ui): Move GitHub token functions outside IIFE for availability
- Move attachGithubTokenCollapseHandler and toggleGithubTokenContent outside IIFE
- These functions need to be available when store renders, before IIFE completes
- Add logging to initializePlugins to track when it's called
- This should fix the 'undefined' error when store tries to attach handlers
* fix(web-ui): Fix GitHub token content collapse/expand functionality
- Element has 'block' class in HTML which conflicts with 'hidden' class
- When hiding: add 'hidden', remove 'block', set display:none inline
- When showing: remove 'hidden', add 'block', remove inline display
- This ensures proper visibility toggle for the GitHub API Configuration section
* perf(web-ui): Optimize GitHub token detection speed
- Call checkGitHubAuthStatus immediately when script loads (if elements exist)
- Call it early in initPluginsPage (before full initialization completes)
- Use requestAnimationFrame instead of setTimeout(100ms) for store render callback
- Reduce save token delay from 300ms to 100ms
- Token detection now happens in parallel with other initialization tasks
- This makes token status visible much faster on page load
* fix(web-ui): Fix all collapse/expand buttons on plugins page
- Fix Installed Plugins section collapse/expand button
- Fix Plugin Store section collapse/expand button
- Fix GitHub Install section collapse/expand button
- Apply same fixes as GitHub token button:
* Clone buttons to remove existing listeners
* Handle block/hidden class conflicts properly
* Add proper event prevention (stopPropagation/preventDefault)
* Add logging for debugging
- All collapse/expand buttons should now work correctly
* fix(web-ui): Fix syntax error in setupGitHubInstallHandlers
- Ensure all handler setup code is inside the function
- Add comment to mark function end clearly
* refactor(web-ui): Remove collapse buttons from Installed Plugins and Plugin Store
- Remove collapse/expand buttons from Installed Plugins section
- Remove collapse/expand buttons from Plugin Store section
- Remove related JavaScript handler code
- These sections are now always visible for better UX
- GitHub token section still has collapse functionality
---------
Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
@@ -52,6 +52,8 @@ class PluginStoreManager:
|
||||
self.cache_timeout = 3600 # 1 hour cache timeout
|
||||
self.registry_cache_timeout = 300 # 5 minutes for registry cache
|
||||
self.github_token = self._load_github_token()
|
||||
self._token_validation_cache = {} # Cache for token validation results: {token: (is_valid, timestamp, error_message)}
|
||||
self._token_validation_cache_timeout = 300 # 5 minutes cache for token validation
|
||||
|
||||
# Ensure plugins directory exists
|
||||
self.plugins_dir.mkdir(exist_ok=True)
|
||||
@@ -75,6 +77,83 @@ class PluginStoreManager:
|
||||
self.logger.debug(f"Could not load GitHub token: {e}")
|
||||
return None
|
||||
|
||||
def _validate_github_token(self, token: str) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate a GitHub token by making a lightweight API call.
|
||||
|
||||
Args:
|
||||
token: GitHub personal access token to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
- is_valid: True if token is valid, False otherwise
|
||||
- error_message: None if valid, error description if invalid
|
||||
"""
|
||||
if not token:
|
||||
return (False, "No token provided")
|
||||
|
||||
# Check cache first
|
||||
cache_key = token[:10] # Use first 10 chars as cache key for privacy
|
||||
if cache_key in self._token_validation_cache:
|
||||
cached_valid, cached_time, cached_error = self._token_validation_cache[cache_key]
|
||||
if time.time() - cached_time < self._token_validation_cache_timeout:
|
||||
return (cached_valid, cached_error)
|
||||
|
||||
# Validate token by making a lightweight API call to /user endpoint
|
||||
try:
|
||||
api_url = "https://api.github.com/user"
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'LEDMatrix-Plugin-Manager/1.0',
|
||||
'Authorization': f'token {token}'
|
||||
}
|
||||
|
||||
response = requests.get(api_url, headers=headers, timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Token is valid
|
||||
result = (True, None)
|
||||
self._token_validation_cache[cache_key] = (True, time.time(), None)
|
||||
return result
|
||||
elif response.status_code == 401:
|
||||
# Token is invalid or expired
|
||||
error_msg = "Token is invalid or expired"
|
||||
result = (False, error_msg)
|
||||
self._token_validation_cache[cache_key] = (False, time.time(), error_msg)
|
||||
return result
|
||||
elif response.status_code == 403:
|
||||
# Rate limit or forbidden (but token might be valid)
|
||||
# Check if it's a rate limit issue
|
||||
if 'rate limit' in response.text.lower():
|
||||
error_msg = "Rate limit exceeded"
|
||||
else:
|
||||
error_msg = "Token lacks required permissions"
|
||||
result = (False, error_msg)
|
||||
self._token_validation_cache[cache_key] = (False, time.time(), error_msg)
|
||||
return result
|
||||
else:
|
||||
# Other error
|
||||
error_msg = f"GitHub API error: {response.status_code}"
|
||||
result = (False, error_msg)
|
||||
self._token_validation_cache[cache_key] = (False, time.time(), error_msg)
|
||||
return result
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "GitHub API request timed out"
|
||||
result = (False, error_msg)
|
||||
# Don't cache timeout errors
|
||||
return result
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Network error: {str(e)}"
|
||||
result = (False, error_msg)
|
||||
# Don't cache network errors
|
||||
return result
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
result = (False, error_msg)
|
||||
# Don't cache unexpected errors
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _iso_to_date(iso_timestamp: str) -> str:
|
||||
"""Convert an ISO timestamp to YYYY-MM-DD string."""
|
||||
|
||||
@@ -2765,20 +2765,49 @@ def list_plugin_store():
|
||||
|
||||
@api_v3.route('/plugins/store/github-status', methods=['GET'])
|
||||
def get_github_auth_status():
|
||||
"""Check if GitHub authentication is configured"""
|
||||
"""Check if GitHub authentication is configured and validate token"""
|
||||
try:
|
||||
if not api_v3.plugin_store_manager:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin store manager not initialized'}), 500
|
||||
|
||||
# Check if GitHub token is configured
|
||||
has_token = api_v3.plugin_store_manager.github_token is not None and len(api_v3.plugin_store_manager.github_token) > 0
|
||||
token = api_v3.plugin_store_manager.github_token
|
||||
|
||||
# Check if GitHub token is configured
|
||||
if not token or len(token) == 0:
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'authenticated': has_token,
|
||||
'rate_limit': 5000 if has_token else 60,
|
||||
'message': 'GitHub API authenticated' if has_token else 'No GitHub token configured'
|
||||
'token_status': 'none',
|
||||
'authenticated': False,
|
||||
'rate_limit': 60,
|
||||
'message': 'No GitHub token configured',
|
||||
'error': None
|
||||
}
|
||||
})
|
||||
|
||||
# Validate the token
|
||||
is_valid, error_message = api_v3.plugin_store_manager._validate_github_token(token)
|
||||
|
||||
if is_valid:
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'token_status': 'valid',
|
||||
'authenticated': True,
|
||||
'rate_limit': 5000,
|
||||
'message': 'GitHub API authenticated',
|
||||
'error': None
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'token_status': 'invalid',
|
||||
'authenticated': False,
|
||||
'rate_limit': 60,
|
||||
'message': f'GitHub token is invalid: {error_message}' if error_message else 'GitHub token is invalid',
|
||||
'error': error_message
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
|
||||
@@ -449,6 +449,215 @@ if (_PLUGIN_DEBUG_EARLY) {
|
||||
}
|
||||
}
|
||||
|
||||
// GitHub Token Collapse Handler - Define early so it's available before IIFE
|
||||
console.log('[DEFINE] Defining attachGithubTokenCollapseHandler function...');
|
||||
window.attachGithubTokenCollapseHandler = function() {
|
||||
console.log('[attachGithubTokenCollapseHandler] Starting...');
|
||||
const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse');
|
||||
console.log('[attachGithubTokenCollapseHandler] Button found:', !!toggleTokenCollapseBtn);
|
||||
if (!toggleTokenCollapseBtn) {
|
||||
console.warn('[attachGithubTokenCollapseHandler] GitHub token collapse button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[attachGithubTokenCollapseHandler] Checking toggleGithubTokenContent...', {
|
||||
exists: typeof window.toggleGithubTokenContent
|
||||
});
|
||||
if (!window.toggleGithubTokenContent) {
|
||||
console.warn('[attachGithubTokenCollapseHandler] toggleGithubTokenContent function not defined');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing listeners by cloning the button
|
||||
const parent = toggleTokenCollapseBtn.parentNode;
|
||||
if (!parent) {
|
||||
console.warn('[attachGithubTokenCollapseHandler] Button parent not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const newBtn = toggleTokenCollapseBtn.cloneNode(true);
|
||||
parent.replaceChild(newBtn, toggleTokenCollapseBtn);
|
||||
|
||||
// Attach listener to the new button
|
||||
newBtn.addEventListener('click', function(e) {
|
||||
console.log('[attachGithubTokenCollapseHandler] Button clicked, calling toggleGithubTokenContent');
|
||||
window.toggleGithubTokenContent(e);
|
||||
});
|
||||
|
||||
console.log('[attachGithubTokenCollapseHandler] Handler attached to button:', newBtn.id);
|
||||
};
|
||||
|
||||
// Toggle GitHub Token Settings section
|
||||
console.log('[DEFINE] Defining toggleGithubTokenContent function...');
|
||||
window.toggleGithubTokenContent = function(e) {
|
||||
console.log('[toggleGithubTokenContent] called', e);
|
||||
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const tokenContent = document.getElementById('github-token-content');
|
||||
const tokenIconCollapse = document.getElementById('github-token-icon-collapse');
|
||||
const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse');
|
||||
|
||||
console.log('[toggleGithubTokenContent] Elements found:', {
|
||||
tokenContent: !!tokenContent,
|
||||
tokenIconCollapse: !!tokenIconCollapse,
|
||||
toggleTokenCollapseBtn: !!toggleTokenCollapseBtn
|
||||
});
|
||||
|
||||
if (!tokenContent || !toggleTokenCollapseBtn) {
|
||||
console.warn('[toggleGithubTokenContent] GitHub token content or button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasHiddenClass = tokenContent.classList.contains('hidden');
|
||||
const computedDisplay = window.getComputedStyle(tokenContent).display;
|
||||
|
||||
console.log('[toggleGithubTokenContent] Current state:', {
|
||||
hasHiddenClass,
|
||||
computedDisplay,
|
||||
buttonText: toggleTokenCollapseBtn.querySelector('span')?.textContent
|
||||
});
|
||||
|
||||
if (hasHiddenClass || computedDisplay === 'none') {
|
||||
// Show content - remove hidden class, add block class, remove inline display
|
||||
tokenContent.classList.remove('hidden');
|
||||
tokenContent.classList.add('block');
|
||||
tokenContent.style.removeProperty('display');
|
||||
if (tokenIconCollapse) {
|
||||
tokenIconCollapse.classList.remove('fa-chevron-down');
|
||||
tokenIconCollapse.classList.add('fa-chevron-up');
|
||||
}
|
||||
const span = toggleTokenCollapseBtn.querySelector('span');
|
||||
if (span) span.textContent = 'Collapse';
|
||||
console.log('[toggleGithubTokenContent] Content shown - removed hidden, added block');
|
||||
} else {
|
||||
// Hide content - add hidden class, remove block class, ensure display is none
|
||||
tokenContent.classList.add('hidden');
|
||||
tokenContent.classList.remove('block');
|
||||
tokenContent.style.display = 'none';
|
||||
if (tokenIconCollapse) {
|
||||
tokenIconCollapse.classList.remove('fa-chevron-up');
|
||||
tokenIconCollapse.classList.add('fa-chevron-down');
|
||||
}
|
||||
const span = toggleTokenCollapseBtn.querySelector('span');
|
||||
if (span) span.textContent = 'Expand';
|
||||
console.log('[toggleGithubTokenContent] Content hidden - added hidden, removed block, set display:none');
|
||||
}
|
||||
};
|
||||
|
||||
// GitHub Authentication Status - Define early so it's available in IIFE
|
||||
// Shows warning banner only when token is missing or invalid
|
||||
// The token itself is never exposed to the frontend for security
|
||||
// Returns a Promise so it can be awaited
|
||||
console.log('[DEFINE] Defining checkGitHubAuthStatus function...');
|
||||
window.checkGitHubAuthStatus = function checkGitHubAuthStatus() {
|
||||
console.log('[checkGitHubAuthStatus] Starting...');
|
||||
return fetch('/api/v3/plugins/store/github-status')
|
||||
.then(response => {
|
||||
console.log('checkGitHubAuthStatus: Response status:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('checkGitHubAuthStatus: Data received:', data);
|
||||
if (data.status === 'success') {
|
||||
const authData = data.data;
|
||||
const tokenStatus = authData.token_status || (authData.authenticated ? 'valid' : 'none');
|
||||
console.log('checkGitHubAuthStatus: Token status:', tokenStatus);
|
||||
const warning = document.getElementById('github-auth-warning');
|
||||
const settings = document.getElementById('github-token-settings');
|
||||
const rateLimit = document.getElementById('rate-limit-count');
|
||||
console.log('checkGitHubAuthStatus: Elements found:', {
|
||||
warning: !!warning,
|
||||
settings: !!settings,
|
||||
rateLimit: !!rateLimit
|
||||
});
|
||||
|
||||
// Show warning only when token is missing ('none') or invalid ('invalid')
|
||||
if (tokenStatus === 'none' || tokenStatus === 'invalid') {
|
||||
// Check if user has dismissed the warning (stored in session storage)
|
||||
const dismissed = sessionStorage.getItem('github-auth-warning-dismissed');
|
||||
if (!dismissed) {
|
||||
if (warning && rateLimit) {
|
||||
rateLimit.textContent = authData.rate_limit;
|
||||
|
||||
// Update warning message for invalid tokens
|
||||
if (tokenStatus === 'invalid' && authData.error) {
|
||||
const warningText = warning.querySelector('p.text-sm.text-yellow-700');
|
||||
if (warningText) {
|
||||
// Preserve the structure but update the message
|
||||
const errorMsg = authData.message || authData.error;
|
||||
warningText.innerHTML = `<strong>Token Invalid:</strong> ${errorMsg}. Please update your GitHub token to increase API rate limits to 5,000 requests/hour.`;
|
||||
}
|
||||
}
|
||||
// For 'none' status, use the default message from HTML template
|
||||
|
||||
// Show warning using both classList and style.display
|
||||
warning.classList.remove('hidden');
|
||||
warning.style.display = '';
|
||||
console.log(`GitHub token status: ${tokenStatus} - showing API limit warning`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure settings panel is accessible when token is missing or invalid
|
||||
// Panel can be opened via "Configure Token" link in warning
|
||||
// Don't force it to be visible, but don't prevent it from being shown
|
||||
} else if (tokenStatus === 'valid') {
|
||||
// Token is valid - hide warning and ensure settings panel is visible but collapsed
|
||||
if (warning) {
|
||||
// Hide warning using both classList and style.display
|
||||
warning.classList.add('hidden');
|
||||
warning.style.display = 'none';
|
||||
console.log('GitHub token is valid - hiding API limit warning');
|
||||
}
|
||||
|
||||
// Make settings panel visible but collapsed (accessible for token management)
|
||||
if (settings) {
|
||||
// Remove hidden class from panel itself - make it visible using both methods
|
||||
settings.classList.remove('hidden');
|
||||
settings.style.display = '';
|
||||
|
||||
// Always collapse the content when token is valid (user must click expand)
|
||||
const tokenContent = document.getElementById('github-token-content');
|
||||
if (tokenContent) {
|
||||
// Collapse the content - add hidden, remove block, set display none
|
||||
tokenContent.classList.add('hidden');
|
||||
tokenContent.classList.remove('block');
|
||||
tokenContent.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update collapse button state to show "Expand"
|
||||
const tokenIconCollapse = document.getElementById('github-token-icon-collapse');
|
||||
if (tokenIconCollapse) {
|
||||
tokenIconCollapse.classList.remove('fa-chevron-up');
|
||||
tokenIconCollapse.classList.add('fa-chevron-down');
|
||||
}
|
||||
|
||||
const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse');
|
||||
if (toggleTokenCollapseBtn) {
|
||||
const span = toggleTokenCollapseBtn.querySelector('span');
|
||||
if (span) span.textContent = 'Expand';
|
||||
|
||||
// Ensure event listener is attached
|
||||
if (window.attachGithubTokenCollapseHandler) {
|
||||
window.attachGithubTokenCollapseHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear dismissal flag when token becomes valid
|
||||
sessionStorage.removeItem('github-auth-warning-dismissed');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking GitHub auth status:', error);
|
||||
console.error('Error stack:', error.stack || 'No stack trace');
|
||||
});
|
||||
};
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
@@ -567,6 +776,13 @@ window.initPluginsPage = function() {
|
||||
window.pluginManager.initializing = true;
|
||||
window.__pluginDomReady = true;
|
||||
|
||||
// Check GitHub auth status immediately (don't wait for full initialization)
|
||||
// This can run in parallel with other initialization
|
||||
if (window.checkGitHubAuthStatus) {
|
||||
console.log('[INIT] Checking GitHub auth status immediately...');
|
||||
window.checkGitHubAuthStatus();
|
||||
}
|
||||
|
||||
// If we fetched data before the DOM existed, render it now
|
||||
if (window.__pendingInstalledPlugins) {
|
||||
console.log('[RENDER] Applying pending installed plugins data');
|
||||
@@ -706,17 +922,33 @@ function initializePluginPageWhenReady() {
|
||||
let pluginsInitialized = false;
|
||||
|
||||
function initializePlugins() {
|
||||
console.log('[initializePlugins] Called, pluginsInitialized:', pluginsInitialized);
|
||||
// Guard against multiple initializations
|
||||
if (pluginsInitialized) {
|
||||
console.log('[initializePlugins] Already initialized, skipping');
|
||||
pluginLog('[INIT] Plugins already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
pluginsInitialized = true;
|
||||
|
||||
console.log('[initializePlugins] Starting initialization...');
|
||||
pluginLog('[INIT] Initializing plugins...');
|
||||
|
||||
// Check GitHub authentication status
|
||||
checkGitHubAuthStatus();
|
||||
console.log('[INIT] Checking for checkGitHubAuthStatus function...', {
|
||||
exists: typeof window.checkGitHubAuthStatus,
|
||||
type: typeof window.checkGitHubAuthStatus
|
||||
});
|
||||
if (window.checkGitHubAuthStatus) {
|
||||
console.log('[INIT] Calling checkGitHubAuthStatus...');
|
||||
try {
|
||||
window.checkGitHubAuthStatus();
|
||||
} catch (error) {
|
||||
console.error('[INIT] Error calling checkGitHubAuthStatus:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('[INIT] checkGitHubAuthStatus not available yet');
|
||||
}
|
||||
|
||||
// Load both installed plugins and plugin store
|
||||
loadInstalledPlugins();
|
||||
@@ -3826,6 +4058,37 @@ function searchPluginStore(fetchCommitInfo = true) {
|
||||
} catch (e) {
|
||||
console.warn('Could not update store count:', e);
|
||||
}
|
||||
|
||||
// Ensure GitHub token collapse handler is attached after store is rendered
|
||||
// The button might not exist until the store content is loaded
|
||||
console.log('[STORE] Checking for attachGithubTokenCollapseHandler...', {
|
||||
exists: typeof window.attachGithubTokenCollapseHandler,
|
||||
checkGitHubAuthStatus: typeof window.checkGitHubAuthStatus
|
||||
});
|
||||
if (window.attachGithubTokenCollapseHandler) {
|
||||
// Use requestAnimationFrame for faster execution (runs on next frame, ~16ms)
|
||||
requestAnimationFrame(() => {
|
||||
console.log('[STORE] Re-attaching GitHub token collapse handler after store render');
|
||||
try {
|
||||
window.attachGithubTokenCollapseHandler();
|
||||
} catch (error) {
|
||||
console.error('[STORE] Error attaching collapse handler:', error);
|
||||
}
|
||||
// Also check auth status to update UI (already checked earlier, but refresh to be sure)
|
||||
if (window.checkGitHubAuthStatus) {
|
||||
console.log('[STORE] Refreshing GitHub auth status after store render...');
|
||||
try {
|
||||
window.checkGitHubAuthStatus();
|
||||
} catch (error) {
|
||||
console.error('[STORE] Error calling checkGitHubAuthStatus:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('[STORE] checkGitHubAuthStatus not available');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('[STORE] attachGithubTokenCollapseHandler not available');
|
||||
}
|
||||
} else {
|
||||
showError('Failed to search plugin store: ' + data.message);
|
||||
try {
|
||||
@@ -3977,78 +4240,17 @@ window.installPlugin = function(pluginId, branch = null) {
|
||||
}
|
||||
|
||||
function setupCollapsibleSections() {
|
||||
// Toggle Installed Plugins section
|
||||
const toggleInstalledBtn = document.getElementById('toggle-installed-plugins');
|
||||
const installedContent = document.getElementById('installed-plugins-content');
|
||||
const installedIcon = document.getElementById('installed-plugins-icon');
|
||||
console.log('[setupCollapsibleSections] Setting up collapsible sections...');
|
||||
|
||||
if (toggleInstalledBtn && installedContent) {
|
||||
toggleInstalledBtn.addEventListener('click', function() {
|
||||
const isHidden = installedContent.style.display === 'none' || installedContent.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
installedContent.style.display = 'block';
|
||||
installedContent.classList.remove('hidden');
|
||||
installedIcon.classList.remove('fa-chevron-down');
|
||||
installedIcon.classList.add('fa-chevron-up');
|
||||
toggleInstalledBtn.querySelector('span').textContent = 'Collapse';
|
||||
} else {
|
||||
installedContent.style.display = 'none';
|
||||
installedContent.classList.add('hidden');
|
||||
installedIcon.classList.remove('fa-chevron-up');
|
||||
installedIcon.classList.add('fa-chevron-down');
|
||||
toggleInstalledBtn.querySelector('span').textContent = 'Expand';
|
||||
}
|
||||
});
|
||||
// Installed Plugins and Plugin Store sections no longer have collapse buttons
|
||||
// They are always visible
|
||||
|
||||
// Functions are now defined outside IIFE, just attach the handler
|
||||
if (window.attachGithubTokenCollapseHandler) {
|
||||
window.attachGithubTokenCollapseHandler();
|
||||
}
|
||||
|
||||
// Toggle Plugin Store section
|
||||
const toggleStoreBtn = document.getElementById('toggle-plugin-store');
|
||||
const storeContent = document.getElementById('plugin-store-content');
|
||||
const storeIcon = document.getElementById('plugin-store-icon');
|
||||
|
||||
if (toggleStoreBtn && storeContent) {
|
||||
toggleStoreBtn.addEventListener('click', function() {
|
||||
const isHidden = storeContent.style.display === 'none' || storeContent.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
storeContent.style.display = 'block';
|
||||
storeContent.classList.remove('hidden');
|
||||
storeIcon.classList.remove('fa-chevron-down');
|
||||
storeIcon.classList.add('fa-chevron-up');
|
||||
toggleStoreBtn.querySelector('span').textContent = 'Collapse';
|
||||
} else {
|
||||
storeContent.style.display = 'none';
|
||||
storeContent.classList.add('hidden');
|
||||
storeIcon.classList.remove('fa-chevron-up');
|
||||
storeIcon.classList.add('fa-chevron-down');
|
||||
toggleStoreBtn.querySelector('span').textContent = 'Expand';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle GitHub Token Settings section
|
||||
const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse');
|
||||
const tokenContent = document.getElementById('github-token-content');
|
||||
const tokenIconCollapse = document.getElementById('github-token-icon-collapse');
|
||||
|
||||
if (toggleTokenCollapseBtn && tokenContent) {
|
||||
toggleTokenCollapseBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Prevent triggering the close button
|
||||
const isHidden = tokenContent.style.display === 'none' || tokenContent.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
tokenContent.style.display = 'block';
|
||||
tokenContent.classList.remove('hidden');
|
||||
tokenIconCollapse.classList.remove('fa-chevron-down');
|
||||
tokenIconCollapse.classList.add('fa-chevron-up');
|
||||
toggleTokenCollapseBtn.querySelector('span').textContent = 'Collapse';
|
||||
} else {
|
||||
tokenContent.style.display = 'none';
|
||||
tokenContent.classList.add('hidden');
|
||||
tokenIconCollapse.classList.remove('fa-chevron-up');
|
||||
tokenIconCollapse.classList.add('fa-chevron-down');
|
||||
toggleTokenCollapseBtn.querySelector('span').textContent = 'Expand';
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log('[setupCollapsibleSections] Collapsible sections setup complete');
|
||||
}
|
||||
|
||||
function loadSavedRepositories() {
|
||||
@@ -4135,26 +4337,66 @@ window.removeSavedRepository = function(repoUrl) {
|
||||
}
|
||||
|
||||
function setupGitHubInstallHandlers() {
|
||||
console.log('[setupGitHubInstallHandlers] Setting up GitHub install handlers...');
|
||||
|
||||
// Toggle GitHub install section visibility
|
||||
const toggleBtn = document.getElementById('toggle-github-install');
|
||||
const installSection = document.getElementById('github-install-section');
|
||||
const icon = document.getElementById('github-install-icon');
|
||||
|
||||
console.log('[setupGitHubInstallHandlers] Elements found:', {
|
||||
button: !!toggleBtn,
|
||||
section: !!installSection,
|
||||
icon: !!icon
|
||||
});
|
||||
|
||||
if (toggleBtn && installSection) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
const isHidden = installSection.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
installSection.classList.remove('hidden');
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
toggleBtn.querySelector('span').textContent = 'Hide';
|
||||
// Clone button to remove any existing listeners
|
||||
const parent = toggleBtn.parentNode;
|
||||
if (parent) {
|
||||
const newBtn = toggleBtn.cloneNode(true);
|
||||
parent.replaceChild(newBtn, toggleBtn);
|
||||
|
||||
newBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('[setupGitHubInstallHandlers] GitHub install toggle clicked');
|
||||
|
||||
const section = document.getElementById('github-install-section');
|
||||
const iconEl = document.getElementById('github-install-icon');
|
||||
const btn = document.getElementById('toggle-github-install');
|
||||
|
||||
if (!section || !btn) return;
|
||||
|
||||
const hasHiddenClass = section.classList.contains('hidden');
|
||||
const computedDisplay = window.getComputedStyle(section).display;
|
||||
|
||||
if (hasHiddenClass || computedDisplay === 'none') {
|
||||
// Show section - remove hidden, ensure visible
|
||||
section.classList.remove('hidden');
|
||||
section.style.removeProperty('display');
|
||||
if (iconEl) {
|
||||
iconEl.classList.remove('fa-chevron-down');
|
||||
iconEl.classList.add('fa-chevron-up');
|
||||
}
|
||||
const span = btn.querySelector('span');
|
||||
if (span) span.textContent = 'Hide';
|
||||
} else {
|
||||
installSection.classList.add('hidden');
|
||||
icon.classList.remove('fa-chevron-up');
|
||||
icon.classList.add('fa-chevron-down');
|
||||
toggleBtn.querySelector('span').textContent = 'Show';
|
||||
// Hide section - add hidden, set display none
|
||||
section.classList.add('hidden');
|
||||
section.style.display = 'none';
|
||||
if (iconEl) {
|
||||
iconEl.classList.remove('fa-chevron-up');
|
||||
iconEl.classList.add('fa-chevron-down');
|
||||
}
|
||||
const span = btn.querySelector('span');
|
||||
if (span) span.textContent = 'Show';
|
||||
}
|
||||
});
|
||||
console.log('[setupGitHubInstallHandlers] Handler attached');
|
||||
}
|
||||
} else {
|
||||
console.warn('[setupGitHubInstallHandlers] Required elements not found');
|
||||
}
|
||||
|
||||
// Install single plugin from URL
|
||||
@@ -4669,31 +4911,47 @@ function togglePasswordVisibility(fieldId) {
|
||||
}
|
||||
|
||||
// GitHub Token Configuration Functions
|
||||
window.toggleGithubTokenSettings = function() {
|
||||
// Open GitHub Token Settings panel (only opens, doesn't close)
|
||||
// Used when user clicks "Configure Token" link
|
||||
window.openGithubTokenSettings = function() {
|
||||
const settings = document.getElementById('github-token-settings');
|
||||
const warning = document.getElementById('github-auth-warning');
|
||||
const tokenContent = document.getElementById('github-token-content');
|
||||
|
||||
if (settings) {
|
||||
// Remove inline style if present to avoid conflicts
|
||||
if (settings.style.display !== undefined) {
|
||||
// Show settings panel using both methods
|
||||
settings.classList.remove('hidden');
|
||||
settings.style.display = '';
|
||||
|
||||
// Expand the content when opening
|
||||
if (tokenContent) {
|
||||
tokenContent.style.removeProperty('display');
|
||||
tokenContent.classList.remove('hidden');
|
||||
|
||||
// Update collapse button state
|
||||
const tokenIconCollapse = document.getElementById('github-token-icon-collapse');
|
||||
const toggleTokenCollapseBtn = document.getElementById('toggle-github-token-collapse');
|
||||
if (tokenIconCollapse) {
|
||||
tokenIconCollapse.classList.remove('fa-chevron-down');
|
||||
tokenIconCollapse.classList.add('fa-chevron-up');
|
||||
}
|
||||
if (toggleTokenCollapseBtn) {
|
||||
const span = toggleTokenCollapseBtn.querySelector('span');
|
||||
if (span) span.textContent = 'Collapse';
|
||||
}
|
||||
}
|
||||
// Toggle Tailwind hidden class
|
||||
settings.classList.toggle('hidden');
|
||||
|
||||
const isOpening = !settings.classList.contains('hidden');
|
||||
|
||||
// When opening settings, hide the warning banner (they're now combined)
|
||||
if (isOpening && warning) {
|
||||
// When opening settings, hide the warning banner
|
||||
if (warning) {
|
||||
warning.classList.add('hidden');
|
||||
warning.style.display = 'none';
|
||||
// Clear any dismissal state since user is actively configuring
|
||||
sessionStorage.removeItem('github-auth-warning-dismissed');
|
||||
}
|
||||
|
||||
// Load token when opening the panel
|
||||
if (isOpening) {
|
||||
loadGithubToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleGithubTokenVisibility = function() {
|
||||
@@ -4845,13 +5103,17 @@ window.saveGithubToken = function() {
|
||||
// Clear input field for security (user can reload if needed)
|
||||
input.value = '';
|
||||
|
||||
// Refresh GitHub auth status to update UI (this will hide warning and settings)
|
||||
checkGitHubAuthStatus();
|
||||
// Clear the dismissal flag so warning can properly hide/show based on token status
|
||||
sessionStorage.removeItem('github-auth-warning-dismissed');
|
||||
|
||||
// Hide the settings panel after successful save
|
||||
// Small delay to ensure backend has reloaded the token, then refresh status
|
||||
// checkGitHubAuthStatus() will handle collapsing the panel automatically
|
||||
// Reduced delay from 300ms to 100ms - backend should reload quickly
|
||||
setTimeout(() => {
|
||||
toggleGithubTokenSettings();
|
||||
}, 1500);
|
||||
if (window.checkGitHubAuthStatus) {
|
||||
window.checkGitHubAuthStatus();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to save token');
|
||||
}
|
||||
@@ -4870,60 +5132,18 @@ window.saveGithubToken = function() {
|
||||
});
|
||||
}
|
||||
|
||||
// GitHub Authentication Status
|
||||
// Only shows the warning banner if no GitHub token is configured
|
||||
// The token itself is never exposed to the frontend for security
|
||||
function checkGitHubAuthStatus() {
|
||||
fetch('/api/v3/plugins/store/github-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
const authData = data.data;
|
||||
|
||||
// Only show the banner if no GitHub token is configured
|
||||
if (!authData.authenticated) {
|
||||
// Check if user has dismissed the warning (stored in session storage)
|
||||
const dismissed = sessionStorage.getItem('github-auth-warning-dismissed');
|
||||
if (!dismissed) {
|
||||
const warning = document.getElementById('github-auth-warning');
|
||||
const rateLimit = document.getElementById('rate-limit-count');
|
||||
|
||||
if (warning && rateLimit) {
|
||||
rateLimit.textContent = authData.rate_limit;
|
||||
warning.classList.remove('hidden');
|
||||
console.log('GitHub token not configured - showing API limit warning');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Token is configured - hide both warning and settings
|
||||
const warning = document.getElementById('github-auth-warning');
|
||||
const settings = document.getElementById('github-token-settings');
|
||||
|
||||
if (warning) {
|
||||
warning.classList.add('hidden');
|
||||
console.log('GitHub token is configured - API limit warning hidden');
|
||||
}
|
||||
|
||||
if (settings) {
|
||||
settings.classList.add('hidden');
|
||||
console.log('GitHub token is configured - hiding settings panel');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking GitHub auth status:', error);
|
||||
});
|
||||
}
|
||||
|
||||
window.dismissGithubWarning = function() {
|
||||
const warning = document.getElementById('github-auth-warning');
|
||||
const settings = document.getElementById('github-token-settings');
|
||||
if (warning) {
|
||||
// Hide warning using both classList and style.display
|
||||
warning.classList.add('hidden');
|
||||
warning.style.display = 'none';
|
||||
// Also hide settings if it's open (since they're combined now)
|
||||
if (settings && !settings.classList.contains('hidden')) {
|
||||
settings.classList.add('hidden');
|
||||
settings.style.display = 'none';
|
||||
}
|
||||
// Remember dismissal for this session
|
||||
sessionStorage.setItem('github-auth-warning-dismissed', 'true');
|
||||
@@ -5639,6 +5859,12 @@ if (_PLUGIN_DEBUG_EARLY) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check GitHub auth status immediately if elements exist (don't wait for full initialization)
|
||||
if (window.checkGitHubAuthStatus && document.getElementById('github-auth-warning')) {
|
||||
console.log('[EARLY] Checking GitHub auth status immediately on script load...');
|
||||
window.checkGitHubAuthStatus();
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
const installedGrid = document.getElementById('installed-plugins-grid');
|
||||
if (installedGrid) {
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
<h3 class="text-lg font-bold text-gray-900">Installed Plugins</h3>
|
||||
<span id="installed-count" class="text-sm text-gray-500 font-medium">0 installed</span>
|
||||
</div>
|
||||
<button id="toggle-installed-plugins" class="text-sm text-gray-600 hover:text-gray-900 hover:text-blue-600 transition-colors flex items-center font-medium">
|
||||
<i class="fas fa-chevron-down mr-1" id="installed-plugins-icon"></i>
|
||||
<span>Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="installed-plugins-content" class="block">
|
||||
<div id="installed-plugins-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
@@ -49,10 +45,6 @@
|
||||
<i class="fas fa-spinner fa-spin mr-1"></i>Loading...
|
||||
</span>
|
||||
</div>
|
||||
<button id="toggle-plugin-store" class="text-sm text-gray-600 hover:text-gray-900 hover:text-blue-600 transition-colors flex items-center font-medium">
|
||||
<i class="fas fa-chevron-down mr-1" id="plugin-store-icon"></i>
|
||||
<span>Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="plugin-store-content" class="block">
|
||||
@@ -75,7 +67,7 @@
|
||||
Create a GitHub Token →
|
||||
</a>
|
||||
<span class="mx-2">|</span>
|
||||
<a href="#" onclick="event.preventDefault(); toggleGithubTokenSettings()" class="font-medium underline hover:text-yellow-800">
|
||||
<a href="#" onclick="event.preventDefault(); openGithubTokenSettings()" class="font-medium underline hover:text-yellow-800">
|
||||
Configure Token
|
||||
</a>
|
||||
</p>
|
||||
@@ -100,9 +92,6 @@
|
||||
<i class="fas fa-chevron-up mr-1" id="github-token-icon-collapse"></i>
|
||||
<span>Collapse</span>
|
||||
</button>
|
||||
<button onclick="toggleGithubTokenSettings()" class="text-gray-500 hover:text-gray-700 transition-colors">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user