/** * ds-project-selector.js * Project selector component for workdesk header * MVP1: Enforces project selection before tools can be used * FIXED: Now uses authenticated apiClient instead of direct fetch() */ import contextStore from '../../stores/context-store.js'; import apiClient from '../../services/api-client.js'; import { ComponentHelpers } from '../../utils/component-helpers.js'; class DSProjectSelector extends HTMLElement { constructor() { super(); this.projects = []; this.isLoading = false; this.selectedProject = contextStore.get('projectId'); } async connectedCallback() { this.render(); await this.loadProjects(); this.setupEventListeners(); // Subscribe to context changes this.unsubscribe = contextStore.subscribeToKey('projectId', (newValue) => { this.selectedProject = newValue; this.updateSelectedDisplay(); }); // Bind auth change handler to this component this.handleAuthChange = async (event) => { console.log('[DSProjectSelector] Auth state changed, reloading projects'); await this.reloadProjects(); }; // Listen for custom auth-change events (fires when tokens are refreshed) document.addEventListener('auth-change', this.handleAuthChange); } disconnectedCallback() { if (this.unsubscribe) { this.unsubscribe(); } // Clean up auth change listener if (this.handleAuthChange) { document.removeEventListener('auth-change', this.handleAuthChange); } // Clean up document click listener for closing dropdown if (this.closeDropdownHandler) { document.removeEventListener('click', this.closeDropdownHandler); } } async loadProjects() { this.isLoading = true; this.updateLoadingState(); try { // Fetch projects from authenticated API client // This ensures Authorization header is sent with the request this.projects = await apiClient.getProjects(); console.log(`[DSProjectSelector] Loaded ${this.projects.length} projects`); // If no project selected but we have projects, show prompt if (!this.selectedProject && this.projects.length > 0) { this.showProjectModal(); } this.renderDropdown(); } catch (error) { console.error('[DSProjectSelector] Failed to load projects:', error); // Fallback: Create mock admin-ui project for development this.projects = [{ id: 'admin-ui', name: 'Admin UI (Default)', description: 'Design System Server Admin UI' }]; // Auto-select if no project selected if (!this.selectedProject) { try { contextStore.setProject('admin-ui'); this.selectedProject = 'admin-ui'; } catch (storeError) { console.error('[DSProjectSelector] Error setting project:', storeError); this.selectedProject = 'admin-ui'; } } this.renderDropdown(); } finally { this.isLoading = false; this.updateLoadingState(); } } /** * Public method to reload projects - called when auth state changes */ async reloadProjects() { console.log('[DSProjectSelector] Reloading projects due to auth state change'); await this.loadProjects(); } setupEventListeners() { const button = this.querySelector('#project-selector-button'); const dropdown = this.querySelector('#project-dropdown'); if (button && dropdown) { // Add click listener to button (delegation handles via event target check) button.addEventListener('click', (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block'; }); } // Add click listeners to dropdown items const projectOptions = this.querySelectorAll('.project-option'); projectOptions.forEach(option => { option.addEventListener('click', (e) => { e.stopPropagation(); const projectId = option.dataset.projectId; this.selectProject(projectId); }); }); // Close dropdown when clicking outside - stored for cleanup if (!this.closeDropdownHandler) { this.closeDropdownHandler = (e) => { if (!this.contains(e.target) && dropdown) { dropdown.style.display = 'none'; } }; document.addEventListener('click', this.closeDropdownHandler); } } selectProject(projectId) { const project = this.projects.find(p => p.id === projectId); if (!project) { console.error('[DSProjectSelector] Project not found:', projectId); return; } try { contextStore.setProject(projectId); this.selectedProject = projectId; // Close dropdown const dropdown = this.querySelector('#project-dropdown'); if (dropdown) { dropdown.style.display = 'none'; } this.updateSelectedDisplay(); ComponentHelpers.showToast?.(`Switched to project: ${project.name}`, 'success'); // Notify other components of project change this.dispatchEvent(new CustomEvent('project-changed', { detail: { projectId }, bubbles: true, composed: true })); } catch (error) { console.error('[DSProjectSelector] Error selecting project:', error); ComponentHelpers.showToast?.(`Failed to select project: ${error.message}`, 'error'); } } showProjectModal() { const modal = document.createElement('div'); modal.id = 'project-selection-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const content = document.createElement('div'); content.style.cssText = ` background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 24px; max-width: 500px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); `; // Use event delegation instead of attaching listeners to individual buttons content.innerHTML = `

Select a Project

Please select a project to start working. All tools require an active project.

${this.projects.map(project => ` `).join('')}
`; modal.appendChild(content); // Store reference to component for event handlers const component = this; // Use event delegation on content container const buttonContainer = content.querySelector('#project-buttons-container'); if (buttonContainer) { buttonContainer.addEventListener('click', (e) => { const btn = e.target.closest('.project-modal-button'); if (btn) { e.preventDefault(); e.stopPropagation(); const projectId = btn.dataset.projectId; console.log('[DSProjectSelector] Modal button clicked:', projectId); try { component.selectProject(projectId); console.log('[DSProjectSelector] Project selected successfully'); } catch (err) { console.error('[DSProjectSelector] Error selecting project:', err); } finally { // Ensure modal is always removed if (modal && modal.parentNode) { modal.remove(); } } } }); } // Close modal when clicking outside the content area modal.addEventListener('click', (e) => { if (e.target === modal) { console.log('[DSProjectSelector] Closing modal (clicked outside)'); modal.remove(); } }); document.body.appendChild(modal); console.log('[DSProjectSelector] Project selection modal shown'); } updateSelectedDisplay() { const button = this.querySelector('#project-selector-button'); if (!button) return; const selectedProject = this.projects.find(p => p.id === this.selectedProject); if (selectedProject) { button.innerHTML = ` Project: ${ComponentHelpers.escapeHtml(selectedProject.name)} `; } else { button.innerHTML = ` Select Project `; } } updateLoadingState() { const button = this.querySelector('#project-selector-button'); if (!button) return; if (this.isLoading) { button.disabled = true; button.innerHTML = 'Loading projects...'; } else { button.disabled = false; this.updateSelectedDisplay(); } } renderDropdown() { const dropdown = this.querySelector('#project-dropdown'); if (!dropdown) return; if (this.projects.length === 0) { dropdown.innerHTML = `
No projects available
`; return; } dropdown.innerHTML = ` ${this.projects.map(project => `
${this.selectedProject === project.id ? '✓ ' : ''}${ComponentHelpers.escapeHtml(project.name)}
${project.description ? `
${ComponentHelpers.escapeHtml(project.description)}
` : ''}
`).join('')} `; // Re-attach event listeners to dropdown items this.setupEventListeners(); } render() { this.innerHTML = `
`; } } customElements.define('ds-project-selector', DSProjectSelector); export default DSProjectSelector;