"""
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}")