""" Project Config Editor Dialog Allows creating and editing project configurations for versioning """ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, QLineEdit, QCheckBox, QPushButton, QTextEdit, QDialogButtonBox, QFileDialog, QMessageBox, QLabel ) from PyQt6.QtCore import Qt from loguru import logger from pathlib import Path from core.database import Database class ProjectConfigDialog(QDialog): """Dialog for creating/editing project configurations""" def __init__(self, parent, db: Database, project_id: int = None): super().__init__(parent) self.db = db self.project_id = project_id # Determine mode self.is_edit = project_id is not None self.setWindowTitle("Edit Project Config" if self.is_edit else "New Project Config") self.setMinimumSize(700, 500) self._setup_ui() if self.is_edit: self._load_project_data() def _setup_ui(self): """Setup dialog UI""" layout = QVBoxLayout(self) layout.setSpacing(15) # Group 1: Basic Info basic_group = QGroupBox("Project Information") basic_layout = QFormLayout(basic_group) self.name_edit = QLineEdit() self.name_edit.setPlaceholderText("e.g., Scheduler, MyApp, WebScraper") basic_layout.addRow("Project Name *:", self.name_edit) self.description_edit = QLineEdit() self.description_edit.setPlaceholderText("Optional description") basic_layout.addRow("Description:", self.description_edit) layout.addWidget(basic_group) # Group 2: Paths paths_group = QGroupBox("Project Paths") paths_layout = QFormLayout(paths_group) # Info label info_label = QLabel( "Source: Local development path\n" "Server: Base path on server (e.g., \\\\SERVER\\PYTHON\\)" ) info_label.setStyleSheet("color: #666; font-size: 10px;") paths_layout.addRow("", info_label) # Source path with browse button source_layout = QHBoxLayout() self.source_edit = QLineEdit() self.source_edit.setPlaceholderText("e.g., C:\\PYTHON\\MyProject") source_layout.addWidget(self.source_edit) browse_source_btn = QPushButton("Browse...") browse_source_btn.clicked.connect(self._browse_source) source_layout.addWidget(browse_source_btn) paths_layout.addRow("Source Path *:", source_layout) # Server base path with browse button server_layout = QHBoxLayout() self.server_edit = QLineEdit() self.server_edit.setPlaceholderText("e.g., \\\\SERVER\\PYTHON\\ or C:\\Projects\\") server_layout.addWidget(self.server_edit) browse_server_btn = QPushButton("Browse...") browse_server_btn.clicked.connect(self._browse_server) server_layout.addWidget(browse_server_btn) paths_layout.addRow("Server Base Path *:", server_layout) self.enabled_check = QCheckBox("Enable this project config") self.enabled_check.setChecked(True) paths_layout.addRow("", self.enabled_check) layout.addWidget(paths_group) # Group 3: Exclusions exclusions_group = QGroupBox("Exclusion Patterns") exclusions_layout = QVBoxLayout(exclusions_group) exclusions_label = QLabel( "Files and directories to exclude from versions (one pattern per line):" ) exclusions_layout.addWidget(exclusions_label) self.exclusions_edit = QTextEdit() self.exclusions_edit.setPlaceholderText( "Examples:\n" "venv/ # Exclude venv directory\n" "__pycache__/ # Exclude Python cache\n" "*.log # Exclude log files\n" "*.db # Exclude database files\n" "*.pyc # Exclude compiled Python\n" ".git/ # Exclude git repository" ) self.exclusions_edit.setMaximumHeight(150) exclusions_layout.addWidget(self.exclusions_edit) exclusions_help = QLabel( "Use folder/ for directories, *.ext for file patterns. Lines starting with # are comments." ) exclusions_help.setStyleSheet("color: #666; font-size: 10px;") exclusions_layout.addWidget(exclusions_help) layout.addWidget(exclusions_group) # Info section info_section = QLabel( "How it works:
" "• Upload Version: Syncs source to server and creates ZIP snapshot
" "• Versions are stored: {Server Base Path}/{Project Name}_versions/
" "• Active code is at: {Server Base Path}/{Project Name}/" ) info_section.setStyleSheet("color: #444; font-size: 10px; background-color: #f5f5f5; padding: 10px; border-radius: 5px;") info_section.setWordWrap(True) layout.addWidget(info_section) # Dialog buttons button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self._save_project) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def _load_project_data(self): """Load existing project data""" try: project = self.db.get_project_config(self.project_id) if not project: QMessageBox.critical(self, "Error", f"Project {self.project_id} not found") self.reject() return # Populate fields self.name_edit.setText(project['name']) self.description_edit.setText(project['description'] or '') self.source_edit.setText(project['source_path']) self.server_edit.setText(project['server_base_path']) self.exclusions_edit.setPlainText(project['exclude_patterns'] or '') self.enabled_check.setChecked(bool(project['enabled'])) except Exception as e: logger.error(f"Error loading project data: {e}") QMessageBox.critical(self, "Error", f"Failed to load project: {e}") self.reject() def _browse_source(self): """Browse for source directory""" current_path = self.source_edit.text() path = QFileDialog.getExistingDirectory( self, "Select Source Directory", current_path if current_path else "" ) if path: self.source_edit.setText(path) def _browse_server(self): """Browse for server base directory""" current_path = self.server_edit.text() path = QFileDialog.getExistingDirectory( self, "Select Server Base Directory", current_path if current_path else "" ) if path: self.server_edit.setText(path) def _save_project(self): """Validate and save project configuration""" # Validate required fields name = self.name_edit.text().strip() source_path = self.source_edit.text().strip() server_base_path = self.server_edit.text().strip() if not name: QMessageBox.warning(self, "Validation Error", "Project name is required") self.name_edit.setFocus() return if not source_path: QMessageBox.warning(self, "Validation Error", "Source path is required") self.source_edit.setFocus() return if not server_base_path: QMessageBox.warning(self, "Validation Error", "Server base path is required") self.server_edit.setFocus() return # Validate source path exists if not Path(source_path).exists(): reply = QMessageBox.question( self, "Path Not Found", f"Source path does not exist:\n{source_path}\n\nContinue anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # Check for duplicate name (only on create or name change) if not self.is_edit or self.name_edit.text() != self.db.get_project_config(self.project_id)['name']: existing = self.db.get_all_project_configs() if any(p['name'] == name and p['id'] != self.project_id for p in existing): QMessageBox.warning( self, "Duplicate Name", f"A project named '{name}' already exists. Please choose a different name." ) self.name_edit.setFocus() return # Get other fields description = self.description_edit.text().strip() or None exclude_patterns = self.exclusions_edit.toPlainText().strip() enabled = self.enabled_check.isChecked() try: if self.is_edit: # Update existing project self.db.update_project_config( project_id=self.project_id, name=name, description=description, source_path=source_path, server_base_path=server_base_path, exclude_patterns=exclude_patterns, enabled=enabled ) logger.info(f"Updated project config '{name}'") QMessageBox.information(self, "Success", f"Project '{name}' updated successfully") else: # Create new project project_id = self.db.create_project_config( name=name, source_path=source_path, server_base_path=server_base_path, description=description, exclude_patterns=exclude_patterns, enabled=enabled ) logger.info(f"Created project config '{name}' with ID {project_id}") QMessageBox.information(self, "Success", f"Project '{name}' created successfully") self.accept() except Exception as e: logger.error(f"Error saving project config: {e}") QMessageBox.critical(self, "Error", f"Failed to save project: {e}")