Refactor SettingsDialog: Remove target and forked repository dropdowns, update GitHub PAT section, and clean up async repo loading methods. Enhance WorkflowItem with data reconstruction and add fetch_comments method for GitHub issues/PRs.
This commit is contained in:
@@ -1479,6 +1479,9 @@ Current file content:
|
||||
class GitHubCopilotProvider(AIProvider):
|
||||
"""GitHub Copilot provider using GitHub Models API"""
|
||||
|
||||
# GitHub Models API endpoint
|
||||
GITHUB_MODELS_API_URL = "https://models.inference.ai.azure.com/chat/completions"
|
||||
|
||||
def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]:
|
||||
"""Use diff-based approach for surgical edits"""
|
||||
|
||||
@@ -1505,7 +1508,7 @@ class GitHubCopilotProvider(AIProvider):
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
||||
url = self.GITHUB_MODELS_API_URL
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -1869,7 +1872,7 @@ Return the complete updated file content now (NO explanatory text):"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
||||
url = self.GITHUB_MODELS_API_URL
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -1941,7 +1944,7 @@ Generate the new content now:"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
||||
url = self.GITHUB_MODELS_API_URL
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -2018,7 +2021,7 @@ Find the exact text to correct:"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
||||
url = self.GITHUB_MODELS_API_URL
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Cache Manager for Work Items and UUF Items
|
||||
Cache Manager for GitHub PRs and Issues
|
||||
Stores fetched items in temporary cache to avoid reloading on every app start
|
||||
"""
|
||||
|
||||
@@ -13,7 +13,7 @@ from hashlib import md5
|
||||
|
||||
|
||||
class CacheManager:
|
||||
"""Manages caching of work items and UUF items"""
|
||||
"""Manages caching of GitHub PRs and Issues"""
|
||||
|
||||
def __init__(self, cache_duration_hours: int = 24):
|
||||
"""
|
||||
@@ -23,7 +23,7 @@ class CacheManager:
|
||||
cache_duration_hours: How long cache is valid (default 24 hours)
|
||||
"""
|
||||
self.cache_duration_seconds = cache_duration_hours * 3600
|
||||
self.cache_dir = Path(tempfile.gettempdir()) / "devops_to_github_cache"
|
||||
self.cache_dir = Path(tempfile.gettempdir()) / "github_pulse_cache"
|
||||
self.cache_dir.mkdir(exist_ok=True)
|
||||
|
||||
def _get_cache_key(self, source_type: str, identifier: str) -> str:
|
||||
@@ -50,14 +50,14 @@ class CacheManager:
|
||||
|
||||
def load_from_cache(self, source_type: str, identifier: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
Load work items from cache
|
||||
Load GitHub items from cache
|
||||
|
||||
Args:
|
||||
source_type: 'azure_devops' or 'uuf'
|
||||
identifier: query URL hash or config hash
|
||||
source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
|
||||
identifier: repository identifier or config hash
|
||||
|
||||
Returns:
|
||||
List of work items if cache is valid, None otherwise
|
||||
List of items if cache is valid, None otherwise
|
||||
"""
|
||||
if not self.is_cache_valid(source_type, identifier):
|
||||
return None
|
||||
@@ -81,12 +81,12 @@ class CacheManager:
|
||||
|
||||
def save_to_cache(self, source_type: str, identifier: str, items: List[Dict[str, Any]]) -> bool:
|
||||
"""
|
||||
Save work items to cache
|
||||
Save GitHub items to cache
|
||||
|
||||
Args:
|
||||
source_type: 'azure_devops' or 'uuf'
|
||||
identifier: query URL hash or config hash
|
||||
items: List of work items to cache
|
||||
source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
|
||||
identifier: repository identifier or config hash
|
||||
items: List of items to cache (PRs or Issues)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
|
||||
@@ -229,9 +229,7 @@ class GitHubGQL:
|
||||
if self.dry_run:
|
||||
# Return sample data for dry run
|
||||
return [
|
||||
"username/fabric-docs",
|
||||
"username/azure-docs",
|
||||
"username/powerbi-docs"
|
||||
"username/repo_name",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -324,8 +322,8 @@ class GitHubGQL:
|
||||
|
||||
if self.dry_run:
|
||||
return {
|
||||
"target_alternatives": ["microsoftdocs/fabric-docs-pr"],
|
||||
"fork_alternatives": ["b-tsammons/azure-docs-pr"]
|
||||
"target_alternatives": ["username/target_repo_name"],
|
||||
"fork_alternatives": ["username/fork_repo_name"]
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -67,8 +67,6 @@ class MainGUI:
|
||||
self.diff_text_ref = ft.Ref[ft.TextField]()
|
||||
self.log_text_ref = ft.Ref[ft.TextField]()
|
||||
self.edit_button_ref = ft.Ref[ft.IconButton]()
|
||||
self.prev_button_ref = ft.Ref[ft.IconButton]()
|
||||
self.next_button_ref = ft.Ref[ft.IconButton]()
|
||||
self.go_button_ref = ft.Ref[ft.ElevatedButton]()
|
||||
|
||||
# Mode and filter refs
|
||||
@@ -84,6 +82,10 @@ class MainGUI:
|
||||
# DataTable ref for all items
|
||||
self.items_table_ref = ft.Ref[ft.DataTable]()
|
||||
|
||||
# All items display
|
||||
self.all_items_container_ref = ft.Ref[ft.Column]()
|
||||
self.item_detail_dialog_ref = ft.Ref[ft.AlertDialog]()
|
||||
|
||||
# Sidebar state
|
||||
self.sidebar_visible = True
|
||||
self.sidebar_ref = ft.Ref[ft.Container]()
|
||||
@@ -196,9 +198,10 @@ class MainGUI:
|
||||
async def _async_init(self):
|
||||
"""Async initialization"""
|
||||
await asyncio.sleep(0.5)
|
||||
await self._auto_load_cached_items()
|
||||
await self._load_custom_instructions()
|
||||
await self._init_load_repos()
|
||||
# Auto-load cached items after repos are loaded
|
||||
await self._auto_load_cached_items()
|
||||
|
||||
def _toggle_sidebar(self, e):
|
||||
"""Toggle sidebar visibility"""
|
||||
@@ -335,6 +338,23 @@ class MainGUI:
|
||||
expand=True,
|
||||
on_change=self._on_workflow_item_selected,
|
||||
),
|
||||
ft.Divider(height=10),
|
||||
ft.Text("All Items", weight=ft.FontWeight.BOLD, size=14),
|
||||
ft.Container(
|
||||
content=ft.Column(
|
||||
ref=self.all_items_container_ref,
|
||||
controls=[
|
||||
ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True, text_align=ft.TextAlign.CENTER)
|
||||
],
|
||||
spacing=10,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||
),
|
||||
height=300,
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=8,
|
||||
padding=5,
|
||||
),
|
||||
],
|
||||
spacing=10,
|
||||
)
|
||||
@@ -431,20 +451,6 @@ class MainGUI:
|
||||
# Navigation buttons
|
||||
nav_buttons = ft.Row(
|
||||
[
|
||||
ft.IconButton(
|
||||
ref=self.prev_button_ref,
|
||||
icon=ft.icons.ARROW_BACK,
|
||||
tooltip="Previous",
|
||||
on_click=self._previous_item,
|
||||
disabled=True,
|
||||
),
|
||||
ft.IconButton(
|
||||
ref=self.next_button_ref,
|
||||
icon=ft.icons.ARROW_FORWARD,
|
||||
tooltip="Next",
|
||||
on_click=self._next_item,
|
||||
disabled=True,
|
||||
),
|
||||
ft.Container(expand=True),
|
||||
ft.ElevatedButton(
|
||||
"Go",
|
||||
@@ -623,14 +629,17 @@ class MainGUI:
|
||||
items_table = ft.DataTable(
|
||||
ref=self.items_table_ref,
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("Repo")),
|
||||
ft.DataColumn(ft.Text("Type")),
|
||||
ft.DataColumn(ft.Text("ID")),
|
||||
ft.DataColumn(ft.Text("Title")),
|
||||
ft.DataColumn(ft.Text("Nature")),
|
||||
ft.DataColumn(ft.Text("GitHub Repo")),
|
||||
ft.DataColumn(ft.Text("ms.author")),
|
||||
ft.DataColumn(ft.Text("Author")),
|
||||
ft.DataColumn(ft.Text("Status")),
|
||||
],
|
||||
rows=[],
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=8,
|
||||
heading_row_color=ft.colors.BLUE_GREY_100,
|
||||
)
|
||||
|
||||
set_current_button = ft.ElevatedButton(
|
||||
@@ -707,6 +716,9 @@ class MainGUI:
|
||||
self.workflow_item_dropdown_ref.current.options = []
|
||||
self.page.update()
|
||||
|
||||
# Auto-load cached items for the newly selected repos
|
||||
self.page.run_task(self._auto_load_cached_items_on_repo_change)
|
||||
|
||||
def _on_workflow_item_selected(self, e):
|
||||
"""Handle workflow item selection"""
|
||||
if not self.workflow_item_dropdown_ref.current:
|
||||
@@ -789,6 +801,369 @@ class MainGUI:
|
||||
# Implementation would populate fields with workflow item data
|
||||
pass
|
||||
|
||||
def _populate_all_items(self):
|
||||
"""Populate the all items list with all loaded PRs and Issues"""
|
||||
if not self.all_items_container_ref.current:
|
||||
return
|
||||
|
||||
# Collect all items from workflow_items
|
||||
all_items = []
|
||||
for key, items in self.workflow_items.items():
|
||||
all_items.extend(items)
|
||||
|
||||
if not all_items:
|
||||
self.all_items_container_ref.current.controls = [
|
||||
ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True)
|
||||
]
|
||||
else:
|
||||
# Sort by updated_at (most recent first)
|
||||
all_items.sort(key=lambda x: x.updated_at if hasattr(x, 'updated_at') else '', reverse=True)
|
||||
|
||||
# Create item cards
|
||||
cards = []
|
||||
for item in all_items:
|
||||
cards.append(self._create_item_card(item))
|
||||
|
||||
self.all_items_container_ref.current.controls = cards
|
||||
|
||||
self.page.update()
|
||||
|
||||
def _create_item_card(self, item):
|
||||
"""Create a card for a workflow item"""
|
||||
# Determine repo source label
|
||||
repo_label = "Target" if item.repo_source == "target" else "Fork"
|
||||
repo_color = ft.colors.BLUE if item.repo_source == "target" else ft.colors.PURPLE
|
||||
|
||||
# Determine type label
|
||||
type_label = "PR" if item.item_type == "pull_request" else "Issue"
|
||||
type_color = ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE
|
||||
|
||||
# Create card
|
||||
return ft.Container(
|
||||
content=ft.Row(
|
||||
[
|
||||
# Repo source badge
|
||||
ft.Container(
|
||||
content=ft.Text(repo_label, size=10, weight=ft.FontWeight.BOLD),
|
||||
bgcolor=repo_color,
|
||||
padding=ft.padding.symmetric(horizontal=8, vertical=4),
|
||||
border_radius=4,
|
||||
),
|
||||
# Type badge
|
||||
ft.Container(
|
||||
content=ft.Text(type_label, size=10, weight=ft.FontWeight.BOLD),
|
||||
bgcolor=type_color,
|
||||
padding=ft.padding.symmetric(horizontal=8, vertical=4),
|
||||
border_radius=4,
|
||||
),
|
||||
# Title
|
||||
ft.Text(
|
||||
f"#{item.number}: {item.title}",
|
||||
size=12,
|
||||
expand=True,
|
||||
overflow=ft.TextOverflow.ELLIPSIS,
|
||||
),
|
||||
# Select button
|
||||
ft.IconButton(
|
||||
icon=ft.icons.CHECK_CIRCLE_OUTLINE,
|
||||
icon_size=16,
|
||||
tooltip="Select as current item",
|
||||
on_click=lambda e, it=item: self._select_item_as_current(it),
|
||||
),
|
||||
# View details button
|
||||
ft.IconButton(
|
||||
icon=ft.icons.OPEN_IN_NEW,
|
||||
icon_size=16,
|
||||
tooltip="View details",
|
||||
on_click=lambda e, it=item: self._show_item_detail(it),
|
||||
),
|
||||
],
|
||||
spacing=8,
|
||||
alignment=ft.MainAxisAlignment.START,
|
||||
),
|
||||
padding=8,
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=4,
|
||||
bgcolor=ft.colors.GREY_800,
|
||||
)
|
||||
|
||||
def _populate_all_items_table(self):
|
||||
"""Populate the DataTable in the All Items tab with all loaded PRs and Issues"""
|
||||
if not self.items_table_ref.current:
|
||||
return
|
||||
|
||||
# Collect all items from workflow_items
|
||||
all_items = []
|
||||
for key, items in self.workflow_items.items():
|
||||
all_items.extend(items)
|
||||
|
||||
if not all_items:
|
||||
self.items_table_ref.current.rows = []
|
||||
else:
|
||||
# Sort by updated_at (most recent first)
|
||||
all_items.sort(key=lambda x: x.updated_at if hasattr(x, 'updated_at') else '', reverse=True)
|
||||
|
||||
# Create table rows
|
||||
rows = []
|
||||
for item in all_items:
|
||||
# Determine repo source and type
|
||||
repo_source = "Target" if item.repo_source == "target" else "Fork"
|
||||
item_type = "PR" if item.item_type == "pull_request" else "Issue"
|
||||
|
||||
# Get author (item.author is already a string, not a dict)
|
||||
author = item.author if item.author else 'Unknown'
|
||||
|
||||
# Get state
|
||||
state = item.state if hasattr(item, 'state') else 'unknown'
|
||||
|
||||
# Get repo name
|
||||
config = self.config_manager.get_config()
|
||||
if item.repo_source == "target":
|
||||
repo_name = config.get('GITHUB_REPO', '')
|
||||
else:
|
||||
repo_name = config.get('FORKED_REPO', '')
|
||||
|
||||
# Create row with clickable button
|
||||
row = ft.DataRow(
|
||||
cells=[
|
||||
ft.DataCell(ft.Text(f"{repo_source}: {repo_name.split('/')[-1] if '/' in repo_name else repo_name}", size=12)),
|
||||
ft.DataCell(ft.Text(item_type, size=12)),
|
||||
ft.DataCell(ft.Text(f"#{item.number}", size=12)),
|
||||
ft.DataCell(ft.Text(item.title[:50] + "..." if len(item.title) > 50 else item.title, size=12)),
|
||||
ft.DataCell(ft.Text(author, size=12)),
|
||||
ft.DataCell(ft.Text(state, size=12)),
|
||||
],
|
||||
on_select_changed=lambda e, it=item: self._show_item_detail(it) if e.control.selected else None,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
self.items_table_ref.current.rows = rows
|
||||
|
||||
self.page.update()
|
||||
|
||||
def _select_item_as_current(self, item):
|
||||
"""Select an item as the current workflow item in the dropdown"""
|
||||
if not self.workflow_item_dropdown_ref.current:
|
||||
return
|
||||
|
||||
# Update filters to match the selected item
|
||||
# Set repo source (target/fork)
|
||||
if self.repo_source_ref.current:
|
||||
self.repo_source_ref.current.value = item.repo_source
|
||||
|
||||
# Set item type (pull_request/issue)
|
||||
if self.item_type_ref.current:
|
||||
self.item_type_ref.current.value = item.item_type
|
||||
|
||||
# Re-filter the workflow items with the new settings
|
||||
self._filter_workflow_items()
|
||||
|
||||
# Set the dropdown value to this item's title
|
||||
self.workflow_item_dropdown_ref.current.value = item.title
|
||||
|
||||
# Display the item
|
||||
self._display_workflow_item(item)
|
||||
|
||||
# Update the page
|
||||
self.page.update()
|
||||
|
||||
# Show confirmation
|
||||
item_type_label = "PR" if item.item_type == "pull_request" else "Issue"
|
||||
repo_label = "Target" if item.repo_source == "target" else "Fork"
|
||||
self._show_snackbar(f"Selected {item_type_label} from {repo_label}: {item.title}", error=False)
|
||||
|
||||
def _show_item_detail(self, item):
|
||||
"""Show detail dialog for a workflow item"""
|
||||
# Get repo string for fetching comments
|
||||
config = self.config_manager.get_config()
|
||||
if item.repo_source == "target":
|
||||
repo_str = config.get('GITHUB_REPO', '')
|
||||
else:
|
||||
repo_str = config.get('FORKED_REPO', '')
|
||||
|
||||
# Build the dialog
|
||||
dialog = self._build_item_detail_dialog(item, repo_str)
|
||||
|
||||
# Use Flet 0.28+ API: page.open() instead of page.dialog
|
||||
self.page.open(dialog)
|
||||
|
||||
def _build_item_detail_dialog(self, item, repo_str):
|
||||
"""Build the detail dialog with tabs for Main (Preview) and System (extracted data)"""
|
||||
|
||||
# Get repo name for display
|
||||
config = self.config_manager.get_config()
|
||||
if item.repo_source == "target":
|
||||
repo_name = config.get('GITHUB_REPO', '')
|
||||
else:
|
||||
repo_name = config.get('FORKED_REPO', '')
|
||||
|
||||
# Create header with repo and item info
|
||||
header = ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Row([
|
||||
ft.Icon(ft.icons.SOURCE, size=16),
|
||||
ft.Text(repo_name, size=12, weight=ft.FontWeight.BOLD),
|
||||
ft.Container(
|
||||
content=ft.Text(
|
||||
"PR" if item.item_type == "pull_request" else "Issue",
|
||||
size=10,
|
||||
color=ft.colors.WHITE,
|
||||
),
|
||||
bgcolor=ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE,
|
||||
padding=ft.padding.symmetric(horizontal=8, vertical=2),
|
||||
border_radius=4,
|
||||
),
|
||||
ft.Text(f"#{item.number}", size=12, color=ft.colors.GREY_400),
|
||||
], spacing=8),
|
||||
ft.Text(item.title, size=14, weight=ft.FontWeight.BOLD),
|
||||
ft.Row([
|
||||
ft.Text(
|
||||
f"by @{item.author if item.author else 'Unknown'}",
|
||||
size=11,
|
||||
color=ft.colors.GREY_400,
|
||||
),
|
||||
ft.Text(
|
||||
f"• {item.state}",
|
||||
size=11,
|
||||
color=ft.colors.GREEN if item.state == "open" else ft.colors.PURPLE,
|
||||
),
|
||||
], spacing=5),
|
||||
], spacing=5),
|
||||
padding=10,
|
||||
bgcolor=ft.colors.GREY_900,
|
||||
border_radius=8,
|
||||
)
|
||||
|
||||
# Create body preview
|
||||
body_preview = ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Text("Description", size=12, weight=ft.FontWeight.BOLD),
|
||||
ft.Container(
|
||||
content=ft.Text(
|
||||
item.body if item.body else "No description provided",
|
||||
size=11,
|
||||
selectable=True,
|
||||
),
|
||||
padding=10,
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=4,
|
||||
bgcolor=ft.colors.GREY_900,
|
||||
),
|
||||
], spacing=5),
|
||||
)
|
||||
|
||||
# Fetch comments
|
||||
comments = []
|
||||
if repo_str:
|
||||
try:
|
||||
workflow_manager = self._get_workflow_manager()
|
||||
comments = workflow_manager.fetch_comments(repo_str, item.number, item.item_type == "pull_request")
|
||||
print(f"Fetched {len(comments)} comments for {item.item_type} #{item.number}")
|
||||
except Exception as e:
|
||||
print(f"Error fetching comments: {e}")
|
||||
if self.logger:
|
||||
self.logger.log(f"Error fetching comments: {e}")
|
||||
|
||||
# Build comments display
|
||||
comments_widgets = []
|
||||
if comments:
|
||||
for comment in comments:
|
||||
comments_widgets.append(
|
||||
ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Row([
|
||||
ft.Text(f"@{comment['user']}", weight=ft.FontWeight.BOLD, size=12),
|
||||
ft.Text(comment['created_at'][:10] if comment.get('created_at') else '', size=10, color=ft.colors.GREY_600),
|
||||
]),
|
||||
ft.Text(comment['body'], size=11, selectable=True),
|
||||
],
|
||||
spacing=5,
|
||||
),
|
||||
padding=8,
|
||||
margin=ft.margin.only(bottom=8),
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=4,
|
||||
bgcolor=ft.colors.GREY_800,
|
||||
)
|
||||
)
|
||||
else:
|
||||
comments_widgets.append(ft.Text("No comments yet", italic=True, color=ft.colors.GREY_500, size=11))
|
||||
|
||||
# Comments section
|
||||
comments_section = ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Text(f"Comments ({len(comments)})", size=12, weight=ft.FontWeight.BOLD),
|
||||
ft.Column(
|
||||
controls=comments_widgets,
|
||||
spacing=5,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
),
|
||||
], spacing=5),
|
||||
)
|
||||
|
||||
# Main content (no tabs, just single scrollable content)
|
||||
main_content = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
header,
|
||||
body_preview,
|
||||
comments_section,
|
||||
ft.Row([
|
||||
ft.ElevatedButton(
|
||||
"Open in GitHub",
|
||||
icon=ft.icons.OPEN_IN_BROWSER,
|
||||
on_click=lambda e: self.page.launch_url(item.url),
|
||||
),
|
||||
ft.TextButton(
|
||||
"Copy URL",
|
||||
icon=ft.icons.COPY,
|
||||
on_click=lambda e: self._copy_to_clipboard(item.url),
|
||||
),
|
||||
], spacing=10),
|
||||
],
|
||||
spacing=15,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
),
|
||||
padding=10,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
# Create close handler that will close this specific dialog
|
||||
def close_handler(e):
|
||||
self.page.close(dialog)
|
||||
|
||||
# Create dialog
|
||||
dialog = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text(f"{item.item_type.upper()} #{item.number}: {item.title}"),
|
||||
content=ft.Container(
|
||||
content=main_content,
|
||||
width=800,
|
||||
height=600,
|
||||
),
|
||||
actions=[
|
||||
ft.TextButton("Close", on_click=close_handler),
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
)
|
||||
|
||||
return dialog
|
||||
|
||||
def _copy_to_clipboard(self, text):
|
||||
"""Copy text to clipboard and show notification"""
|
||||
self.page.set_clipboard(text)
|
||||
self._show_snackbar("URL copied to clipboard!", error=False)
|
||||
|
||||
def _get_workflow_manager(self):
|
||||
"""Get or create a WorkflowManager instance"""
|
||||
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
raise ValueError("GitHub token not configured")
|
||||
|
||||
from .workflow import WorkflowManager
|
||||
return WorkflowManager(github_token, self.logger)
|
||||
|
||||
def _previous_item(self, e):
|
||||
"""Navigate to previous item"""
|
||||
if self.current_item_index > 0:
|
||||
@@ -796,13 +1171,6 @@ class MainGUI:
|
||||
self._display_current_item()
|
||||
self._update_navigation_buttons()
|
||||
|
||||
def _next_item(self, e):
|
||||
"""Navigate to next item"""
|
||||
if self.current_item_index < len(self.current_work_items) - 1:
|
||||
self.current_item_index += 1
|
||||
self._display_current_item()
|
||||
self._update_navigation_buttons()
|
||||
|
||||
def _toggle_edit_mode(self, e):
|
||||
"""Toggle edit mode for proposed new text"""
|
||||
if not self.proposed_new_text_ref.current or not self.edit_button_ref.current:
|
||||
@@ -875,14 +1243,172 @@ class MainGUI:
|
||||
# ===== Async Operations =====
|
||||
|
||||
async def _auto_load_cached_items(self):
|
||||
"""Auto-load cached items on startup"""
|
||||
"""Auto-load cached items on startup if available"""
|
||||
print("=" * 60)
|
||||
print("🔄 Auto-loading cached items on startup...")
|
||||
print("=" * 60)
|
||||
|
||||
def load_cached():
|
||||
try:
|
||||
# Try to load from cache
|
||||
if self.cache_manager:
|
||||
# Implementation would load cached items
|
||||
pass
|
||||
# Get configured repos
|
||||
target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None
|
||||
forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None
|
||||
|
||||
if not target_repo and not forked_repo:
|
||||
print("No repositories configured, skipping auto-load")
|
||||
return
|
||||
|
||||
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
print("No GitHub token configured, skipping auto-load")
|
||||
return
|
||||
|
||||
items_loaded = False
|
||||
|
||||
# Try to load target repo items from cache
|
||||
if target_repo and not target_repo.startswith('---') and '/' in target_repo:
|
||||
cached_prs = self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None
|
||||
cached_issues = self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None
|
||||
|
||||
if cached_prs is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs]
|
||||
print(f"✓ Auto-loaded {len(cached_prs)} PRs from cache (target)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Auto-loaded {len(cached_prs)} PRs from cache (target)")
|
||||
items_loaded = True
|
||||
|
||||
if cached_issues is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues]
|
||||
print(f"✓ Auto-loaded {len(cached_issues)} issues from cache (target)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Auto-loaded {len(cached_issues)} issues from cache (target)")
|
||||
items_loaded = True
|
||||
|
||||
# Try to load fork repo items from cache
|
||||
if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
|
||||
cached_fork_prs = self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None
|
||||
cached_fork_issues = self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None
|
||||
|
||||
if cached_fork_prs is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs]
|
||||
print(f"✓ Auto-loaded {len(cached_fork_prs)} PRs from cache (fork)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Auto-loaded {len(cached_fork_prs)} PRs from cache (fork)")
|
||||
items_loaded = True
|
||||
|
||||
if cached_fork_issues is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues]
|
||||
print(f"✓ Auto-loaded {len(cached_fork_issues)} issues from cache (fork)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Auto-loaded {len(cached_fork_issues)} issues from cache (fork)")
|
||||
items_loaded = True
|
||||
|
||||
if items_loaded:
|
||||
# Filter and update UI
|
||||
self.page.run_task(self._filter_workflow_items_async)
|
||||
|
||||
# Populate all items list in sidebar
|
||||
self._populate_all_items()
|
||||
|
||||
# Populate all items table in the All Items tab
|
||||
self._populate_all_items_table()
|
||||
|
||||
print("✅ Auto-load completed successfully")
|
||||
else:
|
||||
print("No cached items found, waiting for manual load")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error auto-loading cached items: {e}")
|
||||
print(f"Error during auto-load: {e}")
|
||||
if self.logger:
|
||||
self.logger.log(f"Error during auto-load: {e}")
|
||||
|
||||
await asyncio.to_thread(load_cached)
|
||||
|
||||
async def _auto_load_cached_items_on_repo_change(self):
|
||||
"""Auto-load cached items when repository selection changes"""
|
||||
print("🔄 Repository changed - checking for cached items...")
|
||||
|
||||
def load_cached():
|
||||
try:
|
||||
# Get configured repos
|
||||
target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None
|
||||
forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None
|
||||
|
||||
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
print("No GitHub token configured")
|
||||
return
|
||||
|
||||
items_loaded = False
|
||||
|
||||
# Try to load target repo items from cache
|
||||
if target_repo and not target_repo.startswith('---') and '/' in target_repo:
|
||||
cached_prs = self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None
|
||||
cached_issues = self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None
|
||||
|
||||
if cached_prs is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs]
|
||||
print(f"✓ Loaded {len(cached_prs)} cached PRs for target: {target_repo}")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_prs)} cached PRs for target: {target_repo}")
|
||||
items_loaded = True
|
||||
|
||||
if cached_issues is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues]
|
||||
print(f"✓ Loaded {len(cached_issues)} cached issues for target: {target_repo}")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_issues)} cached issues for target: {target_repo}")
|
||||
items_loaded = True
|
||||
|
||||
# Try to load fork repo items from cache
|
||||
if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
|
||||
cached_fork_prs = self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None
|
||||
cached_fork_issues = self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None
|
||||
|
||||
if cached_fork_prs is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs]
|
||||
print(f"✓ Loaded {len(cached_fork_prs)} cached PRs for fork: {forked_repo}")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_fork_prs)} cached PRs for fork: {forked_repo}")
|
||||
items_loaded = True
|
||||
|
||||
if cached_fork_issues is not None:
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues]
|
||||
print(f"✓ Loaded {len(cached_fork_issues)} cached issues for fork: {forked_repo}")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_fork_issues)} cached issues for fork: {forked_repo}")
|
||||
items_loaded = True
|
||||
|
||||
if items_loaded:
|
||||
# Filter and update UI
|
||||
self.page.run_task(self._filter_workflow_items_async)
|
||||
|
||||
# Populate all items list in sidebar
|
||||
self._populate_all_items()
|
||||
|
||||
# Populate all items table in the All Items tab
|
||||
self._populate_all_items_table()
|
||||
|
||||
print("✅ Cached items loaded for selected repositories")
|
||||
if self.logger:
|
||||
self.logger.log("✅ Cached items loaded for selected repositories")
|
||||
else:
|
||||
print("No cached items found for selected repositories")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading cached items on repo change: {e}")
|
||||
if self.logger:
|
||||
self.logger.log(f"Error loading cached items on repo change: {e}")
|
||||
|
||||
await asyncio.to_thread(load_cached)
|
||||
|
||||
async def _load_custom_instructions(self):
|
||||
"""Load custom instructions from config"""
|
||||
@@ -949,8 +1475,178 @@ class MainGUI:
|
||||
|
||||
async def _search_target_repos_async(self):
|
||||
"""Search for repositories on GitHub"""
|
||||
# Implementation would search GitHub repos
|
||||
pass
|
||||
# Create search dialog
|
||||
search_input = ft.TextField(
|
||||
label="Search for repository",
|
||||
hint_text="Enter owner/repo or search term",
|
||||
expand=True,
|
||||
autofocus=True,
|
||||
)
|
||||
|
||||
results_list = ft.ListView(
|
||||
expand=True,
|
||||
spacing=5,
|
||||
padding=10,
|
||||
)
|
||||
|
||||
def perform_search(e):
|
||||
search_term = search_input.value.strip()
|
||||
if not search_term:
|
||||
return
|
||||
|
||||
# Clear previous results
|
||||
results_list.controls.clear()
|
||||
results_list.controls.append(
|
||||
ft.Text("Searching...", color=ft.colors.GREY_400, italic=True)
|
||||
)
|
||||
self.page.update()
|
||||
|
||||
# Search GitHub
|
||||
try:
|
||||
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
results_list.controls.clear()
|
||||
results_list.controls.append(
|
||||
ft.Text("GitHub token not configured", color=ft.colors.RED)
|
||||
)
|
||||
self.page.update()
|
||||
return
|
||||
|
||||
from .workflow import GitHubRepoFetcher
|
||||
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
|
||||
|
||||
# Check if it's a direct repo reference (owner/repo)
|
||||
if '/' in search_term and len(search_term.split('/')) == 2:
|
||||
# Try to get the specific repo
|
||||
repos = repo_fetcher.search_repositories(search_term, per_page=1)
|
||||
if repos:
|
||||
results_list.controls.clear()
|
||||
for repo in repos:
|
||||
repo_name = repo_fetcher.get_repo_names([repo])[0] if repo_fetcher.get_repo_names([repo]) else None
|
||||
if repo_name:
|
||||
results_list.controls.append(
|
||||
self._create_repo_result_item(repo_name, repo, search_dialog)
|
||||
)
|
||||
else:
|
||||
results_list.controls.clear()
|
||||
results_list.controls.append(
|
||||
ft.Text("Repository not found or you don't have access", color=ft.colors.ORANGE)
|
||||
)
|
||||
else:
|
||||
# Search for repos
|
||||
repos = repo_fetcher.search_repositories(search_term, per_page=10)
|
||||
results_list.controls.clear()
|
||||
|
||||
if repos:
|
||||
for repo in repos:
|
||||
repo_name = repo_fetcher.get_repo_names([repo])[0] if repo_fetcher.get_repo_names([repo]) else None
|
||||
if repo_name:
|
||||
results_list.controls.append(
|
||||
self._create_repo_result_item(repo_name, repo, search_dialog)
|
||||
)
|
||||
else:
|
||||
results_list.controls.append(
|
||||
ft.Text("No repositories found", color=ft.colors.GREY_400)
|
||||
)
|
||||
|
||||
self.page.update()
|
||||
|
||||
except Exception as ex:
|
||||
results_list.controls.clear()
|
||||
results_list.controls.append(
|
||||
ft.Text(f"Error searching: {str(ex)}", color=ft.colors.RED)
|
||||
)
|
||||
self.page.update()
|
||||
|
||||
# Create dialog
|
||||
def close_dialog(e):
|
||||
self.page.close(search_dialog)
|
||||
|
||||
search_dialog = ft.AlertDialog(
|
||||
modal=True,
|
||||
title=ft.Text("Search GitHub Repositories"),
|
||||
content=ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Row([
|
||||
search_input,
|
||||
ft.IconButton(
|
||||
icon=ft.icons.SEARCH,
|
||||
tooltip="Search",
|
||||
on_click=perform_search,
|
||||
),
|
||||
]),
|
||||
ft.Divider(),
|
||||
results_list,
|
||||
], spacing=10),
|
||||
width=600,
|
||||
height=400,
|
||||
),
|
||||
actions=[
|
||||
ft.TextButton("Cancel", on_click=close_dialog),
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
)
|
||||
|
||||
# Handle Enter key in search input
|
||||
search_input.on_submit = perform_search
|
||||
|
||||
self.page.open(search_dialog)
|
||||
|
||||
def _create_repo_result_item(self, repo_name, repo_data, dialog):
|
||||
"""Create a repository result item"""
|
||||
# Get repo description
|
||||
description = repo_data.get('description', 'No description')
|
||||
if not description:
|
||||
description = 'No description'
|
||||
|
||||
# Get visibility
|
||||
is_private = repo_data.get('private', False)
|
||||
visibility_text = "Private" if is_private else "Public"
|
||||
visibility_color = ft.colors.ORANGE if is_private else ft.colors.GREEN
|
||||
|
||||
def select_repo(e):
|
||||
# Add to dropdown options if not already there
|
||||
if self.target_repo_dropdown_ref.current:
|
||||
current_options = [opt.key for opt in self.target_repo_dropdown_ref.current.options]
|
||||
if repo_name not in current_options:
|
||||
self.target_repo_dropdown_ref.current.options.append(
|
||||
ft.dropdown.Option(repo_name)
|
||||
)
|
||||
|
||||
# Select this repo
|
||||
self.target_repo_dropdown_ref.current.value = repo_name
|
||||
|
||||
# Save to config
|
||||
config = self.config_manager.get_config()
|
||||
config['GITHUB_REPO'] = repo_name
|
||||
self.config_manager.save_configuration(config)
|
||||
|
||||
self.page.update()
|
||||
|
||||
# Close dialog
|
||||
self.page.close(dialog)
|
||||
self._show_snackbar(f"Selected repository: {repo_name}", error=False)
|
||||
|
||||
return ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Row([
|
||||
ft.Text(repo_name, weight=ft.FontWeight.BOLD, size=14),
|
||||
ft.Container(
|
||||
content=ft.Text(visibility_text, size=10, color=ft.colors.WHITE),
|
||||
bgcolor=visibility_color,
|
||||
padding=ft.padding.symmetric(horizontal=8, vertical=2),
|
||||
border_radius=4,
|
||||
),
|
||||
], spacing=10),
|
||||
ft.Text(description, size=12, color=ft.colors.GREY_400),
|
||||
], spacing=5),
|
||||
padding=10,
|
||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||
border_radius=4,
|
||||
bgcolor=ft.colors.GREY_800,
|
||||
on_click=select_repo,
|
||||
ink=True,
|
||||
)
|
||||
|
||||
async def _load_forked_repos_async(self):
|
||||
"""Load forked repositories"""
|
||||
@@ -1020,6 +1716,19 @@ class MainGUI:
|
||||
|
||||
async def _load_workflow_items_async(self):
|
||||
"""Load workflow items (PRs/Issues)"""
|
||||
# Check if items are already loaded to determine if this is a refresh
|
||||
items_already_loaded = any(len(items) > 0 for items in self.workflow_items.values())
|
||||
force_refresh = items_already_loaded
|
||||
|
||||
if force_refresh:
|
||||
print("=" * 60)
|
||||
print("🔄 Refreshing Items (forcing API fetch)...")
|
||||
print("=" * 60)
|
||||
if self.logger:
|
||||
self.logger.log("=" * 60)
|
||||
self.logger.log("🔄 Refreshing Items - forcing fresh fetch from GitHub API")
|
||||
self.logger.log("=" * 60)
|
||||
else:
|
||||
print("=" * 60)
|
||||
print("🔄 Load Items button clicked!")
|
||||
print("=" * 60)
|
||||
@@ -1068,10 +1777,39 @@ class MainGUI:
|
||||
if self.logger:
|
||||
self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}")
|
||||
|
||||
# Try to load from cache first (unless forcing refresh)
|
||||
cached_prs = None if force_refresh else (self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None)
|
||||
cached_issues = None if force_refresh else (self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None)
|
||||
|
||||
if cached_prs is not None and not force_refresh:
|
||||
# Convert cached dicts back to WorkflowItem objects
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs]
|
||||
print(f"✓ Loaded {len(cached_prs)} PRs from cache")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_prs)} PRs from cache")
|
||||
else:
|
||||
print(f"Calling workflow_manager.fetch_pull_requests('{target_repo}')...")
|
||||
self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo)
|
||||
self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo, repo_source='target')
|
||||
# Convert to dicts and save to cache
|
||||
if self.cache_manager:
|
||||
items_as_dicts = [item.to_dict() for item in self.workflow_items['target_prs']]
|
||||
self.cache_manager.save_to_cache('target_prs', target_repo, items_as_dicts)
|
||||
|
||||
if cached_issues is not None and not force_refresh:
|
||||
# Convert cached dicts back to WorkflowItem objects
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues]
|
||||
print(f"✓ Loaded {len(cached_issues)} issues from cache")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_issues)} issues from cache")
|
||||
else:
|
||||
print(f"Calling workflow_manager.fetch_issues('{target_repo}')...")
|
||||
self.workflow_items['target_issues'] = workflow_manager.fetch_issues(target_repo)
|
||||
self.workflow_items['target_issues'] = workflow_manager.fetch_issues(target_repo, repo_source='target')
|
||||
# Convert to dicts and save to cache
|
||||
if self.cache_manager:
|
||||
items_as_dicts = [item.to_dict() for item in self.workflow_items['target_issues']]
|
||||
self.cache_manager.save_to_cache('target_issues', target_repo, items_as_dicts)
|
||||
|
||||
pr_count = len(self.workflow_items.get('target_prs', []))
|
||||
issue_count = len(self.workflow_items.get('target_issues', []))
|
||||
@@ -1088,14 +1826,51 @@ class MainGUI:
|
||||
if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
|
||||
if self.logger:
|
||||
self.logger.log(f"Loading PRs and issues from forked repo: {forked_repo}")
|
||||
self.workflow_items['fork_prs'] = workflow_manager.fetch_pull_requests(forked_repo)
|
||||
self.workflow_items['fork_issues'] = workflow_manager.fetch_issues(forked_repo)
|
||||
|
||||
# Try to load from cache first (unless forcing refresh)
|
||||
cached_fork_prs = None if force_refresh else (self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None)
|
||||
cached_fork_issues = None if force_refresh else (self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None)
|
||||
|
||||
if cached_fork_prs is not None and not force_refresh:
|
||||
# Convert cached dicts back to WorkflowItem objects
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs]
|
||||
print(f"✓ Loaded {len(cached_fork_prs)} PRs from cache (fork)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_fork_prs)} PRs from cache (fork)")
|
||||
else:
|
||||
self.workflow_items['fork_prs'] = workflow_manager.fetch_pull_requests(forked_repo, repo_source='fork')
|
||||
# Convert to dicts and save to cache
|
||||
if self.cache_manager:
|
||||
items_as_dicts = [item.to_dict() for item in self.workflow_items['fork_prs']]
|
||||
self.cache_manager.save_to_cache('fork_prs', forked_repo, items_as_dicts)
|
||||
|
||||
if cached_fork_issues is not None and not force_refresh:
|
||||
# Convert cached dicts back to WorkflowItem objects
|
||||
from .workflow import WorkflowItem
|
||||
self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues]
|
||||
print(f"✓ Loaded {len(cached_fork_issues)} issues from cache (fork)")
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ Loaded {len(cached_fork_issues)} issues from cache (fork)")
|
||||
else:
|
||||
self.workflow_items['fork_issues'] = workflow_manager.fetch_issues(forked_repo, repo_source='fork')
|
||||
# Convert to dicts and save to cache
|
||||
if self.cache_manager:
|
||||
items_as_dicts = [item.to_dict() for item in self.workflow_items['fork_issues']]
|
||||
self.cache_manager.save_to_cache('fork_issues', forked_repo, items_as_dicts)
|
||||
|
||||
if self.logger:
|
||||
self.logger.log(f"Loaded {len(self.workflow_items.get('fork_prs', []))} PRs and {len(self.workflow_items.get('fork_issues', []))} issues from forked repo")
|
||||
|
||||
# Filter and update UI
|
||||
self.page.run_task(self._filter_workflow_items_async)
|
||||
|
||||
# Populate all items list in sidebar
|
||||
self._populate_all_items()
|
||||
|
||||
# Populate all items table in the All Items tab
|
||||
self._populate_all_items_table()
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.log(f"Error loading workflow items: {e}")
|
||||
@@ -1136,18 +1911,6 @@ class MainGUI:
|
||||
self.page.update()
|
||||
self._update_navigation_buttons()
|
||||
|
||||
def _update_navigation_buttons(self):
|
||||
"""Update navigation button states"""
|
||||
if self.prev_button_ref.current:
|
||||
self.prev_button_ref.current.disabled = (self.current_item_index == 0)
|
||||
|
||||
if self.next_button_ref.current:
|
||||
self.next_button_ref.current.disabled = (
|
||||
self.current_item_index >= len(self.current_work_items) - 1
|
||||
)
|
||||
|
||||
self.page.update()
|
||||
|
||||
def update_status(self, message: str):
|
||||
"""Update status message"""
|
||||
if self.status_text_ref.current:
|
||||
|
||||
@@ -7,10 +7,7 @@ import flet as ft
|
||||
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
|
||||
ft.icons = ft.Icons
|
||||
ft.colors = ft.Colors
|
||||
import threading
|
||||
import subprocess
|
||||
from typing import Dict, Any, Optional
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
@@ -27,13 +24,7 @@ class SettingsDialog:
|
||||
self.entries = {}
|
||||
self.dialog_ref = ft.Ref[ft.AlertDialog]()
|
||||
|
||||
# Repository data
|
||||
self.target_repos = []
|
||||
self.forked_repos = []
|
||||
|
||||
# Dropdown refs
|
||||
self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||
self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||
self.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||
|
||||
@@ -71,8 +62,6 @@ class SettingsDialog:
|
||||
"""Initialize async operations"""
|
||||
await asyncio.sleep(0.1)
|
||||
await self._scan_repos_async()
|
||||
await self._load_target_repos_async()
|
||||
await self._load_user_forks_async()
|
||||
|
||||
def _create_dialog(self) -> ft.AlertDialog:
|
||||
"""Create the settings dialog"""
|
||||
@@ -136,7 +125,7 @@ class SettingsDialog:
|
||||
controls = []
|
||||
|
||||
# GitHub Configuration Section
|
||||
controls.append(self._create_section_header("🐙 GitHub Configuration"))
|
||||
controls.append(self._create_section_header("🐙 GitHub Personal Access Token"))
|
||||
|
||||
# GitHub PAT
|
||||
github_pat = ft.TextField(
|
||||
@@ -150,71 +139,6 @@ class SettingsDialog:
|
||||
self.entries['GITHUB_PAT'] = github_pat
|
||||
controls.append(github_pat)
|
||||
|
||||
# Target Repository
|
||||
controls.append(ft.Text("Target Repository", weight=ft.FontWeight.BOLD, size=14))
|
||||
target_repo_row = ft.Row(
|
||||
[
|
||||
ft.Dropdown(
|
||||
ref=self.target_repo_dropdown_ref,
|
||||
label="Target Repository",
|
||||
value=self.config.get('GITHUB_REPO', ''),
|
||||
options=[],
|
||||
hint_text="Select or type repository",
|
||||
expand=True,
|
||||
on_change=lambda e: self._on_target_repo_search(e),
|
||||
),
|
||||
ft.IconButton(
|
||||
icon=ft.icons.REFRESH,
|
||||
tooltip="Refresh",
|
||||
on_click=lambda e: self.page.run_task(self._refresh_target_repos_async),
|
||||
),
|
||||
ft.IconButton(
|
||||
icon=ft.icons.SEARCH,
|
||||
tooltip="Search",
|
||||
on_click=lambda e: self.page.run_task(self._search_target_repos_async),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
)
|
||||
controls.append(target_repo_row)
|
||||
controls.append(ft.Text(
|
||||
"ℹ️ Upstream repo where PRs will be created. Type to search all GitHub repos.",
|
||||
size=12,
|
||||
color="grey400",
|
||||
))
|
||||
|
||||
# Forked Repository
|
||||
controls.append(ft.Text("Forked Repository", weight=ft.FontWeight.BOLD, size=14))
|
||||
forked_repo_row = ft.Row(
|
||||
[
|
||||
ft.Dropdown(
|
||||
ref=self.forked_repo_dropdown_ref,
|
||||
label="Forked Repository",
|
||||
value=self.config.get('FORKED_REPO', ''),
|
||||
options=[],
|
||||
hint_text="Select your fork",
|
||||
expand=True,
|
||||
),
|
||||
ft.IconButton(
|
||||
icon=ft.icons.REFRESH,
|
||||
tooltip="Refresh",
|
||||
on_click=lambda e: self.page.run_task(self._refresh_forked_repos_async),
|
||||
),
|
||||
ft.IconButton(
|
||||
icon=ft.icons.DOWNLOAD,
|
||||
tooltip="Clone",
|
||||
on_click=self._clone_forked_repo,
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
)
|
||||
controls.append(forked_repo_row)
|
||||
controls.append(ft.Text(
|
||||
"ℹ️ Your fork where changes will be made. Leave empty to auto-detect from document URL.",
|
||||
size=12,
|
||||
color="grey400",
|
||||
))
|
||||
|
||||
# General Options Section
|
||||
controls.append(self._create_section_header("⚙️ General Options"))
|
||||
|
||||
@@ -268,8 +192,7 @@ class SettingsDialog:
|
||||
"💡 Repository Setup Guide:\n"
|
||||
" • Local Repo Path: Where your fork repos are cloned (e.g., C:\\git\\repos)\n"
|
||||
" • Detected Repos: Shows your local fork (e.g., yourname/repo)\n"
|
||||
" • Target Repository: Upstream repo for PRs (e.g., microsoft/repo)\n"
|
||||
" • Fork Workflow: Work on your fork locally, create PRs to upstream",
|
||||
" • Note: Target and Fork repositories are configured in the main GUI",
|
||||
size=12,
|
||||
color="grey400",
|
||||
),
|
||||
@@ -283,9 +206,7 @@ class SettingsDialog:
|
||||
content=ft.Text(
|
||||
"💡 Getting Started:\n"
|
||||
"1. Create a GitHub Personal Access Token\n"
|
||||
"2. Configure GitHub repositories:\n"
|
||||
" • Target Repository: Where PRs will be created\n"
|
||||
" • Forked Repository: Your fork where changes are made\n"
|
||||
"2. Configure GitHub repositories in the main GUI\n"
|
||||
"3. Set Local Repo Path for automatic repository detection\n"
|
||||
"4. Configure AI provider in the AI tab (optional)\n"
|
||||
"5. Test your connection before processing items",
|
||||
@@ -517,214 +438,6 @@ class SettingsDialog:
|
||||
except Exception as e:
|
||||
print(f"Error in _scan_repos_async: {e}")
|
||||
|
||||
async def _load_target_repos_async(self):
|
||||
"""Load target repos (with push/admin access) asynchronously"""
|
||||
def load_repos():
|
||||
try:
|
||||
github_token = self.config.get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
return
|
||||
|
||||
from .workflow import GitHubRepoFetcher
|
||||
repo_fetcher = GitHubRepoFetcher(github_token)
|
||||
repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push')
|
||||
self.target_repos = repo_fetcher.get_repo_names(repos)
|
||||
|
||||
# Update UI on main thread
|
||||
if self.target_repo_dropdown_ref.current:
|
||||
self.page.run_task(self._update_target_dropdown_async)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading target repos: {e}")
|
||||
|
||||
await asyncio.to_thread(load_repos)
|
||||
|
||||
async def _update_target_dropdown_async(self):
|
||||
"""Update the target repository dropdown"""
|
||||
try:
|
||||
if not self.target_repo_dropdown_ref.current:
|
||||
return
|
||||
|
||||
options = []
|
||||
if self.target_repos:
|
||||
options.append(ft.dropdown.Option("--- Your Repos (with edit access) ---", disabled=True))
|
||||
options.extend([ft.dropdown.Option(repo) for repo in self.target_repos])
|
||||
|
||||
self.target_repo_dropdown_ref.current.options = options
|
||||
self.page.update()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating target dropdown: {e}")
|
||||
|
||||
async def _refresh_target_repos_async(self):
|
||||
"""Refresh target repositories"""
|
||||
await self._load_target_repos_async()
|
||||
|
||||
async def _search_target_repos_async(self):
|
||||
"""Search for repositories on GitHub"""
|
||||
if not self.target_repo_dropdown_ref.current:
|
||||
return
|
||||
|
||||
query = self.target_repo_dropdown_ref.current.value.strip()
|
||||
if not query:
|
||||
return
|
||||
|
||||
def search_repos():
|
||||
try:
|
||||
github_token = self.config.get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
return
|
||||
|
||||
from .workflow import GitHubRepoFetcher
|
||||
repo_fetcher = GitHubRepoFetcher(github_token)
|
||||
repos = repo_fetcher.search_repositories(query, per_page=50)
|
||||
search_results = repo_fetcher.get_repo_names(repos)
|
||||
|
||||
# Update UI
|
||||
if self.target_repo_dropdown_ref.current:
|
||||
options = []
|
||||
if self.target_repos:
|
||||
options.append(ft.dropdown.Option("--- Your Repos (with edit access) ---", disabled=True))
|
||||
options.extend([ft.dropdown.Option(repo) for repo in self.target_repos])
|
||||
|
||||
if search_results:
|
||||
options.append(ft.dropdown.Option(f"--- Search Results for \"{query}\" ---", disabled=True))
|
||||
options.extend([ft.dropdown.Option(repo) for repo in search_results])
|
||||
|
||||
self.target_repo_dropdown_ref.current.options = options
|
||||
self.page.update()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error searching repos: {e}")
|
||||
|
||||
await asyncio.to_thread(search_repos)
|
||||
|
||||
def _on_target_repo_search(self, e):
|
||||
"""Handle typing in target repo field for auto-search"""
|
||||
# Debounce search - could be implemented with a timer
|
||||
pass
|
||||
|
||||
async def _load_user_forks_async(self):
|
||||
"""Load user's GitHub forks asynchronously"""
|
||||
def load_forks():
|
||||
try:
|
||||
github_token = self.config.get('GITHUB_PAT', '')
|
||||
if not github_token:
|
||||
return
|
||||
|
||||
from .workflow import GitHubRepoFetcher
|
||||
repo_fetcher = GitHubRepoFetcher(github_token)
|
||||
repos = repo_fetcher.fetch_user_repos(repo_type='owner')
|
||||
self.forked_repos = repo_fetcher.get_repo_names(repos)
|
||||
|
||||
# Update UI
|
||||
if self.forked_repo_dropdown_ref.current:
|
||||
self.page.run_task(self._update_forked_dropdown_async)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading user forks: {e}")
|
||||
|
||||
await asyncio.to_thread(load_forks)
|
||||
|
||||
async def _update_forked_dropdown_async(self):
|
||||
"""Update the forked repository dropdown with GitHub forks"""
|
||||
try:
|
||||
if not self.forked_repo_dropdown_ref.current:
|
||||
return
|
||||
|
||||
options = []
|
||||
|
||||
# Add local repos
|
||||
local_repo_path = self.config.get('LOCAL_REPO_PATH', '')
|
||||
if local_repo_path:
|
||||
try:
|
||||
from .utils import LocalRepositoryScanner
|
||||
local_repos = LocalRepositoryScanner.scan_local_repos(local_repo_path)
|
||||
if local_repos:
|
||||
options.append(ft.dropdown.Option("--- Local Repositories ---", disabled=True))
|
||||
options.extend([ft.dropdown.Option(repo) for repo in local_repos])
|
||||
except Exception as e:
|
||||
print(f"Error scanning local repos: {e}")
|
||||
|
||||
# Add GitHub repos
|
||||
if self.forked_repos:
|
||||
options.append(ft.dropdown.Option("--- Your GitHub Repos ---", disabled=True))
|
||||
options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos])
|
||||
|
||||
self.forked_repo_dropdown_ref.current.options = options
|
||||
self.page.update()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating forked dropdown: {e}")
|
||||
|
||||
async def _refresh_forked_repos_async(self):
|
||||
"""Refresh the forked repositories dropdown"""
|
||||
await self._load_user_forks_async()
|
||||
await self._update_forked_dropdown_async()
|
||||
|
||||
def _clone_forked_repo(self, e):
|
||||
"""Clone the selected forked repository to the local repo path"""
|
||||
if not self.forked_repo_dropdown_ref.current:
|
||||
return
|
||||
|
||||
selected_repo = self.forked_repo_dropdown_ref.current.value.strip()
|
||||
|
||||
if not selected_repo or selected_repo.startswith('---'):
|
||||
self._show_alert("Invalid Selection", "Please select a repository, not a section header.")
|
||||
return
|
||||
|
||||
local_repo_path = self.config.get('LOCAL_REPO_PATH', '').strip()
|
||||
if not local_repo_path:
|
||||
self._show_alert("Local Path Not Configured", "Please configure the Local Repository Path in settings first.")
|
||||
return
|
||||
|
||||
# Start clone in background
|
||||
self.page.run_task(lambda: self._clone_repo_async(selected_repo, local_repo_path))
|
||||
|
||||
async def _clone_repo_async(self, repo_name: str, local_repo_path: str):
|
||||
"""Clone repository asynchronously"""
|
||||
try:
|
||||
os.makedirs(local_repo_path, exist_ok=True)
|
||||
|
||||
if '/' not in repo_name:
|
||||
self._show_alert("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):
|
||||
# Show confirmation dialog
|
||||
self._show_alert(
|
||||
"Directory Exists",
|
||||
f"The directory '{folder_name}' already exists. Clone may fail if it's already a git repository."
|
||||
)
|
||||
return
|
||||
|
||||
clone_url = f"https://github.com/{repo_name}.git"
|
||||
|
||||
# Show progress
|
||||
self._show_alert("Cloning Repository", f"Cloning {repo_name}...\nThis may take a few moments.")
|
||||
|
||||
# Run git clone
|
||||
result = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
['git', 'clone', clone_url, target_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self._show_alert("Clone Successful", f"Successfully cloned {repo_name}!\n\nLocation: {folder_name}/")
|
||||
await self._refresh_forked_repos_async()
|
||||
else:
|
||||
error_msg = result.stderr if result.stderr else result.stdout
|
||||
self._show_alert("Clone Failed", f"Failed to clone {repo_name}.\n\nError:\n{error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
self._show_alert("Clone Error", f"An error occurred while cloning:\n{str(e)}")
|
||||
|
||||
async def _scan_ollama_models_async(self):
|
||||
"""Scan Ollama server for available models"""
|
||||
ollama_url = self.entries.get('OLLAMA_URL').value.strip() if 'OLLAMA_URL' in self.entries else ''
|
||||
@@ -767,7 +480,6 @@ class SettingsDialog:
|
||||
if len(model_names) > 10:
|
||||
models_text += f"\n\n...and {len(model_names) - 10} more"
|
||||
|
||||
self._show_alert("Models Found", f"Found {len(model_names)} model(s):\n\n{models_text}")
|
||||
else:
|
||||
self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull <model>' to download models.")
|
||||
|
||||
@@ -857,10 +569,6 @@ class SettingsDialog:
|
||||
config_values[key] = value
|
||||
|
||||
# Handle dropdown values specially
|
||||
if self.target_repo_dropdown_ref.current:
|
||||
config_values['GITHUB_REPO'] = self.target_repo_dropdown_ref.current.value or ''
|
||||
if self.forked_repo_dropdown_ref.current:
|
||||
config_values['FORKED_REPO'] = self.forked_repo_dropdown_ref.current.value or ''
|
||||
if self.ollama_model_dropdown_ref.current:
|
||||
config_values['OLLAMA_MODEL'] = self.ollama_model_dropdown_ref.current.value or ''
|
||||
|
||||
|
||||
@@ -646,11 +646,6 @@ CUSTOM_INSTRUCTIONS=
|
||||
print(f"Error creating default .env file: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Removed EnhancedContentBuilders class - was specific to Azure DevOps
|
||||
# Use ContentBuilders class for generic GitHub automation instead
|
||||
|
||||
|
||||
# Compatibility functions for direct function access
|
||||
def get_next_pr_number(provider_key: str) -> int:
|
||||
"""Compatibility function for direct access to PR number generation"""
|
||||
|
||||
@@ -70,6 +70,7 @@ class WorkflowItem:
|
||||
return {
|
||||
'item_type': self.item_type,
|
||||
'repo_source': self.repo_source,
|
||||
'data': self.data, # Include raw data for full reconstruction
|
||||
'number': self.number,
|
||||
'title': self.title,
|
||||
'state': self.state,
|
||||
@@ -77,6 +78,7 @@ class WorkflowItem:
|
||||
'updated_at': self.updated_at,
|
||||
'body': self.body,
|
||||
'url': self.url,
|
||||
'api_url': self.api_url,
|
||||
'author': self.author,
|
||||
'author_url': self.author_url,
|
||||
'labels': self.labels,
|
||||
@@ -89,6 +91,16 @@ class WorkflowItem:
|
||||
'comments_count': self.comments_count
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'WorkflowItem':
|
||||
"""Create WorkflowItem from dictionary (for cache deserialization)"""
|
||||
# Extract the raw GitHub API data if available, otherwise use the dict itself
|
||||
raw_data = data.get('data', data)
|
||||
item_type = data.get('item_type', 'issue')
|
||||
repo_source = data.get('repo_source', 'target')
|
||||
|
||||
return cls(item_type, raw_data, repo_source)
|
||||
|
||||
|
||||
class GitHubRepoFetcher:
|
||||
"""Fetches repository information from GitHub"""
|
||||
@@ -516,3 +528,69 @@ class WorkflowManager:
|
||||
if any(label in item.labels for label in label_filter)]
|
||||
|
||||
return filtered
|
||||
|
||||
def fetch_comments(self, repo_str: str, issue_number: int, is_pull_request: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch comments for an issue or pull request
|
||||
|
||||
Args:
|
||||
repo_str: Repository string in format "owner/repo"
|
||||
issue_number: Issue or PR number
|
||||
is_pull_request: Whether this is a pull request (for PR-specific comments)
|
||||
|
||||
Returns:
|
||||
List of comment dictionaries with keys: 'user', 'body', 'created_at', 'updated_at'
|
||||
"""
|
||||
try:
|
||||
# Parse repository string
|
||||
if '/' not in repo_str:
|
||||
self.log(f"Invalid repository format: {repo_str}")
|
||||
return []
|
||||
|
||||
owner, repo = repo_str.split('/', 1)
|
||||
|
||||
# Fetch issue/PR comments (these are the same endpoint for both issues and PRs)
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||
print(f"DEBUG: Fetching comments from URL: {url}", flush=True)
|
||||
|
||||
response = requests.get(url, headers=self.headers)
|
||||
print(f"DEBUG: Response status code: {response.status_code}", flush=True)
|
||||
print(f"DEBUG: Response headers: {dict(response.headers)}", flush=True)
|
||||
print(f"DEBUG: Response text length: {len(response.text)}", flush=True)
|
||||
print(f"DEBUG: Response content (first 500): {response.text[:500]}", flush=True)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
response_data = response.json()
|
||||
print(f"DEBUG: Response data type: {type(response_data)}", flush=True)
|
||||
print(f"DEBUG: Number of items: {len(response_data) if isinstance(response_data, list) else 'Not a list'}", flush=True)
|
||||
|
||||
if isinstance(response_data, list) and len(response_data) > 0:
|
||||
print(f"DEBUG: First item keys: {list(response_data[0].keys())}", flush=True)
|
||||
|
||||
comments = []
|
||||
for comment_data in response_data:
|
||||
comments.append({
|
||||
'user': comment_data.get('user', {}).get('login', 'unknown'),
|
||||
'body': comment_data.get('body', ''),
|
||||
'created_at': comment_data.get('created_at', ''),
|
||||
'updated_at': comment_data.get('updated_at', ''),
|
||||
'url': comment_data.get('html_url', '')
|
||||
})
|
||||
|
||||
self.log(f"Fetched {len(comments)} comments for {repo_str} #{issue_number}")
|
||||
print(f"DEBUG: Successfully parsed {len(comments)} comments", flush=True)
|
||||
return comments
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.log(f"Error fetching comments for {repo_str} #{issue_number}: {e}")
|
||||
print(f"DEBUG: RequestException occurred: {e}", flush=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
except Exception as e:
|
||||
self.log(f"Unexpected error fetching comments: {e}")
|
||||
print(f"DEBUG: Exception occurred: {e}", flush=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user