673 lines
25 KiB
Python
673 lines
25 KiB
Python
"""Project list widget showing all discovered projects."""
|
|
|
|
import subprocess
|
|
import webbrowser
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import Qt, Signal
|
|
from PySide6.QtGui import QAction
|
|
from PySide6.QtWidgets import (
|
|
QDialog,
|
|
QLabel,
|
|
QListWidget,
|
|
QListWidgetItem,
|
|
QMenu,
|
|
QMessageBox,
|
|
QProgressBar,
|
|
QPushButton,
|
|
QTextEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from development_hub.dialogs import DeployDocsThread, RebuildMainDocsThread, DocsPreviewDialog, UpdateDocsThread
|
|
from development_hub.project_discovery import Project, discover_projects
|
|
|
|
|
|
class ProjectListWidget(QWidget):
|
|
"""Widget displaying the list of projects with context menu actions."""
|
|
|
|
project_selected = Signal(Project)
|
|
open_terminal_requested = Signal(Project)
|
|
open_dashboard_requested = Signal(Project)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._projects: list[Project] = []
|
|
self._deploy_thread: DeployDocsThread | None = None
|
|
self._rebuild_thread: RebuildMainDocsThread | None = None
|
|
self._update_docs_thread: UpdateDocsThread | None = None
|
|
self._progress_dialog: QDialog | None = None
|
|
self._stashed_project: Project | None = None
|
|
self._update_docs_state: dict | None = None # Stores backup, project, etc during async update
|
|
self._setup_ui()
|
|
self.refresh()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# Header
|
|
header = QLabel("PROJECTS")
|
|
header.setObjectName("header")
|
|
layout.addWidget(header)
|
|
|
|
# Project list
|
|
self.list_widget = QListWidget()
|
|
self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.list_widget.customContextMenuRequested.connect(self._show_context_menu)
|
|
self.list_widget.itemClicked.connect(self._on_item_clicked)
|
|
self.list_widget.itemDoubleClicked.connect(self._on_item_double_clicked)
|
|
layout.addWidget(self.list_widget)
|
|
|
|
def refresh(self):
|
|
"""Refresh the project list from the build configuration."""
|
|
self.list_widget.clear()
|
|
self._projects = discover_projects()
|
|
|
|
for project in self._projects:
|
|
item = QListWidgetItem(project.title)
|
|
item.setData(Qt.ItemDataRole.UserRole, project)
|
|
|
|
# Add tooltip with project info
|
|
tooltip = f"{project.tagline}\n\nPath: {project.path}"
|
|
if not project.exists:
|
|
tooltip += "\n\n(Directory not found)"
|
|
item.setForeground(Qt.GlobalColor.darkGray)
|
|
item.setToolTip(tooltip)
|
|
|
|
self.list_widget.addItem(item)
|
|
|
|
def _get_selected_project(self) -> Project | None:
|
|
"""Get the currently selected project."""
|
|
item = self.list_widget.currentItem()
|
|
if item:
|
|
return item.data(Qt.ItemDataRole.UserRole)
|
|
return None
|
|
|
|
def _on_item_clicked(self, item: QListWidgetItem):
|
|
"""Handle single click on project."""
|
|
project = item.data(Qt.ItemDataRole.UserRole)
|
|
self.project_selected.emit(project)
|
|
|
|
def _on_item_double_clicked(self, item: QListWidgetItem):
|
|
"""Handle double-click - open project dashboard."""
|
|
project = item.data(Qt.ItemDataRole.UserRole)
|
|
if project.exists:
|
|
self.open_dashboard_requested.emit(project)
|
|
|
|
def _show_context_menu(self, position):
|
|
"""Show context menu for project actions."""
|
|
item = self.list_widget.itemAt(position)
|
|
if not item:
|
|
return
|
|
|
|
project = item.data(Qt.ItemDataRole.UserRole)
|
|
menu = QMenu(self)
|
|
|
|
# Open Dashboard (top item)
|
|
open_dashboard = QAction("Open Dashboard", self)
|
|
open_dashboard.triggered.connect(lambda: self.open_dashboard_requested.emit(project))
|
|
open_dashboard.setEnabled(project.exists)
|
|
menu.addAction(open_dashboard)
|
|
|
|
# Open Terminal
|
|
open_terminal = QAction("Open Terminal", self)
|
|
open_terminal.triggered.connect(lambda: self.open_terminal_requested.emit(project))
|
|
open_terminal.setEnabled(project.exists)
|
|
menu.addAction(open_terminal)
|
|
|
|
# Open in Editor
|
|
open_editor = QAction("Open in Editor", self)
|
|
open_editor.triggered.connect(lambda: self._open_in_editor(project))
|
|
open_editor.setEnabled(project.exists)
|
|
menu.addAction(open_editor)
|
|
|
|
menu.addSeparator()
|
|
|
|
# View on Gitea
|
|
view_gitea = QAction("View on Gitea", self)
|
|
view_gitea.triggered.connect(lambda: webbrowser.open(project.gitea_url))
|
|
menu.addAction(view_gitea)
|
|
|
|
# View Documentation
|
|
view_docs = QAction("View Documentation", self)
|
|
view_docs.triggered.connect(lambda: webbrowser.open(project.docs_url))
|
|
menu.addAction(view_docs)
|
|
|
|
menu.addSeparator()
|
|
|
|
# Update Documentation (AI-powered)
|
|
update_docs = QAction("Update Documentation...", self)
|
|
update_docs.triggered.connect(lambda: self._update_docs(project))
|
|
menu.addAction(update_docs)
|
|
|
|
# Deploy Docs
|
|
deploy_docs = QAction("Deploy Docs", self)
|
|
deploy_docs.triggered.connect(lambda: self._deploy_docs(project))
|
|
menu.addAction(deploy_docs)
|
|
|
|
# Rebuild Main Docs
|
|
rebuild_docs = QAction("Rebuild Main Docs", self)
|
|
rebuild_docs.triggered.connect(self._rebuild_main_docs)
|
|
menu.addAction(rebuild_docs)
|
|
|
|
menu.exec(self.list_widget.mapToGlobal(position))
|
|
|
|
def _open_in_editor(self, project: Project):
|
|
"""Open project in the default editor."""
|
|
import os
|
|
import shutil
|
|
|
|
# Try common editors in order of preference
|
|
editors = [
|
|
os.environ.get("EDITOR"),
|
|
"pycharm",
|
|
"code", # VS Code
|
|
"subl", # Sublime Text
|
|
"gedit",
|
|
"xdg-open",
|
|
]
|
|
|
|
for editor in editors:
|
|
if editor and shutil.which(editor):
|
|
subprocess.Popen([editor, str(project.path)])
|
|
return
|
|
|
|
def _create_progress_dialog(self, title: str) -> QDialog:
|
|
"""Create a progress dialog with output display."""
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle(title)
|
|
dialog.setMinimumWidth(500)
|
|
dialog.setMinimumHeight(300)
|
|
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
output_text = QTextEdit()
|
|
output_text.setReadOnly(True)
|
|
output_text.setObjectName("output_text")
|
|
layout.addWidget(output_text)
|
|
|
|
progress_bar = QProgressBar()
|
|
progress_bar.setRange(0, 0) # Indeterminate
|
|
progress_bar.setObjectName("progress_bar")
|
|
layout.addWidget(progress_bar)
|
|
|
|
# OK button (hidden until complete)
|
|
ok_button = QPushButton("OK")
|
|
ok_button.setObjectName("ok_button")
|
|
ok_button.clicked.connect(dialog.accept)
|
|
ok_button.hide()
|
|
layout.addWidget(ok_button)
|
|
|
|
return dialog
|
|
|
|
def _deploy_docs(self, project: Project):
|
|
"""Deploy documentation for project asynchronously."""
|
|
build_script = Path.home() / "PycharmProjects/project-docs/scripts/build-public-docs.sh"
|
|
if not build_script.exists():
|
|
QMessageBox.warning(
|
|
self,
|
|
"Deploy Failed",
|
|
"Build script not found:\n\n" + str(build_script)
|
|
)
|
|
return
|
|
|
|
# Check for uncommitted changes in project repo
|
|
stashed = False
|
|
if project.exists:
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "status", "--porcelain"],
|
|
cwd=project.path,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
has_changes = bool(result.stdout.strip())
|
|
|
|
if has_changes:
|
|
# Show dialog asking what to do
|
|
dialog = QMessageBox(self)
|
|
dialog.setWindowTitle("Uncommitted Changes")
|
|
dialog.setText(
|
|
f"{project.title} has uncommitted changes.\n\n"
|
|
"The deploy script needs to switch branches, which requires "
|
|
"a clean working directory."
|
|
)
|
|
dialog.setInformativeText("What would you like to do?")
|
|
|
|
stash_btn = dialog.addButton("Stash Changes", QMessageBox.ButtonRole.AcceptRole)
|
|
commit_btn = dialog.addButton("Commit Changes", QMessageBox.ButtonRole.AcceptRole)
|
|
cancel_btn = dialog.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
|
|
dialog.setDefaultButton(stash_btn)
|
|
dialog.exec()
|
|
|
|
clicked = dialog.clickedButton()
|
|
|
|
if clicked == cancel_btn:
|
|
return
|
|
|
|
if clicked == stash_btn:
|
|
# Stash changes
|
|
result = subprocess.run(
|
|
["git", "stash", "push", "-m", "Auto-stash before docs deploy"],
|
|
cwd=project.path,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Stash Failed",
|
|
f"Failed to stash changes:\n\n{result.stderr}"
|
|
)
|
|
return
|
|
stashed = True
|
|
|
|
elif clicked == commit_btn:
|
|
# Show commit dialog
|
|
from PySide6.QtWidgets import QInputDialog
|
|
message, ok = QInputDialog.getText(
|
|
self,
|
|
"Commit Message",
|
|
"Enter commit message:",
|
|
text="Update before docs deploy"
|
|
)
|
|
if not ok or not message.strip():
|
|
return
|
|
|
|
# Stage all and commit
|
|
subprocess.run(
|
|
["git", "add", "-A"],
|
|
cwd=project.path,
|
|
capture_output=True,
|
|
)
|
|
result = subprocess.run(
|
|
["git", "commit", "-m", message.strip()],
|
|
cwd=project.path,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Commit Failed",
|
|
f"Failed to commit changes:\n\n{result.stderr}"
|
|
)
|
|
return
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Git Error",
|
|
f"Error checking git status:\n\n{e}"
|
|
)
|
|
return
|
|
|
|
# Store stash state for restoration after deploy
|
|
self._stashed_project = project if stashed else None
|
|
|
|
# Create and show progress dialog
|
|
self._progress_dialog = self._create_progress_dialog(f"Deploying {project.title} Docs")
|
|
self._progress_dialog.show()
|
|
|
|
# Start async deploy
|
|
self._deploy_thread = DeployDocsThread(project.key, project.title, project.docs_url)
|
|
self._deploy_thread.output.connect(self._on_deploy_output)
|
|
self._deploy_thread.finished.connect(self._on_deploy_finished)
|
|
self._deploy_thread.start()
|
|
|
|
def _on_deploy_output(self, line: str):
|
|
"""Handle output from deploy thread."""
|
|
if self._progress_dialog:
|
|
output_text = self._progress_dialog.findChild(QTextEdit, "output_text")
|
|
if output_text:
|
|
output_text.append(line)
|
|
scrollbar = output_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
def _on_deploy_finished(self, success: bool, message: str):
|
|
"""Handle deploy completion."""
|
|
# Restore stashed changes if any
|
|
if self._stashed_project:
|
|
subprocess.run(
|
|
["git", "stash", "pop"],
|
|
cwd=self._stashed_project.path,
|
|
capture_output=True,
|
|
)
|
|
self._stashed_project = None
|
|
|
|
if self._progress_dialog:
|
|
# Hide progress bar
|
|
progress_bar = self._progress_dialog.findChild(QProgressBar, "progress_bar")
|
|
if progress_bar:
|
|
progress_bar.hide()
|
|
|
|
# Add result to output
|
|
output_text = self._progress_dialog.findChild(QTextEdit, "output_text")
|
|
if output_text:
|
|
if success:
|
|
output_text.append(f"\n✓ {message}")
|
|
else:
|
|
output_text.append(f"\n✗ {message}")
|
|
|
|
# Change dialog to be closeable
|
|
self._progress_dialog.setWindowTitle(
|
|
"Deploy Complete" if success else "Deploy Failed"
|
|
)
|
|
|
|
# Show OK button
|
|
ok_button = self._progress_dialog.findChild(QPushButton, "ok_button")
|
|
if ok_button:
|
|
ok_button.show()
|
|
|
|
self._deploy_thread = None
|
|
|
|
def _rebuild_main_docs(self):
|
|
"""Rebuild the main documentation site asynchronously."""
|
|
# Create and show progress dialog
|
|
self._progress_dialog = self._create_progress_dialog("Rebuilding Main Docs")
|
|
self._progress_dialog.show()
|
|
|
|
# Start async rebuild
|
|
self._rebuild_thread = RebuildMainDocsThread()
|
|
self._rebuild_thread.output.connect(self._on_rebuild_output)
|
|
self._rebuild_thread.finished.connect(self._on_rebuild_finished)
|
|
self._rebuild_thread.start()
|
|
|
|
def _on_rebuild_output(self, line: str):
|
|
"""Handle output from rebuild thread."""
|
|
if self._progress_dialog:
|
|
output_text = self._progress_dialog.findChild(QTextEdit, "output_text")
|
|
if output_text:
|
|
output_text.append(line)
|
|
scrollbar = output_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
def _on_rebuild_finished(self, success: bool, message: str):
|
|
"""Handle rebuild completion."""
|
|
if self._progress_dialog:
|
|
# Hide progress bar
|
|
progress_bar = self._progress_dialog.findChild(QProgressBar, "progress_bar")
|
|
if progress_bar:
|
|
progress_bar.hide()
|
|
|
|
# Add result to output
|
|
output_text = self._progress_dialog.findChild(QTextEdit, "output_text")
|
|
if output_text:
|
|
if success:
|
|
output_text.append(f"\n✓ {message}")
|
|
else:
|
|
output_text.append(f"\n✗ {message}")
|
|
|
|
# Change dialog title
|
|
self._progress_dialog.setWindowTitle(
|
|
"Rebuild Complete" if success else "Rebuild Failed"
|
|
)
|
|
|
|
# Show OK button
|
|
ok_button = self._progress_dialog.findChild(QPushButton, "ok_button")
|
|
if ok_button:
|
|
ok_button.show()
|
|
|
|
self._rebuild_thread = None
|
|
|
|
def _update_docs(self, project: Project):
|
|
"""Update documentation using CmdForge update-docs tool with preview."""
|
|
from PySide6.QtWidgets import QMessageBox, QProgressDialog
|
|
from PySide6.QtCore import Qt, QTimer
|
|
from pathlib import Path
|
|
|
|
# Confirm before starting
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Update Documentation",
|
|
f"This will use AI to analyze {project.title} and generate updated documentation.\n\n"
|
|
f"You will be able to review all changes before they are applied.\n\n"
|
|
f"Continue?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.Yes
|
|
)
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
# Determine docs path
|
|
docs_path = Path.home() / "PycharmProjects" / "project-docs" / "docs" / "projects" / project.key
|
|
doc_files = ["overview.md", "goals.md", "milestones.md", "todos.md"]
|
|
|
|
# Read existing docs as backup
|
|
backup = {}
|
|
for filename in doc_files:
|
|
file_path = docs_path / filename
|
|
if file_path.exists():
|
|
backup[filename] = file_path.read_text()
|
|
else:
|
|
backup[filename] = ""
|
|
|
|
# Store state for callback
|
|
self._update_docs_state = {
|
|
'project': project,
|
|
'docs_path': docs_path,
|
|
'doc_files': doc_files,
|
|
'backup': backup,
|
|
'dot_count': 0,
|
|
}
|
|
|
|
# Show progress dialog with animated dots on separate line
|
|
base_text = f"Generating documentation for {project.title}"
|
|
self._progress_dialog = QProgressDialog(
|
|
f"{base_text}\n.",
|
|
"Cancel",
|
|
0, 0,
|
|
self
|
|
)
|
|
self._progress_dialog.setWindowTitle("Updating Documentation")
|
|
self._progress_dialog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
self._progress_dialog.setMinimumDuration(0)
|
|
self._progress_dialog.setMinimumWidth(400)
|
|
self._progress_dialog.setAutoClose(False) # Don't auto-close
|
|
self._progress_dialog.setAutoReset(False) # Don't auto-reset
|
|
# Prevent closing via X button or ESC - only Cancel button works
|
|
self._progress_dialog.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False)
|
|
self._progress_dialog.canceled.connect(self._on_update_docs_canceled)
|
|
self._progress_dialog.show()
|
|
|
|
# Set up timer for animated dots (on separate line to avoid text shifting)
|
|
def update_dots():
|
|
if self._update_docs_state:
|
|
self._update_docs_state['dot_count'] = (self._update_docs_state['dot_count'] % 20) + 1
|
|
dots = '.' * self._update_docs_state['dot_count']
|
|
if self._progress_dialog:
|
|
self._progress_dialog.setLabelText(f"{base_text}\n{dots}")
|
|
|
|
self._dot_timer = QTimer()
|
|
self._dot_timer.timeout.connect(update_dots)
|
|
self._dot_timer.start(250) # Update every 250ms
|
|
|
|
# Start async thread
|
|
self._update_docs_thread = UpdateDocsThread(project.key)
|
|
self._update_docs_thread.finished.connect(self._on_update_docs_finished)
|
|
self._update_docs_thread.start()
|
|
|
|
def _on_update_docs_canceled(self):
|
|
"""Handle user canceling the update docs progress."""
|
|
if self._dot_timer:
|
|
self._dot_timer.stop()
|
|
if self._update_docs_thread:
|
|
self._update_docs_thread.terminate()
|
|
self._update_docs_thread = None
|
|
# Store backup before clearing state so we can restore if needed
|
|
if self._update_docs_state:
|
|
backup = self._update_docs_state.get('backup', {})
|
|
docs_path = self._update_docs_state.get('docs_path')
|
|
# Restore backups since we're canceling
|
|
if docs_path and backup:
|
|
from pathlib import Path
|
|
for filename, content in backup.items():
|
|
file_path = Path(docs_path) / filename
|
|
if content:
|
|
file_path.write_text(content)
|
|
self._update_docs_state = None
|
|
|
|
def _on_update_docs_finished(self, success: bool, message: str, detail: str):
|
|
"""Handle update docs thread completion."""
|
|
from PySide6.QtWidgets import QMessageBox
|
|
from pathlib import Path
|
|
|
|
# Stop timer
|
|
if hasattr(self, '_dot_timer') and self._dot_timer:
|
|
self._dot_timer.stop()
|
|
|
|
# Close progress dialog - disconnect canceled signal first to prevent it from clearing state
|
|
if self._progress_dialog:
|
|
try:
|
|
self._progress_dialog.canceled.disconnect(self._on_update_docs_canceled)
|
|
except TypeError:
|
|
pass # Already disconnected
|
|
self._progress_dialog.close()
|
|
self._progress_dialog = None
|
|
|
|
# Check if we still have state (not canceled by user before thread finished)
|
|
if not self._update_docs_state:
|
|
return
|
|
|
|
state = self._update_docs_state
|
|
project = state['project']
|
|
docs_path = state['docs_path']
|
|
doc_files = state['doc_files']
|
|
backup = state['backup']
|
|
|
|
self._update_docs_state = None
|
|
self._update_docs_thread = None
|
|
|
|
if not success:
|
|
if message == "timeout":
|
|
QMessageBox.warning(
|
|
self,
|
|
"Update Timeout",
|
|
f"Documentation generation timed out. Try running manually:\n\n"
|
|
f"update-docs --project {project.key}"
|
|
)
|
|
elif message == "not_found":
|
|
QMessageBox.warning(
|
|
self,
|
|
"Tool Not Found",
|
|
"CmdForge update-docs tool not found.\n\n"
|
|
"Make sure CmdForge is installed and the update-docs tool exists."
|
|
)
|
|
elif message == "error":
|
|
QMessageBox.warning(
|
|
self,
|
|
"Update Error",
|
|
f"Error running update-docs:\n\n{detail}"
|
|
)
|
|
else:
|
|
# Show the actual error output
|
|
error_text = message if message else "Unknown error (no output)"
|
|
QMessageBox.warning(
|
|
self,
|
|
"Update Failed",
|
|
f"Failed to generate documentation:\n\n{error_text[:1000]}"
|
|
)
|
|
return
|
|
|
|
# Read the newly generated docs
|
|
new_docs = {}
|
|
for filename in doc_files:
|
|
file_path = docs_path / filename
|
|
if file_path.exists():
|
|
new_docs[filename] = file_path.read_text()
|
|
else:
|
|
new_docs[filename] = ""
|
|
|
|
# Build changes dict: filename -> (old_content, new_content)
|
|
changes = {}
|
|
for filename in doc_files:
|
|
changes[filename] = (backup[filename], new_docs.get(filename, ""))
|
|
|
|
# Show preview dialog
|
|
try:
|
|
dialog = DocsPreviewDialog(changes, self)
|
|
dialog.exec()
|
|
except Exception as e:
|
|
QMessageBox.critical(
|
|
self,
|
|
"Dialog Error",
|
|
f"Error showing preview dialog:\n\n{e}"
|
|
)
|
|
# Restore backups on error
|
|
for filename, content in backup.items():
|
|
file_path = docs_path / filename
|
|
if content:
|
|
file_path.write_text(content)
|
|
return
|
|
|
|
if dialog.was_accepted():
|
|
# Get only the selected (and potentially edited) changes
|
|
selected_changes = dialog.get_selected_changes()
|
|
|
|
if not selected_changes:
|
|
QMessageBox.information(
|
|
self,
|
|
"No Changes",
|
|
"No files were selected for update."
|
|
)
|
|
# Restore all backups since nothing was selected
|
|
for filename, content in backup.items():
|
|
file_path = docs_path / filename
|
|
if content:
|
|
file_path.write_text(content)
|
|
return
|
|
|
|
# Write only the selected files (with user's edits if any)
|
|
for filename, new_content in selected_changes.items():
|
|
file_path = docs_path / filename
|
|
file_path.write_text(new_content)
|
|
|
|
# Restore backups for files that were NOT selected
|
|
for filename in doc_files:
|
|
if filename not in selected_changes:
|
|
file_path = docs_path / filename
|
|
if backup[filename]:
|
|
file_path.write_text(backup[filename])
|
|
|
|
# Ask about deployment
|
|
deploy_reply = QMessageBox.question(
|
|
self,
|
|
"Deploy Documentation",
|
|
f"Changes have been saved ({len(selected_changes)} file(s) updated).\n\n"
|
|
f"Would you like to deploy the documentation to Gitea Pages?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if deploy_reply == QMessageBox.StandardButton.Yes:
|
|
# Use async deploy (reuse existing deploy infrastructure)
|
|
self._deploy_docs(project)
|
|
else:
|
|
QMessageBox.information(
|
|
self,
|
|
"Changes Saved",
|
|
f"Documentation changes have been saved locally ({len(selected_changes)} file(s)).\n\n"
|
|
"You can deploy later using the 'Deploy Docs' option."
|
|
)
|
|
else:
|
|
# User rejected - restore backup
|
|
for filename, content in backup.items():
|
|
file_path = docs_path / filename
|
|
if content:
|
|
file_path.write_text(content)
|
|
elif file_path.exists():
|
|
# File was created but user rejected - delete it
|
|
file_path.unlink()
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Changes Discarded",
|
|
"Documentation changes have been discarded.\n\n"
|
|
"Original files have been restored."
|
|
)
|