diff --git a/docs/jira-knowledge-guide.md b/docs/jira-knowledge-guide.md index e79d792..47d96ed 100644 --- a/docs/jira-knowledge-guide.md +++ b/docs/jira-knowledge-guide.md @@ -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 diff --git a/i18n/libreoffice_view.py b/i18n/libreoffice_view.py index 12e61af..1a82f8a 100644 --- a/i18n/libreoffice_view.py +++ b/i18n/libreoffice_view.py @@ -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": ("What is Project ID?
" + "Project ID is the identifier of a project in Cowork Local. " + "This value links knowledge from Jira to the correct project in Cowork.

" + "Where to find it:
" + "You can get the Project ID from the currently open project in Cowork " + "or from the current project configuration.

" + "Example: cowork-local

" + "Common mistake:
" + "Do not enter a Jira Project Key or Jira Issue Key here."), + "ja": ("Project IDとは?
" + "Project IDはCowork Local内のプロジェクト識別子です。" + "この値でJiraのナレッジをCoworkの正しいプロジェクトに紐付けます。

" + "確認方法:
" + "Coworkで開いているプロジェクト、または現在のプロジェクト設定から取得できます。

" + "例: cowork-local

" + "よくある間違い:
" + "ここにJiraプロジェクトキーやJira課題キーを入力しないでください。"), + "vi": ("Project ID là gì?
" + "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.

" + "Cách lấy:
" + "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.

" + "Ví dụ: cowork-local

" + "Lỗi thường gặp:
" + "Không nhập Jira Project Key hoặc Jira Issue Key vào ô này.")}, + "connectors.jira_kb_jira_key_help": { + "en": ("What is Jira Key?
" + "Jira Key is the short code of a Jira project — not an issue code.

" + "Where to find it:
" + "Open any issue in Jira. If the issue code is ABC-123, then the Jira Key is ABC.
" + "You can also find it in Jira Project Settings.

" + "Example:
" + "Issue: ABC-123 → Jira Key: ABC

" + "Common mistake:
" + "Do not enter ABC-123. Only enter ABC."), + "ja": ("Jira Keyとは?
" + "Jira KeyはJiraプロジェクトの短いコードです。課題コードではありません。

" + "確認方法:
" + "Jiraで任意の課題を開きます。課題コードがABC-123なら、Jira KeyはABCです。
" + "Jiraプロジェクト設定でも確認できます。

" + "例:
" + "課題: ABC-123 → Jira Key: ABC

" + "よくある間違い:
" + "ABC-123と入力しないでください。ABCのみ入力します。"), + "vi": ("Jira Key là gì?
" + "Jira Key là mã ngắn của Jira project, không phải mã của một issue.

" + "Cách lấy:
" + "Mở một issue bất kỳ trong Jira. Nếu issue có mã ABC-123 thì Jira Key là ABC.
" + "Bạn cũng có thể xem Jira Key trong Project settings của Jira.

" + "Ví dụ:
" + "Issue: ABC-123 → Jira Key: ABC

" + "Lỗi thường gặp:
" + "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 " diff --git a/tests/ui/test_jira_knowledge_help_ux.py b/tests/ui/test_jira_knowledge_help_ux.py new file mode 100644 index 0000000..6e1bc51 --- /dev/null +++ b/tests/ui/test_jira_knowledge_help_ux.py @@ -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}" \ No newline at end of file diff --git a/ui/connectors_panel.py b/ui/connectors_panel.py index e64fcff..009f10c 100644 --- a/ui/connectors_panel.py +++ b/ui/connectors_panel.py @@ -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( + "" + tr("connectors.jira_kb_project_id_title") + "
" + + tr("connectors.jira_kb_project_id_help") + + "

" + tr("connectors.jira_kb_jira_key_title") + "
" + + 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):