From 45168a08d9af00f6527627c5c014a01cdf875231 Mon Sep 17 00:00:00 2001 From: Eike David Lenz Date: Thu, 15 Jan 2026 15:07:40 +0100 Subject: [PATCH 1/3] fix issue selector --- Makefile | 1 + issueSelector.js | 688 +++++++++++++++++++++++++---------------------- 2 files changed, 366 insertions(+), 323 deletions(-) diff --git a/Makefile b/Makefile index f249105..06b8644 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ build: clean # Create extension package mkdir -p build/ gnome-extensions pack -f \ + --extra-source=avatarLoader.js \ --extra-source=metadata.json \ --extra-source=LICENSE \ --extra-source=README.md \ diff --git a/issueSelector.js b/issueSelector.js index e3ccc06..30de96a 100644 --- a/issueSelector.js +++ b/issueSelector.js @@ -1,383 +1,425 @@ -import GObject from 'gi://GObject'; -import St from 'gi://St'; -import Clutter from 'gi://Clutter'; -import GLib from 'gi://GLib'; -import Soup from 'gi://Soup'; -import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; -import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import GObject from "gi://GObject"; +import St from "gi://St"; +import Clutter from "gi://Clutter"; +import GLib from "gi://GLib"; +import Soup from "gi://Soup"; +import * as ModalDialog from "resource:///org/gnome/shell/ui/modalDialog.js"; +import * as Main from "resource:///org/gnome/shell/ui/main.js"; -import {AvatarLoader} from './avatarLoader.js'; +import { AvatarLoader } from "./avatarLoader.js"; export const IssueSelectorDialog = GObject.registerClass( -class IssueSelectorDialog extends ModalDialog.ModalDialog { + class IssueSelectorDialog extends ModalDialog.ModalDialog { _init(settings, gettext, onSelected) { - super._init({ styleClass: 'gitlab-issue-selector-dialog' }); - - this._settings = settings; - this._ = gettext; - this._onSelected = onSelected; - this._httpSession = new Soup.Session(); - this._avatarLoader = new AvatarLoader(settings, this._httpSession); - this._projects = []; - this._allIssues = []; - this._selectedProject = null; - - this._buildUI(); - this._loadProjects(); + super._init({ styleClass: "gitlab-issue-selector-dialog" }); + + this._settings = settings; + this._ = gettext; + this._onSelected = onSelected; + this._httpSession = new Soup.Session(); + this._avatarLoader = new AvatarLoader(settings, this._httpSession); + this._projects = []; + this._allIssues = []; + this._selectedProject = null; + + this._buildUI(); + this._loadProjects(); } _buildUI() { - // Main container - let content = new St.BoxLayout({ - vertical: true, - style_class: 'gitlab-selector-content', - style: 'min-width: 600px; min-height: 500px;' - }); + // Main container + let content = new St.BoxLayout({ + vertical: true, + style_class: "gitlab-selector-content", + style: "min-width: 600px; min-height: 500px;", + }); + + // Title + let title = new St.Label({ + text: this._("Select a project and an issue"), + style_class: "gitlab-selector-title", + style: "font-size: 16px; font-weight: bold; margin-bottom: 10px;", + }); + content.add_child(title); + + // Project section + let projectBox = new St.BoxLayout({ + vertical: true, + style: "margin-bottom: 20px;", + }); + + let projectLabel = new St.Label({ + text: this._("Project"), + style: "font-weight: bold; margin-bottom: 5px;", + }); + projectBox.add_child(projectLabel); + + // Project search + this._projectSearchEntry = new St.Entry({ + hint_text: this._("Search project..."), + can_focus: true, + track_hover: true, + style: "margin-bottom: 5px;", + }); + this._projectSearchEntry.clutter_text.connect("text-changed", () => { + this._filterProjects(); + }); + projectBox.add_child(this._projectSearchEntry); + + // Project list container with scrolling + let projectScrollView = new St.ScrollView({ + style: "border: 1px solid #555; border-radius: 5px; height: 180px;", + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC, + }); + + this._projectList = new St.BoxLayout({ + vertical: true, + style_class: "gitlab-project-list", + }); + projectScrollView.add_child(this._projectList); + projectBox.add_child(projectScrollView); + + content.add_child(projectBox); + + // Issue section + let issueBox = new St.BoxLayout({ + vertical: true, + style: "margin-bottom: 20px;", + }); + + let issueLabel = new St.Label({ + text: this._("Issue"), + style: "font-weight: bold; margin-bottom: 5px;", + }); + issueBox.add_child(issueLabel); + + // Issue search + this._issueSearchEntry = new St.Entry({ + hint_text: this._("Search issue..."), + can_focus: true, + track_hover: true, + style: "margin-bottom: 5px;", + }); + this._issueSearchEntry.clutter_text.connect("text-changed", () => { + this._filterIssues(); + }); + issueBox.add_child(this._issueSearchEntry); + + // Issue list container with scrolling + let issueScrollView = new St.ScrollView({ + style: "border: 1px solid #555; border-radius: 5px; height: 180px;", + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC, + }); + + this._issueList = new St.BoxLayout({ + vertical: true, + style_class: "gitlab-issue-list", + }); + issueScrollView.add_child(this._issueList); + issueBox.add_child(issueScrollView); + + content.add_child(issueBox); + + // Loading indicator + this._loadingLabel = new St.Label({ + text: this._("Loading..."), + style: "font-style: italic; color: #999;", + }); + content.add_child(this._loadingLabel); + + this.contentLayout.add_child(content); + + // Buttons + this.setButtons([ + { + label: this._("Cancel"), + action: () => this.close(), + key: Clutter.KEY_Escape, + }, + { + label: this._("Select"), + action: () => this._onSelect(), + default: true, + }, + ]); + + this._selectedProjectWidget = null; + this._selectedIssueWidget = null; + } - // Title - let title = new St.Label({ - text: this._('Select a project and an issue'), - style_class: 'gitlab-selector-title', - style: 'font-size: 16px; font-weight: bold; margin-bottom: 10px;' - }); - content.add_child(title); + _loadProjects() { + this._showLoading(this._("Loading projects...")); - // Project section - let projectBox = new St.BoxLayout({ - vertical: true, - style: 'margin-bottom: 20px;' - }); + try { + const url = this._settings.get_string("gitlab-url"); + const token = this._settings.get_string("gitlab-token"); - let projectLabel = new St.Label({ - text: this._('Project'), - style: 'font-weight: bold; margin-bottom: 5px;' - }); - projectBox.add_child(projectLabel); - - // Project search - this._projectSearchEntry = new St.Entry({ - hint_text: this._('Search project...'), - can_focus: true, - track_hover: true, - style: 'margin-bottom: 5px;' - }); - this._projectSearchEntry.clutter_text.connect('text-changed', () => { - this._filterProjects(); - }); - projectBox.add_child(this._projectSearchEntry); + log(`GitLab Issue Selector: Fetching projects from URL: ${url}`); + log( + `GitLab Issue Selector: Using token length: ${token ? token.length : 0}`, + ); - // Project list container with scrolling - let projectScrollView = new St.ScrollView({ - style: 'border: 1px solid #555; border-radius: 5px; height: 180px;', - hscrollbar_policy: St.PolicyType.NEVER, - vscrollbar_policy: St.PolicyType.AUTOMATIC - }); + const apiUrl = `${url}/api/v4/projects?membership=true&per_page=100&order_by=last_activity_at`; - this._projectList = new St.BoxLayout({ - vertical: true, - style_class: 'gitlab-project-list' - }); - projectScrollView.add_child(this._projectList); - projectBox.add_child(projectScrollView); + const message = Soup.Message.new("GET", apiUrl); + message.request_headers.append("PRIVATE-TOKEN", token); - content.add_child(projectBox); + this._httpSession.send_and_read_async( + message, + GLib.PRIORITY_DEFAULT, + null, + (session, result) => { + try { + const bytes = session.send_and_read_finish(result); + const decoder = new TextDecoder("utf-8"); + const response = decoder.decode(bytes.get_data()); + + log( + `GitLab Issue Selector: API response status code: ${message.status_code}`, + ); + log( + `GitLab Issue Selector: API response length: ${response.length}`, + ); + + if (message.status_code === 200) { + log("GitLab Issue Selector: Parsing projects JSON..."); + this._projects = JSON.parse(response); + log( + `GitLab Issue Selector: Loaded ${this._projects.length} projects`, + ); + this._updateProjectList(); + this._hideLoading(); + } else { + log( + `GitLab Issue Selector: Error fetching projects: ${message.status_code}`, + ); + log(`GitLab Issue Selector: Response body: ${response}`); + this._showLoading(`${this._("Error")}: ${message.status_code}`); + } + } catch (e) { + log( + `GitLab Issue Selector: Error parsing projects: ${e.message}`, + ); + log(`GitLab Issue Selector: Full error: ${e.stack}`); + log(`GitLab Issue Selector: Response: ${response}`); + this._showLoading(`${this._("Error")}: ${e.message}`); + } + }, + ); + } catch (e) { + log(`GitLab Issue Selector: Error loading projects: ${e.message}`); + log(`GitLab Issue Selector: Full error: ${e.stack}`); + this._showLoading(`${this._("Error")}: ${e.message}`); + } + } - // Issue section - let issueBox = new St.BoxLayout({ - vertical: true, - style: 'margin-bottom: 20px;' + _updateProjectList() { + this._projectList.destroy_all_children(); + + const searchText = this._projectSearchEntry.get_text().toLowerCase(); + let filteredProjects = searchText + ? this._projects.filter( + (p) => + p.name.toLowerCase().includes(searchText) || + p.path_with_namespace.toLowerCase().includes(searchText), + ) + : this._projects; + + filteredProjects = filteredProjects.sort((a, b) => + a.path_with_namespace.localeCompare(b.path_with_namespace), + ); + + for (let project of filteredProjects) { + let item = new St.Button({ + style_class: "gitlab-list-item", + style: "padding: 8px; border-radius: 3px;", + can_focus: true, + track_hover: true, + x_expand: true, + x_align: Clutter.ActorAlign.FILL, }); - let issueLabel = new St.Label({ - text: this._('Issue'), - style: 'font-weight: bold; margin-bottom: 5px;' - }); - issueBox.add_child(issueLabel); - - // Issue search - this._issueSearchEntry = new St.Entry({ - hint_text: this._('Search issue...'), - can_focus: true, - track_hover: true, - style: 'margin-bottom: 5px;' + // Create horizontal box for icon + text + let box = new St.BoxLayout({ + vertical: false, + x_align: Clutter.ActorAlign.START, + x_expand: true, + style: "spacing: 8px;", }); - this._issueSearchEntry.clutter_text.connect('text-changed', () => { - this._filterIssues(); - }); - issueBox.add_child(this._issueSearchEntry); - // Issue list container with scrolling - let issueScrollView = new St.ScrollView({ - style: 'border: 1px solid #555; border-radius: 5px; height: 180px;', - hscrollbar_policy: St.PolicyType.NEVER, - vscrollbar_policy: St.PolicyType.AUTOMATIC + // Add project/group icon + let icon = new St.Icon({ + icon_name: "folder-symbolic", + icon_size: 24, + style: "width: 24px; height: 24px; border-radius: 3px;", }); - this._issueList = new St.BoxLayout({ - vertical: true, - style_class: 'gitlab-issue-list' - }); - issueScrollView.add_child(this._issueList); - issueBox.add_child(issueScrollView); + // Load project avatar using shared loader + const namespace = project.namespace || null; + this._avatarLoader.loadProjectAvatar(project.id, namespace, icon); - content.add_child(issueBox); + box.add_child(icon); - // Loading indicator - this._loadingLabel = new St.Label({ - text: this._('Loading...'), - style: 'font-style: italic; color: #999;' + let label = new St.Label({ + text: project.path_with_namespace, + style: "font-size: 12px;", + y_align: Clutter.ActorAlign.CENTER, }); - content.add_child(this._loadingLabel); - - this.contentLayout.add_child(content); - - // Buttons - this.setButtons([ - { - label: this._('Cancel'), - action: () => this.close(), - key: Clutter.KEY_Escape - }, - { - label: this._('Select'), - action: () => this._onSelect(), - default: true - } - ]); + box.add_child(label); - this._selectedProjectWidget = null; - this._selectedIssueWidget = null; - } - - _loadProjects() { - this._showLoading(this._('Loading projects...')); - - const url = this._settings.get_string('gitlab-url'); - const token = this._settings.get_string('gitlab-token'); - - const apiUrl = `${url}/api/v4/projects?membership=true&per_page=100&order_by=last_activity_at`; + item.set_child(box); - const message = Soup.Message.new('GET', apiUrl); - message.request_headers.append('PRIVATE-TOKEN', token); - - this._httpSession.send_and_read_async( - message, - GLib.PRIORITY_DEFAULT, - null, - (session, result) => { - try { - const bytes = session.send_and_read_finish(result); - const decoder = new TextDecoder('utf-8'); - const response = decoder.decode(bytes.get_data()); - - if (message.status_code === 200) { - this._projects = JSON.parse(response); - this._updateProjectList(); - this._hideLoading(); - } else { - this._showLoading(`${this._('Error')}: ${message.status_code}`); - } - } catch (e) { - this._showLoading(`${this._('Error')}: ${e.message}`); - } - } - ); - } - - _updateProjectList() { - this._projectList.destroy_all_children(); - - const searchText = this._projectSearchEntry.get_text().toLowerCase(); - let filteredProjects = searchText - ? this._projects.filter(p => p.name.toLowerCase().includes(searchText) || - p.path_with_namespace.toLowerCase().includes(searchText)) - : this._projects; - - filteredProjects = filteredProjects.sort((a, b) => - a.path_with_namespace.localeCompare(b.path_with_namespace) - ); + item.connect("clicked", () => { + this._selectProject(project, item); + }); - for (let project of filteredProjects) { - let item = new St.Button({ - style_class: 'gitlab-list-item', - style: 'padding: 8px; border-radius: 3px;', - can_focus: true, - track_hover: true, - x_expand: true, - x_align: Clutter.ActorAlign.FILL - }); - - // Create horizontal box for icon + text - let box = new St.BoxLayout({ - vertical: false, - x_align: Clutter.ActorAlign.START, - x_expand: true, - style: 'spacing: 8px;' - }); - - // Add project/group icon - let icon = new St.Icon({ - icon_name: 'folder-symbolic', - icon_size: 24, - style: 'width: 24px; height: 24px; border-radius: 3px;' - }); - - // Load project avatar using shared loader - const namespace = project.namespace || null; - this._avatarLoader.loadProjectAvatar(project.id, namespace, icon); - - box.add_child(icon); - - let label = new St.Label({ - text: project.path_with_namespace, - style: 'font-size: 12px;', - y_align: Clutter.ActorAlign.CENTER - }); - box.add_child(label); - - item.set_child(box); - - item.connect('clicked', () => { - this._selectProject(project, item); - }); - - this._projectList.add_child(item); - } + this._projectList.add_child(item); + } } _selectProject(project, widget) { - this._selectedProject = project; - - // Highlight selected project - if (this._selectedProjectWidget) { - this._selectedProjectWidget.style = 'padding: 8px; border-radius: 3px;'; - } - this._selectedProjectWidget = widget; - widget.style = 'padding: 8px; border-radius: 3px; background-color: #4a90d9;'; - - // Load issues for this project - this._loadIssues(project.id); + this._selectedProject = project; + + // Highlight selected project + if (this._selectedProjectWidget) { + this._selectedProjectWidget.style = "padding: 8px; border-radius: 3px;"; + } + this._selectedProjectWidget = widget; + widget.style = + "padding: 8px; border-radius: 3px; background-color: #4a90d9;"; + + // Load issues for this project + this._loadIssues(project.id); } _loadIssues(projectId) { - this._showLoading(this._('Loading issues...')); + this._showLoading(this._("Loading issues...")); + + const url = this._settings.get_string("gitlab-url"); + const token = this._settings.get_string("gitlab-token"); + + const apiUrl = `${url}/api/v4/projects/${projectId}/issues?state=opened&per_page=100`; + + const message = Soup.Message.new("GET", apiUrl); + message.request_headers.append("PRIVATE-TOKEN", token); + + this._httpSession.send_and_read_async( + message, + GLib.PRIORITY_DEFAULT, + null, + (session, result) => { + try { + const bytes = session.send_and_read_finish(result); + const decoder = new TextDecoder("utf-8"); + const response = decoder.decode(bytes.get_data()); + + if (message.status_code === 200) { + this._allIssues = JSON.parse(response); + this._updateIssueList(); + this._hideLoading(); + } else { + this._showLoading(`${this._("Error")}: ${message.status_code}`); + } + } catch (e) { + this._showLoading(`${this._("Error")}: ${e.message}`); + } + }, + ); + } - const url = this._settings.get_string('gitlab-url'); - const token = this._settings.get_string('gitlab-token'); + _updateIssueList() { + this._issueList.destroy_all_children(); + + const searchText = this._issueSearchEntry.get_text().toLowerCase(); + const filteredIssues = searchText + ? this._allIssues.filter( + (i) => + i.title.toLowerCase().includes(searchText) || + i.iid.toString().includes(searchText), + ) + : this._allIssues; + + for (let issue of filteredIssues) { + let item = new St.Button({ + style_class: "gitlab-list-item", + style: "padding: 8px; border-radius: 3px;", + can_focus: true, + track_hover: true, + x_expand: true, + x_align: Clutter.ActorAlign.FILL, + }); - const apiUrl = `${url}/api/v4/projects/${projectId}/issues?state=opened&per_page=100`; + let label = new St.Label({ + text: `#${issue.iid} - ${issue.title}`, + style: "font-size: 12px;", + x_align: Clutter.ActorAlign.START, + x_expand: true, + }); + item.set_child(label); - const message = Soup.Message.new('GET', apiUrl); - message.request_headers.append('PRIVATE-TOKEN', token); + item.connect("clicked", () => { + this._selectIssue(issue, item); + }); - this._httpSession.send_and_read_async( - message, - GLib.PRIORITY_DEFAULT, - null, - (session, result) => { - try { - const bytes = session.send_and_read_finish(result); - const decoder = new TextDecoder('utf-8'); - const response = decoder.decode(bytes.get_data()); - - if (message.status_code === 200) { - this._allIssues = JSON.parse(response); - this._updateIssueList(); - this._hideLoading(); - } else { - this._showLoading(`${this._('Error')}: ${message.status_code}`); - } - } catch (e) { - this._showLoading(`${this._('Error')}: ${e.message}`); - } - } - ); - } + this._issueList.add_child(item); + } - _updateIssueList() { - this._issueList.destroy_all_children(); - - const searchText = this._issueSearchEntry.get_text().toLowerCase(); - const filteredIssues = searchText - ? this._allIssues.filter(i => - i.title.toLowerCase().includes(searchText) || - i.iid.toString().includes(searchText)) - : this._allIssues; - - for (let issue of filteredIssues) { - let item = new St.Button({ - style_class: 'gitlab-list-item', - style: 'padding: 8px; border-radius: 3px;', - can_focus: true, - track_hover: true, - x_expand: true, - x_align: Clutter.ActorAlign.FILL - }); - - let label = new St.Label({ - text: `#${issue.iid} - ${issue.title}`, - style: 'font-size: 12px;', - x_align: Clutter.ActorAlign.START, - x_expand: true - }); - item.set_child(label); - - item.connect('clicked', () => { - this._selectIssue(issue, item); - }); - - this._issueList.add_child(item); - } - - if (filteredIssues.length === 0) { - let emptyLabel = new St.Label({ - text: this._('No issues found'), - style: 'padding: 20px; font-style: italic; color: #999;' - }); - this._issueList.add_child(emptyLabel); - } + if (filteredIssues.length === 0) { + let emptyLabel = new St.Label({ + text: this._("No issues found"), + style: "padding: 20px; font-style: italic; color: #999;", + }); + this._issueList.add_child(emptyLabel); + } } _selectIssue(issue, widget) { - this._selectedIssue = issue; - - // Highlight selected issue - if (this._selectedIssueWidget) { - this._selectedIssueWidget.style = 'padding: 8px; border-radius: 3px;'; - } - this._selectedIssueWidget = widget; - widget.style = 'padding: 8px; border-radius: 3px; background-color: #4a90d9;'; + this._selectedIssue = issue; + + // Highlight selected issue + if (this._selectedIssueWidget) { + this._selectedIssueWidget.style = "padding: 8px; border-radius: 3px;"; + } + this._selectedIssueWidget = widget; + widget.style = + "padding: 8px; border-radius: 3px; background-color: #4a90d9;"; } _filterProjects() { - this._updateProjectList(); + this._updateProjectList(); } _filterIssues() { - this._updateIssueList(); + this._updateIssueList(); } _showLoading(text) { - this._loadingLabel.text = text; - this._loadingLabel.show(); + this._loadingLabel.text = text; + this._loadingLabel.show(); } _hideLoading() { - this._loadingLabel.hide(); + this._loadingLabel.hide(); } _onSelect() { - if (!this._selectedProject || !this._selectedIssue) { - Main.notify(this._('GitLab Time Tracking'), this._('Please select a project and an issue')); - return; - } + if (!this._selectedProject || !this._selectedIssue) { + Main.notify( + this._("GitLab Time Tracking"), + this._("Please select a project and an issue"), + ); + return; + } - this._onSelected(this._selectedProject, this._selectedIssue); - this.close(); + this._onSelected(this._selectedProject, this._selectedIssue); + this.close(); } destroy() { - this._httpSession.abort(); - super.destroy(); + this._httpSession.abort(); + super.destroy(); } -}); + }, +); From 97f5c38945c14c3332adf6e158b90594d77d1174 Mon Sep 17 00:00:00 2001 From: David Grieser Date: Thu, 15 Jan 2026 15:20:18 +0100 Subject: [PATCH 2/3] style(issue selector): reformat imports, use single quotes, simplify UI building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - switched all double‑quoted strings to single quotes for consistency - aligned import statements and removed unnecessary spacing - condensed UI construction code with clearer layout definitions - refactored project and issue loading logic for better readability - added inline comments and minor variable renames without altering functionality - updated button definitions and loading indicator handling to match new style conventions --- issueSelector.js | 710 +++++++++++++++++++++++------------------------ 1 file changed, 345 insertions(+), 365 deletions(-) diff --git a/issueSelector.js b/issueSelector.js index 30de96a..06b5aad 100644 --- a/issueSelector.js +++ b/issueSelector.js @@ -1,425 +1,405 @@ -import GObject from "gi://GObject"; -import St from "gi://St"; -import Clutter from "gi://Clutter"; -import GLib from "gi://GLib"; -import Soup from "gi://Soup"; -import * as ModalDialog from "resource:///org/gnome/shell/ui/modalDialog.js"; -import * as Main from "resource:///org/gnome/shell/ui/main.js"; +import GObject from 'gi://GObject'; +import St from 'gi://St'; +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import Soup from 'gi://Soup'; +import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; -import { AvatarLoader } from "./avatarLoader.js"; +import {AvatarLoader} from './avatarLoader.js'; export const IssueSelectorDialog = GObject.registerClass( - class IssueSelectorDialog extends ModalDialog.ModalDialog { +class IssueSelectorDialog extends ModalDialog.ModalDialog { _init(settings, gettext, onSelected) { - super._init({ styleClass: "gitlab-issue-selector-dialog" }); - - this._settings = settings; - this._ = gettext; - this._onSelected = onSelected; - this._httpSession = new Soup.Session(); - this._avatarLoader = new AvatarLoader(settings, this._httpSession); - this._projects = []; - this._allIssues = []; - this._selectedProject = null; - - this._buildUI(); - this._loadProjects(); + super._init({ styleClass: 'gitlab-issue-selector-dialog' }); + + this._settings = settings; + this._ = gettext; + this._onSelected = onSelected; + this._httpSession = new Soup.Session(); + this._avatarLoader = new AvatarLoader(settings, this._httpSession); + this._projects = []; + this._allIssues = []; + this._selectedProject = null; + + this._buildUI(); + this._loadProjects(); } _buildUI() { - // Main container - let content = new St.BoxLayout({ - vertical: true, - style_class: "gitlab-selector-content", - style: "min-width: 600px; min-height: 500px;", - }); - - // Title - let title = new St.Label({ - text: this._("Select a project and an issue"), - style_class: "gitlab-selector-title", - style: "font-size: 16px; font-weight: bold; margin-bottom: 10px;", - }); - content.add_child(title); - - // Project section - let projectBox = new St.BoxLayout({ - vertical: true, - style: "margin-bottom: 20px;", - }); - - let projectLabel = new St.Label({ - text: this._("Project"), - style: "font-weight: bold; margin-bottom: 5px;", - }); - projectBox.add_child(projectLabel); - - // Project search - this._projectSearchEntry = new St.Entry({ - hint_text: this._("Search project..."), - can_focus: true, - track_hover: true, - style: "margin-bottom: 5px;", - }); - this._projectSearchEntry.clutter_text.connect("text-changed", () => { - this._filterProjects(); - }); - projectBox.add_child(this._projectSearchEntry); - - // Project list container with scrolling - let projectScrollView = new St.ScrollView({ - style: "border: 1px solid #555; border-radius: 5px; height: 180px;", - hscrollbar_policy: St.PolicyType.NEVER, - vscrollbar_policy: St.PolicyType.AUTOMATIC, - }); - - this._projectList = new St.BoxLayout({ - vertical: true, - style_class: "gitlab-project-list", - }); - projectScrollView.add_child(this._projectList); - projectBox.add_child(projectScrollView); - - content.add_child(projectBox); - - // Issue section - let issueBox = new St.BoxLayout({ - vertical: true, - style: "margin-bottom: 20px;", - }); - - let issueLabel = new St.Label({ - text: this._("Issue"), - style: "font-weight: bold; margin-bottom: 5px;", - }); - issueBox.add_child(issueLabel); - - // Issue search - this._issueSearchEntry = new St.Entry({ - hint_text: this._("Search issue..."), - can_focus: true, - track_hover: true, - style: "margin-bottom: 5px;", - }); - this._issueSearchEntry.clutter_text.connect("text-changed", () => { - this._filterIssues(); - }); - issueBox.add_child(this._issueSearchEntry); - - // Issue list container with scrolling - let issueScrollView = new St.ScrollView({ - style: "border: 1px solid #555; border-radius: 5px; height: 180px;", - hscrollbar_policy: St.PolicyType.NEVER, - vscrollbar_policy: St.PolicyType.AUTOMATIC, - }); - - this._issueList = new St.BoxLayout({ - vertical: true, - style_class: "gitlab-issue-list", - }); - issueScrollView.add_child(this._issueList); - issueBox.add_child(issueScrollView); - - content.add_child(issueBox); - - // Loading indicator - this._loadingLabel = new St.Label({ - text: this._("Loading..."), - style: "font-style: italic; color: #999;", - }); - content.add_child(this._loadingLabel); - - this.contentLayout.add_child(content); - - // Buttons - this.setButtons([ - { - label: this._("Cancel"), - action: () => this.close(), - key: Clutter.KEY_Escape, - }, - { - label: this._("Select"), - action: () => this._onSelect(), - default: true, - }, - ]); - - this._selectedProjectWidget = null; - this._selectedIssueWidget = null; - } + // Main container + let content = new St.BoxLayout({ + vertical: true, + style_class: 'gitlab-selector-content', + style: 'min-width: 600px; min-height: 500px;' + }); - _loadProjects() { - this._showLoading(this._("Loading projects...")); + // Title + let title = new St.Label({ + text: this._('Select a project and an issue'), + style_class: 'gitlab-selector-title', + style: 'font-size: 16px; font-weight: bold; margin-bottom: 10px;' + }); + content.add_child(title); - try { - const url = this._settings.get_string("gitlab-url"); - const token = this._settings.get_string("gitlab-token"); + // Project section + let projectBox = new St.BoxLayout({ + vertical: true, + style: 'margin-bottom: 20px;' + }); - log(`GitLab Issue Selector: Fetching projects from URL: ${url}`); - log( - `GitLab Issue Selector: Using token length: ${token ? token.length : 0}`, - ); + let projectLabel = new St.Label({ + text: this._('Project'), + style: 'font-weight: bold; margin-bottom: 5px;' + }); + projectBox.add_child(projectLabel); + + // Project search + this._projectSearchEntry = new St.Entry({ + hint_text: this._('Search project...'), + can_focus: true, + track_hover: true, + style: 'margin-bottom: 5px;' + }); + this._projectSearchEntry.clutter_text.connect('text-changed', () => { + this._filterProjects(); + }); + projectBox.add_child(this._projectSearchEntry); - const apiUrl = `${url}/api/v4/projects?membership=true&per_page=100&order_by=last_activity_at`; + // Project list container with scrolling + let projectScrollView = new St.ScrollView({ + style: 'border: 1px solid #555; border-radius: 5px; height: 180px;', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC + }); - const message = Soup.Message.new("GET", apiUrl); - message.request_headers.append("PRIVATE-TOKEN", token); + this._projectList = new St.BoxLayout({ + vertical: true, + style_class: 'gitlab-project-list' + }); + projectScrollView.add_child(this._projectList); + projectBox.add_child(projectScrollView); - this._httpSession.send_and_read_async( - message, - GLib.PRIORITY_DEFAULT, - null, - (session, result) => { - try { - const bytes = session.send_and_read_finish(result); - const decoder = new TextDecoder("utf-8"); - const response = decoder.decode(bytes.get_data()); - - log( - `GitLab Issue Selector: API response status code: ${message.status_code}`, - ); - log( - `GitLab Issue Selector: API response length: ${response.length}`, - ); - - if (message.status_code === 200) { - log("GitLab Issue Selector: Parsing projects JSON..."); - this._projects = JSON.parse(response); - log( - `GitLab Issue Selector: Loaded ${this._projects.length} projects`, - ); - this._updateProjectList(); - this._hideLoading(); - } else { - log( - `GitLab Issue Selector: Error fetching projects: ${message.status_code}`, - ); - log(`GitLab Issue Selector: Response body: ${response}`); - this._showLoading(`${this._("Error")}: ${message.status_code}`); - } - } catch (e) { - log( - `GitLab Issue Selector: Error parsing projects: ${e.message}`, - ); - log(`GitLab Issue Selector: Full error: ${e.stack}`); - log(`GitLab Issue Selector: Response: ${response}`); - this._showLoading(`${this._("Error")}: ${e.message}`); - } - }, - ); - } catch (e) { - log(`GitLab Issue Selector: Error loading projects: ${e.message}`); - log(`GitLab Issue Selector: Full error: ${e.stack}`); - this._showLoading(`${this._("Error")}: ${e.message}`); - } - } + content.add_child(projectBox); - _updateProjectList() { - this._projectList.destroy_all_children(); - - const searchText = this._projectSearchEntry.get_text().toLowerCase(); - let filteredProjects = searchText - ? this._projects.filter( - (p) => - p.name.toLowerCase().includes(searchText) || - p.path_with_namespace.toLowerCase().includes(searchText), - ) - : this._projects; - - filteredProjects = filteredProjects.sort((a, b) => - a.path_with_namespace.localeCompare(b.path_with_namespace), - ); - - for (let project of filteredProjects) { - let item = new St.Button({ - style_class: "gitlab-list-item", - style: "padding: 8px; border-radius: 3px;", - can_focus: true, - track_hover: true, - x_expand: true, - x_align: Clutter.ActorAlign.FILL, + // Issue section + let issueBox = new St.BoxLayout({ + vertical: true, + style: 'margin-bottom: 20px;' }); - // Create horizontal box for icon + text - let box = new St.BoxLayout({ - vertical: false, - x_align: Clutter.ActorAlign.START, - x_expand: true, - style: "spacing: 8px;", + let issueLabel = new St.Label({ + text: this._('Issue'), + style: 'font-weight: bold; margin-bottom: 5px;' + }); + issueBox.add_child(issueLabel); + + // Issue search + this._issueSearchEntry = new St.Entry({ + hint_text: this._('Search issue...'), + can_focus: true, + track_hover: true, + style: 'margin-bottom: 5px;' }); + this._issueSearchEntry.clutter_text.connect('text-changed', () => { + this._filterIssues(); + }); + issueBox.add_child(this._issueSearchEntry); - // Add project/group icon - let icon = new St.Icon({ - icon_name: "folder-symbolic", - icon_size: 24, - style: "width: 24px; height: 24px; border-radius: 3px;", + // Issue list container with scrolling + let issueScrollView = new St.ScrollView({ + style: 'border: 1px solid #555; border-radius: 5px; height: 180px;', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.AUTOMATIC }); - // Load project avatar using shared loader - const namespace = project.namespace || null; - this._avatarLoader.loadProjectAvatar(project.id, namespace, icon); + this._issueList = new St.BoxLayout({ + vertical: true, + style_class: 'gitlab-issue-list' + }); + issueScrollView.add_child(this._issueList); + issueBox.add_child(issueScrollView); - box.add_child(icon); + content.add_child(issueBox); - let label = new St.Label({ - text: project.path_with_namespace, - style: "font-size: 12px;", - y_align: Clutter.ActorAlign.CENTER, + // Loading indicator + this._loadingLabel = new St.Label({ + text: this._('Loading...'), + style: 'font-style: italic; color: #999;' }); - box.add_child(label); + content.add_child(this._loadingLabel); + + this.contentLayout.add_child(content); + + // Buttons + this.setButtons([ + { + label: this._('Cancel'), + action: () => this.close(), + key: Clutter.KEY_Escape + }, + { + label: this._('Select'), + action: () => this._onSelect(), + default: true + } + ]); - item.set_child(box); + this._selectedProjectWidget = null; + this._selectedIssueWidget = null; + } - item.connect("clicked", () => { - this._selectProject(project, item); - }); + _loadProjects() { + this._showLoading(this._('Loading projects...')); + + try { + const url = this._settings.get_string('gitlab-url'); + const token = this._settings.get_string('gitlab-token'); + + log(`GitLab Issue Selector: Fetching projects from URL: ${url}`); + log(`GitLab Issue Selector: Using token length: ${token ? token.length : 0}`); + + const apiUrl = `${url}/api/v4/projects?membership=true&per_page=100&order_by=last_activity_at`; + + const message = Soup.Message.new('GET', apiUrl); + message.request_headers.append('PRIVATE-TOKEN', token); + + this._httpSession.send_and_read_async( + message, + GLib.PRIORITY_DEFAULT, + null, + (session, result) => { + let response = ''; + try { + const bytes = session.send_and_read_finish(result); + const decoder = new TextDecoder('utf-8'); + response = decoder.decode(bytes.get_data()); + + log(`GitLab Issue Selector: API response status code: ${message.status_code}`); + log(`GitLab Issue Selector: API response length: ${response.length}`); + + if (message.status_code === 200) { + log('GitLab Issue Selector: Parsing projects JSON...'); + this._projects = JSON.parse(response); + log(`GitLab Issue Selector: Loaded ${this._projects.length} projects`); + this._updateProjectList(); + this._hideLoading(); + } else { + log(`GitLab Issue Selector: Error fetching projects: ${message.status_code}`); + log(`GitLab Issue Selector: Response body: ${response}`); + this._showLoading(`${this._('Error')}: ${message.status_code}`); + } + } catch (e) { + log(`GitLab Issue Selector: Error parsing projects: ${e.message}`); + log(`GitLab Issue Selector: Full error: ${e.stack}`); + if (response) { + log(`GitLab Issue Selector: Response: ${response}`); + } + this._showLoading(`${this._('Error')}: ${e.message}`); + } + } + ); + } catch (e) { + log(`GitLab Issue Selector: Error loading projects: ${e.message}`); + log(`GitLab Issue Selector: Full error: ${e.stack}`); + this._showLoading(`${this._('Error')}: ${e.message}`); + } + } + + _updateProjectList() { + this._projectList.destroy_all_children(); + + const searchText = this._projectSearchEntry.get_text().toLowerCase(); + let filteredProjects = searchText + ? this._projects.filter(p => p.name.toLowerCase().includes(searchText) || + p.path_with_namespace.toLowerCase().includes(searchText)) + : this._projects; - this._projectList.add_child(item); - } + filteredProjects = filteredProjects.sort((a, b) => + a.path_with_namespace.localeCompare(b.path_with_namespace) + ); + + for (let project of filteredProjects) { + let item = new St.Button({ + style_class: 'gitlab-list-item', + style: 'padding: 8px; border-radius: 3px;', + can_focus: true, + track_hover: true, + x_expand: true, + x_align: Clutter.ActorAlign.FILL + }); + + // Create horizontal box for icon + text + let box = new St.BoxLayout({ + vertical: false, + x_align: Clutter.ActorAlign.START, + x_expand: true, + style: 'spacing: 8px;' + }); + + // Add project/group icon + let icon = new St.Icon({ + icon_name: 'folder-symbolic', + icon_size: 24, + style: 'width: 24px; height: 24px; border-radius: 3px;' + }); + + // Load project avatar using shared loader + const namespace = project.namespace || null; + this._avatarLoader.loadProjectAvatar(project.id, namespace, icon); + + box.add_child(icon); + + let label = new St.Label({ + text: project.path_with_namespace, + style: 'font-size: 12px;', + y_align: Clutter.ActorAlign.CENTER + }); + box.add_child(label); + + item.set_child(box); + + item.connect('clicked', () => { + this._selectProject(project, item); + }); + + this._projectList.add_child(item); + } } _selectProject(project, widget) { - this._selectedProject = project; - - // Highlight selected project - if (this._selectedProjectWidget) { - this._selectedProjectWidget.style = "padding: 8px; border-radius: 3px;"; - } - this._selectedProjectWidget = widget; - widget.style = - "padding: 8px; border-radius: 3px; background-color: #4a90d9;"; - - // Load issues for this project - this._loadIssues(project.id); + this._selectedProject = project; + + // Highlight selected project + if (this._selectedProjectWidget) { + this._selectedProjectWidget.style = 'padding: 8px; border-radius: 3px;'; + } + this._selectedProjectWidget = widget; + widget.style = 'padding: 8px; border-radius: 3px; background-color: #4a90d9;'; + + // Load issues for this project + this._loadIssues(project.id); } _loadIssues(projectId) { - this._showLoading(this._("Loading issues...")); - - const url = this._settings.get_string("gitlab-url"); - const token = this._settings.get_string("gitlab-token"); - - const apiUrl = `${url}/api/v4/projects/${projectId}/issues?state=opened&per_page=100`; - - const message = Soup.Message.new("GET", apiUrl); - message.request_headers.append("PRIVATE-TOKEN", token); - - this._httpSession.send_and_read_async( - message, - GLib.PRIORITY_DEFAULT, - null, - (session, result) => { - try { - const bytes = session.send_and_read_finish(result); - const decoder = new TextDecoder("utf-8"); - const response = decoder.decode(bytes.get_data()); - - if (message.status_code === 200) { - this._allIssues = JSON.parse(response); - this._updateIssueList(); - this._hideLoading(); - } else { - this._showLoading(`${this._("Error")}: ${message.status_code}`); - } - } catch (e) { - this._showLoading(`${this._("Error")}: ${e.message}`); - } - }, - ); - } + this._showLoading(this._('Loading issues...')); - _updateIssueList() { - this._issueList.destroy_all_children(); - - const searchText = this._issueSearchEntry.get_text().toLowerCase(); - const filteredIssues = searchText - ? this._allIssues.filter( - (i) => - i.title.toLowerCase().includes(searchText) || - i.iid.toString().includes(searchText), - ) - : this._allIssues; - - for (let issue of filteredIssues) { - let item = new St.Button({ - style_class: "gitlab-list-item", - style: "padding: 8px; border-radius: 3px;", - can_focus: true, - track_hover: true, - x_expand: true, - x_align: Clutter.ActorAlign.FILL, - }); + const url = this._settings.get_string('gitlab-url'); + const token = this._settings.get_string('gitlab-token'); - let label = new St.Label({ - text: `#${issue.iid} - ${issue.title}`, - style: "font-size: 12px;", - x_align: Clutter.ActorAlign.START, - x_expand: true, - }); - item.set_child(label); + const apiUrl = `${url}/api/v4/projects/${projectId}/issues?state=opened&per_page=100`; - item.connect("clicked", () => { - this._selectIssue(issue, item); - }); + const message = Soup.Message.new('GET', apiUrl); + message.request_headers.append('PRIVATE-TOKEN', token); - this._issueList.add_child(item); - } + this._httpSession.send_and_read_async( + message, + GLib.PRIORITY_DEFAULT, + null, + (session, result) => { + try { + const bytes = session.send_and_read_finish(result); + const decoder = new TextDecoder('utf-8'); + const response = decoder.decode(bytes.get_data()); + + if (message.status_code === 200) { + this._allIssues = JSON.parse(response); + this._updateIssueList(); + this._hideLoading(); + } else { + this._showLoading(`${this._('Error')}: ${message.status_code}`); + } + } catch (e) { + this._showLoading(`${this._('Error')}: ${e.message}`); + } + } + ); + } - if (filteredIssues.length === 0) { - let emptyLabel = new St.Label({ - text: this._("No issues found"), - style: "padding: 20px; font-style: italic; color: #999;", - }); - this._issueList.add_child(emptyLabel); - } + _updateIssueList() { + this._issueList.destroy_all_children(); + + const searchText = this._issueSearchEntry.get_text().toLowerCase(); + const filteredIssues = searchText + ? this._allIssues.filter(i => + i.title.toLowerCase().includes(searchText) || + i.iid.toString().includes(searchText)) + : this._allIssues; + + for (let issue of filteredIssues) { + let item = new St.Button({ + style_class: 'gitlab-list-item', + style: 'padding: 8px; border-radius: 3px;', + can_focus: true, + track_hover: true, + x_expand: true, + x_align: Clutter.ActorAlign.FILL + }); + + let label = new St.Label({ + text: `#${issue.iid} - ${issue.title}`, + style: 'font-size: 12px;', + x_align: Clutter.ActorAlign.START, + x_expand: true + }); + item.set_child(label); + + item.connect('clicked', () => { + this._selectIssue(issue, item); + }); + + this._issueList.add_child(item); + } + + if (filteredIssues.length === 0) { + let emptyLabel = new St.Label({ + text: this._('No issues found'), + style: 'padding: 20px; font-style: italic; color: #999;' + }); + this._issueList.add_child(emptyLabel); + } } _selectIssue(issue, widget) { - this._selectedIssue = issue; - - // Highlight selected issue - if (this._selectedIssueWidget) { - this._selectedIssueWidget.style = "padding: 8px; border-radius: 3px;"; - } - this._selectedIssueWidget = widget; - widget.style = - "padding: 8px; border-radius: 3px; background-color: #4a90d9;"; + this._selectedIssue = issue; + + // Highlight selected issue + if (this._selectedIssueWidget) { + this._selectedIssueWidget.style = 'padding: 8px; border-radius: 3px;'; + } + this._selectedIssueWidget = widget; + widget.style = 'padding: 8px; border-radius: 3px; background-color: #4a90d9;'; } _filterProjects() { - this._updateProjectList(); + this._updateProjectList(); } _filterIssues() { - this._updateIssueList(); + this._updateIssueList(); } _showLoading(text) { - this._loadingLabel.text = text; - this._loadingLabel.show(); + this._loadingLabel.text = text; + this._loadingLabel.show(); } _hideLoading() { - this._loadingLabel.hide(); + this._loadingLabel.hide(); } _onSelect() { - if (!this._selectedProject || !this._selectedIssue) { - Main.notify( - this._("GitLab Time Tracking"), - this._("Please select a project and an issue"), - ); - return; - } + if (!this._selectedProject || !this._selectedIssue) { + Main.notify(this._('GitLab Time Tracking'), this._('Please select a project and an issue')); + return; + } - this._onSelected(this._selectedProject, this._selectedIssue); - this.close(); + this._onSelected(this._selectedProject, this._selectedIssue); + this.close(); } destroy() { - this._httpSession.abort(); - super.destroy(); + this._httpSession.abort(); + super.destroy(); } - }, -); +}); From 003c12567e77872592ad7ec7de8b33717e22d1e1 Mon Sep 17 00:00:00 2001 From: David Grieser Date: Thu, 15 Jan 2026 15:36:42 +0100 Subject: [PATCH 3/3] fix(auth): prompt user to configure server on auth errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Notify users and display a loading message when GitLab API returns 401 or 403, guiding them to set the server URL and token. - Clear and refresh project and issue lists in the selector and report dialogs to reflect the missing configuration. - Extend time‑sending component to show an error notification on authentication failures. --- extension.js | 2 ++ issueSelector.js | 12 ++++++++++++ reportDialog.js | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/extension.js b/extension.js index a83f466..4d99ce7 100644 --- a/extension.js +++ b/extension.js @@ -357,6 +357,8 @@ class GitLabIssuesIndicator extends PanelMenu.Button { try { if (message.status_code === 201 || message.status_code === 200) { Main.notify(this._('GitLab Issues Timer'), `${this._('Time sent')}: ${duration} ${this._('on issue')} #${this._selectedIssue.iid}`); + } else if (message.status_code === 401 || message.status_code === 403) { + Main.notify(this._('Error'), this._('Please configure the server URL and token in preferences')); } else { Main.notify(this._('Error'), `${this._('Unable to send time')}: ${message.status_code}`); } diff --git a/issueSelector.js b/issueSelector.js index 06b5aad..5e10f71 100644 --- a/issueSelector.js +++ b/issueSelector.js @@ -184,6 +184,12 @@ class IssueSelectorDialog extends ModalDialog.ModalDialog { log(`GitLab Issue Selector: Loaded ${this._projects.length} projects`); this._updateProjectList(); this._hideLoading(); + } else if (message.status_code === 401 || message.status_code === 403) { + const authMessage = this._('Please configure the server URL and token in preferences'); + this._projects = []; + this._updateProjectList(); + this._showLoading(authMessage); + Main.notify(this._('Error'), authMessage); } else { log(`GitLab Issue Selector: Error fetching projects: ${message.status_code}`); log(`GitLab Issue Selector: Response body: ${response}`); @@ -306,6 +312,12 @@ class IssueSelectorDialog extends ModalDialog.ModalDialog { this._allIssues = JSON.parse(response); this._updateIssueList(); this._hideLoading(); + } else if (message.status_code === 401 || message.status_code === 403) { + const authMessage = this._('Please configure the server URL and token in preferences'); + this._allIssues = []; + this._updateIssueList(); + this._showLoading(authMessage); + Main.notify(this._('Error'), authMessage); } else { this._showLoading(`${this._('Error')}: ${message.status_code}`); } diff --git a/reportDialog.js b/reportDialog.js index 7cd8a66..0142862 100644 --- a/reportDialog.js +++ b/reportDialog.js @@ -299,6 +299,10 @@ class ReportDialog extends ModalDialog.ModalDialog { this._selectProject(project, null); } } + } else if (message.status_code === 401 || message.status_code === 403) { + const authMessage = this._('Please configure the server URL and token in preferences'); + this._projects = []; + Main.notify(this._('Error'), authMessage); } else { Main.notify(this._('Error'), `${this._('Unable to load projects')}: ${message.status_code}`); } @@ -448,6 +452,9 @@ class ReportDialog extends ModalDialog.ModalDialog { if (message.status_code === 200) { const issues = JSON.parse(response); this._processReportData(issues); + } else if (message.status_code === 401 || message.status_code === 403) { + this._hideLoading(); + Main.notify(this._('Error'), this._('Please configure the server URL and token in preferences')); } else { this._hideLoading(); Main.notify(this._('Error'), `${this._('Unable to load report')}: ${message.status_code}`);