Skip to content

Commit 19e0f03

Browse files
esunejamshale
authored andcommitted
feat(admin): allow renaming existing scenarios
Add a rename action next to the existing hide/show toggle on each scenario tab, backed by a small modal and a hook function that updates only the scenario name while keeping its id/slug stable. Signed-off-by: Emiliano Suñé <2395873+esune@users.noreply.github.com>
1 parent 48d7ec5 commit 19e0f03

3 files changed

Lines changed: 148 additions & 1 deletion

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { XMarkIcon } from '@heroicons/react/24/outline'
2+
import { useEffect, useState } from 'react'
3+
4+
import { ErrorBanner } from '../../ErrorBanner'
5+
6+
interface RenameScenarioModalProps {
7+
isOpen: boolean
8+
currentName: string
9+
onClose: () => void
10+
onSave: (newName: string) => Promise<void>
11+
}
12+
13+
export function RenameScenarioModal({ isOpen, currentName, onClose, onSave }: RenameScenarioModalProps) {
14+
const [scenarioName, setScenarioName] = useState(currentName)
15+
const [isLoading, setIsLoading] = useState(false)
16+
const [error, setError] = useState<string | null>(null)
17+
18+
// Reset the input whenever a new scenario is opened for renaming
19+
useEffect(() => {
20+
if (isOpen) {
21+
setScenarioName(currentName)
22+
setError(null)
23+
}
24+
}, [isOpen, currentName])
25+
26+
const handleClose = () => {
27+
setError(null)
28+
onClose()
29+
}
30+
31+
const handleSave = async () => {
32+
if (!scenarioName.trim()) return
33+
34+
try {
35+
setIsLoading(true)
36+
setError(null)
37+
await onSave(scenarioName.trim())
38+
} catch (err) {
39+
const errorMsg = err instanceof Error ? err.message : 'Failed to rename scenario'
40+
setError(errorMsg)
41+
} finally {
42+
setIsLoading(false)
43+
}
44+
}
45+
46+
if (!isOpen) return null
47+
48+
return (
49+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
50+
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full mx-4">
51+
{/* Header */}
52+
<div className="flex items-center justify-between p-6 border-b border-gray-200">
53+
<h2 className="text-lg font-semibold text-bcgov-black">Rename Scenario</h2>
54+
<button
55+
onClick={handleClose}
56+
className="text-gray-400 hover:text-gray-600 transition-colors"
57+
aria-label="Close"
58+
>
59+
<XMarkIcon className="w-5 h-5" />
60+
</button>
61+
</div>
62+
63+
<ErrorBanner error={error} onDismiss={() => setError(null)} />
64+
65+
{/* Content */}
66+
<div className="p-6">
67+
<label htmlFor="renameScenarioName" className="block text-sm font-medium text-bcgov-black mb-2">
68+
Scenario Name
69+
</label>
70+
<input
71+
id="renameScenarioName"
72+
type="text"
73+
value={scenarioName}
74+
onChange={(e) => setScenarioName(e.target.value)}
75+
placeholder="e.g., Student Discount, Room Booking"
76+
disabled={isLoading}
77+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-bcgov-blue focus:border-transparent disabled:bg-gray-100"
78+
/>
79+
</div>
80+
81+
{/* Footer */}
82+
<div className="flex justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50">
83+
<button
84+
onClick={handleClose}
85+
disabled={isLoading}
86+
className="px-4 py-2 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors disabled:bg-gray-100 disabled:cursor-not-allowed"
87+
>
88+
Cancel
89+
</button>
90+
<button
91+
onClick={handleSave}
92+
disabled={!scenarioName.trim() || isLoading}
93+
className="px-4 py-2 bg-bcgov-blue text-white font-medium rounded-lg hover:bg-bcgov-blue-dark transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
94+
>
95+
{isLoading ? 'Saving...' : 'Save'}
96+
</button>
97+
</div>
98+
</div>
99+
</div>
100+
)
101+
}

frontend/src/admin/components/showcase/scenarios/ScenariosTab.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ScenarioScreen, Showcase } from '../../../types'
22

3-
import { EyeIcon, EyeSlashIcon, PlusIcon } from '@heroicons/react/24/outline'
3+
import { EyeIcon, EyeSlashIcon, PencilIcon, PlusIcon } from '@heroicons/react/24/outline'
44
import { useEffect, useState, useRef } from 'react'
55
import { useAuth } from 'react-oidc-context'
66
import { useNavigate } from 'react-router-dom'
@@ -17,6 +17,7 @@ import { CreateConnectionAndProofScreensModal } from '../modals/CreateConnection
1717
import { CreateOrEditScreenModal } from '../modals/CreateOrEditScreenModal'
1818
import { CreateScenarioModal } from '../modals/CreateScenarioModal'
1919
import { DeleteConfirmationModal } from '../modals/DeleteConfirmationModal'
20+
import { RenameScenarioModal } from '../modals/RenameScenarioModal'
2021

2122
import { ScenarioTimeline } from './ScenarioTimeline'
2223

@@ -35,6 +36,7 @@ export function ScenariosTab({ showcase, isNewShowcase, onRefresh, isExpanded, s
3536
const canEdit = useHasRole('creator')
3637
const [activeScenario, setActiveScenario] = useState<string | null>(null)
3738
const [isCreateScenarioModalOpen, setIsCreateScenarioModalOpen] = useState(false)
39+
const [renamingScenario, setRenamingScenario] = useState<{ id: string; name: string } | null>(null)
3840
const [isCreateConnectionProofModalOpen, setIsCreateConnectionProofModalOpen] = useState(false)
3941
const [hoverIdx, setHoverIdx] = useState<string | number | null>(null)
4042
const [iframeRefreshKey, setIframeRefreshKey] = useState(0)
@@ -60,6 +62,8 @@ export function ScenariosTab({ showcase, isNewShowcase, onRefresh, isExpanded, s
6062
handleDeleteScreen,
6163
handleToggleHidden,
6264
togglingHiddenId,
65+
handleRenameScenario,
66+
renamingScenarioId,
6367
} = useScenarioScreens({ showcase, activeScenario, onRefresh })
6468

6569
useEffect(() => {
@@ -142,6 +146,17 @@ export function ScenariosTab({ showcase, isNewShowcase, onRefresh, isExpanded, s
142146
>
143147
{scenario.name}
144148
</button>
149+
{canEdit && (
150+
<button
151+
onClick={() => setRenamingScenario({ id: scenario.id, name: scenario.name })}
152+
disabled={renamingScenarioId !== null}
153+
title="Rename scenario"
154+
aria-label="Rename scenario"
155+
className="px-1 text-bcgov-darkgrey hover:text-bcgov-black transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
156+
>
157+
<PencilIcon className="w-4 h-4" />
158+
</button>
159+
)}
145160
{canEdit && (
146161
<button
147162
onClick={() => handleToggleHidden(scenario.id)}
@@ -221,6 +236,16 @@ export function ScenariosTab({ showcase, isNewShowcase, onRefresh, isExpanded, s
221236
setActiveScenario(scenarioId)
222237
}}
223238
/>
239+
<RenameScenarioModal
240+
isOpen={renamingScenario !== null}
241+
currentName={renamingScenario?.name ?? ''}
242+
onClose={() => setRenamingScenario(null)}
243+
onSave={async (newName) => {
244+
if (!renamingScenario) return
245+
await handleRenameScenario(renamingScenario.id, newName)
246+
setRenamingScenario(null)
247+
}}
248+
/>
224249
<CreateConnectionAndProofScreensModal
225250
isOpen={isCreateConnectionProofModalOpen}
226251
onClose={() => setIsCreateConnectionProofModalOpen(false)}

frontend/src/admin/hooks/useScenarioScreens.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function useScenarioScreens({ showcase, activeScenario, onRefresh }: UseS
2222
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
2323
const [deleteConfirmIdx, setDeleteConfirmIdx] = useState<number | null>(null)
2424
const [togglingHiddenId, setTogglingHiddenId] = useState<string | null>(null)
25+
const [renamingScenarioId, setRenamingScenarioId] = useState<string | null>(null)
2526

2627
const isEditingPredefinedScreen =
2728
editingScreen?.screenId === 'START' ||
@@ -174,6 +175,24 @@ export function useScenarioScreens({ showcase, activeScenario, onRefresh }: UseS
174175
}
175176
}
176177

178+
const handleRenameScenario = async (scenarioId: string, newName: string) => {
179+
const trimmedName = newName.trim()
180+
const scenario = showcase.scenarios?.find((sc) => sc.id === scenarioId)
181+
if (!showcase.scenarios || !scenario || !trimmedName || renamingScenarioId || trimmedName === scenario.name) return
182+
183+
const updatedScenarios = showcase.scenarios.map((sc) => (sc.id === scenarioId ? { ...sc, name: trimmedName } : sc))
184+
setRenamingScenarioId(scenarioId)
185+
try {
186+
await updateShowcase(auth, showcase.name, { scenarios: updatedScenarios })
187+
await onRefresh?.()
188+
} catch (error) {
189+
log.error('Error renaming scenario:', error)
190+
throw error
191+
} finally {
192+
setRenamingScenarioId(null)
193+
}
194+
}
195+
177196
return {
178197
editingScreenIdx,
179198
editingScreen,
@@ -193,5 +212,7 @@ export function useScenarioScreens({ showcase, activeScenario, onRefresh }: UseS
193212
handleDeleteScreen,
194213
handleToggleHidden,
195214
togglingHiddenId,
215+
handleRenameScenario,
216+
renamingScenarioId,
196217
}
197218
}

0 commit comments

Comments
 (0)