improve Gitea PR plugin with correct filter options

- Replace custom filters with Gitea UI equivalents (your repos, assigned,
  created, review requested, reviewed, mentioned)
- Fix pagination to fetch all pages from issues/search
- Filter out false-positive issues (pull_request: null)
- Use issues/search API for all filters (correct token scopes required)
- Add StreamAction registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 20:09:48 +02:00
parent b635fdb981
commit 5a104a8f95
7 changed files with 77 additions and 30 deletions
+4 -1
View File
@@ -16,9 +16,12 @@
"Repository": "Repository", "Repository": "Repository",
"API Token": "API Token", "API Token": "API Token",
"PR Filter": "PR-Filter", "PR Filter": "PR-Filter",
"Your Repositories": "Eigene Repositories",
"Assigned to me": "Mir zugewiesen", "Assigned to me": "Mir zugewiesen",
"Created by me": "Von mir erstellt",
"Review requested": "Review angefordert", "Review requested": "Review angefordert",
"Both": "Beides", "Reviewed by me": "Von mir bewertet",
"Mentioned me": "Hat mich erwähnt",
"Server": "Server", "Server": "Server",
"Last Rised": "Letzter Anstieg", "Last Rised": "Letzter Anstieg",
"Pull Requests": "Pull Requests", "Pull Requests": "Pull Requests",
+4 -1
View File
@@ -16,9 +16,12 @@
"Repository": "Repository", "Repository": "Repository",
"API Token": "API Token", "API Token": "API Token",
"PR Filter": "PR Filter", "PR Filter": "PR Filter",
"Your Repositories": "Your Repositories",
"Assigned to me": "Assigned to me", "Assigned to me": "Assigned to me",
"Created by me": "Created by me",
"Review requested": "Review requested", "Review requested": "Review requested",
"Both": "Both", "Reviewed by me": "Reviewed by me",
"Mentioned me": "Mentioned me",
"Server": "Server", "Server": "Server",
"Last Rised": "Last Rised", "Last Rised": "Last Rised",
"Pull Requests": "Pull Requests", "Pull Requests": "Pull Requests",
+13
View File
@@ -75,6 +75,19 @@
], ],
"Tooltip": "Shows light color, time and mode from an ESP32 model railway controller via MQTT", "Tooltip": "Shows light color, time and mode from an ESP32 model railway controller via MQTT",
"UUID": "dev.mars3142.ulanzideck.collection.railway" "UUID": "dev.mars3142.ulanzideck.collection.railway"
},
{
"Name": "Model Railway Stream",
"Icon": "assets/icons/railway.png",
"PropertyInspectorPath": "property-inspector/stream/inspector.html",
"States": [
{
"Name": "Default",
"Image": "assets/icons/railway.png"
}
],
"Tooltip": "Displays the live 128x64 monochrome display stream from an ESP32 model railway controller via MQTT",
"UUID": "dev.mars3142.ulanzideck.collection.stream"
} }
], ],
"OS": [ "OS": [
+49 -27
View File
@@ -9,6 +9,7 @@ class GiteaPRAction extends ActionBase {
}; };
this.firstSeen = {}; this.firstSeen = {};
this.settingsLoaded = false; this.settingsLoaded = false;
this.username = null;
this.$UD.onDidReceiveSettings(jsn => { this.$UD.onDidReceiveSettings(jsn => {
if (jsn.context !== this.context) return; if (jsn.context !== this.context) return;
@@ -30,9 +31,18 @@ class GiteaPRAction extends ActionBase {
} }
onRun() { onRun() {
if (this.config.url) { if (!this.config.url) return;
this.$UD.openUrl(`${this.config.url}/pulls?type=your_repositories&sort=&state=open&q=`); const filter = this.config.filter || 'review_requested';
} const filterMap = {
your_repos: 'type=your_repositories',
assigned: 'type=your_repositories&assignee=0',
created: 'type=your_repositories&poster=0',
review_requested: 'type=your_repositories&review_requested=true',
reviewed: 'type=your_repositories&reviewed=true',
mentioned: 'type=your_repositories&mentioned=0',
};
const q = filterMap[filter] || filterMap.your_repos;
this.$UD.openUrl(`${this.config.url}/pulls?${q}&state=open`);
} }
fetchPRs() { fetchPRs() {
@@ -46,32 +56,36 @@ class GiteaPRAction extends ActionBase {
const filter = this.config.filter || 'review_requested'; const filter = this.config.filter || 'review_requested';
const base = `${this.config.url}/api/v1/repos/issues/search?type=pullrequest&state=open&limit=50`; const base = `${this.config.url}/api/v1/repos/issues/search?type=pullrequest&state=open&limit=50`;
const fetchPRs = async (url) => { const fetchPages = async (url, paramChar = '&') => {
const r = await fetch(url, { headers, signal: AbortSignal.timeout(8000) }); const all = [];
console.log('GiteaPR status:', r.status, url); let page = 1;
if (!r.ok) { while (true) {
const body = await r.text(); const r = await fetch(`${url}${paramChar}page=${page}`, { headers, signal: AbortSignal.timeout(8000) });
console.log('GiteaPR error body:', body); if (r.status === 404 || r.status === 403) return [];
return null; if (!r.ok) {
console.log('GiteaPR error:', r.status, url);
return null;
}
const data = await r.json();
if (!Array.isArray(data) || data.length === 0) break;
all.push(...data);
if (data.length < 50) break;
page++;
} }
const data = await r.json(); return all;
return Array.isArray(data) ? data : [];
}; };
let prs; const paramMap = {
if (filter === 'both') { your_repos: '', all: '', both: '',
const [a, b] = await Promise.all([ assigned: '&assigned=true',
fetchPRs(`${base}&assigned=true`), created: '&created=true',
fetchPRs(`${base}&review_requested=true`), review_requested: '&review_requested=true',
]); reviewed: '&reviewed=true',
if (a === null || b === null) { this.renderButton(null, true); return; } mentioned: '&mentioned=true',
const seen = new Set(); };
prs = [...a, ...b].filter(pr => seen.has(pr.id) ? false : seen.add(pr.id)); const raw = await fetchPages(`${base}${paramMap[filter] ?? ''}`);
} else { if (raw === null) { this.renderButton(null, true); return; }
const param = filter === 'assigned' ? '&assigned=true' : '&review_requested=true'; const prs = raw.filter(pr => pr.pull_request != null);
prs = await fetchPRs(`${base}${param}`);
if (prs === null) { this.renderButton(null, true); return; }
}
const wip = prs.filter(pr => pr.title?.startsWith('WIP: ')).length; const wip = prs.filter(pr => pr.title?.startsWith('WIP: ')).length;
const now = Date.now(); const now = Date.now();
@@ -120,7 +134,15 @@ class GiteaPRAction extends ActionBase {
ctx.fillText(isError ? this.$UD.t('API Error') : this.$UD.t('Offline'), 98, 166); ctx.fillText(isError ? this.$UD.t('API Error') : this.$UD.t('Offline'), 98, 166);
} else { } else {
const filter = this.config.filter || 'review_requested'; const filter = this.config.filter || 'review_requested';
const label = filter === 'assigned' ? 'assigned' : filter === 'review_requested' ? 'review req.' : 'open'; const labelMap = {
your_repos: 'your repos',
assigned: 'assigned',
created: 'created',
review_requested: 'review req.',
reviewed: 'reviewed',
mentioned: 'mentioned',
};
const label = labelMap[filter] ?? 'your repos';
const color = count === 0 ? '#6bff6b' : count < 5 ? '#f0c040' : '#ff6b6b'; const color = count === 0 ? '#6bff6b' : count < 5 ? '#f0c040' : '#ff6b6b';
const text = count > 999 ? '999+' : String(count); const text = count > 999 ? '999+' : String(count);
+1
View File
@@ -18,6 +18,7 @@
<script src="./actions/GiteaAction.js"></script> <script src="./actions/GiteaAction.js"></script>
<script src="./actions/GiteaPRAction.js"></script> <script src="./actions/GiteaPRAction.js"></script>
<script src="./actions/RailwayAction.js"></script> <script src="./actions/RailwayAction.js"></script>
<script src="./actions/StreamAction.js"></script>
<script src="./app.js"></script> <script src="./app.js"></script>
</body> </body>
</html> </html>
+2
View File
@@ -24,6 +24,8 @@ $UD.onAdd(jsn => {
ACTION_CACHES[context] = new GiteaPRAction($UD, context); ACTION_CACHES[context] = new GiteaPRAction($UD, context);
} else if (name === 'railway') { } else if (name === 'railway') {
ACTION_CACHES[context] = new RailwayAction($UD, context); ACTION_CACHES[context] = new RailwayAction($UD, context);
} else if (name === 'stream') {
ACTION_CACHES[context] = new StreamAction($UD, context);
} }
} }
+4 -1
View File
@@ -20,9 +20,12 @@
<div class="uspi-item"> <div class="uspi-item">
<div class="uspi-item-label" data-localize>PR Filter</div> <div class="uspi-item-label" data-localize>PR Filter</div>
<select class="uspi-item-value" name="filter"> <select class="uspi-item-value" name="filter">
<option value="your_repos" data-localize>Your Repositories</option>
<option value="assigned" data-localize>Assigned to me</option> <option value="assigned" data-localize>Assigned to me</option>
<option value="created" data-localize>Created by me</option>
<option value="review_requested" selected data-localize>Review requested</option> <option value="review_requested" selected data-localize>Review requested</option>
<option value="both" data-localize>Both</option> <option value="reviewed" data-localize>Reviewed by me</option>
<option value="mentioned" data-localize>Mentioned me</option>
</select> </select>
</div> </div>
<div class="uspi-item"> <div class="uspi-item">