From c651ae6d7a11c77a607543a1afae863b20b6d174 Mon Sep 17 00:00:00 2001 From: Matias Linares Date: Sun, 22 Sep 2024 15:37:36 -0300 Subject: Pull request description and comments working --- kodereviewer/app.py | 8 +-- kodereviewer/data.py | 115 +++++++++++++++++++++++++++-------- kodereviewer/mdconverter.py | 30 --------- kodereviewer/models/__init__.py | 9 +++ kodereviewer/models/comments.py | 89 +++++++++++++++++++++++++++ kodereviewer/models/project.py | 92 ++++++++++++++++++++++++++++ kodereviewer/models/pull_request.py | 75 +++++++++++++++++++++++ kodereviewer/network_manager.py | 25 +++++++- kodereviewer/project.py | 13 +++- kodereviewer/project_model.py | 92 ---------------------------- kodereviewer/pull_request_model.py | 66 -------------------- kodereviewer/qml/CommentDelegate.qml | 77 +++++++++++++++++++++++ kodereviewer/qml/Main.qml | 27 ++++---- kodereviewer/qml/ProjectListPage.qml | 4 +- kodereviewer/qml/PullRequestPage.qml | 84 +++++++++++++++++++++++++ 15 files changed, 570 insertions(+), 236 deletions(-) delete mode 100644 kodereviewer/mdconverter.py create mode 100644 kodereviewer/models/__init__.py create mode 100644 kodereviewer/models/comments.py create mode 100644 kodereviewer/models/project.py create mode 100644 kodereviewer/models/pull_request.py delete mode 100644 kodereviewer/project_model.py delete mode 100644 kodereviewer/pull_request_model.py create mode 100644 kodereviewer/qml/CommentDelegate.qml create mode 100644 kodereviewer/qml/PullRequestPage.qml (limited to 'kodereviewer') diff --git a/kodereviewer/app.py b/kodereviewer/app.py index a2eb5f4..430b5cf 100644 --- a/kodereviewer/app.py +++ b/kodereviewer/app.py @@ -9,10 +9,8 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtCore import QUrl, QByteArray from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType -from kodereviewer.mdconverter import MdConverter -from kodereviewer.project_model import ProjectModel -from kodereviewer.pull_request_model import PullRequestModel from kodereviewer.network_manager import NetworkManager +from kodereviewer.models import CommentModel, PullRequestModel, ProjectModel def main(): @@ -34,8 +32,9 @@ def main(): if not os.environ.get("QT_QUICK_CONTROLS_STYLE"): os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop" - qmlRegisterType(MdConverter, "org.deprecated.kodereviewer", 1, 0, "MdConverter") qmlRegisterType(ProjectModel, "org.deprecated.kodereviewer", 1, 0, "ProjectModel") + + qmlRegisterType(CommentModel, "org.deprecated.kodereviewer", 1, 0, "CommentModel") qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel") qmlRegisterType(NetworkManager, "org.deprecated.kodereviewer", 1, 0, "NetworkManager") @@ -43,6 +42,7 @@ def main(): engine.rootContext().setContextObject(localized_context) base_path = os.path.abspath(os.path.dirname(__file__)) engine.addImportPath(f"file://{base_path}/qml") + engine.addImportPath(f"file://{base_path}/qml/delegates") url = QUrl(f"file://{base_path}/qml/Main.qml") engine.load(url) diff --git a/kodereviewer/data.py b/kodereviewer/data.py index 7d3b73a..eb86097 100644 --- a/kodereviewer/data.py +++ b/kodereviewer/data.py @@ -1,8 +1,9 @@ from datetime import datetime from enum import Enum -from typing import Any, Optional +import json +from typing import Any, Optional, Self -from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug +from PySide6.QtCore import QByteArray, QObject, QSettings, Signal, Slot, Property, qDebug from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory from PySide6.QtQml import QmlElement @@ -29,6 +30,23 @@ class Label(QObject): self.description = data['description'] +class Comment(QObject): + body: str + reactions: dict[str, int] + + created_at: datetime + updated_at: datetime + + user: User + + def __init__(self, data: dict[str, Any]): + super().__init__() + self.body = data['body'] + self.reactions = data['reactions'] + self.created_at = datetime.fromisoformat(data['created_at']) + self.updated_at = datetime.fromisoformat(data['updated_at']) + self.user = User(data['user']) + class State(Enum): OPEN = 'open' @@ -38,33 +56,82 @@ class State(Enum): class PullRequest(QObject): - number: int - title: str - state: State - url: str - body: str | None - created_at: datetime - updated_at: datetime + _number: int + _title: str + _state: State + _url: str + _body: str | None + _created_at: datetime + _updated_at: datetime - user: User - assignee: User | None - labels: list[str] + _user: User + _assignee: User | None + _labels: list[Label] + _comments: list[Comment] + + commentsLoaded = Signal() def __init__(self, data: dict[str, Any]): super().__init__() - self.number = data['number'] - self.state = data['state'] - self.title = data['title'] - self.url = data['html_url'] - self.body = data['body'] - self.created_at = datetime.fromisoformat(data['created_at']) - self.updated_at = datetime.fromisoformat(data['updated_at']) - - self.user = User(data['user']) - self.assignee = None + self._number = data['number'] + self._state = data['state'] + self._title = data['title'] + self._url = data['html_url'] + self._body = data['body'] + self._created_at = datetime.fromisoformat(data['created_at']) + self._updated_at = datetime.fromisoformat(data['updated_at']) + + self._user = User(data['user']) + self._assignee = None if data['assignee'] is not None: - self.assignee = User(data['assignee']) + self._assignee = User(data['assignee']) + + self._labels = [Label(label) for label in data['labels']] + + # At first this is empty until it's updated with a request + self._comments = [] + + def __eq__(self, other: object) -> bool: + if isinstance(other, PullRequest): + return self._number == other._number + return False + + def __str__(self) -> str: + return self._title + + @Property(int, constant=True) + def number(self) -> int: + return self._number + + @Property(str, constant=True) + def title(self) -> str: + return self._title + + @Property(str, constant=True) + def state(self) -> str: + return self._state.value + + @Property(str, constant=True) + def url(self) -> str: + return self._url + + @Property(str, constant=True) + def body(self) -> str: + if self._body is not None: + return self._body + return '' + + @Property(datetime, constant=True) + def created_at(self) -> datetime: + return self._created_at - labels = [Label(label) for label in data['labels']] + @Property(str, constant=True) + def username(self) -> str: + return self._user.username + def load_comments(self, response: QByteArray) -> None: + data = json.loads(response.toStdString()) + self._comments = [Comment(comment) for comment in data] + print(f'emiting new comments {len(self._comments)}') + self.commentsLoaded.emit() diff --git a/kodereviewer/mdconverter.py b/kodereviewer/mdconverter.py deleted file mode 100644 index 1d0a653..0000000 --- a/kodereviewer/mdconverter.py +++ /dev/null @@ -1,30 +0,0 @@ -from markdown import markdown -from PySide6.QtCore import QObject, Signal, Slot, Property -from PySide6.QtQml import QmlElement - -QML_IMPORT_NAME = "org.deprecated.kodereviewer" -QML_IMPORT_MAJOR_VERSION = 1 - - -@QmlElement -class MdConverter(QObject): - """A simple markdown converter""" - - sourceTextChanged = Signal() - - def __init__(self, _source_text=""): - super().__init__() - self._source_text = _source_text - - @Property(str, notify=sourceTextChanged) - def sourceText(self): - return self._source_text - - @sourceText.setter - def sourceText(self, val: str): - self._source_text = val - self.sourceTextChanged.emit() - - @Slot(result=str) - def mdFormat(self): - return markdown(self._source_text) diff --git a/kodereviewer/models/__init__.py b/kodereviewer/models/__init__.py new file mode 100644 index 0000000..853b618 --- /dev/null +++ b/kodereviewer/models/__init__.py @@ -0,0 +1,9 @@ +from kodereviewer.models.comments import CommentModel +from kodereviewer.models.project import ProjectModel +from kodereviewer.models.pull_request import PullRequestModel + +__all__ = [ + 'CommentModel', + 'ProjectModel', + 'PullRequestModel', +] diff --git a/kodereviewer/models/comments.py b/kodereviewer/models/comments.py new file mode 100644 index 0000000..06de3d7 --- /dev/null +++ b/kodereviewer/models/comments.py @@ -0,0 +1,89 @@ +from enum import auto, IntEnum +from typing import Optional + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.data import Comment, PullRequest + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class CommentModel(QAbstractListModel): + + _pull_request: Optional[PullRequest] + + class Roles(IntEnum): + Body = Qt.ItemDataRole.UserRole + 1 + CreatedAt = auto() + UpdatedAt = auto() + Username = auto() + AvatarUrl = auto() + + + def __init__(self): + super().__init__() + self._pull_request = None + + def get_pull_request(self) -> Optional[PullRequest]: + return self._pull_request + + def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: + if pull_request is None: + return + + if self._pull_request is not None and self._pull_request == pull_request: + return + + print(f"Setting up pull request to {pull_request}") + self.beginResetModel() + self._pull_request = pull_request + self._pull_request.commentsLoaded.connect(self._reset_model) + self.endResetModel() + self.pullRequestChanged.emit() + + pullRequestChanged = Signal() + pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, + notify=pullRequestChanged) + + def _reset_model(self) -> None: + self.beginResetModel() + self.endResetModel() + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + if self._pull_request is None: + return None + + comment: Comment = self._pull_request._comments[index.row()] + + if role == self.Roles.Body: + return comment.body + if role == self.Roles.CreatedAt: + return comment.created_at.strftime("%Y-%M-%d %H:%m") + if role == self.Roles.UpdatedAt: + return comment.updated_at.strftime("%Y-%M-%d %H:%m") + if role == self.Roles.Username: + return comment.user.username + if role == self.Roles.AvatarUrl: + return comment.user.avatar_url + if role == Qt.ItemDataRole.DisplayRole: + return comment.body + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + if self._pull_request is None: + return 0 + return len(self._pull_request._comments) + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.Roles.Body: QByteArray(b'body'), + self.Roles.CreatedAt: QByteArray(b'createdAt'), + self.Roles.UpdatedAt: QByteArray(b'updatedAt'), + self.Roles.Username: QByteArray(b'username'), + self.Roles.AvatarUrl: QByteArray(b'avatarUrl'), + } diff --git a/kodereviewer/models/project.py b/kodereviewer/models/project.py new file mode 100644 index 0000000..c676157 --- /dev/null +++ b/kodereviewer/models/project.py @@ -0,0 +1,92 @@ +import json +from os import path +from typing import Any + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class ProjectModel(QAbstractListModel): + """Projects list!""" + + projects: list[Project] + + NameRole = Qt.ItemDataRole.UserRole + 1 + OwnerRole = NameRole + 1 + UrlRole = OwnerRole + 1 + + def __init__(self): + super().__init__() + self.projects = [] + + project_config = self._project_file() + try: + with open(project_config) as fp: + data = json.load(fp) + if isinstance(data, list): + self._load_projects(data) + except OSError as e: + pass + + def _project_file(self) -> str: + app_data = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) + project_config = path.join(app_data, 'projects.json') + return project_config + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + project = self.projects[index.row()] + + if role == Qt.ItemDataRole.DisplayRole: + return project.name + if role == self.NameRole: + return project.name + if role == self.OwnerRole: + return project.owner + if role == self.UrlRole: + return project.url + + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + return len(self.projects) + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.NameRole: QByteArray(b"name"), + self.OwnerRole: QByteArray(b"owner"), + self.UrlRole: QByteArray(b"url") + } + + @Slot(int, result=Project) + def get(self, index: int) -> Project: + return self.projects[index] + + def _load_projects(self, data: list[dict[str, Any]]) -> None: + for project in data: + self.projects.append(Project( + project['name'], + project['owner'], + project['url'] + )) + + def _save_projects(self): + project_config = self._project_file() + data = [{'name': project.name, 'owner': project.owner, 'url': project.url} for project in self.projects] + + with open(self._project_file(), 'w') as fp: + json.dump(data, fp) + + @Slot(str, str, str) + def add(self, name: str, owner: str, url: str) -> None: + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.projects.append(Project(name, owner, url)) + self._save_projects() + self.endInsertRows() diff --git a/kodereviewer/models/pull_request.py b/kodereviewer/models/pull_request.py new file mode 100644 index 0000000..7d08efa --- /dev/null +++ b/kodereviewer/models/pull_request.py @@ -0,0 +1,75 @@ +import json +from os import path +from typing import Any, Optional + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project +from kodereviewer.data import PullRequest + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + +@QmlElement +class PullRequestModel(QAbstractListModel): + + _project: Optional[Project] + + NumberRole = Qt.ItemDataRole.UserRole + 1 + TitleRole = NumberRole + 1 + + def __init__(self): + super().__init__() + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + if self._project is None: + return None + + pull_request = self._project.pullRequests[index.row()] + if role == self.NumberRole: + return pull_request.number + if role == self.TitleRole: + return pull_request.title + if role == Qt.ItemDataRole.DisplayRole: + return f'{pull_request.number} - {pull_request.title}' + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + if self._project is not None: + return len(self._project.pullRequests) + return 0 + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.NumberRole: QByteArray(b"number"), + self.TitleRole: QByteArray(b"title"), + } + + def get_project(self) -> Optional[Project]: + return self._project + + def set_project(self, project: Optional[Project])-> None: + if project is None: + return + + self._project = project + assert self._project is not None + + self._project.pullRequestsChanged.connect(self._reset_model) + + project = Property(Project, fget=get_project, fset=set_project) + + @Slot(int, result=PullRequest) + def get(self, index: int) -> Optional[PullRequest]: + if self._project is not None: + return self._project.pullRequests[index] + return None + + + def _reset_model(self) -> None: + print("Reseting pull request model") + self.beginResetModel() + self.endResetModel() diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py index 2d8b8aa..62b6047 100644 --- a/kodereviewer/network_manager.py +++ b/kodereviewer/network_manager.py @@ -1,3 +1,4 @@ +import re from typing import Optional from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug @@ -9,6 +10,10 @@ from kodereviewer.project import Project QML_IMPORT_NAME = "org.deprecated.kodereviewer" QML_IMPORT_MAJOR_VERSION = 1 +PULL_REQUEST_LIST_URL = re.compile(r'/pulls$') +COMMENT_LIST_URL = re.compile(r'/issues/(\d+)/comments') + + @QmlElement class NetworkManager(QObject): @@ -51,8 +56,26 @@ class NetworkManager(QObject): project = Property(Project, fget=project, fset=set_project) def reply_finished(self, reply: QNetworkReply): - self._project.load_pull_requests(reply.readAll()) + if self._project is None: + print('Project not set') + return + + response_body = reply.readAll() + + if PULL_REQUEST_LIST_URL.search(reply.url().toString()): + self._project.load_pull_requests(response_body) + elif (match := COMMENT_LIST_URL.search(reply.url().toString())): + pull_request_number = int(match.groups()[0]) + pull_request = self._project.find_pull_request(pull_request_number) + if pull_request is not None: + pull_request.load_comments(response_body) + else: + print(f"Can't handle {reply.url()}") @Slot() def getPullRequests(self) -> None: self._manager.get(self._request_factory.createRequest("/pulls")) + + @Slot(int) + def getPullRequestComments(self, number: int) -> None: + self._manager.get(self._request_factory.createRequest(f'/issues/{number}/comments')) diff --git a/kodereviewer/project.py b/kodereviewer/project.py index 669ee90..d96d47b 100644 --- a/kodereviewer/project.py +++ b/kodereviewer/project.py @@ -1,4 +1,6 @@ import json +from typing import Optional + from PySide6.QtCore import QByteArray, QObject, QUrl, Signal, Slot, Property from PySide6.QtQml import QmlElement @@ -16,7 +18,7 @@ class Project(QObject): _url: QUrl _pull_requests: list[PullRequest] - pullRequestChanged = Signal() + pullRequestsChanged = Signal() def __init__(self, name: str, owner: str, url: QUrl): super().__init__() @@ -46,4 +48,11 @@ class Project(QObject): self._pull_requests = [ PullRequest(pr) for pr in data ] - self.pullRequestChanged.emit() + self.pullRequestsChanged.emit() + + def find_pull_request(self, number: int) -> Optional[PullRequest]: + for pr in self._pull_requests: + if pr.number == number: + return pr + + return None diff --git a/kodereviewer/project_model.py b/kodereviewer/project_model.py deleted file mode 100644 index c676157..0000000 --- a/kodereviewer/project_model.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -from os import path -from typing import Any - -from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property -from PySide6.QtQml import QmlElement - -from kodereviewer.project import Project - -QML_IMPORT_NAME = "org.deprecated.kodereviewer" -QML_IMPORT_MAJOR_VERSION = 1 - - -@QmlElement -class ProjectModel(QAbstractListModel): - """Projects list!""" - - projects: list[Project] - - NameRole = Qt.ItemDataRole.UserRole + 1 - OwnerRole = NameRole + 1 - UrlRole = OwnerRole + 1 - - def __init__(self): - super().__init__() - self.projects = [] - - project_config = self._project_file() - try: - with open(project_config) as fp: - data = json.load(fp) - if isinstance(data, list): - self._load_projects(data) - except OSError as e: - pass - - def _project_file(self) -> str: - app_data = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) - project_config = path.join(app_data, 'projects.json') - return project_config - - def data(self, - index: QModelIndex | QPersistentModelIndex, - role: int = Qt.ItemDataRole.DisplayRole) -> object: - project = self.projects[index.row()] - - if role == Qt.ItemDataRole.DisplayRole: - return project.name - if role == self.NameRole: - return project.name - if role == self.OwnerRole: - return project.owner - if role == self.UrlRole: - return project.url - - return None - - def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: - return len(self.projects) - - def roleNames(self) -> dict[int, QByteArray]: - return { - self.NameRole: QByteArray(b"name"), - self.OwnerRole: QByteArray(b"owner"), - self.UrlRole: QByteArray(b"url") - } - - @Slot(int, result=Project) - def get(self, index: int) -> Project: - return self.projects[index] - - def _load_projects(self, data: list[dict[str, Any]]) -> None: - for project in data: - self.projects.append(Project( - project['name'], - project['owner'], - project['url'] - )) - - def _save_projects(self): - project_config = self._project_file() - data = [{'name': project.name, 'owner': project.owner, 'url': project.url} for project in self.projects] - - with open(self._project_file(), 'w') as fp: - json.dump(data, fp) - - @Slot(str, str, str) - def add(self, name: str, owner: str, url: str) -> None: - self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) - self.projects.append(Project(name, owner, url)) - self._save_projects() - self.endInsertRows() diff --git a/kodereviewer/pull_request_model.py b/kodereviewer/pull_request_model.py deleted file mode 100644 index e08bf6e..0000000 --- a/kodereviewer/pull_request_model.py +++ /dev/null @@ -1,66 +0,0 @@ -import json -from os import path -from typing import Any, Optional - -from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property -from PySide6.QtQml import QmlElement - -from kodereviewer.project import Project -from kodereviewer.data import PullRequest - -QML_IMPORT_NAME = "org.deprecated.kodereviewer" -QML_IMPORT_MAJOR_VERSION = 1 - -@QmlElement -class PullRequestModel(QAbstractListModel): - - _project: Optional[Project] - - NumberRole = Qt.ItemDataRole.UserRole + 1 - TitleRole = NumberRole + 1 - - def __init__(self): - super().__init__() - - def data(self, - index: QModelIndex | QPersistentModelIndex, - role: int = Qt.ItemDataRole.DisplayRole) -> object: - if self._project is None: - return None - - pull_request = self._project.pullRequests[index.row()] - if role == self.NumberRole: - return pull_request.number - if role == self.TitleRole: - return pull_request.title - if role == Qt.ItemDataRole.DisplayRole: - return f'{pull_request.number} - {pull_request.title}' - return None - - def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: - if self._project is not None: - return len(self._project.pullRequests) - return 0 - - def roleNames(self) -> dict[int, QByteArray]: - return { - self.NumberRole: QByteArray(b"number"), - self.TitleRole: QByteArray(b"title"), - } - - def get_project(self) -> Optional[Project]: - return self._project - - def set_project(self, project: Optional[Project])-> None: - if project is None: - return - self._project = project - print("Connecting!") - self._project.pullRequestChanged.connect(self._reset_model) - - project = Property(Project, fget=get_project, fset=set_project) - - def _reset_model(self) -> None: - print("Reseting pull request model") - self.beginResetModel() - self.endResetModel() diff --git a/kodereviewer/qml/CommentDelegate.qml b/kodereviewer/qml/CommentDelegate.qml new file mode 100644 index 0000000..bac75d0 --- /dev/null +++ b/kodereviewer/qml/CommentDelegate.qml @@ -0,0 +1,77 @@ +import QtQml +import QtQuick 6.7 +import QtQuick.Layouts 6.7 +import QtQuick.Controls 6.7 as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.components as KirigamiComponents + +Kirigami.AbstractCard { + id: root + required property url avatarUrl + required property string createdAt + required property string username + required property string body + + header: RowLayout { + KirigamiComponents.Avatar { + name: root.username + source: root.avatarUrl + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + } + Kirigami.Heading { + Layout.fillWidth: true + id: commentHeading + level: 3 + text: `@${root.username}` + } + QQC2.Label { + text: new Date(root.createdAt).toLocaleString(Qt.locale(), Locale.ShortFormat) + } + Kirigami.ActionToolBar { + Layout.fillWidth: false + actions: [ + Kirigami.Action { + icon.name: "overflow-menu" + + Kirigami.Action { + text: "Edit" + icon.name: "edit-comment" + } + + Kirigami.Action { + text: "Delete" + icon.name: "delete-comment" + } + } + ] + + position: QQC2.ToolBar.Header + } + } + + contentItem: Item { + implicitWidth: delegateLayout.implicitWidth + implicitHeight: delegateLayout.implicitHeight + ColumnLayout { + id: delegateLayout + spacing: Kirigami.Units.largeSpacing + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + Kirigami.Separator { + Layout.fillWidth: true + } + QQC2.Label { + Layout.fillWidth: true + text: root.body + textFormat: Text.MarkdownText + wrapMode: Text.WordWrap + } + } + } +} diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml index 0b41a05..e7496a6 100644 --- a/kodereviewer/qml/Main.qml +++ b/kodereviewer/qml/Main.qml @@ -53,33 +53,28 @@ Kirigami.ApplicationWindow { ProjectListPage { connection: root.connection project: root.project + + onPullRequestSelected: pr => { + print(pr) + pullRequestPageLoader.item.pullRequest = pr + + } } } } Loader { - id: placeHolderPageLoader + id: pullRequestPageLoader active: false - sourceComponent: Component { - Kirigami.Page { - - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false - title: "Select a pull request" - spacing: Kirigami.Units.largeSpacing * 2 - Kirigami.PlaceholderMessage { - anchors.centerIn: parent - icon.name: "org.deprecated.kodereviewer" - text: "Select a pull request" - } - } + sourceComponent: PullRequestPage { + connection: root.connection } } onProjectSelected: { projectListPageLoader.active = true - placeHolderPageLoader.active = true + pullRequestPageLoader.active = true pageStack.replace(projectListPageLoader.item) - pageStack.push(placeHolderPageLoader.item) + pageStack.push(pullRequestPageLoader.item) } } diff --git a/kodereviewer/qml/ProjectListPage.qml b/kodereviewer/qml/ProjectListPage.qml index 334799e..7ffa8b9 100644 --- a/kodereviewer/qml/ProjectListPage.qml +++ b/kodereviewer/qml/ProjectListPage.qml @@ -14,7 +14,7 @@ Kirigami.Page { required property NetworkManager connection required property Project project - + signal pullRequestSelected(var pullRequest) readonly property int currentWidth: _private.currentWidth + 1 @@ -77,6 +77,8 @@ Kirigami.Page { text: `${number} - ${title}` icon.name: "vcs-merge-request" + + onClicked: root.pullRequestSelected(pullRequestModel.get(index)) } } } diff --git a/kodereviewer/qml/PullRequestPage.qml b/kodereviewer/qml/PullRequestPage.qml new file mode 100644 index 0000000..19defdc --- /dev/null +++ b/kodereviewer/qml/PullRequestPage.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtCore +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard + +import org.deprecated.kodereviewer 1.0 + +Kirigami.ScrollablePage { + id: root + + property var pullRequest + property NetworkManager connection + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + + + CommentModel { + id: commentModel + pullRequest: root.pullRequest + + onPullRequestChanged: root.connection.getPullRequestComments(pullRequest.number) + } + + ListView { + id: listView + model: commentModel + + spacing: Kirigami.Units.largeSpacing * 2 + topMargin: Kirigami.Units.largeSpacing * 2 + rightMargin: Kirigami.Units.largeSpacing * 2 + leftMargin: Kirigami.Units.largeSpacing * 2 + bottomMargin: Kirigami.Units.largeSpacing * 2 // + commentToolbar.heigh + + header: ColumnLayout { + id: headerLayout + visible: !!root.pullRequest + width: ListView.view ? ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin : 0 + + Kirigami.Heading { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + level: 1 + text: root.pullRequest ? root.pullRequest.title : "" + wrapMode: Text.WordWrap + } + + Kirigami.ListSectionHeader { + Layout.fillWidth: true + text: "description" + } + + QQC2.Label { + Layout.fillWidth: true + Layout.fillHeight: false + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + text: root.pullRequest ? root.pullRequest.body : "" + textFormat: Text.MarkdownText + wrapMode: Text.WordWrap + } + + Kirigami.ListSectionHeader { + Layout.fillWidth: true + text: "Comments" + } + } + + delegate: CommentDelegate {} + + Kirigami.PlaceholderMessage { + visible: !root.pullRequest + anchors.centerIn: parent + icon.name: "org.deprecated.kodereviewer" + text: "Select a pull request" + } + } +} -- cgit v1.2.3-70-g09d2