fix: Sprint-Planung einsehbar und Eingang ohne Voll-Reload.
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Successful in 2m46s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Successful in 2m46s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
Geplante Sprints in Plan anwaehlbar, lokales Update beim Commit, Sprint-Auswahl bleibt erhalten. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c19b6296ed
commit
e221b5b09f
|
|
@ -12,12 +12,14 @@ import { useMinWidth } from '../hooks/useMinWidth.js'
|
|||
const PLANNING_STATUSES = new Set(['planned', 'active', 'at_risk'])
|
||||
/** Backend erlaubt Convert für diese Status — UI war nur bei „Freigegeben“ sichtbar. */
|
||||
const COMMITTABLE_STATUSES = new Set(['accepted', 'triaged', 'new'])
|
||||
const SPRINT_TARGET_STORAGE_PREFIX = 'kairo-sprint-target:'
|
||||
|
||||
export function BacklogSection({
|
||||
items,
|
||||
roadmapItems = [],
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
initiativeId = '',
|
||||
sprintPlanningEnabled = false,
|
||||
canManage,
|
||||
onCreate,
|
||||
|
|
@ -46,8 +48,12 @@ export function BacklogSection({
|
|||
const showSprintPlanning = sprintPlanningEnabled && planableSprints.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSprintPlanning) {
|
||||
setSprintTargetId('')
|
||||
if (!showSprintPlanning || !initiativeId) {
|
||||
return
|
||||
}
|
||||
const stored = sessionStorage.getItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId)
|
||||
if (stored && planableSprints.some((cycle) => cycle.id === stored)) {
|
||||
setSprintTargetId(stored)
|
||||
return
|
||||
}
|
||||
if (sprintTargetId && planableSprints.some((cycle) => cycle.id === sprintTargetId)) {
|
||||
|
|
@ -55,7 +61,12 @@ export function BacklogSection({
|
|||
}
|
||||
const preferred = activeWorkCycle?.id || planableSprints[0]?.id || ''
|
||||
setSprintTargetId(preferred)
|
||||
}, [showSprintPlanning, sprintTargetId, planableSprints, activeWorkCycle?.id])
|
||||
}, [showSprintPlanning, initiativeId, planableSprints, activeWorkCycle?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId || !sprintTargetId) return
|
||||
sessionStorage.setItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId, sprintTargetId)
|
||||
}, [initiativeId, sprintTargetId])
|
||||
|
||||
const selectedSprint = planableSprints.find((cycle) => cycle.id === sprintTargetId) || null
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import { EmptyState } from './EmptyState.jsx'
|
|||
export function WorkCyclesPanel({
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
selectedSprintId = '',
|
||||
actionCountByCycleId = {},
|
||||
canManage,
|
||||
onCreate,
|
||||
onActivate,
|
||||
onComplete,
|
||||
onSelectSprint,
|
||||
busy = false,
|
||||
}) {
|
||||
const [title, setTitle] = useState('')
|
||||
|
|
@ -34,8 +37,8 @@ export function WorkCyclesPanel({
|
|||
<div>
|
||||
<h2>Sprint</h2>
|
||||
<p className="section-lead muted">
|
||||
Der aktive Sprint definiert den Sprint-Backlog — committete Arbeitspakete mit
|
||||
Sprint-Zuweisung erscheinen unter Ausführen → Sprint.
|
||||
Sprint anklicken, um den geplanten Sprint-Backlog zu sehen — aktiv oder geplant.
|
||||
Committete Arbeitspakete aus dem Eingang erscheinen beim gewählten Sprint.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -46,12 +49,9 @@ export function WorkCyclesPanel({
|
|||
{activeWorkCycle.goal_description && (
|
||||
<span className="muted"> — {activeWorkCycle.goal_description}</span>
|
||||
)}
|
||||
{typeof activeWorkCycle.open_action_count === 'number' && (
|
||||
<span className="muted"> ({activeWorkCycle.open_action_count} offene APs)</span>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted">Kein aktiver Sprint — committete Arbeitspakete gelten als Product-Ist.</p>
|
||||
<p className="muted">Kein aktiver Sprint — du kannst trotzdem geplante Sprints einsehen.</p>
|
||||
)}
|
||||
|
||||
{canManage && (
|
||||
|
|
@ -91,16 +91,33 @@ export function WorkCyclesPanel({
|
|||
{workCycles.length === 0 ? (
|
||||
<EmptyState message="Noch keine Sprints angelegt." />
|
||||
) : (
|
||||
<ul className="item-list">
|
||||
{workCycles.map((cycle) => (
|
||||
<li key={cycle.id} className="list-item card-list-item work-cycle-row">
|
||||
<div>
|
||||
<ul className="item-list work-cycle-list">
|
||||
{workCycles.map((cycle) => {
|
||||
const isSelected = selectedSprintId === cycle.id
|
||||
const apCount = actionCountByCycleId[cycle.id] || 0
|
||||
return (
|
||||
<li
|
||||
key={cycle.id}
|
||||
className={
|
||||
'list-item card-list-item work-cycle-row' +
|
||||
(isSelected ? ' work-cycle-row--selected' : '')
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="work-cycle-row__select"
|
||||
onClick={() => onSelectSprint?.(cycle.id)}
|
||||
>
|
||||
<strong>{cycle.title}</strong>{' '}
|
||||
<StatusBadge status={cycle.status} />
|
||||
<span className="muted work-cycle-row__count">
|
||||
{apCount} AP{apCount === 1 ? '' : 's'}
|
||||
</span>
|
||||
{cycle.goal_description && (
|
||||
<p className="muted item-meta">{cycle.goal_description}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<div className="work-cycle-row__actions">
|
||||
{canManage && cycle.status === 'active' && typeof onComplete === 'function' && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -121,8 +138,10 @@ export function WorkCyclesPanel({
|
|||
Aktivieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ import { useActors } from '../hooks/useActors.js'
|
|||
import { useSession } from './SessionContext.jsx'
|
||||
import { useOptionalProgramScope } from './ProgramScopeContext.jsx'
|
||||
import { collectProjectSubtreeIds } from '../utils/projectTree.js'
|
||||
import { filterActionsForWorkCycle } from '../utils/workCycleActions.js'
|
||||
|
||||
const InitiativeOperationsContext = createContext(null)
|
||||
|
||||
|
|
@ -118,10 +119,11 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
const [editingAction, setEditingAction] = useState(null)
|
||||
const [formBusy, setFormBusy] = useState(false)
|
||||
const [hideDone, setHideDone] = useState(true)
|
||||
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const load = useCallback(async ({ silent = false } = {}) => {
|
||||
if (sessionLoading || !id) return
|
||||
setLoading(true)
|
||||
if (!silent) setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const [initData, actionData] = await Promise.all([
|
||||
|
|
@ -203,18 +205,63 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
|
||||
const sprintActions = useMemo(() => {
|
||||
if (!activeWorkCycle?.id) return []
|
||||
const cycleId = activeWorkCycle.id
|
||||
let list = hideDone
|
||||
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
|
||||
: actions
|
||||
list = list.filter((a) => a.work_cycle_id === cycleId)
|
||||
if (selectedProjectId) {
|
||||
return filterActionsForWorkCycle(actions, activeWorkCycle.id, { hideDone }).filter((a) => {
|
||||
if (!selectedProjectId) return true
|
||||
const subtreeIds = collectProjectSubtreeIds(projects, selectedProjectId)
|
||||
list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id))
|
||||
}
|
||||
return list
|
||||
return a.project_id && subtreeIds.has(a.project_id)
|
||||
})
|
||||
}, [actions, hideDone, activeWorkCycle?.id, selectedProjectId, projects])
|
||||
|
||||
const selectedSprintActions = useMemo(() => {
|
||||
if (!selectedWorkCycleId) return []
|
||||
return filterActionsForWorkCycle(actions, selectedWorkCycleId, { hideDone }).filter((a) => {
|
||||
if (!selectedProjectId) return true
|
||||
const subtreeIds = collectProjectSubtreeIds(projects, selectedProjectId)
|
||||
return a.project_id && subtreeIds.has(a.project_id)
|
||||
})
|
||||
}, [actions, hideDone, selectedWorkCycleId, selectedProjectId, projects])
|
||||
|
||||
const selectedWorkCycle = useMemo(
|
||||
() => workCycles.find((c) => c.id === selectedWorkCycleId) || null,
|
||||
[workCycles, selectedWorkCycleId],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWorkCycleId && workCycles.some((c) => c.id === selectedWorkCycleId)) {
|
||||
return
|
||||
}
|
||||
if (activeWorkCycle?.id) {
|
||||
setSelectedWorkCycleId(activeWorkCycle.id)
|
||||
return
|
||||
}
|
||||
const planned = workCycles.find((c) => c.status === 'planned')
|
||||
if (planned) setSelectedWorkCycleId(planned.id)
|
||||
else if (workCycles[0]?.id) setSelectedWorkCycleId(workCycles[0].id)
|
||||
else setSelectedWorkCycleId('')
|
||||
}, [workCycles, activeWorkCycle, selectedWorkCycleId])
|
||||
|
||||
function applyConvertResults(results) {
|
||||
const list = Array.isArray(results) ? results : [results]
|
||||
for (const result of list) {
|
||||
if (!result) continue
|
||||
const backlogItem = result.backlog_item
|
||||
const action = result.action
|
||||
if (backlogItem?.id) {
|
||||
setBacklogItems((prev) =>
|
||||
prev.map((item) => (item.id === backlogItem.id ? backlogItem : item)),
|
||||
)
|
||||
}
|
||||
if (action?.id) {
|
||||
setActions((prev) => {
|
||||
if (prev.some((a) => a.id === action.id)) {
|
||||
return prev.map((a) => (a.id === action.id ? action : a))
|
||||
}
|
||||
return [...prev, action]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actionContextById = useMemo(
|
||||
() => Object.fromEntries((steeringSnapshot?.actions || []).map((a) => [a.id, a])),
|
||||
[steeringSnapshot]
|
||||
|
|
@ -423,12 +470,16 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
|
||||
async function handleConvertBacklog(itemId, options = {}) {
|
||||
setFormBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
await convertBacklogToAction(itemId, {
|
||||
const result = await convertBacklogToAction(itemId, {
|
||||
assign_active_sprint: options.work_cycle_id ? false : options.assign_active_sprint ?? true,
|
||||
work_cycle_id: options.work_cycle_id,
|
||||
})
|
||||
await load()
|
||||
applyConvertResults(result)
|
||||
if (options.work_cycle_id) {
|
||||
setSelectedWorkCycleId(options.work_cycle_id)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -439,14 +490,20 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleBulkConvertBacklog(itemIds, options = {}) {
|
||||
if (!itemIds.length) return
|
||||
setFormBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
for (const itemId of itemIds) {
|
||||
await convertBacklogToAction(itemId, {
|
||||
const results = await Promise.all(
|
||||
itemIds.map((itemId) =>
|
||||
convertBacklogToAction(itemId, {
|
||||
assign_active_sprint: options.work_cycle_id ? false : options.assign_active_sprint ?? true,
|
||||
work_cycle_id: options.work_cycle_id,
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
applyConvertResults(results)
|
||||
if (options.work_cycle_id) {
|
||||
setSelectedWorkCycleId(options.work_cycle_id)
|
||||
}
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -457,8 +514,9 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleCreateWorkCycle(body) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await createWorkCycle(id, body)
|
||||
await load()
|
||||
const created = await createWorkCycle(id, body)
|
||||
await load({ silent: true })
|
||||
if (created?.id) setSelectedWorkCycleId(created.id)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -470,7 +528,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await activateWorkCycle(id, workCycleId)
|
||||
await load()
|
||||
await load({ silent: true })
|
||||
setSelectedWorkCycleId(workCycleId)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -482,7 +541,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await completeWorkCycle(id, workCycleId)
|
||||
await load()
|
||||
await load({ silent: true })
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -817,6 +876,10 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
recurringItems,
|
||||
workCycles,
|
||||
activeWorkCycle,
|
||||
selectedWorkCycleId,
|
||||
setSelectedWorkCycleId,
|
||||
selectedWorkCycle,
|
||||
selectedSprintActions,
|
||||
sprintActions,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { LoadingState } from '../../components/LoadingState.jsx'
|
|||
export function InitiativeInboxPage() {
|
||||
const {
|
||||
initiative,
|
||||
initiativeId,
|
||||
backlogItems,
|
||||
roadmapItems,
|
||||
workCycles,
|
||||
|
|
@ -25,7 +26,7 @@ export function InitiativeInboxPage() {
|
|||
return null
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (loading && initiative === null) {
|
||||
return <LoadingState message="Lade Eingang …" />
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ export function InitiativeInboxPage() {
|
|||
{error && <p className="error">{error}</p>}
|
||||
<BacklogSection
|
||||
items={backlogItems}
|
||||
initiativeId={initiativeId}
|
||||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,19 @@ import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
|||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { countActionsByWorkCycle } from '../../utils/workCycleActions.js'
|
||||
|
||||
function PlanSprintInner() {
|
||||
const ops = useInitiativeOperations()
|
||||
const {
|
||||
initiativeId,
|
||||
activeWorkCycle,
|
||||
selectedWorkCycleId,
|
||||
setSelectedWorkCycleId,
|
||||
selectedWorkCycle,
|
||||
selectedSprintActions,
|
||||
workCycles,
|
||||
sprintActions,
|
||||
actions,
|
||||
projects,
|
||||
roadmapItems,
|
||||
capabilities,
|
||||
|
|
@ -40,17 +45,28 @@ function PlanSprintInner() {
|
|||
handleCreateBlockerForAction,
|
||||
} = ops
|
||||
|
||||
const actionCountByCycleId = useMemo(
|
||||
() => countActionsByWorkCycle(actions, { hideDone }),
|
||||
[actions, hideDone],
|
||||
)
|
||||
|
||||
const createSprintAction = useMemo(
|
||||
() => async (payload) => {
|
||||
await handleCreateAction({
|
||||
...payload,
|
||||
work_cycle_id: payload.work_cycle_id || activeWorkCycle?.id || undefined,
|
||||
work_cycle_id: payload.work_cycle_id || selectedWorkCycleId || activeWorkCycle?.id || undefined,
|
||||
})
|
||||
},
|
||||
[handleCreateAction, activeWorkCycle?.id],
|
||||
[handleCreateAction, selectedWorkCycleId, activeWorkCycle?.id],
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
const sprintLead = selectedWorkCycle
|
||||
? selectedWorkCycle.status === 'active'
|
||||
? `Aktiver Sprint „${selectedWorkCycle.title}" — committet aus Eingang oder direkt hier.`
|
||||
: `Geplanter Sprint „${selectedWorkCycle.title}" — Arbeitspakete aus dem Eingang erscheinen hier nach dem Commit.`
|
||||
: 'Wähle einen Sprint in der Liste.'
|
||||
|
||||
if (loading && workCycles.length === 0) {
|
||||
return <LoadingState message="Lade Sprint-Planung …" />
|
||||
}
|
||||
|
||||
|
|
@ -60,16 +76,19 @@ function PlanSprintInner() {
|
|||
<WorkCyclesPanel
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
selectedSprintId={selectedWorkCycleId}
|
||||
actionCountByCycleId={actionCountByCycleId}
|
||||
canManage={capabilities.has('kairo.milestone.manage')}
|
||||
onCreate={handleCreateWorkCycle}
|
||||
onActivate={handleActivateWorkCycle}
|
||||
onComplete={handleCompleteWorkCycle}
|
||||
onSelectSprint={setSelectedWorkCycleId}
|
||||
busy={formBusy}
|
||||
/>
|
||||
{activeWorkCycle && (
|
||||
{selectedWorkCycleId && selectedWorkCycle && (
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={sprintActions}
|
||||
actions={selectedSprintActions}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
actionContextById={actionContextById}
|
||||
|
|
@ -93,9 +112,9 @@ function PlanSprintInner() {
|
|||
actorsUsedFallback={actorsUsedFallback}
|
||||
onReloadActors={reloadActors}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sectionTitle="Sprint-Backlog"
|
||||
sectionLead="Arbeitspakete im aktiven Sprint — committet aus dem Product Backlog (Eingang) oder direkt hier."
|
||||
activeWorkCycle={selectedWorkCycle}
|
||||
sectionTitle={`Sprint-Backlog — ${selectedWorkCycle.title}`}
|
||||
sectionLead={sprintLead}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -670,6 +670,43 @@
|
|||
background: var(--jk-surface);
|
||||
}
|
||||
|
||||
.work-cycle-list .work-cycle-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-cycle-row__select {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.work-cycle-row__select:hover strong {
|
||||
color: var(--jk-primary);
|
||||
}
|
||||
|
||||
.work-cycle-row--selected {
|
||||
box-shadow: inset 3px 0 0 var(--jk-primary);
|
||||
}
|
||||
|
||||
.work-cycle-row__count {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.work-cycle-row__actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.backlog-sprint-setup {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
|
|
|
|||
27
frontend/src/utils/workCycleActions.js
Normal file
27
frontend/src/utils/workCycleActions.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Arbeitspakete eines Sprints (work_cycle) filtern.
|
||||
* @param {Array<{ id: string, work_cycle_id?: string, status?: string }>} actions
|
||||
* @param {string | null | undefined} cycleId
|
||||
* @param {{ hideDone?: boolean }} options
|
||||
*/
|
||||
export function filterActionsForWorkCycle(actions, cycleId, { hideDone = true } = {}) {
|
||||
if (!cycleId) return []
|
||||
let list = Array.isArray(actions) ? actions : []
|
||||
if (hideDone) {
|
||||
list = list.filter((a) => a.status !== 'done' && a.status !== 'discarded')
|
||||
}
|
||||
return list.filter((a) => a.work_cycle_id === cycleId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{ work_cycle_id?: string, status?: string }>} actions
|
||||
*/
|
||||
export function countActionsByWorkCycle(actions, { hideDone = true } = {}) {
|
||||
const counts = {}
|
||||
for (const action of actions || []) {
|
||||
if (!action.work_cycle_id) continue
|
||||
if (hideDone && (action.status === 'done' || action.status === 'discarded')) continue
|
||||
counts[action.work_cycle_id] = (counts[action.work_cycle_id] || 0) + 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
21
frontend/src/utils/workCycleActions.test.js
Normal file
21
frontend/src/utils/workCycleActions.test.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { countActionsByWorkCycle, filterActionsForWorkCycle } from './workCycleActions.js'
|
||||
|
||||
describe('workCycleActions', () => {
|
||||
const actions = [
|
||||
{ id: 'a1', work_cycle_id: 's1', status: 'open' },
|
||||
{ id: 'a2', work_cycle_id: 's1', status: 'done' },
|
||||
{ id: 'a3', work_cycle_id: 's2', status: 'ready' },
|
||||
{ id: 'a4', status: 'open' },
|
||||
]
|
||||
|
||||
it('filters actions for a sprint', () => {
|
||||
expect(filterActionsForWorkCycle(actions, 's1').map((a) => a.id)).toEqual(['a1'])
|
||||
expect(filterActionsForWorkCycle(actions, 's2').map((a) => a.id)).toEqual(['a3'])
|
||||
expect(filterActionsForWorkCycle(actions, 'missing')).toEqual([])
|
||||
})
|
||||
|
||||
it('counts actions per sprint', () => {
|
||||
expect(countActionsByWorkCycle(actions)).toEqual({ s1: 1, s2: 1 })
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user