tr('Kanban'), 'description' => tr(''), 'prefs' => ['wikiplugin_kanban'], 'format' => 'html', 'iconname' => 'th', 'introduced' => 24, 'params' => [ 'trackerId' => [ 'name' => tr('Tracker ID'), 'description' => tr('Tracker to search from'), 'since' => '24.0', 'required' => false, 'default' => 0, 'filter' => 'int', 'profile_reference' => 'tracker', ], 'title' => [ 'name' => tr('Title Field'), 'description' => tr('Title text on each card.'), 'hint' => tr('e.g. "kanbanTitle"'), 'since' => '24.0', 'required' => true, 'filter' => 'word', ], 'description' => [ 'name' => tr('Card description'), 'description' => tr(''), 'hint' => tr('e.g. "kanbanDescription"'), 'since' => '24.0', 'required' => false, 'filter' => 'word', ], 'xaxis' => [ 'name' => tr('X Axis Field'), 'description' => tr('The columns, usually a dropdown list with options such as "Wishes", "Work" and "Done".'), 'hint' => tr('e.g. "kanbanColumns"'), 'since' => '24.0', 'required' => true, 'filter' => 'word', ], 'yaxis' => [ 'name' => tr('Y Axis Field'), 'description' => tr('Sort order for cards within a cell, could be date or a numeric (sortable) field'), 'hint' => tr('e.g. "kanbanOrder"'), 'since' => '24.0', 'required' => true, 'filter' => 'word', ], 'swimlane' => [ 'name' => tr('Swimlane Field'), 'description' => tr('Defines the "rows" or "swimlanes". Can be any list type field'), 'hint' => tr('e.g. "kanbanSwimlanes'), 'since' => '24.0', 'required' => false, 'filter' => 'word', ], 'wip' => [ 'name' => tr('WiP Limit'), 'description' => tr('Work in progress limit is a comma-separated list of numbers defining the WiP limits for every column. If you need a single limit for the whole board, specify just one number here.'), 'since' => '24.0', 'required' => false, 'filter' => 'text', ], ], ]; } function wikiplugin_kanban(string $data, array $params): WikiParser_PluginOutput { global $user, $prefs; static $id = 0; if ($prefs['auth_api_tokens'] !== 'y') { return WikiParser_PluginOutput::userError(tr('Security -> API access is disabled but Kanban plugin needs it.')); } //set defaults $plugininfo = wikiplugin_kanban_info(); $defaults = []; foreach ($plugininfo['params'] as $key => $param) { $defaults[$key] = $param['default'] ?? null; } $params = array_merge($defaults, $params); $jit = new JitFilter($params); $definition = Tracker_Definition::get($jit->trackerId->int()); if (! $definition) { return WikiParser_PluginOutput::userError(tr('Tracker not found.')); } $fields = [ 'title' => $jit->title->word(), 'description' => $jit->description->word(), 'xaxis' => $jit->xaxis->word(), 'yaxis' => $jit->yaxis->word(), 'swimlane' => $jit->swimlane->word(), ]; foreach ($fields as $key => $field) { $field = $definition->getFieldFromPermName($field); if (! $field) { return WikiParser_PluginOutput::userError(tra('Field not found: %0', $key)); } $fields[$key] = $field; } $fieldFactory = $definition->getFieldFactory(); $columnsHandler = $fieldFactory->getHandler($fields['xaxis']); $columns = wikiplugin_kanban_format_list($columnsHandler); $wip = $jit->wip->text(); if (is_numeric($wip)) { $wip = array_fill(0, count($columns), $wip); } elseif ($wip) { $wip = preg_split('/\s*,\s*/', trim($wip)); } if ($wip) { foreach ($columns as $key => $column) { $columns[$key]['wip'] = intval($wip[$key] ?? 1); } } $swimlanesHandler = $fieldFactory->getHandler($fields['swimlane']); $swimlanes = wikiplugin_kanban_format_list($swimlanesHandler); $query = new Search_Query(); $query->filterType('trackeritem'); $query->filterContent((string)$jit->trackerId->int(), 'tracker_id'); $unifiedsearchlib = TikiLib::lib('unifiedsearch'); $unifiedsearchlib->initQuery($query); $matches = WikiParser_PluginMatcher::match($data); $builder = new Search_Query_WikiBuilder($query); $builder->apply($matches); if (! $index = $unifiedsearchlib->getIndex()) { return WikiParser_PluginOutput::userError(tr('Unified search index not found.')); } $result = []; foreach ($query->scroll($index) as $row) { $result[] = $row; } $result = Search_ResultSet::create($result); $result->setId('wpkanban-' . $id); $resultBuilder = new Search_ResultSet_WikiBuilder($result); $resultBuilder->apply($matches); $data .= '{display name="object_id"}'; $plugin = new Search_Formatter_Plugin_ArrayTemplate($data); $usedFields = array_keys($plugin->getFields()); foreach ($fields as $key => $field) { if (! in_array('tracker_field_' . $field['permName'], $usedFields) && ! in_array($field['permName'], $usedFields)) { if ($field['type'] == 'e') { $data .= '{display name="tracker_field_' . $field['permName'] . '" format="categorylist" singleList="y" separator=" "}'; } else { $data .= '{display name="tracker_field_' . $field['permName'] . '" default=" "}'; } } } $fields['object_id'] = ['permName' => 'object_id']; $plugin = new Search_Formatter_Plugin_ArrayTemplate($data); $plugin->setFieldPermNames($fields); $builder = new Search_Formatter_Builder(); $builder->setId('wpkanban-' . $id); $builder->setCount($result->count()); $builder->apply($matches); $builder->setFormatterPlugin($plugin); $formatter = $builder->getFormatter(); $entries = $formatter->getPopulatedList($result, false); $entries = $plugin->renderEntries($entries); $items = []; foreach ($entries as $row) { $swimlane = $row[$fields['swimlane']['permName']]; if (is_array($swimlane)) { $swimlane = array_shift($swimlane); } foreach ($swimlanes as $sw) { if ($swimlane == $sw['id'] || $swimlane == $sw['title']) { $swimlane = $sw['id']; break; } } $column = $row[$fields['xaxis']['permName']]; if (is_array($column)) { $column = array_shift($column); } $found = false; foreach ($columns as $col) { if ($column == $col['id'] || $column == $col['title']) { $found = true; $column = $col['id']; break; } } if (! $found) { continue; } $items[] = [ 'id' => $row['object_id'], 'title' => $row[$fields['title']['permName']], 'description' => $row[$fields['description']['permName']], 'row' => $swimlane, 'column' => $column, 'sortOrder' => $row[$fields['yaxis']['permName']], ]; } $token = TikiLib::lib('api_token')->createToken([ 'type' => 'kanban', 'user' => $user, 'expireAfter' => strtotime("+1 hour"), ]); $smarty = TikiLib::lib('smarty'); $smarty->assign( 'kanbanData', [ 'id' => 'kanban' . ++$id, 'accessToken' => $token['token'], 'trackerId' => $jit->trackerId->int(), 'xaxisField' => $jit->xaxis->word(), 'yaxisField' => $jit->yaxis->word(), 'swimlaneField' => $jit->swimlane->word(), 'titleField' => $jit->title->word(), 'descriptionField' => $jit->description->word(), 'columns' => $columns, 'rows' => $swimlanes, 'cards' => $items, 'user' => $user ] ); TikiLib::lib('header') ->add_jsfile('storage/public/vue-mf/root-config/vue-mf-root-config.min.js') ->add_jsfile('storage/public/vue-mf/kanban/vue-mf-kanban.min.js') ; $out = str_replace(['~np~', '~/np~'], '', $formatter->renderFilters()); $out .= $smarty->fetch('wiki-plugins/wikiplugin_kanban.tpl'); return WikiParser_PluginOutput::html($out); } function wikiplugin_kanban_format_list($handler) { $fieldData = $handler->getFieldData(); $list = $formatted = []; if ($handler->getConfiguration('type') === 'd') { $list = $fieldData['possibilities']; } elseif ($handler->getConfiguration('type') === 'e') { foreach ($fieldData['list'] as $categ) { $list[$categ['categId']] = $categ['name']; } } $non_numeric_keys = array_filter(array_keys($list), function($key) { return ! is_numeric($key); }); $realKey = 1; foreach($list as $key => $val) { if ($non_numeric_keys) { $id = $realKey++; } else { $id = $key; } $formatted[] = ['id' => $id, 'title' => $val, 'value' => $key]; } return $formatted; }