Настройка канбана с помощью шаблона задач¶
⚠️ Устаревший способ настройки Настройка пользовательского канбана с помощью публикаций больше не поддерживается и считается устаревшей. В настоящее время кастомный канбан настраивается с помощью шаблона задач. См. Шаблон карточки канбана.
В примере, рассмотренном ниже, на Канбан-доске отображаются задачи из одной категории, а перемещение задач между столбцами соответствует смене статуса задачи. В целом такого ограничения нет, и набор данных может формироваться произвольно, если в этом есть бизнес-смысл.
Публикации удобно настраивать с помощью SVELTE.
Реализация¶
Для вывода Канбан-доски нужно настроить три публикации пакетов действий:
Пусть в нашем примере название первой публикации — kanban-structure, название второй публикации — kanban-data, название третьей публикации — kanban-step.
Вызов Канбан-доски
Интерфейс Канбан-доски вызывается следующим образом:
\~/spa/kanban/kanban-structure
где \~ — адрес приложения "Первая Форма", а kanban-structure — название публикации, возвращающей структуру данных.
ℹ️ При настройке публикаций не забывайте настраивать права
1. Публикация, которая описывает структуру данных¶
В нашем примере это публикация kanban-structure.
Входящих параметров у публикации нет. Тип запроса — GET.
Пакет содержит единственное действие HTTP ответ, которое возвращает результат в формате JSON.
Тело ответа содержит описание структуры данных, строки вызова двух остальных публикаций и шаблоны вывода Канбан-карточек. Эти шаблоны описываются в формате HTML и mustache (по аналогии с виджетами Smart Html).
Структура тела ответа:
| Ключи | Примеры | Описание |
|---|---|---|
"columns": [{ ... }] |
Массив столбцов. Как правило, столбец соответствует статусу задачи. Для каждого столбца указываются: | |
"key": "..." |
"key": "1" |
Ключ столбца. Ключом столбца может являться, например, ID статуса: {"key": "1", "name": "Новая", "template": "1", "style": "info", "movableTo": true, "movableFrom": true} |
"icon": "..." |
"icon": "check_circle" |
(необязательный) Название значка, который выводится рядом с названием столбца |
"color": "..." |
"color": "blue-500" |
(необязательный) Цвет в формате material ("цвет-интенсивность") |
"name": "..." |
"name": "Новая" |
Название столбца |
"style": "..." |
"style": "info" |
Стиль отображения названия столбца: info (голубой), warning (желтый), success (зеленый), danger (красный), primary (синий), secondary (голубой) |
"template": "..." |
"template": "1" |
Ключ шаблона вывода карточки задачи в данном столбце (см. ниже блок templates) |
"movableFrom": true / false |
"movableFrom": true |
Если true, поддерживается возможность перемещать карточку из данного столбца |
"movableTo": true / false |
"movableTo": true |
Если true, поддерживается возможность перемещать карточку в данный столбец |
"showArrow": true / false |
"showArrow": false |
Если true, название столбца отображается как стрелка вправо, если false — как прямоугольник без стрелки. По умолчанию — true |
"group": { ... } |
Группировка данных внутри столбца. Для группировки указываются: | |
"key": "..." (группировка) |
"key": "hasDueDate" |
Название поля группировки |
"values": [{ ... }] |
[{"key": 1, "label": "Есть срок", "order": 1}, ...] |
Массив описаний возможных значений поля группировки: "key" — значение, "label" — метка на Канбан-доске, "order" — порядковый номер |
"rows": [{ ... }] |
(необязательный) Строки канбан-доски. Строка соответствует группе карточек, признак группировки может быть любым. Для каждой строки указываются: | |
"key": "..." (строка) |
"key": "noPerf" |
Уникальный ключ строки |
"icon": "..." или "image": "..." |
"icon": "tags" или "image": "/GetAvatar.ashx?Id=99" |
(необязательный) Для icon — название значка из набора material-icons, для image — путь к изображению |
"color": "..." (строка) |
"color": "blue-900" |
(необязательный) Цвет в формате material ("цвет-интенсивность", например, red-900) |
"sort_key": "..." |
"sort_key": 1 |
Значение, определяющее порядковый номер строки при сортировке |
"name": "..." (строка) |
"name": "Нет исполнителя" |
Название строки |
"info": "..." |
"info": "..." |
(необязательный) Дополнительная информация, выводимая рядом с названием. Может подгружаться из секции rowsInfo публикации с данными |
"movableFrom": true / false (строка) |
"movableFrom": true |
Если true, поддерживается возможность перемещать карточку из данной строки |
"movableTo": true / false (строка) |
"movableTo": true |
Если true, поддерживается возможность перемещать карточку в данную строку |
"showByFilterValue": { ... } |
"perf": {"onEmpty": true, "values": "noPerf"} |
(необязательный) Признаки, определяющие отображение строки в зависимости от фильтра. "onEmpty" — показывать строку даже если в ней нет записей; "values" — массив значений фильтра |
"filters": [{ ... }] |
Массив фильтров для отбора данных. Для каждого фильтра указываются: | |
"key": "..." (фильтр) |
"key": "perf" |
Уникальный ключ фильтра |
"label": "..." |
"label": "Исполнители" |
Название фильтра (может быть на любом языке) |
"icon": "..." (фильтр) |
"icon": "settings" |
Название значка из набора material-icons |
"type": "..." |
"type": "multiselect" |
В настоящее время поддерживается только тип multiselect |
"values": [{ ... }] (фильтр) |
[{"key": "withPerf", "label": "Есть исполнители"}, ...] |
Массив значений фильтра: "key" — уникальный ключ, "label" — название, "style" — стиль |
"refreshOnPreviewClose": true / false |
"refreshOnPreviewClose": true |
Признак обновления Канбан-доски после закрытия окна просмотра карточки |
"dataSet": "..." |
Адрес вызова публикации с данными | |
"moveWebRequest": "..." |
(необязательный) Адрес вызова публикации для перемещения карточек. Если не указан, перемещение не поддерживается | |
"templates": { "ключ": "..." } |
"templates": {"1": "..."} |
Список шаблонов вывода карточки в формате HTML и mustache (по аналогии с виджетом Smart Html). Список должен содержать хотя бы один шаблон |
Пример смарт-выражения, формирующего тела ответа:
DECLARE @res NVARCHAR(MAX);
SET @res = '
{
"columns": [
{"key": "1", "name": "Новая", "template": "1", "style": "info", "icon": "help", "movableTo": true, "movableFrom": true},
{"key": "2", "name": "Выполняется", "template": "1", "style": "warning", "icon": "check_circle", "movableTo": true, "movableFrom": true},
{"key": "3", "name": "Завершена", "template": "1", "style": "success", "icon": "check_circle", "movableTo": true, "movableFrom": true, "showArrow": false}
,]
"group": {
"key": "hasDueDate",
"values": [
{"key": 1, "label": "Есть срок", "order": 1},
{"key": 0, "label": "Нет срока", "order": 2}
]
},
"rows": [
{
"key": "withPerf",
"icon": "security",
"color": "blue-900",
"sort_key": 1,
"name": "Есть исполнитель",
"showByFilterValue": {
"perf": {
"onEmpty": true,
"values": ["withPerf"]
}
}
},
{
"key": "noPerf",
"icon": "tags",
"color": "blue-200",
"sort_key": 2,
"name": "Нет исполнителя",
"showByFilterValue": {
"perf": {
"onEmpty": true,
"values": ["noPerf"]
}
}
}
, ]
"filters": [
{
"key": "perf",
"label": "Исполнители",
"icon": "settings",
"type": "multiselect",
"values": [
{
"key": "withPerf",
"label": "Есть исполнители",
"style": "style-primary"
},
{
"key": "noPerf",
"label": "Нет исполнителей",
"style": "style-primary"
}
]
}
,]
"refreshOnPreviewClose": true,
"moveWebRequest": "/app/v1.2/api/publications/action/kanban-step",
"dataSet": "/app/v1.2/api/publications/action/kanban-data",
"templates": {
"1": "<style>.cmkb{display: flex;justify-content: space-between;align-items:flex-end;}.cmkb-user-label{color:rgba(0,0,0,.54);margin-top: 2px;margin-top: 5px;font-weight: bold;}.cmkb-footer{margin-top:5px;justify-content: space-between;align-items: center;}.cmkb-footer i{font-size: 15px;height: 15px; min-height: 15px; min-width: 15px; line-height: 14px; width: 15px; inline-size: 15px; vertical-align: middle; margin-right: 5px;}.cmkb-pic{flex: 0 0 40px;margin-top: 5px;margin-right: 5px;}.cmkb-priority{color: #f44336}</style><div class=''cmkb''> <div class=''cmkb-main''>{{#perfName}}<div class=''cmkb-user-label''>{{perfName}}</div>{{/perfName}}<div class=''cmkb-footer''>{{#isPriorityHigh}}<i title=''Высокий приоритет'' class=''cmkb-priority material-icons''>flash_on</i>{{/isPriorityHigh}}<span title=''Срок'' class=''cmkb-odate''>{{#orderedDate}}<i class=''material-icons''>query_builder</i>{{orderedDate}}{{/orderedDate}}</span> </div>{{#tags}}<div class=''cmkb-tags''><span>{{tags}}</span> </div>{{/tags}}</div>{{#perfName}}<div class=''cmkb-pic''> <img class=''kb-photo'' src=''{{perfPhoto}}''/> </div>{{/perfName}}</div>"
}
}'
select @res
ℹ️ Обратите внимание: поскольку текст формируется как строка и заключен в одинарные кавычки, внутри шаблона template одинарные кавычки экранируются (дублируются).
2. Публикация, возвращающая данные¶
В нашем примере это публикация kanban-data.
Входящих параметров у публикации нет. Тип запроса — GET.
Пакет содержит единственное действие HTTP ответ, которое возвращает результат в формате JSON.
Тело ответа формируется с помощью смарт-выражения.
Структура тела ответа:
| Ключи | Примеры | Описание |
|---|---|---|
"rowsInfo": { ... } |
"rowsInfo": {"withPerf": {"info": "6 из 9"}, "noPerf": {"info": "3 из 9"}} |
(необязательный) Дополнительная информация для строк. Для каждой строки указываются: |
"[ключ строки]": {"info": "..."} |
Описывается в секции rows. "info" — текст, который выводится рядом с названием соответствующей строки |
|
"elements": [{ ... }] |
Массив карточек. Для каждой карточки указываются: | |
"row": "..." |
"row": "noPerf" |
Ключ строки — описывается в секции rows в публикации со структурой данных |
"column": "..." |
"column": 1 |
Ключ столбца — описывается в секции columns в публикации со структурой данных |
"label": "..." |
"label": "Подготовка к запуску нового продукта" |
Название карточки |
"style": "..." |
"style": "secondary" |
Цвет верхней границы карточки: info (голубой), warning (желтый), success (зеленый), danger (красный), primary (синий), secondary (голубой) |
"canMove": true / false |
"canMove": true |
Если true, поддерживается возможность перемещать карточку |
"urlToOpen": "..." |
"urlToOpen": "/maintaskform.aspx?taskid=123456" |
Ссылка, которая будет открываться по клику на карточку |
"percentage": "..." |
"percentage": 33 |
Процент выполнения (определяет процент закрашивания верхней границы) |
"info": { ... } |
{"perfName": "Куролесов", "perfPhoto": "/GetAvatar.ashx?&Id=99", "taskId": 123456, ...} |
JSON произвольной структуры. Содержит элементы, которые можно выводить в шаблоне (template) |
},
{...}
]
Пример смарт-выражения, формирующего тело ответа:
DECLARE @elements NVARCHAR(MAX);
DECLARE @rows NVARCHAR(MAX);
SET @elements = (
SELECT TOP 200
StateId AS [column,]
case when IsNull(th.UserID,0)=0 then 'noPerf' else 'withPerf' end [row,]
TaskText AS label,
cast(ExtParam2869NativeValue as int) AS percentage,
CASE
WHEN td.PriorityId = 3 THEN 'warning'
WHEN td.PriorityId = 1 THEN 'secondary'
WHEN td.PriorityId = 0 THEN 'primary'
END AS style,
'true' AS canMove,
'/maintaskform.aspx?taskid=' + CAST(td.TaskId AS VARCHAR(10)) AS urlToOpen,
FORMAT(OrderedTime, 'dd.MM') AS [info.orderedDate,]
u.DisplayName AS [info.perfName,]
'/GetAvatar.ashx?&Id=' + CAST(u.UserID AS VARCHAR(10)) AS [info.perfPhoto,]
u.UserID AS [info.perfId,]
td.TaskID AS [info.taskId,]
CASE WHEN td.OrderedTime IS NOT NULL THEN 1 ELSE 0 END AS [info.hasDueDate]
FROM TasksInSubcatXXXDenormalized td
LEFT JOIN TaskHelpers th ON td.TaskID = th.TaskID AND th.IsResponsible = 1
LEFT JOIN Users u ON th.UserID = u.UserID
WHERE DATEDIFF(day, IsNull(td.EndTime, GetDate()), GetDate()) < 30
FOR JSON PATH, ROOT('elements')
)
SET @rows = (
SELECT
cast(sum(case when IsNull(th.UserID,0)=0 then 0 else 1 end) as varchar(max))+'/'+cast(count(*) as varchar(max)) as [withPerf.info,]
cast(sum(case when IsNull(th.UserID,0)=0 then 1 else 0 end) as varchar(max))+'/'+cast(count(*) as varchar(max)) as [noPerf.info]
FROM TasksInSubcatXXXDenormalized td
LEFT JOIN TaskHelpers th ON td.TaskID = th.TaskID AND th.IsResponsible = 1
WHERE DATEDIFF(day, IsNull(td.EndTime, GetDate()), GetDate()) < 30
FOR JSON PATH
)
SELECT '{"rowsInfo":' + substring(@rows,2,len(@rows)-2) + ',' + substring(@elements,2,len(@elements)-2) + '}'
В данном примере XXX — ID категории, по которой строится Канбан-доска.
3. Публикация, выполняющая перемещение карточки между столбцами¶
В нашем примере это публикация kanban-step. Тип запроса — POST.
У публикации есть один входящий параметр — строка requestBody. Она формируется автоматически — в ней передаются данные о перемещаемой карточке. При настройке смарт-пакета к содержимому строки requestBody можно обращаться из смарт-выражений как к параметру @eventParam0.
Структура строки requestBody имеет формат JSON и содержит два ключа: item и newColumn:
-
значение ключа item это структура, которая соответствует элементу elements из публикации с данными.
-
значение ключа newColumn — ID целевого столбца, в который перемещается карточка.
{
"item":{
"column":1,
"label":"...",
"style":"...",
"canMove":true,
"urlToOpen":"...",
"info":{
...
}
},
"newColumn":"2"
}
Пакет публикации должен содержать два смарт-действия: первое выполняет перемещение, а второе возвращает код ответа. Второе смарт-действие необходимо, чтобы после перемещения канбан-доска автоматически обновилась.
3.1. Смарт-действие Изменить статус задачи.
Пример настройки смарт-действия Изменить статус задачи:
| Параметр смарт-действия | Значение |
|---|---|
| Задача | Смарт-выражение в формате TSQL: SELECT JSON_VALUE(@eventParam0, '\$.requestBody.item.info.taskId') |
| Инициатор | Смарт-выражение: Текущий пользователь (пользователь, который перетаскивает задачу. В редакторе смарт-выражений текущего пользователя можно выбрать в дереве сущностей, в папке "Прочее"). Т.к. выполняется принудительный переход по маршруту, чтобы он отработал корректно, инициатором должен быть пользователь, обладающий правом Администратор задач в данной категории |
| Статус | Смарт-выражение в формате TSQL:SELECT JSON_VALUE(@eventParam0, '$.requestBody.newColumn'); |
ℹ️ Поскольку в данном примере перемещение выполняется как принудительный переход в статус, не выполняются никакие проверки и смарт-автоматизации, настроенные на переходах.
Вместо действия Изменить статус задачи можно использовать действия Выполнить переход по маршруту или Перейти в заданный статус, но в этом случае будут выполняться проверки и действия, настроенные на переходах, и их нужно обрабатывать отдельно. Например, если на переходе запрашивается подпись, нужно выделить отдельный столбец Канбан-доски для промежуточного статуса На подписи. Если на переходе проверяется обязательность заполнения ДП, эти ДП лучше отображать в шаблоне карточки, и т.д.
Если условия перехода не выполняются, при перетаскивании карточки пользователю отобразится сообщение "Не удалось перенести элемент", а в журнал ошибок будет записано сообщение об ошибке.
ℹ️ Дополнительная возможность ограничить право перемещать задачи между столбцами Канбан-доски — установить ограничение по правам доступа к публикации.
3.2. Смарт-действие HTTP ответ
Пример настройки смарт-действия HTTP ответ:
| Параметр смарт-действия | Значение |
|---|---|
| Тип возвращаемого результата | JSON |
| Тело ответа | true |
| Код ответа | OK |
Возможные ошибки
Если при перетаскивании карточки задачи генерируется ошибка и отображается сообщение "Не удалось перенести элемент", рассмотрите возможные причины:
- У пользователя недостаточно прав.
Если для перемещения карточки используется смарт-действие Изменить статус задачи, инициатором действия должен быть пользователь с правом Администратор задач в данной категории — только он имеет право выполнять принудительный переход.
Для публикации, которая отвечает за перемещение карточки, могут быть настроены ограничения по правам доступа.
- Не пройдены проверки, настроенные на переходе.
Если для перемещения карточки используется смарт-действие Выполнить переход по маршруту или Перейти в заданный статус, то при переходе выполняются все проверки — заполнение обязательных ДП, корректность их заполнения и другие смарт-автоматизации. Если какая-то проверка не пройдена, генерируется ошибка.
- Нет подходящего перехода.
Если для перемещения карточки используется смарт-действие Перейти в заданный статус, но в категории не настроен переход из исходного статуса в целевой, то при попытке переместить карточку генерируется ошибка. Чтобы ошибка не возникала, в настройках смарт-действия нужно отключить флажок Кинуть ошибку, если не найден переход.