diff --git a/assets/script.js b/assets/script.js index f9c023f89d1487030653a530a8d00e367f1e8c92..57e471fc7c58c98b83678d8204a47b5e2e32863b 100644 --- a/assets/script.js +++ b/assets/script.js @@ -5,63 +5,96 @@ var lastRequestController = null; $(document).ready(function () { - if (document.getElementById('trac-migrate') === null) { - return; - } + if (document.getElementById('trac-migrate') !== null) { + let source = null; + STUDIP.loadChunk('vue').then(function ({createApp}) { + createApp({ + data: function () { + return { + needle: '', + results: false, + selectedTicket: false, + }; + }, + mounted () { + source = this.$el.dataset.source; + }, + methods: { + searchTickets (event) { + const needle = this.needle.trim(); + if (needle.trim().length < 3) { + return; + } - let source = null; - STUDIP.loadChunk('vue').then(function ({createApp}) { - createApp({ - data: function () { - return { - needle: '', - results: false, - selectedTicket: false, - }; - }, - mounted () { - source = this.$el.dataset.source; - }, - methods: { - searchTickets (event) { - const needle = this.needle.trim(); - if (needle.trim().length < 3) { - return; - } + if (lastRequestController) { + lastRequestController.abort(); + } + + lastRequestController = new AbortController(); + const { signal } = lastRequestController; - if (lastRequestController) { - lastRequestController.abort(); + clearTimeout(searchTimeout); + searchTimeout = setTimeout(function () { + const url = STUDIP.URLHelper.getURL(source, {term: needle}); + fetch(url, { signal }).then(response => this.results = response.json()); + }.bind(this), 300); + } + }, + computed: { + orderedResults () { + const needle = this.needle.trim(); + return Object.values(this.results).sort(function (a, b) { + if (a[0] === needle) { + return 1; + } + if (b[0] === needle) { + return -1; + } + return b[0] - a[0]; + }); } + } + }).$mount('#trac-migrate'); + }); + } else if (document.getElementById('dashboard') !== null) { + const dashboard = document.getElementById('dashboard'); + const issues = JSON.parse(dashboard.dataset.issues); + const qmLabels = JSON.parse(dashboard.dataset.qmLabels); - lastRequestController = new AbortController(); - const { signal } = lastRequestController; + let filters = {}; + Object.values(qmLabels).forEach(abbr => filters[abbr] = null); - clearTimeout(searchTimeout); - searchTimeout = setTimeout(function () { - const url = STUDIP.URLHelper.getURL(source, {term: needle}); - fetch(url, { signal }).then(function (response) { - return response.json(); - }).then(function (json) { - this.results = json; - }.bind(this)); - }.bind(this), 300); - } - }, - computed: { - orderedResults () { - const needle = this.needle.trim(); - return Object.values(this.results).sort(function (a, b) { - if (a[0] === needle) { - return 1; - } - if (b[0] === needle) { - return -1; - } - return b[0] - a[0]; - }); + STUDIP.loadChunk('vue').then(({createApp}) => { + createApp({ + data () { + return { + issues, + qmLabels, + filters + }; + }, + methods: { + getStateForIssueAndQmLabel(issue, qm) { + return issue.qm_states[qm]; + }, + }, + computed: { + filteredIssues() { + return this.issues.filter(issue => { + for (const [key, value] of Object.entries(this.filters)) { + if (value === null) { + continue; + } + if (issue.qm_states[key] !== value) { + return false; + } + } + return true; + }); + } } - } - }).$mount('#trac-migrate'); - }); + }).$mount('#dashboard'); + }); + } }); }(jQuery, STUDIP)); diff --git a/controllers/dashboard.php b/controllers/dashboard.php index c4ede9c43ae512c39950fc78cc6621642547cfbe..0178ad77d8df249c246bd5631215c5b48c651c6c 100644 --- a/controllers/dashboard.php +++ b/controllers/dashboard.php @@ -1,17 +1,6 @@ <?php final class DashboardController extends TracToGitlab\Controller { - const QM_LABEL_MAPPING = [ - 'Anwendungs-Doku' => 'userdoc', - 'Barrierefreiheit' => 'a11y', - 'Code-Review' => 'code', - 'Entwicklungs-Doku' => 'devdoc', - 'Funktionalität' => 'func', - 'GUI-Richtlinien' => 'gui', - 'Schnittstellen' => 'iface', - 'Textstrings' => 'text', - ]; - public function before_filter(&$action, &$args) { parent::before_filter($action, $args); @@ -152,6 +141,6 @@ final class DashboardController extends TracToGitlab\Controller private function getQMLabelMapping() { - return self::QM_LABEL_MAPPING; + return TracToGitlab\GitlabIssue::QM_LABEL_MAPPING; } } diff --git a/lib/GitlabIssue.php b/lib/GitlabIssue.php index e87f1ca9956f9edbae39d06357b89d1c3b91af67..1b1e7bf743cf8d56a1a80ef3a20d8b4606892d34 100644 --- a/lib/GitlabIssue.php +++ b/lib/GitlabIssue.php @@ -1,8 +1,19 @@ <?php namespace TracToGitlab; -final class GitlabIssue +final class GitlabIssue implements \JsonSerializable { + const QM_LABEL_MAPPING = [ + 'Anwendungs-Doku' => 'userdoc', + 'Barrierefreiheit' => 'a11y', + 'Code-Review' => 'code', + 'Entwicklungs-Doku' => 'devdoc', + 'Funktionalität' => 'func', + 'GUI-Richtlinien' => 'gui', + 'Schnittstellen' => 'iface', + 'Textstrings' => 'text', + ]; + private $issue; private $mrs; @@ -36,7 +47,7 @@ final class GitlabIssue return $this->issue[$offset] ?? null; } - public function isClosed() + public function isClosed(): bool { return $this->issue['state'] !== 'opened'; } @@ -69,22 +80,51 @@ final class GitlabIssue return in_array('StEP', $this->issue['labels']); } - public function getIconForQMLabel(string $label) + public function getIconForQMLabel(string $label): string + { + $state = $this->getStateForQMLabel($label); + if ($state === '+') { + return \Icon::create('accept', \Icon::ROLE_STATUS_GREEN); + } + if ($state === '?') { + return \Icon::create('question', \Icon::ROLE_STATUS_YELLOW); + } + if ($state === '-') { + return \Icon::create('decline', \Icon::ROLE_STATUS_RED); + } + + return ''; + } + + public function getStateForQMLabel(string $label): ?string { foreach ($this->issue['labels'] as $l) { if (preg_match("/^QM::{$label}::(.)$/", $l, $match)) { - $state = $match[1]; - if ($state === '+') { - return \Icon::create('accept', \Icon::ROLE_STATUS_GREEN); - } - if ($state === '?') { - return \Icon::create('question', \Icon::ROLE_STATUS_YELLOW); - } - if ($state === '-') { - return \Icon::create('decline', \Icon::ROLE_STATUS_RED); - } + return $match[1]; } } - return ''; + return null; + } + + public function jsonSerialize() + { + $result = [ + 'iid' => $this->iid, + 'title' => $this->title, + 'type' => $this->type, + 'author' => $this->author['username'], + 'assignee' => $this->assignee, + 'web_url' => $this->web_url, + 'closed' => $this->isClosed(), + 'merge_requests' => $this->issue['merge_requests_count'], + 'merged' => $this->isMerged(), + 'qm_states' => array_combine( + array_values(self::QM_LABEL_MAPPING), + array_map(function ($qm) { + return $this->getStateForQMLabel($qm); + }, array_keys(self::QM_LABEL_MAPPING)) + ), + ]; + return $result; } } diff --git a/plugin.manifest b/plugin.manifest index ca5ae143d01fb1b5896d0c366ae83b9107286bdc..cf2fe064ad1a600671c05b58ffb46b0bc3e00b0a 100644 --- a/plugin.manifest +++ b/plugin.manifest @@ -1,5 +1,5 @@ pluginname=Trac to gitlab converter pluginclassname=TracToGitlabPlugin origin=UOL -version=1.2 +version=1.3 studipMinVersion=5.0 diff --git a/views/dashboard/index.php b/views/dashboard/index.php index 32ef334f25e87451766efec4367545866682ec43..f44fc714791099aa7f16ff8f6c2f56a34b019225 100644 --- a/views/dashboard/index.php +++ b/views/dashboard/index.php @@ -4,76 +4,84 @@ * @var array<int, TracToGitlab\GitlabIssue> $issues */ ?> -<table class="default"> - <thead> - <tr> - <th><?= _('Issue') ?></th> - <th><?= _('Typ') ?></th> - <th><?= _('Status') ?></th> - <th><?= _('MR') ?></th> - <th><?= _('Titel') ?></th> - <th><?= _('Autor') ?></th> - <th><?= _('Bearbeiter') ?></th> - <? foreach ($mapping as $label => $abbrevation): ?> - <th> - <abbr title="<?= htmlReady($label) ?>"> - <?= htmlReady($abbrevation) ?> - </abbr> - </th> - <? endforeach; ?> - </tr> - </thead> - <tbody> - <? if (!$issues): ?> - <tr> - <td colspan="<?= 7 + count($mapping) ?>" style="text-align: center"> - <?= _('Keine Issues für diesen Meilenstein und Typ') ?> - </td> - </tr> - <? endif; ?> - <? foreach ($issues as $issue): ?> - <tr> - <td> - <a href="<?= htmlReady($issue->web_url) ?>" target="_blank"> - #<?= htmlReady($issue->iid) ?> - </a> - </td> - <td><?= htmlReady($issue->type) ?></td> - <td> - <? if ($issue->isClosed()): ?> - closed - <? else: ?> - open - <? endif; ?> - </td> - <td> - <? if (!$issue->hasMergeRequests()): ?> - <abbr title="<?= _('Kein MR') ?>"> - <?= Icon::create('decline', Icon::ROLE_STATUS_RED) ?> - </abbr> - <? elseif ($issue->isMerged()): ?> - <abbr title="<?= _('MR bereits gemerget') ?>"> - <?= Icon::create('accept', Icon::ROLE_STATUS_GREEN) ?> - </abbr> - <? else: ?> - <abbr title="<?= _('MR noch nicht gemerget') ?>"> - <?= Icon::create('date', Icon::ROLE_STATUS_YELLOW) ?> - </abbr> - <? endif; ?> - </td> - <td> - <a href="<?= htmlReady($issue->web_url) ?>" target="_blank"> - <?= htmlReady($issue->title) ?> - </a> - </td> - <td><?= htmlReady($issue->author['username']) ?></td> - <td><?= htmlReady($issue->assignee) ?></td> - <? foreach ($mapping as $label => $abbrevation): ?> - <td> - <?= $issue->getIconForQMLabel($label) ?> - </td> - <? endforeach; ?> - </tr> - <? endforeach; ?> - </tbody> -</table> +<div id="dashboard" data-issues='<?= json_encode($issues) ?>' data-qm-labels='<?= json_encode($mapping) ?>' v-cloak> + <table class="default"> + <thead> + <tr> + <th><?= _('Issue') ?></th> + <th><?= _('Typ') ?></th> + <th><?= _('Status') ?></th> + <th><?= _('MR') ?></th> + <th><?= _('Titel') ?></th> + <th><?= _('Autor') ?></th> + <th><?= _('Bearbeiter') ?></th> + <th v-for="(abbr, label) in qmLabels"> + <abbr :title="label">{{ abbr }}</abbr> + </th> + </tr> + </thead> + <tbody> + <? if (!$issues): ?> + <tr> + <td colspan="<?= 7 + count($mapping) ?>" style="text-align: center"> + <?= _('Keine Issues für diesen Meilenstein und Typ') ?> + </td> + </tr> + <? endif; ?> + <tr v-for="issue in filteredIssues"> + <td> + <a :href="issue.web_url" target="_blank"> + #{{ issue.iid }} + </a> + </td> + <td>{{ issue.type }}</td> + <td>{{ issue.closed ? 'closed' : 'open' }}</td> + <td v-if="!issue.merge_requests"> + <abbr title="<?= _('Kein MR') ?>"> + <?= Icon::create('decline', Icon::ROLE_STATUS_RED) ?> + </abbr> + </td> + <td v-else-if="issue.merged"> + <abbr title="<?= _('MR bereits gemerget') ?>"> + <?= Icon::create('accept', Icon::ROLE_STATUS_GREEN) ?> + </abbr> + </td> + <td v-else> + <abbr title="<?= _('MR noch nicht gemerget') ?>"> + <?= Icon::create('date', Icon::ROLE_STATUS_YELLOW) ?> + </abbr> + </td> + <td> + <a :href="issue.web_url" target="_blank"> + {{ issue.title }} + </a> + </td> + <td>{{ issue.author }}</td> + <td>{{ issue.assignee }}</td> + <td v-for="(abbr, label) in qmLabels"> + <studip-icon shape="accept" role="status-green" v-if="getStateForIssueAndQmLabel(issue, abbr) === '+'"></studip-icon> + <studip-icon shape="question" role="status-yellow" v-else-if="getStateForIssueAndQmLabel(issue, abbr) === '?'"></studip-icon> + <studip-icon shape="decline" role="status-red" v-else-if="getStateForIssueAndQmLabel(issue, abbr) === '-'"></studip-icon> + </td> + </tr> + </tr> + </tbody> + </table> + + <mounting-portal mount-to="#layout-sidebar .sidebar" name="sidebar-filter" append> + <div class="sidebar-widget"> + <div class="sidebar-widget-header">Filter</div> + <div class="sidebar-widget-content"> + <label v-for="(abbr, label) in qmLabels" style="display: block;"> + {{ label }} + <select v-model="filters[abbr]" style="display: block;" class="sidebar-selectlist"> + <option :value="null"></option> + <option>+</option> + <option>?</option> + <option>-</option> + </select> + </label> + </div> + </div> + </mounting-portal> +</div>