fix: Sprint-AP Bearbeiten, Sprint-Verschiebung und klarere Aktionsleiste.
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Behebt editingActionId-Bug in Plan/Work-Sprint; Schnellzuweisung AP zu anderem Sprint ohne Voll-Reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
04d1dbf1f9
commit
ee1e1c0671
|
|
@ -3,6 +3,7 @@ import { ACTION_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
|
|||
import { ActorSelect } from './ActorSelect.jsx'
|
||||
import { GateSelect } from './GateSelect.jsx'
|
||||
import { flattenLeafProjectOptions, getLeafProjects } from '../utils/projectTree.js'
|
||||
import { PLANNING_SPRINT_STATUSES } from '../utils/workCycleActions.js'
|
||||
|
||||
function isoToLocalInput(iso) {
|
||||
if (!iso) return ''
|
||||
|
|
@ -47,7 +48,9 @@ export function ActionForm({
|
|||
const defaultActors = initial.assigned_actor_ids || []
|
||||
const leafProjects = getLeafProjects(projects)
|
||||
const projectOptions = flattenLeafProjectOptions(projects)
|
||||
const cycleOptions = workCycles.filter((c) => c.status !== 'completed')
|
||||
const cycleOptions = workCycles.filter(
|
||||
(c) => PLANNING_SPRINT_STATUSES.has(c.status) || c.id === initial.work_cycle_id,
|
||||
)
|
||||
const defaultCycleId =
|
||||
initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
|
||||
|
||||
|
|
@ -108,13 +111,19 @@ export function ActionForm({
|
|||
/>
|
||||
{cycleOptions.length > 0 && (
|
||||
<label>
|
||||
Sprint (optional)
|
||||
Sprint
|
||||
<select name="work_cycle_id" defaultValue={defaultCycleId}>
|
||||
<option value="">— kein Sprint —</option>
|
||||
{cycleOptions.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.id}>
|
||||
{cycle.title}
|
||||
{cycle.status === 'active' ? ' (aktiv)' : ''}
|
||||
{cycle.status === 'active'
|
||||
? ' (aktiv)'
|
||||
: cycle.status === 'planned'
|
||||
? ' (geplant)'
|
||||
: cycle.status === 'reached'
|
||||
? ' (abgeschlossen)'
|
||||
: ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { PriorityBadge } from './PriorityBadge.jsx'
|
|||
import { ActionForm } from './ActionForm.jsx'
|
||||
import { gateTitleById } from './GateSelect.jsx'
|
||||
import { actionTitleById } from '../utils/actionReferences.js'
|
||||
import { SprintTransferSelect } from './SprintTransferSelect.jsx'
|
||||
|
||||
function formatDue(iso) {
|
||||
if (!iso) return null
|
||||
|
|
@ -45,6 +46,7 @@ export function ActionHubCard({
|
|||
onQuickStatus,
|
||||
onCreateBlocker,
|
||||
onUnplanToBacklog,
|
||||
onMoveToSprint,
|
||||
canManage,
|
||||
canManageBlocker,
|
||||
formBusy,
|
||||
|
|
@ -123,6 +125,14 @@ export function ActionHubCard({
|
|||
{action.assigned_actor_ids?.length > 0 && (
|
||||
<span>{action.assigned_actor_ids.length} zugewiesen</span>
|
||||
)}
|
||||
{action.work_cycle_id && (
|
||||
<span>
|
||||
Sprint:{' '}
|
||||
{workCycles.find((c) => c.id === action.work_cycle_id)?.title ||
|
||||
activeWorkCycle?.title ||
|
||||
'…'}
|
||||
</span>
|
||||
)}
|
||||
{due && <span>Fällig: {due}</span>}
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -169,18 +179,29 @@ export function ActionHubCard({
|
|||
|
||||
{canManage && (
|
||||
<footer className="action-hub-card-actions">
|
||||
<select
|
||||
className="inline-select"
|
||||
value={action.status}
|
||||
onChange={(e) => onQuickStatus(e.target.value)}
|
||||
aria-label="Status ändern"
|
||||
>
|
||||
{ACTION_STATUSES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{ACTION_STATUS_LABELS[s]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="action-hub-card-actions__primary">
|
||||
<select
|
||||
className="inline-select"
|
||||
value={action.status}
|
||||
onChange={(e) => onQuickStatus(e.target.value)}
|
||||
aria-label="Status ändern"
|
||||
>
|
||||
{ACTION_STATUSES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{ACTION_STATUS_LABELS[s]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{typeof onMoveToSprint === 'function' && (
|
||||
<SprintTransferSelect
|
||||
action={action}
|
||||
workCycles={workCycles}
|
||||
busy={formBusy}
|
||||
onMove={onMoveToSprint}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-hub-card-actions__secondary">
|
||||
{canManageBlocker && action.status !== 'done' && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -216,6 +237,7 @@ export function ActionHubCard({
|
|||
Details
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function InitiativeActionsHub({
|
|||
onQuickStatus,
|
||||
onCreateBlockerForAction,
|
||||
onUnplanToBacklog,
|
||||
onMoveToSprint,
|
||||
canManage,
|
||||
canManageBlocker,
|
||||
formBusy,
|
||||
|
|
@ -32,6 +33,7 @@ export function InitiativeActionsHub({
|
|||
onReloadActors,
|
||||
sectionTitle = 'Arbeitspakete',
|
||||
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
||||
allActions = [],
|
||||
}) {
|
||||
return (
|
||||
<section className="card initiative-actions-hub">
|
||||
|
|
@ -98,6 +100,7 @@ export function InitiativeActionsHub({
|
|||
onQuickStatus={(status) => onQuickStatus(action, status)}
|
||||
onCreateBlocker={() => onCreateBlockerForAction(action.id)}
|
||||
onUnplanToBacklog={onUnplanToBacklog}
|
||||
onMoveToSprint={onMoveToSprint}
|
||||
canManage={canManage}
|
||||
canManageBlocker={canManageBlocker}
|
||||
formBusy={formBusy}
|
||||
|
|
@ -111,7 +114,7 @@ export function InitiativeActionsHub({
|
|||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
allActions={actions}
|
||||
allActions={allActions.length ? allActions : actions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
46
frontend/src/components/SprintTransferSelect.jsx
Normal file
46
frontend/src/components/SprintTransferSelect.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { listCarryoverTargetSprints } from '../utils/workCycleActions.js'
|
||||
|
||||
/**
|
||||
* Schnellzuweisung: AP in einen anderen Sprint verschieben.
|
||||
*/
|
||||
export function SprintTransferSelect({
|
||||
action,
|
||||
workCycles = [],
|
||||
busy = false,
|
||||
onMove,
|
||||
className = '',
|
||||
}) {
|
||||
if (!action?.work_cycle_id || action.status === 'done' || action.status === 'discarded') {
|
||||
return null
|
||||
}
|
||||
|
||||
const targets = listCarryoverTargetSprints(workCycles, action.work_cycle_id)
|
||||
if (targets.length === 0) return null
|
||||
|
||||
return (
|
||||
<label className={`sprint-transfer-select ${className}`.trim()}>
|
||||
<span className="sr-only">In anderen Sprint verschieben</span>
|
||||
<select
|
||||
className="inline-select"
|
||||
value=""
|
||||
disabled={busy}
|
||||
aria-label={`${action.title} in anderen Sprint verschieben`}
|
||||
onChange={(e) => {
|
||||
const targetId = e.target.value
|
||||
if (targetId && typeof onMove === 'function') {
|
||||
onMove(action, targetId)
|
||||
}
|
||||
e.target.value = ''
|
||||
}}
|
||||
>
|
||||
<option value="">→ Sprint …</option>
|
||||
{targets.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.id}>
|
||||
{cycle.title}
|
||||
{cycle.status === 'active' ? ' (aktiv)' : cycle.status === 'planned' ? ' (geplant)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [showActionForm, setShowActionForm] = useState(false)
|
||||
const [editingAction, setEditingAction] = useState(null)
|
||||
const [editingActionId, setEditingActionId] = useState(null)
|
||||
const [formBusy, setFormBusy] = useState(false)
|
||||
const [hideDone, setHideDone] = useState(true)
|
||||
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
|
||||
|
|
@ -364,7 +364,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
if (capabilities.has('kairo.action.manage')) {
|
||||
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
||||
}
|
||||
setEditingAction(null)
|
||||
setEditingActionId(null)
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -373,6 +373,20 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
}
|
||||
}
|
||||
|
||||
async function handleMoveActionToSprint(action, targetCycleId) {
|
||||
if (!action?.id || !targetCycleId || action.work_cycle_id === targetCycleId) return
|
||||
setFormBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
|
||||
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickStatus(action, status) {
|
||||
if (!capabilities.has('kairo.action.manage')) return
|
||||
try {
|
||||
|
|
@ -966,8 +980,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
error,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
formBusy,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
|
|
@ -983,6 +997,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
handleMethodProfileChange,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleMoveActionToSprint,
|
||||
handleQuickStatus,
|
||||
handleCreateBlocker,
|
||||
handleCreateBlockerForAction,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export function InitiativeExecutionPage() {
|
|||
setHideDone,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
capabilities,
|
||||
formBusy,
|
||||
unlinkedBlockers,
|
||||
|
|
@ -63,11 +63,11 @@ export function InitiativeExecutionPage() {
|
|||
showForm={showActionForm}
|
||||
onToggleForm={() => {
|
||||
setShowActionForm((v) => !v)
|
||||
setEditingAction(null)
|
||||
setEditingActionId(null)
|
||||
}}
|
||||
editingActionId={editingAction}
|
||||
onEditAction={setEditingAction}
|
||||
onCancelEdit={() => setEditingAction(null)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={ops.handleCreateAction}
|
||||
onUpdateAction={ops.handleUpdateAction}
|
||||
onQuickStatus={ops.handleQuickStatus}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ function PlanSprintInner() {
|
|||
formBusy,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
actionContextById,
|
||||
actors,
|
||||
actorsLoading,
|
||||
|
|
@ -45,6 +45,7 @@ function PlanSprintInner() {
|
|||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
handleMoveActionToSprint,
|
||||
handleUnplanAction,
|
||||
} = ops
|
||||
|
||||
|
|
@ -102,14 +103,15 @@ function PlanSprintInner() {
|
|||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingAction?.id}
|
||||
onEditAction={setEditingAction}
|
||||
onCancelEdit={() => setEditingAction(null)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={createSprintAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
|
|
@ -120,6 +122,7 @@ function PlanSprintInner() {
|
|||
onReloadActors={reloadActors}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={selectedWorkCycle}
|
||||
allActions={actions}
|
||||
sectionTitle={`Sprint-Backlog — ${selectedWorkCycle.title}`}
|
||||
sectionLead={sprintLead}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ function WorkSprintInner() {
|
|||
formBusy,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
editingActionId,
|
||||
setEditingActionId,
|
||||
actionContextById,
|
||||
actors,
|
||||
actorsLoading,
|
||||
|
|
@ -38,6 +38,8 @@ function WorkSprintInner() {
|
|||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
handleMoveActionToSprint,
|
||||
handleUnplanAction,
|
||||
initiative,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
|
|
@ -101,13 +103,15 @@ function WorkSprintInner() {
|
|||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingAction?.id}
|
||||
onEditAction={setEditingAction}
|
||||
onCancelEdit={() => setEditingAction(null)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={handleCreateAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
|
|
@ -159,13 +163,15 @@ function WorkSprintInner() {
|
|||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingAction?.id}
|
||||
onEditAction={setEditingAction}
|
||||
onCancelEdit={() => setEditingAction(null)}
|
||||
editingActionId={editingActionId}
|
||||
onEditAction={setEditingActionId}
|
||||
onCancelEdit={() => setEditingActionId(null)}
|
||||
onCreateAction={createAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
onMoveToSprint={handleMoveActionToSprint}
|
||||
onUnplanToBacklog={handleUnplanAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
|
|
@ -176,9 +182,14 @@ function WorkSprintInner() {
|
|||
onReloadActors={reloadActors}
|
||||
workCycles={ops.workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
allActions={ops.actions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!activeWorkCycle && !isProductArchetype && (
|
||||
<EmptyState message="Sprint-Ausführung ist für Product-Vorhaben — wähle ein passendes Vorhaben im Scope." />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -999,13 +999,42 @@
|
|||
|
||||
.action-hub-card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.65rem;
|
||||
border-top: 1px solid var(--jk-border, #e5e7eb);
|
||||
}
|
||||
|
||||
.action-hub-card-actions__primary,
|
||||
.action-hub-card-actions__secondary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-hub-card-actions__primary {
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 1px dashed var(--jk-border, #e5e7eb);
|
||||
}
|
||||
|
||||
.sprint-transfer-select .inline-select {
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.55rem;
|
||||
font-size: 0.8rem;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user