From 85f0a3abdb939e043b32f5e51c41a51586132190 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+github@gmail.com> Date: Wed, 24 May 2023 17:20:13 +0200 Subject: [PATCH] refactor to apps folder --- TracToGitlabPlugin.php | 4 + assets/apps/dashboard.js | 85 +++++++++++++ assets/apps/testing.js | 8 ++ assets/apps/trac-migrate.js | 53 ++++++++ assets/script.js | 151 ++-------------------- controllers/convert.php | 3 + controllers/dashboard.php | 162 ++---------------------- controllers/testing.php | 25 ++++ lib/Controller.php | 23 ++++ lib/GitlabController.php | 2 - lib/SelectableIssuesTrait.php | 159 +++++++++++++++++++++++ views/convert/index.php | 2 +- views/dashboard/index.php | 231 ++++++++++++++++------------------ views/testing/index.php | 64 ++++++++++ views/vue-app.php | 10 ++ 15 files changed, 571 insertions(+), 411 deletions(-) create mode 100644 assets/apps/dashboard.js create mode 100644 assets/apps/testing.js create mode 100644 assets/apps/trac-migrate.js create mode 100644 controllers/testing.php create mode 100644 lib/SelectableIssuesTrait.php create mode 100644 views/testing/index.php create mode 100644 views/vue-app.php diff --git a/TracToGitlabPlugin.php b/TracToGitlabPlugin.php index 9be2f83..20e3e77 100644 --- a/TracToGitlabPlugin.php +++ b/TracToGitlabPlugin.php @@ -78,6 +78,10 @@ final class TracToGitlabPlugin extends TracToGitlab\Plugin implements StandardPl 'dashboard', new Navigation(_('Dashboard'), PluginEngine::getURL($this, [], 'dashboard')) ); + $navigation->addSubnavigation( + 'testing', + new Navigation(_('Testsysteme'), PluginEngine::getURL($this, [], 'testing')) + ); $navigation->addSubnavigation( 'translations', new Navigation(_('Übersetzungen'), PluginEngine::getURL($this, [], 'translations')) diff --git a/assets/apps/dashboard.js b/assets/apps/dashboard.js new file mode 100644 index 0000000..2f739a5 --- /dev/null +++ b/assets/apps/dashboard.js @@ -0,0 +1,85 @@ +export default { + props: { + issues: Array, + qmLabels: Object, + filters: Object, + filterStoreUrl: String, + }, + data () { + let currentFilters = {}; + + Object.values(this.qmLabels).forEach(abbr => { + if (currentFilters[abbr] === undefined) { + currentFilters[abbr] = null; + } + }); + + return { + needle: '', + currentFilters: { ...this.filters } + }; + }, + methods: { + getStateForIssueAndQmLabel(issue, qm) { + return issue.qm_states[qm]; + }, + valueMatchesNeedle(what) { + if (this.needle.length === 0) { + return false; + } + + return what.toLowerCase().includes(this.needle.toLowerCase()); + } + }, + computed: { + colspan() { + return 8 + Object.values(this.qmLabels).length; + }, + filteredIssues() { + let filtered = this.issues.filter(issue => { + for (const [key, value] of Object.entries(this.currentFilters)) { + if (value === null) { + continue; + } + if (issue.qm_states[key] !== value) { + return false; + } + } + return true; + }); + + if (this.needle.length > 0) { + filtered = filtered.filter(issue => { + const ciNeedle = this.needle.toLowerCase(); + return issue.iid.toString().includes(this.needle) + || issue.title.toLowerCase().includes(ciNeedle) + || (issue.assignee ?? '').toLowerCase().includes(ciNeedle) + || (issue.author ?? '').toLowerCase().includes(ciNeedle) + || issue.reviewers.some(reviewer => reviewer.toLowerCase().includes(ciNeedle)); + }); + } + + return filtered; + } + }, + watch: { + currentFilters: { + handler(current) { + console.log('changed filters'); + const data = new URLSearchParams(); + + for (const [label, value] of Object.entries(current)) { + if (value !== null) { + data.append(`filters[${label}]`, value); + } + } + + fetch(this.filterStoreUrl, { + method: 'POST', + body: data + }); + }, + deep: true + } + }, +}; diff --git a/assets/apps/testing.js b/assets/apps/testing.js new file mode 100644 index 0000000..e66e7f0 --- /dev/null +++ b/assets/apps/testing.js @@ -0,0 +1,8 @@ +export default { + props: { + issues: Array + }, + created () { + console.log('created', this.$el); + } +}; diff --git a/assets/apps/trac-migrate.js b/assets/apps/trac-migrate.js new file mode 100644 index 0000000..51a0fb5 --- /dev/null +++ b/assets/apps/trac-migrate.js @@ -0,0 +1,53 @@ +let searchTimeout = null; +let lastRequestController = null; + +export default { + props: { + source: String + }, + data () { + 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; + + clearTimeout(searchTimeout); + searchTimeout = setTimeout(async () => { + const url = STUDIP.URLHelper.getURL(source, {term: needle}); + this.results = await fetch(url, { signal }).then(response => response.json()); + }, 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]; + }); + } + } +}; diff --git a/assets/script.js b/assets/script.js index f4ee60d..3402898 100644 --- a/assets/script.js +++ b/assets/script.js @@ -1,148 +1,23 @@ (function ($, STUDIP) { 'use strict'; - var searchTimeout = null; - var lastRequestController = null; - $(document).ready(function () { - 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; - } - - if (lastRequestController) { - lastRequestController.abort(); - } - - lastRequestController = new AbortController(); - const { signal } = lastRequestController; - - clearTimeout(searchTimeout); - searchTimeout = setTimeout(async () => { - const url = STUDIP.URLHelper.getURL(source, {term: needle}); - this.results = await fetch(url, { signal }).then(response => response.json()); - }, 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]; - }); + document.querySelectorAll('[data-plugin-vue-app]').forEach(node => { + const app = node.dataset.pluginVueApp; + + STUDIP.loadChunk('vue').then(({Vue, createApp}) => { + import(`./apps/${app}.js`).then(({default: config}) => { + config.propsData = {}; + for (const [key, value] of Object.entries(node.dataset)) { + if (['pluginVueApp', 'vCloak'].includes(key)) { + continue; } + config.propsData[key] = JSON.parse(value); } - }).$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); - const filters = JSON.parse(dashboard.dataset.filters) || {}; - const filterStoreUrl = dashboard.dataset.filterStoreUrl; - - Object.values(qmLabels).forEach(abbr => { - if (filters[abbr] === undefined) { - filters[abbr] = null; - } - }); - - STUDIP.loadChunk('vue').then(({createApp}) => { - createApp({ - data () { - return { - needle: '', - issues, - qmLabels, - filters - }; - }, - methods: { - getStateForIssueAndQmLabel(issue, qm) { - return issue.qm_states[qm]; - }, - valueMatchesNeedle(what) { - if (this.needle.length === 0) { - return false; - } - return what.toLowerCase().includes(this.needle.toLowerCase()); - } - }, - computed: { - colspan() { - return 8 + Object.values(qmLabels).length; - }, - filteredIssues() { - let filtered = 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; - }); - - if (this.needle.length > 0) { - filtered = filtered.filter(issue => { - const ciNeedle = this.needle.toLowerCase(); - return issue.iid.toString().includes(this.needle) - || issue.title.toLowerCase().includes(ciNeedle) - || (issue.assignee ?? '').toLowerCase().includes(ciNeedle) - || (issue.author ?? '').toLowerCase().includes(ciNeedle) - || issue.reviewers.some(reviewer => reviewer.toLowerCase().includes(ciNeedle)); - }); - } - - return filtered; - } - }, - watch: { - filters: { - handler(current) { - const data = new URLSearchParams(); - - for (const [label, value] of Object.entries(current)) { - if (value !== null) { - data.append(`filters[${label}]`, value); - } - } - - fetch(filterStoreUrl, { - method: 'POST', - body: data - }); - }, - deep: true - } - } - }).$mount('#dashboard'); + createApp(config).$mount(node); + }); }); - } + }); }); }(jQuery, STUDIP)); diff --git a/controllers/convert.php b/controllers/convert.php index d6c9278..a96af14 100644 --- a/controllers/convert.php +++ b/controllers/convert.php @@ -26,6 +26,9 @@ final class ConvertController extends TracToGitlab\Controller public function index_action() { + $this->setVueAppLayout('trac-migrate', [ + 'source' => $this->quicksearchURL(), + ]); } public function convert_action() diff --git a/controllers/dashboard.php b/controllers/dashboard.php index 775e77c..49c74ba 100644 --- a/controllers/dashboard.php +++ b/controllers/dashboard.php @@ -5,6 +5,8 @@ */ final class DashboardController extends TracToGitlab\Controller { + use \TracToGitlab\SelectableIssuesTrait; + public function before_filter(&$action, &$args) { parent::before_filter($action, $args); @@ -18,63 +20,20 @@ final class DashboardController extends TracToGitlab\Controller $this->setupSidebar(); } - private function getSelected(string $what) - { - $source = $this->user - ? $this->user->getConfiguration()->TRAC2GITLAB_USER_FILTERS - : ($_SESSION['GITLAB_DASHBOARD_SELECTION'] ?? []); - - return $source[$what] ?? $this->getDefaultSelection($what); - } - - private function setSelected(string $what, $value): void - { - if ($value === $this->getSelected($what)) { - return; - } - - if ($this->user) { - $config = $this->user->getConfiguration(); - $config_value = $config->TRAC2GITLAB_USER_FILTERS; - $config_value[$what] = $value; - $config->store('TRAC2GITLAB_USER_FILTERS', $config_value); - } else { - if (!isset($_SESSION['GITLAB_DASHBOARD_SELECTION'])) { - $_SESSION['GITLAB_DASHBOARD_SELECTION'] = []; - } - $_SESSION['GITLAB_DASHBOARD_SELECTION'][$what] = $value; - } - } - - private function getDefaultSelection(string $what) - { - if ($what === 'milestone') { - return array_reverse(array_keys($this->getMilestonesAsSelection()))[0]; - } - - if ($what === 'types') { - return 'TIC,StEP'; - } - - if ($what === 'filters') { - return []; - } - - return null; - } - public function index_action() { - $this->issues = $this->getIssues(); - $this->mapping = $this->getQMLabelMapping(); - $this->filters = $this->getSelected('filters'); - } + $data = [ + 'issues' => $this->getIssues(), + 'qm-labels' => $this->getQMLabelMapping(), + 'filter-store-url' => $this->store_filtersURL(), + ]; - public function select_action(string $what, string $value = null) - { - $this->setSelected($what, Request::get($what, $value)); + $filters = $this->getSelected('filters'); + if ($filters) { + $data['filters'] = $filters; + } - $this->redirect($this->indexURL()); + $this->setVueAppLayout('dashboard', $data); } public function store_filters_action() @@ -88,101 +47,4 @@ final class DashboardController extends TracToGitlab\Controller } } - private function setupSidebar() - { - $options = Sidebar::get()->addWidget(new OptionsWidget()); - $options->addSelect( - _('Meilenstein'), - $this->selectURL('milestone'), - 'milestone', - $this->getMilestonesAsSelection(), - $this->getSelected('milestone') - ); - - $options->addSelect( - _('Typ'), - $this->selectURL('types'), - 'types', - [ - 'TIC,StEP' => 'StEPs und TICs', - 'StEP' => 'StEPs', - 'TIC' => 'TICs', - ], - $this->getSelected('types') - ); - } - - private function getIssues(): array - { - $issues = []; - foreach (explode(',', $this->getSelected('types')) as $type) { - $issues = array_merge( - $issues, - $this->gitlabPager->fetchAll( - $this->gitlab->issues(), - 'all', - [ - $this->gitlabProjectId, - [ - 'sort' => 'asc', - 'scope' => 'all', - 'milestone' => $this->getSelected('milestone'), - 'labels' => $type, - ] - ] - ) - ); - } - usort($issues, function ($a, $b) { - return $a['id'] - $b['id']; - }); - - return array_map(function ($issue) { - $mrs = $this->gitlab->issues()->relatedMergeRequests($this->gitlabProjectId, $issue['iid']); - return new TracToGitlab\GitlabIssue($issue, $mrs); - }, $issues); - } - - private function getMilestones() - { - $milestones = $this->gitlab->milestones()->all( - $this->gitlabProjectId - ); - $milestones = array_filter($milestones, function ($milestone) { - return preg_match('/^Stud\.IP \d+\.\d+$/', $milestone['title']); - }); - return $milestones; - } - - private function getMilestonesAsSelection() - { - $result = []; - foreach ($this->getMilestones() as $milestone) { - $result[$milestone['title']] = $milestone['title']; - } - return $result; - } - - private function getQMLabels() - { - $labels = $this->gitlabPager->fetchAll( - $this->gitlab->projects(), - 'labels', - [$this->gitlabProjectId] - ); - $labels = array_filter($labels, function ($label) { - return strpos($label['name'], 'QM::') === 0; - }); - $labels = array_map(function ($label) { - return substr($label['name'], 4, -3); - }, $labels); - $labels = array_unique($labels); - $labels = array_values($labels); - return $labels; - } - - private function getQMLabelMapping() - { - return TracToGitlab\GitlabIssue::QM_LABEL_MAPPING; - } } diff --git a/controllers/testing.php b/controllers/testing.php new file mode 100644 index 0000000..7d6e419 --- /dev/null +++ b/controllers/testing.php @@ -0,0 +1,25 @@ +<?php +final class TestingController extends TracToGitlab\GitlabController +{ + use TracToGitlab\SelectableIssuesTrait; + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + $this->user = User::findCurrent(); + + Navigation::activateItem('/gitlab-dashboard/testing'); + PageLayout::setTitle(_('Übersicht der Testsysteme')); + $this->activateNavigation('testing'); + + $this->setupSidebar(); + } + + public function index_action(): void + { + $this->setVueAppLayout('testing', [ + 'issues' => $this->getIssues(), + ]); + } +} diff --git a/lib/Controller.php b/lib/Controller.php index 09a987d..76e1efa 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -28,4 +28,27 @@ abstract class Controller extends \PluginController { \Navigation::activateItem('/gitlab-dashboard/' . implode('/', $path)); } + + public function setVueAppLayout(string $name, array $data = []) + { + /** @var \Flexi_Template $layout */ + $layout = $this->get_template_factory()->open('vue-app.php'); + $layout->set_layout($this->layout); + + $layout->vueApp = $name; + $layout->data = array_combine( + array_map( + function ($key) { + return 'data-' . $key; + }, + array_keys($data) + ), + array_map( + 'json_encode', + array_values($data) + ) + ); + + $this->layout = $layout; + } } diff --git a/lib/GitlabController.php b/lib/GitlabController.php index e4ac764..e1c4224 100644 --- a/lib/GitlabController.php +++ b/lib/GitlabController.php @@ -1,8 +1,6 @@ <?php namespace TracToGitlab; -use Gitlab\Api\AbstractApi; - abstract class GitlabController extends Controller { private $gitlabCache; diff --git a/lib/SelectableIssuesTrait.php b/lib/SelectableIssuesTrait.php new file mode 100644 index 0000000..cef00bb --- /dev/null +++ b/lib/SelectableIssuesTrait.php @@ -0,0 +1,159 @@ +<?php +namespace TracToGitlab; + +use OptionsWidget; +use Request; +use Sidebar; + +trait SelectableIssuesTrait +{ + private function setupSidebar() + { + $options = Sidebar::get()->addWidget(new OptionsWidget()); + $options->addSelect( + _('Meilenstein'), + $this->selectURL('milestone'), + 'milestone', + $this->getMilestonesAsSelection(), + $this->getSelected('milestone') + ); + + $options->addSelect( + _('Typ'), + $this->selectURL('types'), + 'types', + [ + 'TIC,StEP' => 'StEPs und TICs', + 'StEP' => 'StEPs', + 'TIC' => 'TICs', + ], + $this->getSelected('types') + ); + } + + protected function getSelected(string $what) + { + $source = $this->user + ? $this->user->getConfiguration()->TRAC2GITLAB_USER_FILTERS + : ($_SESSION['GITLAB_DASHBOARD_SELECTION'] ?? []); + + return $source[$what] ?? $this->getDefaultSelection($what); + } + + protected function setSelected(string $what, $value): void + { + if ($value === $this->getSelected($what)) { + return; + } + + if ($this->user) { + $config = $this->user->getConfiguration(); + $config_value = $config->TRAC2GITLAB_USER_FILTERS; + $config_value[$what] = $value; + $config->store('TRAC2GITLAB_USER_FILTERS', $config_value); + } else { + if (!isset($_SESSION['GITLAB_DASHBOARD_SELECTION'])) { + $_SESSION['GITLAB_DASHBOARD_SELECTION'] = []; + } + $_SESSION['GITLAB_DASHBOARD_SELECTION'][$what] = $value; + } + } + + protected function getDefaultSelection(string $what) + { + if ($what === 'milestone') { + return array_reverse(array_keys($this->getMilestonesAsSelection()))[0]; + } + + if ($what === 'types') { + return 'TIC,StEP'; + } + + if ($what === 'filters') { + return []; + } + + return null; + } + + public function select_action(string $what, string $value = null) + { + $this->setSelected($what, Request::get($what, $value)); + + $this->redirect($this->indexURL()); + } + + private function getIssues(): array + { + $issues = []; + foreach (explode(',', $this->getSelected('types')) as $type) { + $issues = array_merge( + $issues, + $this->gitlabPager->fetchAll( + $this->gitlab->issues(), + 'all', + [ + $this->gitlabProjectId, + [ + 'sort' => 'asc', + 'scope' => 'all', + 'milestone' => $this->getSelected('milestone'), + 'labels' => $type, + ] + ] + ) + ); + } + usort($issues, function ($a, $b) { + return $a['id'] - $b['id']; + }); + + return array_map(function ($issue) { + $mrs = $this->gitlab->issues()->relatedMergeRequests($this->gitlabProjectId, $issue['iid']); + return new GitlabIssue($issue, $mrs); + }, $issues); + } + + private function getMilestones() + { + $milestones = $this->gitlab->milestones()->all( + $this->gitlabProjectId + ); + $milestones = array_filter($milestones, function ($milestone) { + return preg_match('/^Stud\.IP \d+\.\d+$/', $milestone['title']); + }); + return $milestones; + } + + private function getMilestonesAsSelection() + { + $result = []; + foreach ($this->getMilestones() as $milestone) { + $result[$milestone['title']] = $milestone['title']; + } + return $result; + } + + private function getQMLabels() + { + $labels = $this->gitlabPager->fetchAll( + $this->gitlab->projects(), + 'labels', + [$this->gitlabProjectId] + ); + $labels = array_filter($labels, function ($label) { + return strpos($label['name'], 'QM::') === 0; + }); + $labels = array_map(function ($label) { + return substr($label['name'], 4, -3); + }, $labels); + $labels = array_unique($labels); + $labels = array_values($labels); + return $labels; + } + + private function getQMLabelMapping() + { + return GitlabIssue::QM_LABEL_MAPPING; + } +} diff --git a/views/convert/index.php b/views/convert/index.php index c1c7d65..0ed0551 100644 --- a/views/convert/index.php +++ b/views/convert/index.php @@ -1,4 +1,4 @@ -<form action="<?= $controller->convert() ?>" method="post" class="default" id="trac-migrate" data-source="<?= $controller->quicksearch() ?>"> +<form action="<?= $controller->convert() ?>" method="post" class="default" id="trac-migrate"> <fieldset> <legend><?= _('Ticket von trac zu GitLab migrieren') ?></legend> diff --git a/views/dashboard/index.php b/views/dashboard/index.php index 478373d..aa5f02d 100644 --- a/views/dashboard/index.php +++ b/views/dashboard/index.php @@ -5,128 +5,119 @@ * @var array<int, TracToGitlab\GitlabIssue> $issues * @var array<string, string> $filters */ - -$attributes = [ - 'data-issues' => json_encode($issues), - 'data-qm-labels' => json_encode($mapping), - 'data-filters' => json_encode($filters ?: null), - 'data-filter-store-url' => $controller->store_filtersURL(), -]; ?> -<div id="dashboard" v-cloak <?= arrayToHtmlAttributes($attributes) ?>> - <table class="default"> - <caption> - <span v-if="filteredIssues.length !== issues.length"> - {{ filteredIssues.length }} <?= _('von') ?> - </span> - {{ issues.length }} Issues - </caption> - <thead> - <tr> - <th><?= _('Issue') ?></th> - <th><?= _('Typ') ?></th> - <th><?= _('Status') ?></th> - <th><?= _('MR') ?></th> - <th><?= _('Titel') ?></th> - <th><?= _('Autor') ?></th> - <th><?= _('Bearbeiter') ?></th> - <th><?= _('Reviewer') ?></th> - <th v-for="(abbr, label) in qmLabels"> - <abbr :title="label">{{ abbr }}</abbr> - </th> - </tr> - </thead> - <tbody> - <tr v-if="issues.length === 0"> - <td :colspan="colspan" style="text-align: center"> - <?= _('Keine Issues für diesen Meilenstein und Typ') ?> - </td> - </tr> - <tr v-else-if="filteredIssues.length === 0"> - <td :colspan="colspan" style="text-align: center"> - <?= _('Keine Issues passen auf Ihren gewählten Filter') ?> - </td> - </tr> - <tr v-for="issue in filteredIssues"> - <td :class="{'filter-match': valueMatchesNeedle(issue.iid.toString())}"> - <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 :class="{'filter-match': valueMatchesNeedle(issue.title)}"> - <a :href="issue.web_url" target="_blank"> - {{ issue.title }} - </a> - </td> - <td :class="{'filter-match': valueMatchesNeedle(issue.author)}"> - {{ issue.author }} - </td> - <td :class="{'filter-match': valueMatchesNeedle(issue.assignee)}"> - {{ issue.assignee }} - </td> - <td :class="{'filter-match': valueMatchesNeedle(issue.reviewers.join('||'))}"> - <ul v-if="issue.reviewers.length > 0" class="list-csv"> - <li v-for="username in issue.reviewers" :key="`reviewer-${issue.iid}-${username}`">{{ username }}</li> - </ul> - </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> - </tbody> - </table> +<table class="default"> + <caption> + <span v-if="filteredIssues.length !== issues.length"> + {{ filteredIssues.length }} <?= _('von') ?> + </span> + {{ issues.length }} Issues + </caption> + <thead> + <tr> + <th><?= _('Issue') ?></th> + <th><?= _('Typ') ?></th> + <th><?= _('Status') ?></th> + <th><?= _('MR') ?></th> + <th><?= _('Titel') ?></th> + <th><?= _('Autor') ?></th> + <th><?= _('Bearbeiter') ?></th> + <th><?= _('Reviewer') ?></th> + <th v-for="(abbr, label) in qmLabels"> + <abbr :title="label">{{ abbr }}</abbr> + </th> + </tr> + </thead> + <tbody> + <tr v-if="issues.length === 0"> + <td :colspan="colspan" style="text-align: center"> + <?= _('Keine Issues für diesen Meilenstein und Typ') ?> + </td> + </tr> + <tr v-else-if="filteredIssues.length === 0"> + <td :colspan="colspan" style="text-align: center"> + <?= _('Keine Issues passen auf Ihren gewählten Filter') ?> + </td> + </tr> + <tr v-for="issue in filteredIssues"> + <td :class="{'filter-match': valueMatchesNeedle(issue.iid.toString())}"> + <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 :class="{'filter-match': valueMatchesNeedle(issue.title)}"> + <a :href="issue.web_url" target="_blank"> + {{ issue.title }} + </a> + </td> + <td :class="{'filter-match': valueMatchesNeedle(issue.author)}"> + {{ issue.author }} + </td> + <td :class="{'filter-match': valueMatchesNeedle(issue.assignee)}"> + {{ issue.assignee }} + </td> + <td :class="{'filter-match': valueMatchesNeedle(issue.reviewers.join('||'))}"> + <ul v-if="issue.reviewers.length > 0" class="list-csv"> + <li v-for="username in issue.reviewers" :key="`reviewer-${issue.iid}-${username}`">{{ username }}</li> + </ul> + </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> + </tbody> +</table> - <mounting-portal mount-to="#sidebar" name="sidebar-filter" append> - <div class="sidebar-widget sidebar-search"> - <div class="sidebar-widget-header"><?= _('Suche') ?></div> - <div class="sidebar-widget-content"> - <form action="#" @submit="event => event.preventDefault()"> - <ul class="needles"> - <li> - <div class="input-group files-search"> - <input type="text" v-model.trim="needle" placeholder="<?= _('Suchwort') ?>"> - <button class="submit-search" type="submit" title="<?= _('Suche auführen') ?>"> - <?= Icon::create('search')->asImg(20) ?> - </button> - </div> - </li> - </ul> - </form> - </div> +<mounting-portal mount-to="#sidebar" name="sidebar-filter" append> + <div class="sidebar-widget sidebar-search"> + <div class="sidebar-widget-header"><?= _('Suche') ?></div> + <div class="sidebar-widget-content"> + <form action="#" @submit="event => event.preventDefault()"> + <ul class="needles"> + <li> + <div class="input-group files-search"> + <input type="text" v-model.trim="needle" placeholder="<?= _('Suchwort') ?>"> + <button class="submit-search" type="submit" title="<?= _('Suche auführen') ?>"> + <?= Icon::create('search')->asImg(20) ?> + </button> + </div> + </li> + </ul> + </form> </div> + </div> - <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 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="currentFilters[abbr]" style="display: block;" class="sidebar-selectlist"> + <option :value="null"></option> + <option>+</option> + <option>?</option> + <option>-</option> + </select> + </label> </div> - </mounting-portal> -</div> + </div> +</mounting-portal> diff --git a/views/testing/index.php b/views/testing/index.php new file mode 100644 index 0000000..72a303a --- /dev/null +++ b/views/testing/index.php @@ -0,0 +1,64 @@ +<table class="default"> + <caption> + {{ issues.length }} Issues + </caption> + <thead> + <tr> + <th><?= _('Issue') ?></th> + <th><?= _('Typ') ?></th> + <th><?= _('Status') ?></th> + <th><?= _('MR') ?></th> + <th><?= _('Titel') ?></th> + <th><?= _('Autor') ?></th> + <th><?= _('Bearbeiter') ?></th> + <th><?= _('Reviewer') ?></th> + </tr> + </thead> + <tbody> + <tr v-if="issues.length === 0"> + <td :colspan="colspan" style="text-align: center"> + <?= _('Keine Issues für diesen Meilenstein und Typ') ?> + </td> + </tr> + <tr v-for="issue in issues"> + <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> + <ul v-if="issue.reviewers.length > 0" class="list-csv"> + <li v-for="username in issue.reviewers" :key="`reviewer-${issue.iid}-${username}`">{{ username }}</li> + </ul> + </td> + </tr> + </tbody> +</table> diff --git a/views/vue-app.php b/views/vue-app.php new file mode 100644 index 0000000..be06be9 --- /dev/null +++ b/views/vue-app.php @@ -0,0 +1,10 @@ +<?php +/** + * @var string $vueApp + * @var array $data + * @var string $content_for_layout + */ +?> +<div data-plugin-vue-app="<?= htmlReady($vueApp) ?>" <?= arrayToHtmlAttributes($data) ?> v-cloak> + <?= $content_for_layout ?> +</div> -- GitLab