Add version display and bump buttons to publish dialog
- Show local and registry version in publish dialog - Add +Patch/+Minor/+Major bump buttons - Auto-fetch registry version on dialog open - Suggest next version based on latest registry version - Add fork detection and forked_from metadata support - Update registry schema for forked_from/forked_version fields - Display fork indicator when publishing forked tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4756ea906c
commit
16de3e371c
|
|
@ -1,9 +1,10 @@
|
|||
"""Publish tool dialog."""
|
||||
|
||||
from typing import Optional, List
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QPushButton,
|
||||
QHBoxLayout, QLineEdit, QTextEdit, QFormLayout,
|
||||
QProgressBar, QMessageBox, QComboBox
|
||||
QProgressBar, QMessageBox, QComboBox, QFrame, QWidget
|
||||
)
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
|
|
@ -12,6 +13,72 @@ from ...registry_client import RegistryClient, RegistryError
|
|||
from ...config import load_config
|
||||
|
||||
|
||||
def parse_version(version: str) -> tuple:
|
||||
"""Parse version string into tuple of ints."""
|
||||
try:
|
||||
parts = version.split(".")
|
||||
return tuple(int(p) for p in parts[:3])
|
||||
except (ValueError, AttributeError):
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def bump_version(version: str, bump_type: str) -> str:
|
||||
"""Bump version by type (patch, minor, major)."""
|
||||
major, minor, patch = parse_version(version)
|
||||
if bump_type == "major":
|
||||
return f"{major + 1}.0.0"
|
||||
elif bump_type == "minor":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
else: # patch
|
||||
return f"{major}.{minor}.{patch + 1}"
|
||||
|
||||
|
||||
class VersionFetchWorker(QThread):
|
||||
"""Background worker to fetch registry version info."""
|
||||
finished = Signal(dict) # Dict with 'my_version' and 'original_versions'
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, name: str, original_owner: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.original_owner = original_owner
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
config = load_config()
|
||||
client = RegistryClient()
|
||||
client.token = config.registry.token
|
||||
|
||||
result = {
|
||||
"my_version": None,
|
||||
"my_status": None,
|
||||
"original_versions": [],
|
||||
"original_owner": self.original_owner,
|
||||
}
|
||||
|
||||
# Get my published version of this tool
|
||||
if config.registry.token:
|
||||
try:
|
||||
status = client.get_my_tool_status(self.name)
|
||||
result["my_version"] = status.get("version")
|
||||
result["my_status"] = status.get("status")
|
||||
except RegistryError:
|
||||
pass # Tool not published by me yet
|
||||
|
||||
# If this was forked from another owner, get their versions too
|
||||
if self.original_owner:
|
||||
try:
|
||||
result["original_versions"] = client.get_tool_versions(
|
||||
self.original_owner, self.name
|
||||
)
|
||||
except RegistryError:
|
||||
pass
|
||||
|
||||
self.finished.emit(result)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class PublishWorker(QThread):
|
||||
"""Background worker for publishing."""
|
||||
success = Signal(dict)
|
||||
|
|
@ -40,10 +107,114 @@ class PublishDialog(QDialog):
|
|||
def __init__(self, parent, tool: Tool):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Publish Tool")
|
||||
self.setMinimumSize(500, 400)
|
||||
self.setMinimumSize(500, 450)
|
||||
self._tool = tool
|
||||
self._worker = None
|
||||
self._version_worker = None
|
||||
self._my_registry_version: Optional[str] = None
|
||||
self._my_registry_status: Optional[str] = None
|
||||
self._original_versions: List[str] = []
|
||||
self._local_version = self._get_local_version()
|
||||
self._original_owner = self._get_original_owner()
|
||||
self._setup_ui()
|
||||
self._fetch_registry_versions()
|
||||
|
||||
def _get_local_version(self) -> str:
|
||||
"""Get the version from local tool config."""
|
||||
if hasattr(self._tool, 'path') and self._tool.path:
|
||||
try:
|
||||
import yaml
|
||||
config = yaml.safe_load(self._tool.path.read_text())
|
||||
return config.get("version", "1.0.0")
|
||||
except Exception:
|
||||
pass
|
||||
return "1.0.0"
|
||||
|
||||
def _get_original_owner(self) -> Optional[str]:
|
||||
"""Get the original owner if this tool was installed from registry."""
|
||||
if hasattr(self._tool, 'path') and self._tool.path:
|
||||
try:
|
||||
import yaml
|
||||
config = yaml.safe_load(self._tool.path.read_text())
|
||||
# Check if it has forked_from or was installed from registry
|
||||
if config.get("forked_from"):
|
||||
return config["forked_from"].split("/")[0]
|
||||
if config.get("installed_from"):
|
||||
return config["installed_from"].split("/")[0]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _fetch_registry_versions(self):
|
||||
"""Fetch versions from registry in background."""
|
||||
self._version_worker = VersionFetchWorker(self._tool.name, self._original_owner)
|
||||
self._version_worker.finished.connect(self._on_versions_fetched)
|
||||
self._version_worker.error.connect(self._on_versions_error)
|
||||
self._version_worker.start()
|
||||
|
||||
def _on_versions_fetched(self, result: dict):
|
||||
"""Handle registry versions response."""
|
||||
self._my_registry_version = result.get("my_version")
|
||||
self._my_registry_status = result.get("my_status")
|
||||
self._original_versions = result.get("original_versions", [])
|
||||
self._update_version_info()
|
||||
|
||||
def _on_versions_error(self, error: str):
|
||||
"""Handle version fetch error (non-critical)."""
|
||||
self._my_registry_version = None
|
||||
self._original_versions = []
|
||||
self._update_version_info()
|
||||
|
||||
def _update_version_info(self):
|
||||
"""Update version info display."""
|
||||
if self._my_registry_version:
|
||||
# Already published by me - show my latest version
|
||||
self.registry_version_label.setText(
|
||||
f"Registry: v{self._my_registry_version} ({self._my_registry_status or 'unknown'})"
|
||||
)
|
||||
if self._my_registry_status == "approved":
|
||||
self.registry_version_label.setStyleSheet("color: #38a169;") # Green
|
||||
elif self._my_registry_status == "pending":
|
||||
self.registry_version_label.setStyleSheet("color: #d69e2e;") # Yellow
|
||||
else:
|
||||
self.registry_version_label.setStyleSheet("color: #4299e1;") # Blue
|
||||
# Suggest next version
|
||||
suggested = bump_version(self._my_registry_version, "patch")
|
||||
self.version_input.setText(suggested)
|
||||
self._show_bump_buttons(True)
|
||||
elif self._original_versions:
|
||||
# This is a fork - show original tool info
|
||||
latest_original = self._original_versions[0]
|
||||
self.registry_version_label.setText(
|
||||
f"Fork of {self._original_owner}/{self._tool.name} v{latest_original}"
|
||||
)
|
||||
self.registry_version_label.setStyleSheet("color: #805ad5;") # Purple for fork
|
||||
self.version_input.setText("1.0.0") # Start fresh for forks
|
||||
self._show_bump_buttons(False)
|
||||
else:
|
||||
self.registry_version_label.setText("Registry: not published")
|
||||
self.registry_version_label.setStyleSheet("color: #718096;")
|
||||
self.version_input.setText(self._local_version)
|
||||
self._show_bump_buttons(False)
|
||||
|
||||
def _show_bump_buttons(self, show: bool):
|
||||
"""Show or hide version bump buttons."""
|
||||
self.bump_widget.setVisible(show)
|
||||
|
||||
def _bump_patch(self):
|
||||
"""Bump patch version."""
|
||||
if self._my_registry_version:
|
||||
self.version_input.setText(bump_version(self._my_registry_version, "patch"))
|
||||
|
||||
def _bump_minor(self):
|
||||
"""Bump minor version."""
|
||||
if self._my_registry_version:
|
||||
self.version_input.setText(bump_version(self._my_registry_version, "minor"))
|
||||
|
||||
def _bump_major(self):
|
||||
"""Bump major version."""
|
||||
if self._my_registry_version:
|
||||
self.version_input.setText(bump_version(self._my_registry_version, "major"))
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
|
|
@ -55,15 +226,76 @@ class PublishDialog(QDialog):
|
|||
title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Version info section
|
||||
version_frame = QFrame()
|
||||
version_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f7fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
""")
|
||||
version_info_layout = QHBoxLayout(version_frame)
|
||||
version_info_layout.setContentsMargins(12, 8, 12, 8)
|
||||
|
||||
self.local_version_label = QLabel(f"Local: v{self._local_version}")
|
||||
self.local_version_label.setStyleSheet("color: #2d3748; font-weight: 500;")
|
||||
version_info_layout.addWidget(self.local_version_label)
|
||||
|
||||
version_info_layout.addSpacing(20)
|
||||
|
||||
self.registry_version_label = QLabel("Registry: checking...")
|
||||
self.registry_version_label.setStyleSheet("color: #718096;")
|
||||
version_info_layout.addWidget(self.registry_version_label)
|
||||
|
||||
version_info_layout.addStretch()
|
||||
layout.addWidget(version_frame)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
# Version
|
||||
# Version input with bump buttons
|
||||
version_row = QHBoxLayout()
|
||||
self.version_input = QLineEdit()
|
||||
self.version_input.setText("1.0.0")
|
||||
self.version_input.setText(self._local_version)
|
||||
self.version_input.setPlaceholderText("1.0.0")
|
||||
form.addRow("Version:", self.version_input)
|
||||
self.version_input.setMaximumWidth(100)
|
||||
version_row.addWidget(self.version_input)
|
||||
|
||||
# Bump buttons (hidden until we know registry version)
|
||||
self.bump_widget = QWidget()
|
||||
bump_layout = QHBoxLayout(self.bump_widget)
|
||||
bump_layout.setContentsMargins(8, 0, 0, 0)
|
||||
bump_layout.setSpacing(4)
|
||||
|
||||
btn_patch = QPushButton("+Patch")
|
||||
btn_patch.setObjectName("secondary")
|
||||
btn_patch.setFixedWidth(60)
|
||||
btn_patch.setToolTip("Bump patch version (bug fixes)")
|
||||
btn_patch.clicked.connect(self._bump_patch)
|
||||
bump_layout.addWidget(btn_patch)
|
||||
|
||||
btn_minor = QPushButton("+Minor")
|
||||
btn_minor.setObjectName("secondary")
|
||||
btn_minor.setFixedWidth(60)
|
||||
btn_minor.setToolTip("Bump minor version (new features)")
|
||||
btn_minor.clicked.connect(self._bump_minor)
|
||||
bump_layout.addWidget(btn_minor)
|
||||
|
||||
btn_major = QPushButton("+Major")
|
||||
btn_major.setObjectName("secondary")
|
||||
btn_major.setFixedWidth(60)
|
||||
btn_major.setToolTip("Bump major version (breaking changes)")
|
||||
btn_major.clicked.connect(self._bump_major)
|
||||
bump_layout.addWidget(btn_major)
|
||||
|
||||
self.bump_widget.setVisible(False)
|
||||
version_row.addWidget(self.bump_widget)
|
||||
|
||||
version_row.addStretch()
|
||||
form.addRow("Version:", version_row)
|
||||
|
||||
# Category
|
||||
self.category_combo = QComboBox()
|
||||
|
|
@ -154,6 +386,17 @@ class PublishDialog(QDialog):
|
|||
if tags:
|
||||
tool_config["tags"] = tags
|
||||
|
||||
# Add fork metadata if this tool originated from another owner
|
||||
if self._original_owner and self._original_versions:
|
||||
tool_config["forked_from"] = f"{self._original_owner}/{self._tool.name}"
|
||||
# Record which version we forked from (use local version or latest original)
|
||||
forked_version = self._local_version
|
||||
if forked_version in self._original_versions:
|
||||
tool_config["forked_version"] = forked_version
|
||||
else:
|
||||
# Use the latest original version as reference
|
||||
tool_config["forked_version"] = self._original_versions[0]
|
||||
|
||||
# Convert to YAML string
|
||||
config_yaml = yaml.dump(tool_config, default_flow_style=False, sort_keys=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -1777,6 +1777,10 @@ def create_app() -> Flask:
|
|||
# Create a minimal source_json for consistency
|
||||
source_json = json.dumps({"type": "imported", "original_tool": source, "url": source_url})
|
||||
|
||||
# Extract fork metadata
|
||||
forked_from = (data.get("forked_from") or "").strip() or None
|
||||
forked_version = (data.get("forked_version") or "").strip() or None
|
||||
|
||||
if not name or not TOOL_NAME_RE.match(name) or len(name) > MAX_TOOL_NAME_LEN:
|
||||
return error_response("VALIDATION_ERROR", "Invalid tool name")
|
||||
if not version or Semver.parse(version) is None:
|
||||
|
|
@ -1918,8 +1922,9 @@ def create_app() -> Flask:
|
|||
owner, name, version, description, category, tags, config_yaml, readme,
|
||||
publisher_id, deprecated, deprecated_message, replacement, downloads,
|
||||
scrutiny_status, scrutiny_report, source, source_url, source_json,
|
||||
config_hash, visibility, moderation_status, published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
config_hash, visibility, moderation_status, forked_from, forked_version,
|
||||
published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
owner,
|
||||
|
|
@ -1943,6 +1948,8 @@ def create_app() -> Flask:
|
|||
config_hash,
|
||||
visibility,
|
||||
moderation_status,
|
||||
forked_from,
|
||||
forked_version,
|
||||
datetime.utcnow().isoformat(),
|
||||
],
|
||||
)
|
||||
|
|
@ -1957,6 +1964,8 @@ def create_app() -> Flask:
|
|||
"pr_url": "",
|
||||
"status": moderation_status,
|
||||
"visibility": visibility,
|
||||
"forked_from": forked_from,
|
||||
"forked_version": forked_version,
|
||||
"suggestions": suggestions,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ CREATE TABLE IF NOT EXISTS tools (
|
|||
moderation_note TEXT,
|
||||
moderated_by TEXT,
|
||||
moderated_at TIMESTAMP,
|
||||
forked_from TEXT,
|
||||
forked_version TEXT,
|
||||
published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(owner, name, version)
|
||||
);
|
||||
|
|
@ -455,6 +457,8 @@ def migrate_db(conn: sqlite3.Connection) -> None:
|
|||
("moderation_note", "TEXT", "NULL"),
|
||||
("moderated_by", "TEXT", "NULL"),
|
||||
("moderated_at", "TIMESTAMP", "NULL"),
|
||||
("forked_from", "TEXT", "NULL"),
|
||||
("forked_version", "TEXT", "NULL"),
|
||||
]
|
||||
|
||||
for col_name, col_type, default in tools_migrations:
|
||||
|
|
|
|||
Loading…
Reference in New Issue