Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
380
admin-ui/js/components/layout/ds-project-selector.js
Normal file
380
admin-ui/js/components/layout/ds-project-selector.js
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<h2 style="font-size: 16px; margin-bottom: 12px;">Select a Project</h2>
|
||||
<p style="font-size: 12px; color: var(--vscode-text-dim); margin-bottom: 16px;">
|
||||
Please select a project to start working. All tools require an active project.
|
||||
</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;" id="project-buttons-container">
|
||||
${this.projects.map(project => `
|
||||
<button
|
||||
class="project-modal-button"
|
||||
data-project-id="${project.id}"
|
||||
type="button"
|
||||
style="padding: 12px; background: var(--vscode-bg); border: 1px solid var(--vscode-border); border-radius: 4px; cursor: pointer; text-align: left; font-family: inherit; font-size: inherit;"
|
||||
>
|
||||
<div style="font-size: 12px; font-weight: 600;">${ComponentHelpers.escapeHtml(project.name)}</div>
|
||||
${project.description ? `<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<span style="font-size: 11px; color: var(--vscode-text-dim);">Project:</span>
|
||||
<span style="font-size: 12px; font-weight: 600; margin-left: 4px;">${ComponentHelpers.escapeHtml(selectedProject.name)}</span>
|
||||
<span style="margin-left: 6px;">▼</span>
|
||||
`;
|
||||
} else {
|
||||
button.innerHTML = `
|
||||
<span style="font-size: 12px; color: var(--vscode-text-dim);">Select Project</span>
|
||||
<span style="margin-left: 6px;">▼</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingState() {
|
||||
const button = this.querySelector('#project-selector-button');
|
||||
if (!button) return;
|
||||
|
||||
if (this.isLoading) {
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span style="font-size: 11px;">Loading projects...</span>';
|
||||
} else {
|
||||
button.disabled = false;
|
||||
this.updateSelectedDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
renderDropdown() {
|
||||
const dropdown = this.querySelector('#project-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
if (this.projects.length === 0) {
|
||||
dropdown.innerHTML = `
|
||||
<div style="padding: 12px; font-size: 11px; color: var(--vscode-text-dim);">
|
||||
No projects available
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = `
|
||||
${this.projects.map(project => `
|
||||
<div
|
||||
class="project-option"
|
||||
data-project-id="${project.id}"
|
||||
style="
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
${this.selectedProject === project.id ? 'background: var(--vscode-list-activeSelectionBackground);' : ''}
|
||||
"
|
||||
>
|
||||
<div style="font-size: 12px; font-weight: 600;">
|
||||
${this.selectedProject === project.id ? '✓ ' : ''}${ComponentHelpers.escapeHtml(project.name)}
|
||||
</div>
|
||||
${project.description ? `<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 2px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
// Re-attach event listeners to dropdown items
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<button
|
||||
id="project-selector-button"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-sidebar);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--vscode-text);
|
||||
"
|
||||
>
|
||||
<span style="font-size: 12px;">Loading...</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="project-dropdown"
|
||||
style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 250px;
|
||||
background: var(--vscode-sidebar);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>
|
||||
<!-- Projects will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-project-selector', DSProjectSelector);
|
||||
|
||||
export default DSProjectSelector;
|
||||
Reference in New Issue
Block a user