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):
|
class GitHubCopilotProvider(AIProvider):
|
||||||
"""GitHub Copilot provider using GitHub Models API"""
|
"""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]:
|
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"""
|
"""Use diff-based approach for surgical edits"""
|
||||||
|
|
||||||
@@ -1505,7 +1508,7 @@ class GitHubCopilotProvider(AIProvider):
|
|||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
url = self.GITHUB_MODELS_API_URL
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1869,7 +1872,7 @@ Return the complete updated file content now (NO explanatory text):"""
|
|||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
url = self.GITHUB_MODELS_API_URL
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1941,7 +1944,7 @@ Generate the new content now:"""
|
|||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
url = self.GITHUB_MODELS_API_URL
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -2018,7 +2021,7 @@ Find the exact text to correct:"""
|
|||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
url = "https://models.inference.ai.azure.com/chat/completions"
|
url = self.GITHUB_MODELS_API_URL
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"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
|
Stores fetched items in temporary cache to avoid reloading on every app start
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ from hashlib import md5
|
|||||||
|
|
||||||
|
|
||||||
class CacheManager:
|
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):
|
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)
|
cache_duration_hours: How long cache is valid (default 24 hours)
|
||||||
"""
|
"""
|
||||||
self.cache_duration_seconds = cache_duration_hours * 3600
|
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)
|
self.cache_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
def _get_cache_key(self, source_type: str, identifier: str) -> str:
|
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]]]:
|
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:
|
Args:
|
||||||
source_type: 'azure_devops' or 'uuf'
|
source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
|
||||||
identifier: query URL hash or config hash
|
identifier: repository identifier or config hash
|
||||||
|
|
||||||
Returns:
|
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):
|
if not self.is_cache_valid(source_type, identifier):
|
||||||
return None
|
return None
|
||||||
@@ -81,12 +81,12 @@ class CacheManager:
|
|||||||
|
|
||||||
def save_to_cache(self, source_type: str, identifier: str, items: List[Dict[str, Any]]) -> bool:
|
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:
|
Args:
|
||||||
source_type: 'azure_devops' or 'uuf'
|
source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
|
||||||
identifier: query URL hash or config hash
|
identifier: repository identifier or config hash
|
||||||
items: List of work items to cache
|
items: List of items to cache (PRs or Issues)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if successful, False otherwise
|
True if successful, False otherwise
|
||||||
|
|||||||
@@ -229,9 +229,7 @@ class GitHubGQL:
|
|||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
# Return sample data for dry run
|
# Return sample data for dry run
|
||||||
return [
|
return [
|
||||||
"username/fabric-docs",
|
"username/repo_name",
|
||||||
"username/azure-docs",
|
|
||||||
"username/powerbi-docs"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -324,8 +322,8 @@ class GitHubGQL:
|
|||||||
|
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
return {
|
return {
|
||||||
"target_alternatives": ["microsoftdocs/fabric-docs-pr"],
|
"target_alternatives": ["username/target_repo_name"],
|
||||||
"fork_alternatives": ["b-tsammons/azure-docs-pr"]
|
"fork_alternatives": ["username/fork_repo_name"]
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ class MainGUI:
|
|||||||
self.diff_text_ref = ft.Ref[ft.TextField]()
|
self.diff_text_ref = ft.Ref[ft.TextField]()
|
||||||
self.log_text_ref = ft.Ref[ft.TextField]()
|
self.log_text_ref = ft.Ref[ft.TextField]()
|
||||||
self.edit_button_ref = ft.Ref[ft.IconButton]()
|
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]()
|
self.go_button_ref = ft.Ref[ft.ElevatedButton]()
|
||||||
|
|
||||||
# Mode and filter refs
|
# Mode and filter refs
|
||||||
@@ -84,6 +82,10 @@ class MainGUI:
|
|||||||
# DataTable ref for all items
|
# DataTable ref for all items
|
||||||
self.items_table_ref = ft.Ref[ft.DataTable]()
|
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
|
# Sidebar state
|
||||||
self.sidebar_visible = True
|
self.sidebar_visible = True
|
||||||
self.sidebar_ref = ft.Ref[ft.Container]()
|
self.sidebar_ref = ft.Ref[ft.Container]()
|
||||||
@@ -196,9 +198,10 @@ class MainGUI:
|
|||||||
async def _async_init(self):
|
async def _async_init(self):
|
||||||
"""Async initialization"""
|
"""Async initialization"""
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
await self._auto_load_cached_items()
|
|
||||||
await self._load_custom_instructions()
|
await self._load_custom_instructions()
|
||||||
await self._init_load_repos()
|
await self._init_load_repos()
|
||||||
|
# Auto-load cached items after repos are loaded
|
||||||
|
await self._auto_load_cached_items()
|
||||||
|
|
||||||
def _toggle_sidebar(self, e):
|
def _toggle_sidebar(self, e):
|
||||||
"""Toggle sidebar visibility"""
|
"""Toggle sidebar visibility"""
|
||||||
@@ -335,6 +338,23 @@ class MainGUI:
|
|||||||
expand=True,
|
expand=True,
|
||||||
on_change=self._on_workflow_item_selected,
|
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,
|
spacing=10,
|
||||||
)
|
)
|
||||||
@@ -431,20 +451,6 @@ class MainGUI:
|
|||||||
# Navigation buttons
|
# Navigation buttons
|
||||||
nav_buttons = ft.Row(
|
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.Container(expand=True),
|
||||||
ft.ElevatedButton(
|
ft.ElevatedButton(
|
||||||
"Go",
|
"Go",
|
||||||
@@ -623,14 +629,17 @@ class MainGUI:
|
|||||||
items_table = ft.DataTable(
|
items_table = ft.DataTable(
|
||||||
ref=self.items_table_ref,
|
ref=self.items_table_ref,
|
||||||
columns=[
|
columns=[
|
||||||
|
ft.DataColumn(ft.Text("Repo")),
|
||||||
|
ft.DataColumn(ft.Text("Type")),
|
||||||
ft.DataColumn(ft.Text("ID")),
|
ft.DataColumn(ft.Text("ID")),
|
||||||
ft.DataColumn(ft.Text("Title")),
|
ft.DataColumn(ft.Text("Title")),
|
||||||
ft.DataColumn(ft.Text("Nature")),
|
ft.DataColumn(ft.Text("Author")),
|
||||||
ft.DataColumn(ft.Text("GitHub Repo")),
|
|
||||||
ft.DataColumn(ft.Text("ms.author")),
|
|
||||||
ft.DataColumn(ft.Text("Status")),
|
ft.DataColumn(ft.Text("Status")),
|
||||||
],
|
],
|
||||||
rows=[],
|
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(
|
set_current_button = ft.ElevatedButton(
|
||||||
@@ -707,6 +716,9 @@ class MainGUI:
|
|||||||
self.workflow_item_dropdown_ref.current.options = []
|
self.workflow_item_dropdown_ref.current.options = []
|
||||||
self.page.update()
|
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):
|
def _on_workflow_item_selected(self, e):
|
||||||
"""Handle workflow item selection"""
|
"""Handle workflow item selection"""
|
||||||
if not self.workflow_item_dropdown_ref.current:
|
if not self.workflow_item_dropdown_ref.current:
|
||||||
@@ -789,6 +801,369 @@ class MainGUI:
|
|||||||
# Implementation would populate fields with workflow item data
|
# Implementation would populate fields with workflow item data
|
||||||
pass
|
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):
|
def _previous_item(self, e):
|
||||||
"""Navigate to previous item"""
|
"""Navigate to previous item"""
|
||||||
if self.current_item_index > 0:
|
if self.current_item_index > 0:
|
||||||
@@ -796,13 +1171,6 @@ class MainGUI:
|
|||||||
self._display_current_item()
|
self._display_current_item()
|
||||||
self._update_navigation_buttons()
|
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):
|
def _toggle_edit_mode(self, e):
|
||||||
"""Toggle edit mode for proposed new text"""
|
"""Toggle edit mode for proposed new text"""
|
||||||
if not self.proposed_new_text_ref.current or not self.edit_button_ref.current:
|
if not self.proposed_new_text_ref.current or not self.edit_button_ref.current:
|
||||||
@@ -875,14 +1243,172 @@ class MainGUI:
|
|||||||
# ===== Async Operations =====
|
# ===== Async Operations =====
|
||||||
|
|
||||||
async def _auto_load_cached_items(self):
|
async def _auto_load_cached_items(self):
|
||||||
"""Auto-load cached items on startup"""
|
"""Auto-load cached items on startup if available"""
|
||||||
try:
|
print("=" * 60)
|
||||||
# Try to load from cache
|
print("🔄 Auto-loading cached items on startup...")
|
||||||
if self.cache_manager:
|
print("=" * 60)
|
||||||
# Implementation would load cached items
|
|
||||||
pass
|
def load_cached():
|
||||||
except Exception as e:
|
try:
|
||||||
print(f"Error auto-loading cached items: {e}")
|
# 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 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):
|
async def _load_custom_instructions(self):
|
||||||
"""Load custom instructions from config"""
|
"""Load custom instructions from config"""
|
||||||
@@ -949,8 +1475,178 @@ class MainGUI:
|
|||||||
|
|
||||||
async def _search_target_repos_async(self):
|
async def _search_target_repos_async(self):
|
||||||
"""Search for repositories on GitHub"""
|
"""Search for repositories on GitHub"""
|
||||||
# Implementation would search GitHub repos
|
# Create search dialog
|
||||||
pass
|
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):
|
async def _load_forked_repos_async(self):
|
||||||
"""Load forked repositories"""
|
"""Load forked repositories"""
|
||||||
@@ -1020,13 +1716,26 @@ class MainGUI:
|
|||||||
|
|
||||||
async def _load_workflow_items_async(self):
|
async def _load_workflow_items_async(self):
|
||||||
"""Load workflow items (PRs/Issues)"""
|
"""Load workflow items (PRs/Issues)"""
|
||||||
print("=" * 60)
|
# Check if items are already loaded to determine if this is a refresh
|
||||||
print("🔄 Load Items button clicked!")
|
items_already_loaded = any(len(items) > 0 for items in self.workflow_items.values())
|
||||||
print("=" * 60)
|
force_refresh = items_already_loaded
|
||||||
if self.logger:
|
|
||||||
self.logger.log("=" * 60)
|
if force_refresh:
|
||||||
self.logger.log("🔄 Load Items button clicked - starting workflow item load")
|
print("=" * 60)
|
||||||
self.logger.log("=" * 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)
|
||||||
|
if self.logger:
|
||||||
|
self.logger.log("=" * 60)
|
||||||
|
self.logger.log("🔄 Load Items button clicked - starting workflow item load")
|
||||||
|
self.logger.log("=" * 60)
|
||||||
|
|
||||||
def load_items():
|
def load_items():
|
||||||
try:
|
try:
|
||||||
@@ -1068,10 +1777,39 @@ class MainGUI:
|
|||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}")
|
self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}")
|
||||||
|
|
||||||
print(f"Calling workflow_manager.fetch_pull_requests('{target_repo}')...")
|
# Try to load from cache first (unless forcing refresh)
|
||||||
self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo)
|
cached_prs = None if force_refresh else (self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None)
|
||||||
print(f"Calling workflow_manager.fetch_issues('{target_repo}')...")
|
cached_issues = None if force_refresh else (self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None)
|
||||||
self.workflow_items['target_issues'] = workflow_manager.fetch_issues(target_repo)
|
|
||||||
|
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, 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, 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', []))
|
pr_count = len(self.workflow_items.get('target_prs', []))
|
||||||
issue_count = len(self.workflow_items.get('target_issues', []))
|
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 forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log(f"Loading PRs and issues from forked repo: {forked_repo}")
|
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:
|
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")
|
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
|
# Filter and update UI
|
||||||
self.page.run_task(self._filter_workflow_items_async)
|
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:
|
except Exception as e:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.log(f"Error loading workflow items: {e}")
|
self.logger.log(f"Error loading workflow items: {e}")
|
||||||
@@ -1136,18 +1911,6 @@ class MainGUI:
|
|||||||
self.page.update()
|
self.page.update()
|
||||||
self._update_navigation_buttons()
|
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):
|
def update_status(self, message: str):
|
||||||
"""Update status message"""
|
"""Update status message"""
|
||||||
if self.status_text_ref.current:
|
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)
|
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
|
||||||
ft.icons = ft.Icons
|
ft.icons = ft.Icons
|
||||||
ft.colors = ft.Colors
|
ft.colors = ft.Colors
|
||||||
import threading
|
|
||||||
import subprocess
|
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
@@ -27,13 +24,7 @@ class SettingsDialog:
|
|||||||
self.entries = {}
|
self.entries = {}
|
||||||
self.dialog_ref = ft.Ref[ft.AlertDialog]()
|
self.dialog_ref = ft.Ref[ft.AlertDialog]()
|
||||||
|
|
||||||
# Repository data
|
|
||||||
self.target_repos = []
|
|
||||||
self.forked_repos = []
|
|
||||||
|
|
||||||
# Dropdown refs
|
# 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.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
|
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
|
||||||
|
|
||||||
@@ -71,8 +62,6 @@ class SettingsDialog:
|
|||||||
"""Initialize async operations"""
|
"""Initialize async operations"""
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
await self._scan_repos_async()
|
await self._scan_repos_async()
|
||||||
await self._load_target_repos_async()
|
|
||||||
await self._load_user_forks_async()
|
|
||||||
|
|
||||||
def _create_dialog(self) -> ft.AlertDialog:
|
def _create_dialog(self) -> ft.AlertDialog:
|
||||||
"""Create the settings dialog"""
|
"""Create the settings dialog"""
|
||||||
@@ -136,7 +125,7 @@ class SettingsDialog:
|
|||||||
controls = []
|
controls = []
|
||||||
|
|
||||||
# GitHub Configuration Section
|
# 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
|
||||||
github_pat = ft.TextField(
|
github_pat = ft.TextField(
|
||||||
@@ -150,71 +139,6 @@ class SettingsDialog:
|
|||||||
self.entries['GITHUB_PAT'] = github_pat
|
self.entries['GITHUB_PAT'] = github_pat
|
||||||
controls.append(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
|
# General Options Section
|
||||||
controls.append(self._create_section_header("⚙️ General Options"))
|
controls.append(self._create_section_header("⚙️ General Options"))
|
||||||
|
|
||||||
@@ -268,8 +192,7 @@ class SettingsDialog:
|
|||||||
"💡 Repository Setup Guide:\n"
|
"💡 Repository Setup Guide:\n"
|
||||||
" • Local Repo Path: Where your fork repos are cloned (e.g., C:\\git\\repos)\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"
|
" • Detected Repos: Shows your local fork (e.g., yourname/repo)\n"
|
||||||
" • Target Repository: Upstream repo for PRs (e.g., microsoft/repo)\n"
|
" • Note: Target and Fork repositories are configured in the main GUI",
|
||||||
" • Fork Workflow: Work on your fork locally, create PRs to upstream",
|
|
||||||
size=12,
|
size=12,
|
||||||
color="grey400",
|
color="grey400",
|
||||||
),
|
),
|
||||||
@@ -283,9 +206,7 @@ class SettingsDialog:
|
|||||||
content=ft.Text(
|
content=ft.Text(
|
||||||
"💡 Getting Started:\n"
|
"💡 Getting Started:\n"
|
||||||
"1. Create a GitHub Personal Access Token\n"
|
"1. Create a GitHub Personal Access Token\n"
|
||||||
"2. Configure GitHub repositories:\n"
|
"2. Configure GitHub repositories in the main GUI\n"
|
||||||
" • Target Repository: Where PRs will be created\n"
|
|
||||||
" • Forked Repository: Your fork where changes are made\n"
|
|
||||||
"3. Set Local Repo Path for automatic repository detection\n"
|
"3. Set Local Repo Path for automatic repository detection\n"
|
||||||
"4. Configure AI provider in the AI tab (optional)\n"
|
"4. Configure AI provider in the AI tab (optional)\n"
|
||||||
"5. Test your connection before processing items",
|
"5. Test your connection before processing items",
|
||||||
@@ -517,214 +438,6 @@ class SettingsDialog:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in _scan_repos_async: {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):
|
async def _scan_ollama_models_async(self):
|
||||||
"""Scan Ollama server for available models"""
|
"""Scan Ollama server for available models"""
|
||||||
ollama_url = self.entries.get('OLLAMA_URL').value.strip() if 'OLLAMA_URL' in self.entries else ''
|
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:
|
if len(model_names) > 10:
|
||||||
models_text += f"\n\n...and {len(model_names) - 10} more"
|
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:
|
else:
|
||||||
self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull <model>' to download models.")
|
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
|
config_values[key] = value
|
||||||
|
|
||||||
# Handle dropdown values specially
|
# 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:
|
if self.ollama_model_dropdown_ref.current:
|
||||||
config_values['OLLAMA_MODEL'] = self.ollama_model_dropdown_ref.current.value or ''
|
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}")
|
print(f"Error creating default .env file: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Removed EnhancedContentBuilders class - was specific to Azure DevOps
|
|
||||||
# Use ContentBuilders class for generic GitHub automation instead
|
|
||||||
|
|
||||||
|
|
||||||
# Compatibility functions for direct function access
|
# Compatibility functions for direct function access
|
||||||
def get_next_pr_number(provider_key: str) -> int:
|
def get_next_pr_number(provider_key: str) -> int:
|
||||||
"""Compatibility function for direct access to PR number generation"""
|
"""Compatibility function for direct access to PR number generation"""
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class WorkflowItem:
|
|||||||
return {
|
return {
|
||||||
'item_type': self.item_type,
|
'item_type': self.item_type,
|
||||||
'repo_source': self.repo_source,
|
'repo_source': self.repo_source,
|
||||||
|
'data': self.data, # Include raw data for full reconstruction
|
||||||
'number': self.number,
|
'number': self.number,
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
@@ -77,6 +78,7 @@ class WorkflowItem:
|
|||||||
'updated_at': self.updated_at,
|
'updated_at': self.updated_at,
|
||||||
'body': self.body,
|
'body': self.body,
|
||||||
'url': self.url,
|
'url': self.url,
|
||||||
|
'api_url': self.api_url,
|
||||||
'author': self.author,
|
'author': self.author,
|
||||||
'author_url': self.author_url,
|
'author_url': self.author_url,
|
||||||
'labels': self.labels,
|
'labels': self.labels,
|
||||||
@@ -89,6 +91,16 @@ class WorkflowItem:
|
|||||||
'comments_count': self.comments_count
|
'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:
|
class GitHubRepoFetcher:
|
||||||
"""Fetches repository information from GitHub"""
|
"""Fetches repository information from GitHub"""
|
||||||
@@ -516,3 +528,69 @@ class WorkflowManager:
|
|||||||
if any(label in item.labels for label in label_filter)]
|
if any(label in item.labels for label in label_filter)]
|
||||||
|
|
||||||
return filtered
|
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