Feature/jira project knowledge #8

Open
gitea-admin wants to merge 9 commits from feature/jira-project-knowledge into develop
4 changed files with 380 additions and 10 deletions
Showing only changes of commit 42f4a058ea - Show all commits
+4
View File
@@ -34,6 +34,10 @@ Jira Project Knowledge enables Cowork to index Jira issues as searchable project
1. In the same Jira dialog, check **Enable Jira Project Knowledge**.
2. Enter **Project Mapping** in the format `cowork_project_id:JIRA_PROJECT_KEY`.
* Example: `proj-alpha:ALPHA, proj-beta:BETA`
* Click the **ⓘ** icon next to "Project Mapping" for detailed help on:
* **Project ID**: The Cowork project identifier (e.g., `cowork-local`). Find it in your current Cowork project settings.
* **Jira Key**: The Jira project key (e.g., `ALPHA` from issue `ALPHA-123`). Open any Jira issue to find it.
* **Common mistake**: Do not enter issue keys like `ABC-123`. Only enter the project key part `ABC`.
3. Click **Save**.
### 3.3 Initial Sync
+100
View File
@@ -220,6 +220,106 @@ STRINGS: Dict[str, Dict[str, str]] = {
"en": "Enter base URL, email and API token first.",
"ja": "先にベースURL・メール・APIトークンを入力してください。",
"vi": "Hãy nhập Base URL, Email và API token trước."},
# ---- Jira Project Knowledge help tooltips ---------------------------------
"connectors.jira_kb_section": {
"en": "Project Knowledge",
"ja": "プロジェクトナレッジ",
"vi": "Project Knowledge"},
"connectors.jira_kb_enable": {
"en": "Enable Jira Project Knowledge",
"ja": "Jiraプロジェクトナレッジを有効化",
"vi": "Bật Jira Project Knowledge"},
"connectors.jira_kb_mapping_label": {
"en": "Project Mapping",
"ja": "プロジェクトマッピング",
"vi": "Ánh xạ Project"},
"connectors.jira_kb_project_id_title": {
"en": "What is Project ID?",
"ja": "Project IDとは?",
"vi": "Project ID là gì?"},
"connectors.jira_kb_jira_key_title": {
"en": "What is Jira Key?",
"ja": "Jira Keyとは?",
"vi": "Jira Key là gì?"},
"connectors.jira_kb_mapping_hint": {
"en": "Map Cowork projects to Jira project keys. Format: cowork_project_id:JIRA_KEY",
"ja": "CoworkプロジェクトをJiraプロジェクトキーにマッピング。形式: cowork_project_id:JIRA_KEY",
"vi": "Ánh xạ project Cowork với Jira project key. Định dạng: cowork_project_id:JIRA_KEY"},
"connectors.jira_kb_sync_now": {
"en": "Sync Now",
"ja": "今すぐ同期",
"vi": "Đồng bộ ngay"},
"connectors.jira_kb_not_configured": {
"en": "Not configured",
"ja": "未設定",
"vi": "Chưa cấu hình"},
"connectors.jira_kb_disabled": {
"en": "Disabled",
"ja": "無効",
"vi": "Đã tắt"},
"connectors.jira_kb_syncing": {
"en": "Syncing…",
"ja": "同期中…",
"vi": "Đang đồng bộ…"},
"connectors.jira_kb_project_id_help": {
"en": ("<b>What is Project ID?</b><br>"
"Project ID is the identifier of a project in Cowork Local. "
"This value links knowledge from Jira to the correct project in Cowork.<br><br>"
"<b>Where to find it:</b><br>"
"You can get the Project ID from the currently open project in Cowork "
"or from the current project configuration.<br><br>"
"<b>Example:</b> cowork-local<br><br>"
"<b>Common mistake:</b><br>"
"Do not enter a Jira Project Key or Jira Issue Key here."),
"ja": ("<b>Project IDとは?</b><br>"
"Project IDはCowork Local内のプロジェクト識別子です。"
"この値でJiraのナレッジをCoworkの正しいプロジェクトに紐付けます。<br><br>"
"<b>確認方法:</b><br>"
"Coworkで開いているプロジェクト、または現在のプロジェクト設定から取得できます。<br><br>"
"<b>例:</b> cowork-local<br><br>"
"<b>よくある間違い:</b><br>"
"ここにJiraプロジェクトキーやJira課題キーを入力しないでください。"),
"vi": ("<b>Project ID là gì?</b><br>"
"Project ID là định danh của project trong Cowork Local. "
"Giá trị này dùng để gắn knowledge từ Jira với đúng project trong Cowork.<br><br>"
"<b>Cách lấy:</b><br>"
"Bạn có thể lấy Project ID từ project đang mở trong Cowork "
"hoặc từ cấu hình project hiện tại.<br><br>"
"<b>Ví dụ:</b> cowork-local<br><br>"
"<b>Lỗi thường gặp:</b><br>"
"Không nhập Jira Project Key hoặc Jira Issue Key vào ô này.")},
"connectors.jira_kb_jira_key_help": {
"en": ("<b>What is Jira Key?</b><br>"
"Jira Key is the short code of a Jira project — not an issue code.<br><br>"
"<b>Where to find it:</b><br>"
"Open any issue in Jira. If the issue code is ABC-123, then the Jira Key is ABC.<br>"
"You can also find it in Jira Project Settings.<br><br>"
"<b>Example:</b><br>"
"Issue: ABC-123 → Jira Key: ABC<br><br>"
"<b>Common mistake:</b><br>"
"Do not enter ABC-123. Only enter ABC."),
"ja": ("<b>Jira Keyとは?</b><br>"
"Jira KeyはJiraプロジェクトの短いコードです。課題コードではありません。<br><br>"
"<b>確認方法:</b><br>"
"Jiraで任意の課題を開きます。課題コードがABC-123なら、Jira KeyはABCです。<br>"
"Jiraプロジェクト設定でも確認できます。<br><br>"
"<b>例:</b><br>"
"課題: ABC-123 → Jira Key: ABC<br><br>"
"<b>よくある間違い:</b><br>"
"ABC-123と入力しないでください。ABCのみ入力します。"),
"vi": ("<b>Jira Key là gì?</b><br>"
"Jira Key là mã ngắn của Jira project, không phải mã của một issue.<br><br>"
"<b>Cách lấy:</b><br>"
"Mở một issue bất kỳ trong Jira. Nếu issue có mã ABC-123 thì Jira Key là ABC.<br>"
"Bạn cũng có thể xem Jira Key trong Project settings của Jira.<br><br>"
"<b>Ví dụ:</b><br>"
"Issue: ABC-123 → Jira Key: ABC<br><br>"
"<b>Lỗi thường gặp:</b><br>"
"Không nhập ABC-123. Chỉ nhập ABC.")},
"connectors.jira_kb_validation_issue_key": {
"en": "Looks like you entered an Issue Key. Enter only the project key part, e.g. ABC.",
"ja": "課題キーを入力したようです。プロジェクトキー部分のみを入力してください(例: ABC)。",
"vi": "Có vẻ bạn đã nhập Issue Key. Hãy nhập chỉ phần project key, ví dụ ABC."},
"tools_admin.jira_group": {"en": "Jira connection", "ja": "Jira 接続", "vi": "Kết nối Jira"},
"tools_admin.jira_hint": {
"en": "Connect once, then just paste a Jira issue link into Cowork or a Co4E step — the agent "
+193
View File
@@ -0,0 +1,193 @@
"""UX tests for Jira Project Knowledge help tooltips and validation.
Verifies that the JiraConnectDialog shows contextual help icons for
Project ID and Jira Key, renders correct help text, supports keyboard
accessibility, and validates common user mistakes (e.g. entering ABC-123
instead of ABC).
"""
from __future__ import annotations
import pytest
class _Config:
"""Minimal config stub for JiraConnectDialog."""
def __init__(self):
self.data = {
"jira": {"base_url": "", "email": "", "api_token": ""},
"jira_knowledge": {"enabled": False, "projects": {}},
}
def save(self):
pass
class _Ctx:
def __init__(self):
self.config = _Config()
def save(self):
self.config.save()
@pytest.fixture
def dialog(qapp):
from cowork_local.ui.connectors_panel import JiraConnectDialog
ctx = _Ctx()
dlg = JiraConnectDialog(ctx)
yield dlg
dlg.deleteLater()
# ---- Help icon presence ---------------------------------------------------
def test_help_icon_exists(dialog):
"""The mapping label row must contain a help button (?)."""
from PySide6.QtWidgets import QToolButton
dlg = dialog
help_icons = dlg.findChildren(QToolButton)
assert len(help_icons) >= 1, "Help button (?) not found in JiraConnectDialog"
def test_help_icon_has_tooltip(dialog):
"""Help icon must have a non-empty tooltip with both Project ID and Jira Key explanations."""
from PySide6.QtWidgets import QToolButton
dlg = dialog
help_icons = dlg.findChildren(QToolButton)
assert help_icons, "No help button found"
tooltip = help_icons[0].toolTip()
assert tooltip, "Help button has no tooltip"
assert "Project ID" in tooltip, "Tooltip missing Project ID explanation"
assert "Jira Key" in tooltip, "Tooltip missing Jira Key explanation"
# ---- Help text content ----------------------------------------------------
def test_project_id_help_content(dialog):
"""Project ID help must explain what it is, where to find it, example, and common mistake."""
from cowork_local.i18n import tr
text = tr("connectors.jira_kb_project_id_help")
assert "cowork-local" in text.lower() or "Cowork" in text, "Missing example"
# Must warn against entering Jira keys
lower = text.lower()
assert "jira" in lower and ("key" in lower or "issue" in lower), \
"Missing common mistake warning about Jira keys"
def test_jira_key_help_content(dialog):
"""Jira Key help must include the ABC-123 → ABC example."""
from cowork_local.i18n import tr
text = tr("connectors.jira_kb_jira_key_help")
assert "ABC-123" in text, "Missing ABC-123 example"
assert "ABC" in text, "Missing ABC extraction example"
def test_jira_key_help_warns_against_issue_key(dialog):
"""Jira Key help must explicitly warn not to enter ABC-123."""
from cowork_local.i18n import tr
text = tr("connectors.jira_kb_jira_key_help")
lower = text.lower()
# Should contain a warning like "Do not enter ABC-123" or "Không nhập ABC-123"
assert "abc-123" in lower, "Missing warning about entering issue key format"
# ---- Validation -----------------------------------------------------------
def test_validation_shows_on_issue_key_pattern(dialog):
"""Typing 'proj:ABC-123' should show the validation warning."""
dlg = dialog
dlg.show()
dlg.project_mapping.setText("myproject:ABC-123")
assert not dlg.mapping_validation.isHidden(), \
"Validation hint should be shown when Issue Key pattern detected"
assert dlg.mapping_validation.text(), "Validation hint should have text"
def test_validation_hides_on_correct_input(dialog):
"""Typing 'proj:ABC' should NOT show the validation warning."""
dlg = dialog
dlg.show()
dlg.project_mapping.setText("myproject:ABC")
assert dlg.mapping_validation.isHidden(), \
"Validation hint should be hidden for correct Jira Key format"
def test_validation_hides_on_empty(dialog):
"""Empty input should not show validation warning."""
dlg = dialog
dlg.show()
dlg.project_mapping.setText("")
assert dlg.mapping_validation.isHidden(), \
"Validation hint should be hidden on empty input"
def test_validation_multiple_mappings(dialog):
"""Validation should detect issue key pattern even in multi-mapping strings."""
dlg = dialog
dlg.show()
dlg.project_mapping.setText("proj-a:ALPHA, proj-b:DEF-456")
assert not dlg.mapping_validation.isHidden(), \
"Validation should trigger when any mapping contains an Issue Key pattern"
# ---- Existing behavior unchanged ------------------------------------------
def test_save_still_works(dialog):
"""Saving with valid mapping still produces correct config structure."""
dlg = dialog
dlg.url.setText("https://example.atlassian.net")
dlg.email.setText("user@example.com")
dlg.token.setText("test-token")
dlg.project_mapping.setText("myproject:MYKEY")
dlg.kb_enabled.setChecked(True)
dlg._save()
jira_kb = dlg.ctx.config.data.get("jira_knowledge", {})
assert jira_kb["enabled"] is True
assert jira_kb["projects"] == {"myproject": "MYKEY"}
def test_sync_button_present(dialog):
"""Sync Now button must still exist and be functional."""
dlg = dialog
assert dlg.sync_btn is not None
assert dlg.sync_btn.text(), "Sync button should have text"
# ---- Accessibility --------------------------------------------------------
def test_help_icon_cursor(dialog):
"""Help button should be a QToolButton (clickable by nature)."""
from PySide6.QtWidgets import QToolButton
dlg = dialog
help_icons = dlg.findChildren(QToolButton)
assert help_icons, "No help button found"
# QToolButton is inherently clickable, no need for cursor check
assert help_icons[0].text() == "?", "Help button should display '?' text"
# ---- i18n keys exist for all three languages ------------------------------
@pytest.mark.parametrize("lang", ["en", "vi", "ja"])
def test_i18n_keys_exist(lang):
"""All Jira KB help keys must have translations for en, vi, ja."""
from cowork_local.i18n import STRINGS
required_keys = [
"connectors.jira_kb_section",
"connectors.jira_kb_enable",
"connectors.jira_kb_mapping_label",
"connectors.jira_kb_project_id_title",
"connectors.jira_kb_jira_key_title",
"connectors.jira_kb_project_id_help",
"connectors.jira_kb_jira_key_help",
"connectors.jira_kb_validation_issue_key",
"connectors.jira_kb_sync_now",
"connectors.jira_kb_not_configured",
"connectors.jira_kb_disabled",
"connectors.jira_kb_syncing",
]
for key in required_keys:
assert key in STRINGS, f"Missing i18n key: {key}"
entry = STRINGS[key]
assert lang in entry, f"Missing '{lang}' translation for key: {key}"
assert entry[lang], f"Empty '{lang}' translation for key: {key}"
+83 -10
View File
@@ -12,9 +12,11 @@ setup dialog; OneDrive/SharePoint: neither, they only toggle).
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QCheckBox, QDialog, QFormLayout, QFrame, QGroupBox, QHBoxLayout, QLabel,
QLineEdit, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget,
QLineEdit, QMessageBox, QPushButton, QScrollArea, QToolButton, QToolTip,
QVBoxLayout, QWidget,
)
from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES
@@ -81,19 +83,19 @@ class JiraConnectDialog(QDialog):
main_layout.addWidget(conn_group)
# === Project Knowledge Section ===
kb_group = QGroupBox("Project Knowledge")
kb_group = QGroupBox(tr("connectors.jira_kb_section"))
kb_layout = QVBoxLayout(kb_group)
self.kb_enabled = QCheckBox("Enable Jira Project Knowledge")
self.kb_enabled = QCheckBox(tr("connectors.jira_kb_enable"))
self.kb_enabled.setChecked(jira_kb.get("enabled", False))
kb_layout.addWidget(self.kb_enabled)
kb_hint = QLabel("Map Cowork projects to Jira project keys. Format: cowork_project_id:JIRA_KEY")
kb_hint = QLabel(tr("connectors.jira_kb_mapping_hint"))
kb_hint.setObjectName("hint")
kb_hint.setWordWrap(True)
kb_layout.addWidget(kb_hint)
# Project mapping input
# Project mapping input with help icons for Project ID and Jira Key
mapping_form = QFormLayout()
self.project_mapping = QLineEdit()
# Load existing mappings
@@ -102,17 +104,67 @@ class JiraConnectDialog(QDialog):
mapping_str = ", ".join(f"{k}:{v}" for k, v in existing_projects.items())
self.project_mapping.setText(mapping_str)
self.project_mapping.setPlaceholderText("proj-alpha:ALPHA, proj-beta:BETA")
mapping_form.addRow("Project Mapping", self.project_mapping)
# Label with help icon explaining both Project ID and Jira Key
mapping_label = QLabel(tr("connectors.jira_kb_mapping_label"))
self.help_icon = QToolButton()
self.help_icon.setText("?")
self.help_icon.setToolTip(
"<b>" + tr("connectors.jira_kb_project_id_title") + "</b><br>"
+ tr("connectors.jira_kb_project_id_help")
+ "<br><br><b>" + tr("connectors.jira_kb_jira_key_title") + "</b><br>"
+ tr("connectors.jira_kb_jira_key_help")
)
self.help_icon.setStyleSheet("""
QToolButton {
border: 1px solid #5B9BD5;
border-radius: 10px;
background: transparent;
color: #5B9BD5;
font-weight: bold;
padding: 2px 6px;
min-width: 18px;
min-height: 18px;
}
QToolButton:hover {
background: #5B9BD5;
color: white;
}
QToolButton:pressed {
background: #4A8BC7;
color: white;
}
""")
# Native Qt tooltip: hover to show, no click handler needed.
# AutoRaise=False keeps the button always visible (not faded out).
self.help_icon.setAutoRaise(False)
label_row = QHBoxLayout()
label_row.setSpacing(4)
label_row.addWidget(mapping_label)
label_row.addWidget(self.help_icon)
label_row.addStretch(1)
label_widget = QWidget()
label_widget.setLayout(label_row)
mapping_form.addRow(label_widget, self.project_mapping)
# Validation hint for common mistakes (e.g., entering ABC-123 instead of ABC)
self.mapping_validation = QLabel()
self.mapping_validation.setObjectName("hint")
self.mapping_validation.setWordWrap(True)
self.mapping_validation.hide()
mapping_form.addRow("", self.mapping_validation)
self.project_mapping.textChanged.connect(self._validate_mapping)
kb_layout.addLayout(mapping_form)
# Sync controls
sync_row = QHBoxLayout()
self.sync_btn = QPushButton("Sync Now")
self.sync_btn = QPushButton(tr("connectors.jira_kb_sync_now"))
self.sync_btn.clicked.connect(self._trigger_sync)
self.sync_btn.setEnabled(False)
sync_row.addWidget(self.sync_btn)
self.sync_status = QLabel("Not configured")
self.sync_status = QLabel(tr("connectors.jira_kb_not_configured"))
self.sync_status.setObjectName("hint")
sync_row.addWidget(self.sync_status)
sync_row.addStretch(1)
@@ -136,15 +188,36 @@ class JiraConnectDialog(QDialog):
self.kb_enabled.toggled.connect(self._update_sync_state)
self._update_sync_state(self.kb_enabled.isChecked())
def _on_paste(self, text: str) -> None:
"""Auto-fill Base URL from a pasted Jira link."""
import re
# Extract base URL from patterns like https://example.atlassian.net/browse/ABC-123
match = re.search(r"(https?://[^/\s]+\.atlassian\.net)", text.strip())
if match and not self.url.text().strip():
self.url.setText(match.group(1))
def _update_sync_state(self, enabled: bool) -> None:
"""Enable/disable sync controls based on KB checkbox."""
self.sync_btn.setEnabled(enabled)
if not enabled:
self.sync_status.setText("Disabled")
self.sync_status.setText(tr("connectors.jira_kb_disabled"))
def _validate_mapping(self, text: str) -> None:
"""Show inline hint if user appears to enter an Issue Key (ABC-123) instead of just Jira Key (ABC)."""
import re
# Detect pattern like "proj:ABC-123" or "proj:ABC-123, proj2:DEF-456"
# A Jira project key should be uppercase letters only (e.g., ABC), not ABC-123
issue_key_pattern = re.compile(r':\s*[A-Z]+-\d+')
if issue_key_pattern.search(text):
self.mapping_validation.setText(tr("connectors.jira_kb_validation_issue_key"))
self.mapping_validation.setStyleSheet("color: #D4A017; font-style: italic;")
self.mapping_validation.show()
else:
self.mapping_validation.hide()
def _trigger_sync(self) -> None:
"""Trigger a background sync job using JiraSyncService."""
self.sync_status.setText("Syncing...")
self.sync_status.setText(tr("connectors.jira_kb_syncing"))
self.sync_btn.setEnabled(False)
def job(_w):