Здесь я расскажу, как сделать канбан-доску для проекта в Jira, пользуясь только QML и JavaScript. С небольшими доработками вместо Jira вы можете использовать любой другой трекер, имеющий REST API.

Некоторое время назад, теперь уже практически в другой жизни, в мою бытность руководителем проекта, я понял, что теряю представление о занятости участников нашего проекта. Кто-то занимается Большим и Важным делом, кто-то исправляет срочные баги, а может быть кто-то, извините, балду пинает, а я об этом не в курсе и задачи ему не ставлю. И мне захотелось иметь наглядную картинку текущих дел.
Если у вашей организации уже диагностировали kanban в хронической стадии и вы тяготеете ко всему натуральному и осязаемому, то, скорее всего, ваша доска выглядит вроде этой, и у нее тоже есть разделение по этапам процесса:
Взято отсюда.
В моем случае такой вариант не прокатил бы по нескольким причинам.
Во-первых, вся команда, за исключением пары человек, находилась в другом городе, а устраивать видеомитинги мне не казалось рациональным.
Во-вторых, у меня есть стойкое отвращение ко всякому ручному труду и вручную нацеплять бумажки на доску (а больше было некому, см. предыдущий пункт), отслеживать движение задач в трекере и соответственно передвигать бумажки на доске мне претило. Можно было нарисовать карточки в компьютере, в Excel или в Trello, но следить за задачами и передвигать карточки опять пришлось бы самостоятельно.
В-третьих, и самое главное, глядя на эту доску, можно видеть общее состояние дел, находить узкие места на участках конвейера по производству ПО, но в ней совершенно не видно людей и их загрузки.
Поэтому мне нужна была доска:
а) электронная
б) связанная с трекером, т.е. отражающую текущую ситуацию
в) и чтобы столбец на доске соответствовал конкретному человеку

Короче говоря, эту задачу я на тот момент решил, сделал представление на web-страничке. Но о ней ничего вам не расскажу — и трекер тот (PVCS Tracker) не слишком распространен, API у него на dll, да и код странички сейчас не найти.
А сейчас я решил повторить упражнение, взяв в качестве инструментария QML. Выбор объясняется просто — мне он чуть более знаком, чем веб-технологии, и я знаю, как встроить получившийся модуль в свой инструмент, написанный на Python и PyQt.
Да, я знаю, что для Jira существует энное количество плагинов, в которых есть Kanban-доска — поиск в marketplace по слову «kanban» находит 33 варианта.
Но использовать плагин означает, что нужно выбить у руководства его покупку по цене соответствующей числу всех пользователей на Jira, договориться с админами, что они поставят его на сервере и будут поддерживать, и… невозможность его кастомизации под мои нужды, т.к. плагин будет общим для всех. А мне хотелось иметь инструмент, который можно использовать независимо от того, установлено что-то на сервере или нет, и менять, ни на кого не оглядываясь.
Чтобы не утяжелять статью, здесь не будет сказано о том, как сделать:
— авторизацию в Jira
— операции над карточками в QML с передачей вызова в JIRA — редактирование, смена статусов и исполнителей путем drag&drop и т.п.
— работа с фильтрами Jira
Если что-то из этого вам действительно интересно — отпишите об этом в комментариях. Не буду обещать, что немедленно сделаю и распишу в деталях, но, как сказал nmivan, «поставлю в план».
Терминология еще не устоялась, так issue в одних компаниях называют запросом, в других задачей, еще бывают тикеты и заявки. Для сущности filter, которым в Jira отбирают issues, тоже есть куча названий — фильтр, запрос, выборка, список.
Я буду использовать терминологию, принятую в локализованной Jira: issue буду называть запросом, а filter — списком.
Типичный адрес запроса в веб-интерфейсе Jira выглядит так:
Берем протокол и имя хоста, то есть с начала адреса до
Полное описание Jira REST API на сайте Atlassian. Там много всяких функций, которых с каждой версией становится все больше, но реально требуется знать лишь небольшое количество методов:
Получить запрос PROJECT-1234 — вернется JSON с полями запроса. Учтите, что для названия полей будут использоваться внутренние имена, а не те, что вы видите в веб-интерфейсе. Так поле «Статус тестирования» может оказаться
Создать новый запрос. В теле вызова передается JSON с заполняемыми полями запроса. Те поля, что вы не передали, заполнятся значениями по умолчанию.
Изменение (редактирование) полей в запросе. В теле вызова передается JSON, в котором есть два блока — «update» с инструкциями по изменению полей, и «fields» с новыми значениями полей.
Изменяемое поле должно быть только в одном из этих блоков.
Пока хватит на первое время.
Поскольку мы ничего менять и редактировать пока не собираемся, то работать будем анонимно, с запросами на сервере Jira в Atlassian, в проекте «JIRA Server (including JIRA Core)», то есть, фигурально выражаясь, в Самом Главном Проекте Jira. Тем более, что там тоже есть наши люди:

Первым делом рекомендую зайти в веб-интерфейс проекта и сделать поиск запросов по какому-либо условию, например:
Это нужно для того, чтобы убедиться, что вы запрос составили правильно — если это не так, то веб-интерфейс вам скажет.
Условие копируем и подставляем в параметр jql функции search, получится такой URL:
https://jira.atlassian.com/rest/api/2/search?jql=project = JRASERVER and updated <= -1w ORDER BY updated DESC
Открываем его в браузере и получаем JSON. JSON сохраняем браузером в файл с расширением .json, открываем его в Qt Creator — оказывается, что весь файл в одной длинной строке, а затем, следите за руками — форматируем его как QML
Сохраните его под другим именем. C полученным файлом удобнее работать, находить в нем нужные поля и смотреть, в какой структуре находится нужное значение. Оригинальный файл нам пригодится в качестве тестового источника, чтобы лишний раз не ходить на сервер Atlassian.
Имеет смысл также получить список всех полей через запрос
Для создания проекта в Qt Creator воспользуемся стандартным шаблоном «Qt Quick Control Application».
Получится проект, состоящий из main.cpp и main.qml в файле ресурсов qml.qrc.
Их мы трогать пока не будем, займемся более насущными проблемами.
Создаем новый файл IssueCard.qml, визард по умолчанию закинет его в файл ресурсов.
Дизайн карточки, которой будет отображаться запрос, я сначала по быстрому накидал в режиме дизайнера Qt Creator, затем доработал QML вручную.

Кстати, дизайнер QML относительно неплох, особенно по сравнению с первой версией. Наглядно показывается и легко меняется binding положения элементов, автоматом подтягивает компоненты из других qml-файлов в проекте. Почти не падал — всего два раза валил QtCreator, когда я пытался задать градиент (ничего страшного не случилось — автосохранение работает), и еще не смог пережевать DelegateModel — наверное, среду стоило обновить. У дизайнера QML, как и у дизайнера Qt Widgets, есть функция предпросмотра:

В результате получился QML карточки с запросом, файл IssueCard.qml
Для заполнения карточки по запросу добавим новое свойство (property) issue. Свойство даст нам возможность передавать в карточку запрос со всем его содержимым извне за одно присвоение.
И в сигнале на его изменение напишем код, разбирающий значения и распихивающий их по нужным визуальным компонентам.
Как видите, здесь я часто использую функцию JS.getValue, я ее написал для упрощения выборки значения из сложной структуры JSON (если оно там есть), хотя сама функция довольно проста:
Функция лежит в файле methods.js, подключенном в начале IssueCard.qml
Теперь нужно карточки организовать в прокручиваемую по вертикали колонку. Прокрутка очень удобна, когда карточек много. Для прокрутки нужен ListView. Среди примеров, идущих в комплекте с Qt есть пример «QML Dynamic View Ordering Tutorial 3 — Moving Dragged Items», в нём dynamicview.qml — это практически то, что нам нужно, копируем его в проект под именем KanbanColumn.qml.
Только нужно сделать пару доработок
1) Добавить к колонке заголовок и сделать у объекта верхнего уровня свойство, чтобы присваивать название колонки извне.
2) Так как карточка запроса у нас теперь отдельный цельный объект, то заменяем вывод, сделанный в примере через Column и несколько Text, на наш IssueCard
С колонкой дизайнер нам не поможет, потому что он не переваривает DelegateModel. С другой стороны, нам не особо он и нужен, всё можно сделать вручную.
Теперь нужно собрать колонку в общее окно. Создаем файл KanbanWindow.qml, в нем дизайнером размещаем нужные поля.
В простейшем виде получается так:

В ListView надо указать, в свойстве
Выше я еще создал свойство
И не забыть вставить KanbanWindow в окно приложения:
Самое время сделать код, который будет получать список запросов из Jira и заполнять модели в QML.
В QML имеется, хоть и ограниченная, но поддержка XMLHttpRequest и JSON-парсер (на хабре есть подробная статья BlackRaven86). Поэтому у нас есть всё, чтобы написать обращение к серверу и разбор ответа.
Функция запрашивает с сервера (или из локального файла) список запросов, парсит json из ответа, группирует запросы по исполнителям и заполняет модель в QML.
Подключаем функцию к кнопке
И проверяем работу:

В принципе, доска готова. Далее можно заниматься ее улучшениями и развитием.
Чуть не забыл — когда вы попробуете указать URL к настоящему серверу Jira, например, такой:
https://jira.atlassian.com/rest/api/2/search?maxResults=50&jql=project = JRASERVER and updated <= -1w and assignee is not empty ORDER BY updated ASC
и вы под Windows, то у вас, скорее всего, ничего не получится. Проблема в SSL — Qt Creator, запуская программу под отладчиком, не прописывает в окружении путь к библиотекам OpenSSL. Скопируйте libeay32.dll и ssleay32.dll к созданному экзешнику и наслаждайтесь.
Чтобы не вводить каждый раз URL к серверу Jira, стоит сохранять введенную строку и восстанавливать ее при запуске. И да, QML умеет в LocalStorage.
Напишем функции чтения и сохранения параметров.
Добавим вызов сохранения параметров…
… и их восстановление при создании KanbanWindow
Сделав группировку по исполнителям, логично сделать возможность выбора и других вариантов группировки — по статусу, по приоритету и так далее. Так появилась панелька параметров группировки KanbanParams.qml.

Как видите, здесь ComboBox содержит модель с возможными вариантами группировки, и в каждом элементе прописан путь в JSON к значению, которое будет использоваться для определения группы. Таким образом количество вариантов группировок по желанию можно расширить.
На верхнем уровне определены свойства, два из которых — алиасы к внутренним значениям. Алиасы нужны, чтобы можно было присвоить нужное значение, начитанное из LocalStorage. Что же касается свойства groupValuePath:
то оно просто возвращает путь к значению для текущего варианта группировки.
Вставляем KanbanParams в KanbanWindow и у нас получается такое окошко:

Я не буду подробно расписывать, как обрабатываются параметры,потому что мне надоело писать эту статью, смотрите в коде.
Получившейся доской уже можно пользоваться для просмотра текущей ситуации с запросами, но можно ее улучшить:
Код выложен на GitHub.
Содержание
Предыстория
Альтернативы для умных и богатых
Необходимые оговорки
Начало работы с Jira REST API
Создаем проект в Qt Creator
Рисуем дизайн карточки запроса
Описываем колонку карточек
Окно для доски
Пишем код для вызова REST API
LocalStorage для сохранения и восстановления параметров
Добавляем варианты группировки
Что дальше?
Альтернативы для умных и богатых
Необходимые оговорки
Начало работы с Jira REST API
Создаем проект в Qt Creator
Рисуем дизайн карточки запроса
Описываем колонку карточек
Окно для доски
Пишем код для вызова REST API
LocalStorage для сохранения и восстановления параметров
Добавляем варианты группировки
Что дальше?

Предыстория
Некоторое время назад, теперь уже практически в другой жизни, в мою бытность руководителем проекта, я понял, что теряю представление о занятости участников нашего проекта. Кто-то занимается Большим и Важным делом, кто-то исправляет срочные баги, а может быть кто-то, извините, балду пинает, а я об этом не в курсе и задачи ему не ставлю. И мне захотелось иметь наглядную картинку текущих дел.
Если у вашей организации уже диагностировали kanban в хронической стадии и вы тяготеете ко всему натуральному и осязаемому, то, скорее всего, ваша доска выглядит вроде этой, и у нее тоже есть разделение по этапам процесса:

В моем случае такой вариант не прокатил бы по нескольким причинам.
Во-первых, вся команда, за исключением пары человек, находилась в другом городе, а устраивать видеомитинги мне не казалось рациональным.
Во-вторых, у меня есть стойкое отвращение ко всякому ручному труду и вручную нацеплять бумажки на доску (а больше было некому, см. предыдущий пункт), отслеживать движение задач в трекере и соответственно передвигать бумажки на доске мне претило. Можно было нарисовать карточки в компьютере, в Excel или в Trello, но следить за задачами и передвигать карточки опять пришлось бы самостоятельно.
В-третьих, и самое главное, глядя на эту доску, можно видеть общее состояние дел, находить узкие места на участках конвейера по производству ПО, но в ней совершенно не видно людей и их загрузки.
Поэтому мне нужна была доска:
а) электронная
б) связанная с трекером, т.е. отражающую текущую ситуацию
в) и чтобы столбец на доске соответствовал конкретному человеку

Короче говоря, эту задачу я на тот момент решил, сделал представление на web-страничке. Но о ней ничего вам не расскажу — и трекер тот (PVCS Tracker) не слишком распространен, API у него на dll, да и код странички сейчас не найти.
А сейчас я решил повторить упражнение, взяв в качестве инструментария QML. Выбор объясняется просто — мне он чуть более знаком, чем веб-технологии, и я знаю, как встроить получившийся модуль в свой инструмент, написанный на Python и PyQt.
Альтернативы для умных и богатых
Да, я знаю, что для Jira существует энное количество плагинов, в которых есть Kanban-доска — поиск в marketplace по слову «kanban» находит 33 варианта.
Но использовать плагин означает, что нужно выбить у руководства его покупку по цене соответствующей числу всех пользователей на Jira, договориться с админами, что они поставят его на сервере и будут поддерживать, и… невозможность его кастомизации под мои нужды, т.к. плагин будет общим для всех. А мне хотелось иметь инструмент, который можно использовать независимо от того, установлено что-то на сервере или нет, и менять, ни на кого не оглядываясь.
Необходимые оговорки
Чтобы не утяжелять статью, здесь не будет сказано о том, как сделать:
— авторизацию в Jira
— операции над карточками в QML с передачей вызова в JIRA — редактирование, смена статусов и исполнителей путем drag&drop и т.п.
— работа с фильтрами Jira
Если что-то из этого вам действительно интересно — отпишите об этом в комментариях. Не буду обещать, что немедленно сделаю и распишу в деталях, но, как сказал nmivan, «поставлю в план».
Терминология еще не устоялась, так issue в одних компаниях называют запросом, в других задачей, еще бывают тикеты и заявки. Для сущности filter, которым в Jira отбирают issues, тоже есть куча названий — фильтр, запрос, выборка, список.
Я буду использовать терминологию, принятую в локализованной Jira: issue буду называть запросом, а filter — списком.
Начало работы с Jira REST API
Типичный адрес запроса в веб-интерфейсе Jira выглядит так:
https://jira.mycompany.ru/browse/PROJECT-1234
Берем протокол и имя хоста, то есть с начала адреса до
browse, дописываем к нему rest/api/2/ — и у нас получается базовая часть адреса REST APIhttps://jira.mycompany.ru/rest/api/2/
Полное описание Jira REST API на сайте Atlassian. Там много всяких функций, которых с каждой версией становится все больше, но реально требуется знать лишь небольшое количество методов:
GET https://jira.mycompany.ru/rest/api/2/issue/PROJECT-1234Получить запрос PROJECT-1234 — вернется JSON с полями запроса. Учтите, что для названия полей будут использоваться внутренние имена, а не те, что вы видите в веб-интерфейсе. Так поле «Статус тестирования» может оказаться
customfield_10234. Чтобы понять, какое поле какому соответствует, воспользуйтесь запросом /rest/api/2/field.POST https://jira.mycompany.ru/rest/api/2/issueСоздать новый запрос. В теле вызова передается JSON с заполняемыми полями запроса. Те поля, что вы не передали, заполнятся значениями по умолчанию.
PUT https://jira.mycompany.ru/rest/api/2/issue/PROJECT-1234Изменение (редактирование) полей в запросе. В теле вызова передается JSON, в котором есть два блока — «update» с инструкциями по изменению полей, и «fields» с новыми значениями полей.
Изменяемое поле должно быть только в одном из этих блоков.
Пример
{ "update": { "summary":[ {"set":"Bug in business logic"} ], "components":[{"set":""}], "timetracking":[ {"edit":{"originalEstimate":"1w 1d","remainingEstimate":"4d"}} ], "labels":[ {"add":"triaged"}, {"remove":"blocker"}] }, "fields":{ "summary":"This is a shorthand for a set operation on the summary field", "customfield_10010":1, "customfield_10000":"This is a shorthand for a set operation on a text custom field" } }
GET https://jira.mycompany.ru/rest/api/2/search?jql=... — получить список запросов, соответствующего условиям на языке JQLПример
{ expand: "schema,names", startAt: 0, maxResults: 10, total: 738, issues: [{ expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", id: "947068", self: "https://jira.atlassian.com/rest/api/2/issue/947068", key: "JRASERVER-66937", fields: { customfield_18232: null, ...
POST https://jira.mycompany.ru/rest/api/2/search — тоже самое для сложных условий, не умещающихся в строку URLGET https://jira.mycompany.ru/rest/api/2/field — получить описания всех полей, которые могут использоваться в запросах. Пока хватит на первое время.
Поскольку мы ничего менять и редактировать пока не собираемся, то работать будем анонимно, с запросами на сервере Jira в Atlassian, в проекте «JIRA Server (including JIRA Core)», то есть, фигурально выражаясь, в Самом Главном Проекте Jira. Тем более, что там тоже есть наши люди:

Первым делом рекомендую зайти в веб-интерфейс проекта и сделать поиск запросов по какому-либо условию, например:
project = JRASERVER and updated <= -1w ORDER BY updated DESCЭто нужно для того, чтобы убедиться, что вы запрос составили правильно — если это не так, то веб-интерфейс вам скажет.
Условие копируем и подставляем в параметр jql функции search, получится такой URL:
https://jira.atlassian.com/rest/api/2/search?jql=project = JRASERVER and updated <= -1w ORDER BY updated DESC
Открываем его в браузере и получаем JSON. JSON сохраняем браузером в файл с расширением .json, открываем его в Qt Creator — оказывается, что весь файл в одной длинной строке, а затем, следите за руками — форматируем его как QML
гифка

Сохраните его под другим именем. C полученным файлом удобнее работать, находить в нем нужные поля и смотреть, в какой структуре находится нужное значение. Оригинальный файл нам пригодится в качестве тестового источника, чтобы лишний раз не ходить на сервер Atlassian.
Имеет смысл также получить список всех полей через запрос
rest/api/2/field, чтобы определять, под каким идентификатором числится нужное вам поле.Создаем проект в Qt Creator
Для создания проекта в Qt Creator воспользуемся стандартным шаблоном «Qt Quick Control Application».
Получится проект, состоящий из main.cpp и main.qml в файле ресурсов qml.qrc.
main.qml
import QtQuick 2.3 import QtQuick.Controls 1.2 ApplicationWindow { id: applicationWindow1 visible: true width: 649 height: 480 title: qsTr("Hello World") menuBar: MenuBar { Menu { title: qsTr("File") MenuItem { text: qsTr("&Open") onTriggered: console.log("Open action triggered"); } MenuItem { text: qsTr("Exit") onTriggered: Qt.quit(); } } } }
Их мы трогать пока не будем, займемся более насущными проблемами.
Рисуем дизайн карточки с запросом
Создаем новый файл IssueCard.qml, визард по умолчанию закинет его в файл ресурсов.
Дизайн карточки, которой будет отображаться запрос, я сначала по быстрому накидал в режиме дизайнера Qt Creator, затем доработал QML вручную.

Кстати, дизайнер QML относительно неплох, особенно по сравнению с первой версией. Наглядно показывается и легко меняется binding положения элементов, автоматом подтягивает компоненты из других qml-файлов в проекте. Почти не падал — всего два раза валил QtCreator, когда я пытался задать градиент (ничего страшного не случилось — автосохранение работает), и еще не смог пережевать DelegateModel — наверное, среду стоило обновить. У дизайнера QML, как и у дизайнера Qt Widgets, есть функция предпросмотра:

В результате получился QML карточки с запросом, файл IssueCard.qml
Код
import QtQuick 2.0 import "methods.js" as JS Rectangle { id: rectangle1 color: "#f1dada" radius: 10 gradient: Gradient { GradientStop { position: 0.00; color: "#f5f2d8"; } GradientStop { position: 1.00; color: "#ffffff"; } } border.color: "#abfdf4" width: 300 height: 150 Text { id: keyText text: "JIRASERVER-1001" property string url: "" anchors.top: parent.top anchors.topMargin: 8 anchors.left: parent.left anchors.leftMargin: 8 font.bold: true font.pixelSize: 14 MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: Qt.openUrlExternally(parent.url) } } Text { id: summaryText y: 51 height: 42 color: "#002f7b" text: "Create a Global permission for Auditing teams to have full read only access to the instance" anchors.right: parent.right anchors.rightMargin: 8 anchors.left: parent.left anchors.leftMargin: 8 wrapMode: Text.WordWrap font.pixelSize: 15 textFormat: Text.PlainText } Image { id: priorityImage x: 276 width: 16 height: 16 anchors.top: parent.top anchors.topMargin: 9 anchors.right: parent.right anchors.rightMargin: 8 source: "minor.svg" } Image { id: typeImage x: 276 width: 16 height: 16 anchors.top: parent.top anchors.topMargin: 9 anchors.right: priorityImage.left anchors.rightMargin: 4 source: "" } Text { id: dateText x: 198 y: 31 color: "#949090" text: "13.03.2018 17:11" anchors.right: parent.right anchors.rightMargin: 8 font.pixelSize: 12 } Text { id: creatorText y: 31 color: "#949090" text: "Chung Park Chan" anchors.left: parent.left anchors.leftMargin: 8 font.pixelSize: 12 } Text { id: assigneeText x: 218 y: 128 text: "Kiran Shekhar" anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.rightMargin: 8 anchors.right: parent.right font.pixelSize: 12 } }
Для заполнения карточки по запросу добавим новое свойство (property) issue. Свойство даст нам возможность передавать в карточку запрос со всем его содержимым извне за одно присвоение.
property var issue: null
И в сигнале на его изменение напишем код, разбирающий значения и распихивающий их по нужным визуальным компонентам.
onIssueChanged: { var self = JS.getValue(issue,"self") var re = new RegExp("(https*:\/\/[^\/]+\/).+") var key = JS.getValue(issue,"key") var url = self.replace(re,'$1')+'browse/'+key keyText.text = key keyText.url = url summaryText.text = JS.getValue(issue,"fields/summary") dateText.text = (new Date(JS.getValue(issue,"fields/created"))).toLocaleString() creatorText.text = JS.getValue(issue,"fields/creator/displayName") var v = JS.getValue(issue,"fields/assignee/displayName") assigneeText.text = v === null ? "(no assigned)" : v var img = JS.getValue(issue,"fields/priority/iconUrl") var txt = JS.getValue(issue,"fields/priority/name") priorityImage.source = typeof img == 'undefined' || img === null ? "" : img img = JS.getValue(issue,"fields/issuetype/iconUrl") typeImage.source = typeof img == 'undefined' || img === null ? "" : img }
Как видите, здесь я часто использую функцию JS.getValue, я ее написал для упрощения выборки значения из сложной структуры JSON (если оно там есть), хотя сама функция довольно проста:
function getValue(json, path) { var arr = path.split('/'); for(var i=0; i<arr.length && json; i++) { json = json[arr[i]]; } return json; }
Функция лежит в файле methods.js, подключенном в начале IssueCard.qml
Описываем колонку карточек
Теперь нужно карточки организовать в прокручиваемую по вертикали колонку. Прокрутка очень удобна, когда карточек много. Для прокрутки нужен ListView. Среди примеров, идущих в комплекте с Qt есть пример «QML Dynamic View Ordering Tutorial 3 — Moving Dragged Items», в нём dynamicview.qml — это практически то, что нам нужно, копируем его в проект под именем KanbanColumn.qml.
Только нужно сделать пару доработок
1) Добавить к колонке заголовок и сделать у объекта верхнего уровня свойство, чтобы присваивать название колонки извне.
Код
Rectangle { id: root // новое свойство property string title: "" ... // остальной код // Заголовок столбца Rectangle { id: titleRect anchors { top: parent.top left: parent.left right: parent.right margins: 2 } color: "#cfe5ff" height: titleText.height+10 Text { id: titleText text: root.title font.bold: true horizontalAlignment: Text.AlignHCenter font.pointSize: 12 anchors.centerIn: parent } } }
2) Так как карточка запроса у нас теперь отдельный цельный объект, то заменяем вывод, сделанный в примере через Column и несколько Text, на наш IssueCard
Было
Rectangle { id: content ... width: dragArea.width; height: column.implicitHeight + 4 color: dragArea.held ? "lightsteelblue" : "white" Behavior on color { ColorAnimation { duration: 100 } } radius: 2 ... Column { id: column anchors { fill: parent; margins: 2 } Text { text: 'Name: ' + name } Text { text: 'Type: ' + type } Text { text: 'Age: ' + age } Text { text: 'Size: ' + size } } }
Стало
Item { id: content ... width: dragArea.width; height: card.height + 4 ... IssueCard { id: card issue: issueRecord anchors { fill: parent; margins: 2 } } // Закрашивание карточки при перетаскивании мышью Rectangle { anchors.fill: parent color: "lightsteelblue" visible: dragArea.held // показывать только при перетаскивании opacity: 0.5 } }
С колонкой дизайнер нам не поможет, потому что он не переваривает DelegateModel. С другой стороны, нам не особо он и нужен, всё можно сделать вручную.
Окно для доски
Теперь нужно собрать колонку в общее окно. Создаем файл KanbanWindow.qml, в нем дизайнером размещаем нужные поля.
В простейшем виде получается так:

KanbanWindow.qml
import QtQuick 2.0 import QtQuick.Controls 1.2 Rectangle { id: rectangle1 width: 640 height: 480 color: "#e0edf6" clip: true Item { id: row1 anchors { top: parent.top left: parent.left right: parent.right margins: 4 } height: queryTE.height TextField { id: queryTE text: "file:///C:/Projects/qml/search.json" anchors.rightMargin: 4 anchors.right: goButton.left anchors.left: parent.left anchors.leftMargin: 0 } Button { id: goButton text: qsTr("Go") anchors.right: parent.right onClicked: JS.readIssues(queryTE.text) } } ListView { anchors{ top: row1.bottom bottom: parent.bottom right: parent.right left: parent.left margins: 4 } orientation: ListView.Horizontal clip: true } }
В ListView надо указать, в свойстве
delegate, что элементы модели будут показываться в виде колонок KanbanColumn, в каждую из которых надо передать список запросов, назовем его issueList. Также создадим пустую модель и тоже дадим ей имя model.Rectangle { property var mainModel: [] ... ListView { ... model: ListModel { id: model } delegate: KanbanColumn { anchors.top: parent.top anchors.bottom: parent.bottom // 'groupName' title: groupName issues: issueList } } }
Выше я еще создал свойство
mainModel — оно нам послужит для временного хранения данных.И не забыть вставить KanbanWindow в окно приложения:
ApplicationWindow { id: applicationWindow1 visible: true width: 649 height: 480 title: qsTr("Hello World") ... KanbanWindow { anchors.fill: parent } }
Пишем код для вызова REST API
Самое время сделать код, который будет получать список запросов из Jira и заполнять модели в QML.
В QML имеется, хоть и ограниченная, но поддержка XMLHttpRequest и JSON-парсер (на хабре есть подробная статья BlackRaven86). Поэтому у нас есть всё, чтобы написать обращение к серверу и разбор ответа.
function readIssuesSimple(queryUrl) { var doc = new XMLHttpRequest(); doc.onreadystatechange = function() { if (doc.readyState == XMLHttpRequest.DONE) { var data = JSON.parse(doc.responseText); mainModel = data["issues"] model.clear() var list = mainModel // группируем запросы по исполнителям var gPath = "fields/assignee/displayName" var models = {} for(var i in list) { var item = list[i] var g = getValue(item, gPath) if(!(g in models)) models[g] = [] models[g].push({ issueRecord: item } ) } // собрали списки запросов, передаем их в модель QML // модель будет содержать столько записей, сколько найдено групп for(g in models) { var iss = models[g] if(g === null) g = '(null)' // здесь 'model' - имя модели в QML model.append({ groupName: g, issueList: iss }); } } } doc.open("GET", queryUrl); doc.send(); }
Функция запрашивает с сервера (или из локального файла) список запросов, парсит json из ответа, группирует запросы по исполнителям и заполняет модель в QML.
Подключаем функцию к кнопке
Button { id: goButton text: qsTr("Go") anchors.right: parent.right onClicked: JS.readIssuesSimple(queryTE.text) }
И проверяем работу:

гифка

В принципе, доска готова. Далее можно заниматься ее улучшениями и развитием.
Чуть не забыл — когда вы попробуете указать URL к настоящему серверу Jira, например, такой:
https://jira.atlassian.com/rest/api/2/search?maxResults=50&jql=project = JRASERVER and updated <= -1w and assignee is not empty ORDER BY updated ASC
и вы под Windows, то у вас, скорее всего, ничего не получится. Проблема в SSL — Qt Creator, запуская программу под отладчиком, не прописывает в окружении путь к библиотекам OpenSSL. Скопируйте libeay32.dll и ssleay32.dll к созданному экзешнику и наслаждайтесь.
LocalStorage для сохранения и восстановления параметров
Чтобы не вводить каждый раз URL к серверу Jira, стоит сохранять введенную строку и восстанавливать ее при запуске. И да, QML умеет в LocalStorage.
Напишем функции чтения и сохранения параметров.
function loadSettings() { var dbConn = LocalStorage.openDatabaseSync("JKanban", "1.0", "", 1000000); dbConn.transaction( function(tx) { // Create the database if it doesn't already exist tx.executeSql('CREATE TABLE IF NOT EXISTS Settings(skey TEXT, svalue TEXT)'); var rs = tx.executeSql('select skey, svalue from Settings') var r = "" var c = rs.rows.length for(var i = 0; i < rs.rows.length; i++) { var skey = rs.rows.item(i).skey var svalue = rs.rows.item(i).svalue if(skey === 'query') queryTE.text = svalue } } ) } function saveSetting(skey, svalue) { var dbConn = LocalStorage.openDatabaseSync("JKanban", "1.0", "", 1000000); dbConn.transaction( function(tx) { tx.executeSql('delete from Settings where skey = ?', [ skey ]); tx.executeSql('INSERT INTO Settings VALUES(?, ?)', [ skey, svalue ]); } ) }
Добавим вызов сохранения параметров…
function readIssuesSimple(queryUrl) { saveSetting('query',queryUrl)
… и их восстановление при создании KanbanWindow
Rectangle { id: rectangle1 width: 640 height: 480 color: "#e0edf6" clip: true Component.onCompleted: JS.loadSettings() ....
Добавляем варианты группировки
Сделав группировку по исполнителям, логично сделать возможность выбора и других вариантов группировки — по статусу, по приоритету и так далее. Так появилась панелька параметров группировки KanbanParams.qml.

KanbanParams.qml
import QtQuick 2.0 import QtQuick.Controls 1.2 import QtQuick.LocalStorage 2.0 import "methods.js" as JS Item { width: 480 height: cbGroupField.height property alias groupVariant: cbGroupField.currentIndex property string groupValuePath: cbGroupField.model.get(cbGroupField.currentIndex).namePath property alias groupList: groupsTE.text Text { id: label height: cbGroupField.height text: qsTr("Группировать:") verticalAlignment: Text.AlignVCenter } ComboBox { id: cbGroupField anchors { left: label.right; leftMargin: 4 } model: ListModel { ListElement { text: qsTr("по статусам") namePath: "fields/status/name" } ListElement { text: qsTr("по исполнителям") namePath: "fields/assignee/displayName" } ListElement { text: qsTr("по создателям") namePath: "fields/creator/displayName" } ListElement { text: qsTr("по типам запросов") namePath: "fields/issuetype/name" } ListElement { text: qsTr("по приоритетам") namePath: "fields/priority/name" } } } TextField { id: groupsTE text: '' anchors { right: buttonGroups.left rightMargin: 4 left: cbGroupField.right leftMargin: 4 } } Button { id: buttonGroups text: qsTr("Перерисовать") anchors.right: parent.right onClicked: JS.repaintKanban() } }
Как видите, здесь ComboBox содержит модель с возможными вариантами группировки, и в каждом элементе прописан путь в JSON к значению, которое будет использоваться для определения группы. Таким образом количество вариантов группировок по желанию можно расширить.
На верхнем уровне определены свойства, два из которых — алиасы к внутренним значениям. Алиасы нужны, чтобы можно было присвоить нужное значение, начитанное из LocalStorage. Что же касается свойства groupValuePath:
property string groupValuePath: cbGroupField.model.get(cbGroupField.currentIndex).namePath
то оно просто возвращает путь к значению для текущего варианта группировки.
Вставляем KanbanParams в KanbanWindow и у нас получается такое окошко:

Я не буду подробно расписывать, как обрабатываются параметры,
Что дальше?
Получившейся доской уже можно пользоваться для просмотра текущей ситуации с запросами, но можно ее улучшить:
- Сделать сортировку карточек в столбцах. Например, по приоритету запроса. И цветовую дифференциацию
штанов, пардон, запросов по приоритетам и типам запросов. Я пробовал — очень удобно, рекомендую. - Сделать перетаскивание карточек между столбцами с присвоением значения, соответствующего новому столбцу. Кстати, статус таким образом не изменить, поскольку в Jira статус меняется не присвоением, а переходом (transition).
- Сделать ввод новых запросов прямо в доске.
- Для предыдущих двух пунктов потребуется авторизация. Надо делать.
- Поскольку здесь нет ничего, кроме QML, проект можно собрать под Android и iOS — должно работать без переделок.
Код выложен на GitHub.
