Refactor GitHub automation tool:

- Updated WorkItemFieldExtractor to be more generic and removed Azure DevOps specific references.
- Removed the EnhancedContentBuilders class as it was specific to Azure DevOps.
- Deleted work_item_processor.py as it was no longer needed.
- Introduced workflow.py to manage GitHub workflow items (issues and pull requests) with improved structure and functionality.
- Enhanced logging and error handling across the new workflow management system.
This commit is contained in:
b-tsammmons
2025-11-11 22:46:02 -10:00
parent d6da461d10
commit 0f41a3e750
15 changed files with 2003 additions and 1446 deletions
+643 -53
View File
@@ -12,9 +12,8 @@ from typing import List, Dict, Any, Optional
from .utils import Logger
from .settings_dialog import SettingsDialog
from .work_item_processor import WorkItemProcessor
from .azure_devops_api import AzureDevOpsAPI
from .dataverse_api import DataverseAPI
# Removed imports: WorkItemProcessor, AzureDevOpsAPI, DataverseAPI
# These were specific to Azure DevOps integration
class HyperlinkDialog:
@@ -135,9 +134,10 @@ class MainGUI:
# Initialize logger after GUI is created
self.logger = Logger(self.log_text)
# Initialize work item processor
self.work_item_processor = WorkItemProcessor(self.logger, self.config_manager.get_config())
# Initialize work item processor - REMOVED (was Azure DevOps specific)
# self.work_item_processor = WorkItemProcessor(self.logger, self.config_manager.get_config())
self.work_item_processor = None # Placeholder for future implementation
# Initialize cache manager
from .cache_manager import CacheManager
@@ -222,7 +222,7 @@ class MainGUI:
title_frame.columnconfigure(0, weight=1)
# Title
title_label = ttk.Label(title_frame, text="MicrosoftDocFlow v3",
title_label = ttk.Label(title_frame, text="GitHub Pulse",
font=('Arial', 16, 'bold'))
title_label.grid(row=0, column=0, sticky=tk.W)
@@ -237,53 +237,177 @@ class MainGUI:
self.settings_button.grid(row=0, column=2, sticky=tk.E, padx=(5, 0))
def _create_controls_section(self, parent):
"""Create work item controls section"""
# Work Item Details group frame
workitem_frame = ttk.LabelFrame(parent, text="📋 Work Item Details",
style='WorkItem.TLabelframe', padding="15")
workitem_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15), padx=5)
workitem_frame.columnconfigure(2, weight=1)
# Controls row
controls_row = ttk.Frame(workitem_frame)
controls_row.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
controls_row.columnconfigure(6, weight=1)
# Fetch buttons
self.fetch_button = ttk.Button(controls_row, text="📥 Fetch Work Items",
command=self.start_fetch_work_items)
self.fetch_button.grid(row=0, column=0, padx=(0, 10))
"""Create GitHub Tools section"""
# GitHub Tools group frame
tools_frame = ttk.LabelFrame(parent, text="🔧 GitHub Tools",
style='WorkItem.TLabelframe', padding="15")
tools_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15), padx=5)
tools_frame.columnconfigure(1, weight=1)
self.fetch_uuf_button = ttk.Button(controls_row, text="📋 Fetch UUF Items",
command=self.start_fetch_uuf_items)
self.fetch_uuf_button.grid(row=0, column=1, padx=(0, 15))
# Initialize workflow data
self.target_repos = []
self.forked_repos = []
self.workflow_items = []
self.current_workflow_items = []
# Navigation buttons
self.prev_button = ttk.Button(controls_row, text="← Previous",
command=self.previous_item, state='disabled')
self.prev_button.grid(row=0, column=2, padx=(0, 5))
# Get current config
config = self.config_manager.get_config()
self.next_button = ttk.Button(controls_row, text="Next →",
command=self.next_item, state='disabled')
self.next_button.grid(row=0, column=3, padx=(0, 15))
# Row 0: Mode Selection
mode_frame = ttk.Frame(tools_frame)
mode_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10), padx=5)
# Action type dropdown
self.action_type_var = tk.StringVar(value="Create Issue")
self.action_type_dropdown = ttk.Combobox(controls_row, textvariable=self.action_type_var,
values=["Create Issue", "Create PR"],
state="readonly", width=15)
self.action_type_dropdown.grid(row=0, column=4, padx=(0, 5))
self.action_type_dropdown.bind("<<ComboboxSelected>>", lambda e: self.update_action_button_text())
ttk.Label(mode_frame, text="Mode:", font=('Arial', 10, 'bold')).grid(
row=0, column=0, sticky=tk.W, padx=(0, 10))
# GO button
self.go_button = ttk.Button(controls_row, text="🚀 GO",
command=self.create_github_resource, state='disabled')
self.go_button.grid(row=0, column=5, padx=(0, 20))
self.tools_mode_var = tk.StringVar(value="action")
create_radio = ttk.Radiobutton(mode_frame, text="✏️ Create PR/Issue", variable=self.tools_mode_var,
value="create", command=self._on_mode_changed)
create_radio.grid(row=0, column=1, padx=(0, 15))
action_radio = ttk.Radiobutton(mode_frame, text="📋 Action Existing PR/Issue", variable=self.tools_mode_var,
value="action", command=self._on_mode_changed)
action_radio.grid(row=0, column=2, padx=(0, 10))
# Separator
ttk.Separator(tools_frame, orient='horizontal').grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(5, 10))
# Row 2: Target Repository
self.target_repo_label = ttk.Label(tools_frame, text="Target Repository:", font=('Arial', 10, 'bold'))
self.target_repo_label.grid(row=2, column=0, sticky=tk.W, pady=5, padx=5)
target_frame = ttk.Frame(tools_frame)
target_frame.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
target_frame.columnconfigure(0, weight=1)
self.target_repo_var = tk.StringVar(value=config.get('GITHUB_REPO', ''))
self.target_repo_dropdown = ttk.Combobox(target_frame, textvariable=self.target_repo_var,
values=[''], width=60)
self.target_repo_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
self.target_repo_dropdown.bind('<KeyRelease>', self._on_target_repo_search)
self.target_repo_dropdown.bind('<<ComboboxSelected>>', lambda e: self._on_repo_selection_changed())
refresh_target_btn = ttk.Button(target_frame, text="🔄", width=3,
command=self._refresh_target_repos)
refresh_target_btn.grid(row=0, column=1, padx=(0, 2))
search_target_btn = ttk.Button(target_frame, text="🔍", width=3,
command=self._search_target_repos)
search_target_btn.grid(row=0, column=2)
# Row 3: Forked Repository
self.forked_repo_label = ttk.Label(tools_frame, text="Forked Repository:", font=('Arial', 10, 'bold'))
self.forked_repo_label.grid(row=3, column=0, sticky=tk.W, pady=5, padx=5)
self.fork_frame = ttk.Frame(tools_frame)
self.fork_frame.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
self.fork_frame.columnconfigure(0, weight=1)
self.forked_repo_var = tk.StringVar(value=config.get('FORKED_REPO', ''))
self.forked_repo_dropdown = ttk.Combobox(self.fork_frame, textvariable=self.forked_repo_var,
values=[''], width=60)
self.forked_repo_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
self.forked_repo_dropdown.bind('<<ComboboxSelected>>', lambda e: self._on_repo_selection_changed())
refresh_fork_btn = ttk.Button(self.fork_frame, text="🔄", width=3,
command=self._refresh_forked_repos)
refresh_fork_btn.grid(row=0, column=1, padx=(0, 2))
clone_fork_btn = ttk.Button(self.fork_frame, text="📥", width=3,
command=self._clone_forked_repo)
clone_fork_btn.grid(row=0, column=2)
# Row 4: Action Mode Controls (View toggles and load button)
self.action_controls_row = ttk.Frame(tools_frame)
self.action_controls_row.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 5), padx=5)
self.action_controls_row.columnconfigure(2, weight=1)
# Repo source toggle
ttk.Label(self.action_controls_row, text="View:", font=('Arial', 10, 'bold')).grid(
row=0, column=0, sticky=tk.W, padx=(0, 5))
self.repo_source_var = tk.StringVar(value="target")
target_radio = ttk.Radiobutton(self.action_controls_row, text="Target", variable=self.repo_source_var,
value="target", command=self._filter_workflow_items)
target_radio.grid(row=0, column=1, padx=(0, 5))
fork_radio = ttk.Radiobutton(self.action_controls_row, text="Fork", variable=self.repo_source_var,
value="fork", command=self._filter_workflow_items)
fork_radio.grid(row=0, column=2, padx=(0, 15))
# Item type toggle
self.item_type_var = tk.StringVar(value="pull_request")
pr_radio = ttk.Radiobutton(self.action_controls_row, text="PRs", variable=self.item_type_var,
value="pull_request", command=self._filter_workflow_items)
pr_radio.grid(row=0, column=3, padx=(0, 5))
issue_radio = ttk.Radiobutton(self.action_controls_row, text="Issues", variable=self.item_type_var,
value="issue", command=self._filter_workflow_items)
issue_radio.grid(row=0, column=4, padx=(0, 15))
# Fetch button
self.fetch_workflow_btn = ttk.Button(self.action_controls_row, text="📥 Load Items",
command=self._load_workflow_items)
self.fetch_workflow_btn.grid(row=0, column=5, padx=(0, 10))
# Item counter
self.item_counter_label = ttk.Label(controls_row, text="No items loaded",
self.item_counter_label = ttk.Label(self.action_controls_row, text="No items loaded",
font=('Arial', 9, 'italic'))
self.item_counter_label.grid(row=0, column=6, sticky=tk.E)
# Row 5: Workflow items dropdown (Action Mode)
self.action_item_label = ttk.Label(tools_frame, text="Select Item:", font=('Arial', 10, 'bold'))
self.action_item_label.grid(row=5, column=0, sticky=tk.W, pady=5, padx=5)
action_item_dropdown_frame = ttk.Frame(tools_frame)
action_item_dropdown_frame.grid(row=5, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
action_item_dropdown_frame.columnconfigure(0, weight=1)
self.workflow_item_var = tk.StringVar()
self.workflow_item_dropdown = ttk.Combobox(action_item_dropdown_frame, textvariable=self.workflow_item_var,
values=[''], width=60, state='readonly')
self.workflow_item_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E))
self.workflow_item_dropdown.bind('<<ComboboxSelected>>', self._on_workflow_item_selected)
# Row 4-5: Create Mode Controls (hidden by default)
self.create_controls_row = ttk.Frame(tools_frame)
self.create_controls_row.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 5), padx=5)
self.create_controls_row.columnconfigure(1, weight=1)
# Create type selection
ttk.Label(self.create_controls_row, text="Create:", font=('Arial', 10, 'bold')).grid(
row=0, column=0, sticky=tk.W, padx=(0, 5))
self.create_type_var = tk.StringVar(value="pull_request")
create_pr_radio = ttk.Radiobutton(self.create_controls_row, text="📝 Pull Request",
variable=self.create_type_var, value="pull_request")
create_pr_radio.grid(row=0, column=1, padx=(0, 15), sticky=tk.W)
create_issue_radio = ttk.Radiobutton(self.create_controls_row, text="🐛 Issue",
variable=self.create_type_var, value="issue")
create_issue_radio.grid(row=0, column=2, padx=(0, 15), sticky=tk.W)
# Create button
self.create_item_btn = ttk.Button(self.create_controls_row, text="✏️ Create New",
command=self._create_new_item)
self.create_item_btn.grid(row=0, column=3, padx=(0, 10))
# Store references for show/hide
self.action_mode_widgets = [
self.action_controls_row,
self.action_item_label,
action_item_dropdown_frame
]
self.create_mode_widgets = [
self.create_controls_row
]
# Initialize mode (show action, hide create)
self._on_mode_changed()
# Start loading repos
self.root.after(100, self._init_load_repos)
def _create_status_section(self, parent):
"""Create progress and status section"""
@@ -664,7 +788,10 @@ class MainGUI:
# Process items
self.current_work_items = []
for item in work_items:
processed_item = self.work_item_processor.process_work_item(item)
# REMOVED: Azure DevOps specific processing
# processed_item = self.work_item_processor.process_work_item(item)
# TODO: Implement custom item processing here
processed_item = item # Placeholder
if processed_item:
self.current_work_items.append(processed_item)
@@ -723,7 +850,10 @@ class MainGUI:
# Process items
self.current_work_items = []
for item in uuf_items:
processed_item = self.work_item_processor.process_uuf_item(item)
# REMOVED: UUF/Dataverse specific processing
# processed_item = self.work_item_processor.process_uuf_item(item)
# TODO: Implement custom item processing here
processed_item = item # Placeholder
if processed_item:
self.current_work_items.append(processed_item)
@@ -1850,14 +1980,28 @@ Proposed new text:
api_key = config.get('OPENAI_API_KEY', '').strip()
elif ai_provider in ['github-copilot', 'copilot', 'github_copilot']:
api_key = config.get('GITHUB_TOKEN', '').strip()
elif ai_provider == 'ollama':
api_key = config.get('OLLAMA_API_KEY', '').strip() # Optional for Ollama
else:
api_key = ''
github_token = config.get('GITHUB_PAT', '').strip()
local_repo_path = config.get('LOCAL_REPO_PATH', '').strip() or None
if not api_key:
# Validate API key (except for Ollama where it's optional)
if not api_key and ai_provider != 'ollama':
raise ValueError(f"No API key configured for {ai_provider}. Please configure in Settings.")
# Get Ollama-specific configuration
ollama_url = None
ollama_model = None
if ai_provider == 'ollama':
ollama_url = config.get('OLLAMA_URL', '').strip()
ollama_model = config.get('OLLAMA_MODEL', '').strip()
if not ollama_url:
raise ValueError("Ollama Server URL not configured. Please configure in Settings.")
if not ollama_model:
raise ValueError("Ollama Model not selected. Please configure in Settings.")
self.logger.log(f"Using AI Provider: {ai_provider.upper()}")
# Create AI manager
@@ -1865,7 +2009,7 @@ Proposed new text:
ai_manager = AIManager(self.logger)
# Create AI provider instance
ai_provider_instance = ai_manager.create_ai_provider(ai_provider, api_key)
ai_provider_instance = ai_manager.create_ai_provider(ai_provider, api_key, ollama_url, ollama_model)
if not ai_provider_instance:
raise ValueError(f"Failed to create {ai_provider} provider")
@@ -2161,19 +2305,465 @@ Proposed new text:
try:
config = self.config_manager.get_config()
ai_provider = config.get('AI_PROVIDER', '').strip().lower()
if not ai_provider or ai_provider == 'none' or ai_provider == '':
return # No AI provider selected
# Check if this provider requires special modules
if ai_provider not in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot']:
return # Unknown provider, skip check
# Check module availability using AI manager
self.ai_manager.check_and_install_ai_modules(ai_provider, self.root)
except Exception as e:
self.logger.log(f"Error checking AI provider setup: {str(e)}")
# ===== GitHub Tools Methods =====
def _init_load_repos(self):
"""Initialize loading of repositories"""
self._load_target_repos_async()
self._load_forked_repos_async()
def _load_target_repos_async(self):
"""Load target repositories asynchronously"""
def load_repos():
try:
config = self.config_manager.get_config()
github_token = config.get('GITHUB_PAT', '')
if not github_token:
return
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push')
self.target_repos = repo_fetcher.get_repo_names(repos)
# Update dropdown on main thread
self.root.after(0, self._update_target_dropdown)
except Exception as e:
self.logger.log(f"Error loading target repos: {e}")
threading.Thread(target=load_repos, daemon=True).start()
def _update_target_dropdown(self):
"""Update the target repository dropdown"""
try:
current_values = [''] # Start with empty option
# Add user's repos with edit access
if self.target_repos:
current_values.append('--- Your Repos (with edit access) ---')
current_values.extend(self.target_repos)
self.target_repo_dropdown['values'] = current_values
except Exception as e:
self.logger.log(f"Error updating target dropdown: {e}")
def _refresh_target_repos(self):
"""Refresh target repositories"""
self._load_target_repos_async()
def _search_target_repos(self):
"""Search for repositories on GitHub"""
query = self.target_repo_var.get().strip()
if not query:
return
def search_repos():
try:
config = self.config_manager.get_config()
github_token = config.get('GITHUB_PAT', '')
if not github_token:
return
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
repos = repo_fetcher.search_repositories(query, per_page=50)
search_results = repo_fetcher.get_repo_names(repos)
# Update dropdown on main thread
self.root.after(0, lambda: self._update_target_search_results(search_results))
except Exception as e:
self.logger.log(f"Error searching repos: {e}")
threading.Thread(target=search_repos, daemon=True).start()
def _update_target_search_results(self, search_results):
"""Update target dropdown with search results"""
try:
current_values = ['']
# Add user's repos
if self.target_repos:
current_values.append('--- Your Repos (with edit access) ---')
current_values.extend(self.target_repos)
# Add search results
if search_results:
current_values.append('--- Search Results ---')
current_values.extend(search_results)
self.target_repo_dropdown['values'] = current_values
except Exception as e:
self.logger.log(f"Error updating search results: {e}")
def _on_target_repo_search(self, _event):
"""Handle typing in target repo field for auto-search"""
# Debounce: only search after user stops typing for 500ms
if hasattr(self, '_search_timer'):
self.root.after_cancel(self._search_timer)
query = self.target_repo_var.get().strip()
if len(query) >= 3: # Only search if at least 3 characters
self._search_timer = self.root.after(500, self._search_target_repos)
def _load_forked_repos_async(self):
"""Load forked repositories asynchronously"""
def load_forks():
try:
config = self.config_manager.get_config()
github_token = config.get('GITHUB_PAT', '')
local_repo_path = config.get('LOCAL_REPO_PATH', '')
# Load local repos
local_repos = []
if local_repo_path:
try:
from .utils import LocalRepositoryScanner
local_repos = LocalRepositoryScanner.scan_local_repos(local_repo_path)
except Exception as e:
self.logger.log(f"Error scanning local repos: {e}")
# Load GitHub repos
github_repos = []
if github_token:
try:
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
repos = repo_fetcher.fetch_user_repos(repo_type='owner')
github_repos = repo_fetcher.get_repo_names(repos)
except Exception as e:
self.logger.log(f"Error loading GitHub repos: {e}")
self.forked_repos = {'local': local_repos, 'github': github_repos}
# Update dropdown on main thread
self.root.after(0, self._update_forked_dropdown)
except Exception as e:
self.logger.log(f"Error loading forked repos: {e}")
threading.Thread(target=load_forks, daemon=True).start()
def _update_forked_dropdown(self):
"""Update the forked repository dropdown"""
try:
current_values = [''] # Start with empty option
# Add local repos
if self.forked_repos.get('local'):
current_values.append('--- Local Repositories ---')
current_values.extend(self.forked_repos['local'])
# Add GitHub repos
if self.forked_repos.get('github'):
current_values.append('--- Your GitHub Forks ---')
current_values.extend(self.forked_repos['github'])
self.forked_repo_dropdown['values'] = current_values
except Exception as e:
self.logger.log(f"Error updating forked dropdown: {e}")
def _refresh_forked_repos(self):
"""Refresh forked repositories"""
self._load_forked_repos_async()
def _clone_forked_repo(self):
"""Clone the selected forked repository"""
selected_repo = self.forked_repo_var.get().strip()
# Validate selection
if not selected_repo:
messagebox.showwarning("No Repository Selected",
"Please select a repository to clone.")
return
# Check if it's a section header
if selected_repo.startswith('---'):
messagebox.showwarning("Invalid Selection",
"Please select a repository, not a section header.")
return
config = self.config_manager.get_config()
local_repo_path = config.get('LOCAL_REPO_PATH', '').strip()
if not local_repo_path:
messagebox.showwarning("Local Path Not Configured",
"Please configure the Local Repository Path in settings first.")
return
# Clone logic (similar to settings_dialog.py)
import subprocess
import os
try:
os.makedirs(local_repo_path, exist_ok=True)
except Exception as e:
messagebox.showerror("Directory Error",
f"Could not create local repository directory:\n{str(e)}")
return
# Extract repo name
repo_name = selected_repo
if '/' not in repo_name:
messagebox.showerror("Invalid Repository",
"Repository must be in 'owner/repo' format.")
return
folder_name = repo_name.split('/')[-1]
target_path = os.path.join(local_repo_path, folder_name)
if os.path.exists(target_path):
response = messagebox.askyesno("Directory Exists",
f"The directory '{folder_name}' already exists.\n\n"
f"Do you want to continue anyway?")
if not response:
return
clone_url = f"https://github.com/{repo_name}.git"
def clone_repo():
try:
result = subprocess.run(
['git', 'clone', clone_url, target_path],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
self.root.after(0, lambda: messagebox.showinfo(
"Clone Successful",
f"Successfully cloned {repo_name}!"))
self.root.after(0, self._refresh_forked_repos)
else:
error_msg = result.stderr if result.stderr else result.stdout
self.root.after(0, lambda: messagebox.showerror(
"Clone Failed",
f"Failed to clone {repo_name}.\n\n{error_msg}"))
except subprocess.TimeoutExpired:
self.root.after(0, lambda: messagebox.showerror(
"Clone Timeout",
f"Cloning {repo_name} timed out after 5 minutes."))
except FileNotFoundError:
self.root.after(0, lambda: messagebox.showerror(
"Git Not Found",
"Git is not installed or not found in PATH."))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror(
"Clone Error",
f"An error occurred while cloning:\n{str(e)}"))
messagebox.showinfo("Cloning Repository",
f"Cloning {repo_name} to:\n{target_path}\n\n"
f"This may take a few moments...")
threading.Thread(target=clone_repo, daemon=True).start()
def _on_mode_changed(self):
"""Handle mode change between Create and Action"""
mode = self.tools_mode_var.get()
if mode == "action":
# Show action mode widgets
for widget in self.action_mode_widgets:
widget.grid()
# Hide create mode widgets
for widget in self.create_mode_widgets:
widget.grid_remove()
else: # create mode
# Hide action mode widgets
for widget in self.action_mode_widgets:
widget.grid_remove()
# Show create mode widgets
for widget in self.create_mode_widgets:
widget.grid()
def _create_new_item(self):
"""Handle creating a new PR or Issue"""
create_type = self.create_type_var.get()
target_repo = self.target_repo_var.get().strip()
# Skip section headers
if target_repo.startswith('---'):
target_repo = ''
if not target_repo:
messagebox.showwarning("No Repository Selected",
"Please select a target repository.")
return
if create_type == "pull_request":
# TODO: Implement PR creation workflow
messagebox.showinfo("Create Pull Request",
f"PR creation workflow for {target_repo} will be implemented here.\n\n"
"This will open the PR creation interface in the tabs below.")
else: # issue
# TODO: Implement Issue creation workflow
messagebox.showinfo("Create Issue",
f"Issue creation workflow for {target_repo} will be implemented here.\n\n"
"This will open the Issue creation interface in the tabs below.")
def _on_repo_selection_changed(self):
"""Handle repository selection change"""
# Clear workflow items when repos change
self.workflow_items = []
self.current_workflow_items = []
self.workflow_item_dropdown['values'] = ['']
self.workflow_item_var.set('')
self.item_counter_label.config(text="No items loaded")
def _load_workflow_items(self):
"""Load workflow items from selected repositories"""
target_repo = self.target_repo_var.get().strip()
forked_repo = self.forked_repo_var.get().strip()
# Skip section headers
if target_repo.startswith('---'):
target_repo = ''
if forked_repo.startswith('---'):
forked_repo = ''
if not target_repo and not forked_repo:
messagebox.showwarning("No Repositories Selected",
"Please select at least one repository.")
return
self.progress.start()
self.update_status("Loading workflow items...")
def load_items():
try:
config = self.config_manager.get_config()
github_token = config.get('GITHUB_PAT', '')
from .workflow import WorkflowManager
workflow_manager = WorkflowManager(github_token, self.logger)
# Fetch all items
results = workflow_manager.fetch_all_workflow_items(
target_repo=target_repo if target_repo else None,
fork_repo=forked_repo if forked_repo else None,
include_issues=True,
include_prs=True,
state='open' # Only load open items
)
self.workflow_items = results
# Update UI on main thread
self.root.after(0, self._on_workflow_items_loaded)
except Exception as e:
self.logger.log(f"Error loading workflow items: {e}")
self.root.after(0, lambda: self.update_status("Failed to load workflow items"))
self.root.after(0, lambda: messagebox.showerror(
"Load Error",
f"Failed to load workflow items:\n{str(e)}"))
finally:
self.root.after(0, self.progress.stop)
threading.Thread(target=load_items, daemon=True).start()
def _on_workflow_items_loaded(self):
"""Handle workflow items loaded"""
total_items = sum(len(items) for items in self.workflow_items.values())
self.logger.log(f"Loaded {total_items} workflow items")
self.update_status(f"Loaded {total_items} workflow items")
# Apply current filters
self._filter_workflow_items()
def _filter_workflow_items(self):
"""Filter workflow items based on current selections"""
if not self.workflow_items:
return
repo_source = self.repo_source_var.get() # 'target' or 'fork'
item_type = self.item_type_var.get() # 'pull_request' or 'issue'
# Get the appropriate list
# WorkflowManager returns keys like: 'target_prs', 'target_issues', 'fork_prs', 'fork_issues'
key = f"{repo_source}_prs" if item_type == 'pull_request' else f"{repo_source}_issues"
filtered_items = self.workflow_items.get(key, [])
# Update dropdown
self.current_workflow_items = filtered_items
item_options = [
f"#{item.number} - {item.title}" for item in filtered_items
]
self.workflow_item_dropdown['values'] = item_options if item_options else ['']
self.workflow_item_var.set('')
# Update counter
count = len(filtered_items)
source_name = "Target" if repo_source == "target" else "Fork"
type_name = "PRs" if item_type == "pull_request" else "Issues"
self.item_counter_label.config(text=f"{count} {source_name} {type_name}")
def _on_workflow_item_selected(self, _event):
"""Handle workflow item selection"""
selected = self.workflow_item_var.get()
if not selected:
return
# Extract item number from selection
try:
item_number = int(selected.split('#')[1].split(' ')[0])
# Find the item
for item in self.current_workflow_items:
if item.number == item_number:
self._display_workflow_item(item)
break
except Exception as e:
self.logger.log(f"Error selecting workflow item: {e}")
def _display_workflow_item(self, item):
"""Display workflow item details"""
# Update Current Work Item tab
self.work_item_id_label.config(text=f"{item.item_type.upper()} #{item.number}")
# Update nature text
self.nature_text.config(state='normal')
self.nature_text.delete('1.0', tk.END)
self.nature_text.insert('1.0', item.title)
self.nature_text.config(state='disabled')
# Update URL
self.doc_url_text.config(state='normal')
self.doc_url_text.delete('1.0', tk.END)
self.doc_url_text.insert('1.0', item.url)
self.doc_url_text.config(state='disabled')
# Update description
self.description_text.config(state='normal')
self.description_text.delete('1.0', tk.END)
self.description_text.insert('1.0', item.body or 'No description')
self.description_text.config(state='disabled')
self.logger.log(f"Displaying {item.item_type} #{item.number}: {item.title}")
def display_current_item(self):
"""Display current work item (public method for compatibility)"""