Projekte im Ticketsystem: Anlegen, Archiv und Anzeige repariert

- Neues Projekt (Projekte-Seite): Repo-Name blieb nach dem ersten
  Buchstaben der Subdomain stehen ("m" statt "musterfirma")
- Ticket "Projekt waehlen": archivierte Repos nach Flag ausblenden, nicht
  nach status (reaktivierte fehlten, archivierte wurden angeboten)
- Projektliste: "Aktive Projekte" + neue Ansicht "Alle inkl. Archiv";
  Liste wird auch nach einem Anlege-Fehler neu geladen
- Kundenauswahl: "Kein Kunde" entfernt Kunde und Ticket wirklich (mit
  Rueckfrage); beim Kundenwechsel bleibt kein fremdes Ticket haengen
- Zuordnen-Assistent ueberschreibt eine vorhandene Website nicht mehr
- Neues Projekt am Ticket bekommt die Art Website
- Umlaute in Subdomains wie im Portal (mueller statt m-ller)
- Preview-Block nur mit gespeicherter Subdomain; archivierte Projekte
  erst reaktivieren, bevor sie bearbeitet werden
- Repo-Import beim Aufklappen hoechstens einmal pro Minute

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 13:23:01 +00:00
1 parent 78bc99a73b
commit f4c8b235b9
13 files changed
+269 -92

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { throttleRuns } = await loadModule('src/lib/throttleRuns.js')
test('Repo-Import hoechstens einmal pro Minute, laufender Abgleich wird geteilt', async () => {
let runs = 0
let release
const throttled = throttleRuns(() => { runs += 1; return new Promise((r) => { release = r }) }, 60000)
const a = throttled()
const b = throttled()
assert.equal(a, b)
await new Promise((r) => setTimeout(r, 0))
release({ created: 0 })
await a
assert.equal(runs, 1)
assert.equal(await throttled(), null)
assert.equal(runs, 1)
const later = throttled({ now: Date.now() + 61000 })
await new Promise((r) => setTimeout(r, 0))
release({ created: 1 })
assert.deepEqual(await later, { created: 1 })
assert.equal(runs, 2)
})
test('Fehler wird weitergereicht, der naechste Versuch wartet trotzdem die Minute ab', async () => {
const throttled = throttleRuns(async () => { throw new Error('weg') }, 60000)
await assert.rejects(throttled(), /weg/)
assert.equal(await throttled(), null)
})
+30 -3
View File
@@ -2,7 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { isOutsideOrg, needsPreviewButton, assignSummary, assignSequentially } = await loadModule(
const { isOutsideOrg, needsPreviewButton, assignSummary, assignSequentially, assignableProjects, ticketIdForCustomer } = await loadModule(
'src/lib/projectAssign.js'
)
@@ -17,9 +17,36 @@ test('isOutsideOrg', () => {
})
test('needsPreviewButton', () => {
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'deployed' }), false)
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'deployed' }), false)
assert.equal(needsPreviewButton({ previewUrl: '', status: 'synced' }), true)
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'pending' }), true)
// gerade angelegt, der Runner baut noch (status noch synced/pending): kein "Preview einschalten"
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'synced' }), false)
assert.equal(needsPreviewButton({ subdomain: 'x', previewUrl: 'https://x', status: 'pending' }), false)
// archiviert: erst reaktivieren
assert.equal(needsPreviewButton({ archived: true, status: 'archived' }), false)
})
test('assignableProjects: Flag archived zaehlt, nicht status', () => {
const all = [
{ $id: 'a', repoFullName: 'WEBklar/a', status: 'archived', archived: false }, // reaktiviert, alter status
{ $id: 'b', repoFullName: 'JUSN/b', status: 'synced', archived: true }, // in Gitea archiviert
{ $id: 'c', repoFullName: 'WEBklar/c', customerId: 'k2' }, // anderer Kunde
{ $id: 'd', repoFullName: 'WEBklar/d', customerId: 'k1' },
{ $id: 'e', repoFullName: 'WEBklar/e', isTemplate: true },
{ $id: 'f', projectName: 'ohne Repo' },
{ $id: 'g', repoFullName: 'WEBklar/g' },
]
const ids = assignableProjects(all, { customerId: 'k1' }, new Set(['g'])).map((p) => p.$id)
assert.deepEqual(ids, ['a', 'd'])
})
test('ticketIdForCustomer: altes Ticket nur beim gleichen Kunden', () => {
const p = { customerId: 'A', ticketId: 'tA' }
const wo = { tA: { customerId: 'A' }, tB: { customerId: 'B' } }
assert.equal(ticketIdForCustomer(p, 'A', wo), 'tA')
assert.equal(ticketIdForCustomer(p, 'B', wo), '')
assert.equal(ticketIdForCustomer({ customerId: 'A', ticketId: 'tB' }, 'B', wo), 'tB')
assert.equal(ticketIdForCustomer({ customerId: '', ticketId: '' }, 'B', wo), '')
})
test('assignSummary', () => {
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { projectCounts, projectsInView } = await loadModule('src/lib/projectList.js')
const P = [
{ $id: 'a' },
{ $id: 'b', customerId: 'k' },
{ $id: 'c', archived: true },
{ $id: 'd', isTemplate: true },
{ $id: 'e', isTemplate: true, archived: true },
]
const ids = (list) => list.map((p) => p.$id)
test('Aktive Projekte ohne Archiv und Vorlagen', () => {
assert.deepEqual(ids(projectsInView(P, 'all')), ['a', 'b'])
assert.deepEqual(ids(projectsInView(P, 'unassigned')), ['a'])
assert.deepEqual(ids(projectsInView(P, 'assigned')), ['b'])
})
test('Alle inkl. Archiv: wirklich alles (Vorlagen nur fuer Admins)', () => {
assert.deepEqual(ids(projectsInView(P, 'everything')), ['a', 'b', 'c'])
assert.deepEqual(ids(projectsInView(P, 'everything', { isAdmin: true })), ['a', 'b', 'c', 'd', 'e'])
})
test('Archiviert und Vorlagen', () => {
assert.deepEqual(ids(projectsInView(P, 'archived')), ['c'])
assert.deepEqual(ids(projectsInView(P, 'archived', { isAdmin: true })), ['c', 'e'])
assert.deepEqual(ids(projectsInView(P, 'templates')), ['d'])
})
test('Zaehler passen zu den Ansichten', () => {
assert.deepEqual(projectCounts(P), { all: 2, unassigned: 1, assigned: 1, archived: 1, templates: 1, everything: 3 })
assert.deepEqual(projectCounts(P, { isAdmin: true }).everything, 5)
})
+8 -1
View File
@@ -2,7 +2,14 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { suggestSubdomain } = await loadModule('src/lib/subdomain.js')
const { suggestSubdomain, slugify } = await loadModule('src/lib/subdomain.js')
test('slugify schreibt Umlaute aus wie das Portal', () => {
assert.equal(slugify('Müller & Söhne'), 'mueller-soehne')
assert.equal(slugify(' ZZ Test Ablauf '), 'zz-test-ablauf')
assert.equal(slugify('x'.repeat(70)), 'x'.repeat(63))
assert.equal(slugify(null), '')
})
test('Kundenname vor Projektname', () => {
assert.equal(suggestSubdomain('Döner Nation', 'D-ner-Nation'), 'doener-nation')