@@ -22,7 +22,7 @@ import { RootStackParamList } from '../navigation/types';
2222import { type JobResponse } from '../services/api' ;
2323import { trpc } from '../trpc/client' ;
2424import { useTheme } from '../hooks/useTheme' ;
25- import type { ThemeColors } from '../utils/theme' ;
25+ import type { ThemeColors , ThemeShadows } from '../utils/theme' ;
2626
2727type Props = {
2828 navigation : NativeStackNavigationProp < RootStackParamList , 'Jobs' > ;
@@ -86,6 +86,109 @@ export function formatRelative(iso: string | null | undefined): string {
8686 return new Date ( iso ) . toLocaleDateString ( ) ;
8787}
8888
89+ const keyExtractor = ( job : JobResponse ) => job . id ;
90+
91+ const JobCard = React . memo ( function JobCard ( {
92+ job,
93+ workflowName,
94+ colors,
95+ shadows,
96+ onOpen,
97+ onCancel,
98+ } : {
99+ job : JobResponse ;
100+ workflowName : string ;
101+ colors : ThemeColors ;
102+ shadows : ThemeShadows ;
103+ onOpen : ( jobId : string ) => void ;
104+ onCancel : ( job : JobResponse ) => void ;
105+ } ) {
106+ const handleOpen = useCallback ( ( ) => onOpen ( job . id ) , [ onOpen , job . id ] ) ;
107+ const handleCancel = useCallback ( ( ) => onCancel ( job ) , [ onCancel , job ] ) ;
108+
109+ const variant = statusVariant ( job . status ) ;
110+ const variantColor = statusColorFor ( colors , variant ) ;
111+ const duration = formatDuration ( job . started_at , job . finished_at ) ;
112+ const isRunning = variant === 'running' || variant === 'queued' ;
113+
114+ return (
115+ < TouchableOpacity
116+ onPress = { handleOpen }
117+ activeOpacity = { 0.8 }
118+ accessibilityRole = "button"
119+ accessibilityLabel = { `Open job ${ workflowName } ` }
120+ style = { [
121+ styles . card ,
122+ shadows . small ,
123+ { backgroundColor : colors . cardBg , borderColor : colors . borderLight } ,
124+ ] }
125+ >
126+ < View style = { styles . cardHeader } >
127+ < View style = { [ styles . statusPill , { backgroundColor : variantColor + '20' } ] } >
128+ < View style = { [ styles . statusDot , { backgroundColor : variantColor } ] } />
129+ < Text style = { [ styles . statusText , { color : variantColor } ] } >
130+ { job . status }
131+ </ Text >
132+ </ View >
133+ < Text style = { [ styles . timeText , { color : colors . textTertiary } ] } >
134+ { formatRelative ( job . started_at ) }
135+ </ Text >
136+ </ View >
137+
138+ < Text style = { [ styles . title , { color : colors . text } ] } numberOfLines = { 1 } >
139+ { workflowName }
140+ </ Text >
141+ < Text style = { [ styles . idText , { color : colors . textTertiary } ] } numberOfLines = { 1 } >
142+ Job { job . id }
143+ </ Text >
144+
145+ < View style = { styles . metaRow } >
146+ { duration ? (
147+ < View style = { styles . metaItem } >
148+ < Ionicons name = "time-outline" size = { 13 } color = { colors . textSecondary } />
149+ < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } > { duration } </ Text >
150+ </ View >
151+ ) : null }
152+ { job . job_type ? (
153+ < View style = { styles . metaItem } >
154+ < Ionicons name = "layers-outline" size = { 13 } color = { colors . textSecondary } />
155+ < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } > { job . job_type } </ Text >
156+ </ View >
157+ ) : null }
158+ { typeof job . cost === 'number' ? (
159+ < View style = { styles . metaItem } >
160+ < Ionicons name = "card-outline" size = { 13 } color = { colors . textSecondary } />
161+ < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } >
162+ ${ job . cost . toFixed ( 4 ) }
163+ </ Text >
164+ </ View >
165+ ) : null }
166+ </ View >
167+
168+ { job . error ? (
169+ < View style = { [ styles . errorBox , { backgroundColor : colors . error + '14' } ] } >
170+ < Ionicons name = "alert-circle-outline" size = { 13 } color = { colors . error } />
171+ < Text style = { [ styles . errorText , { color : colors . error } ] } numberOfLines = { 3 } >
172+ { job . error }
173+ </ Text >
174+ </ View >
175+ ) : null }
176+
177+ { isRunning ? (
178+ < TouchableOpacity
179+ onPress = { handleCancel }
180+ style = { [ styles . cancelBtn , { borderColor : colors . error } ] }
181+ accessibilityRole = "button"
182+ accessibilityLabel = "Cancel job"
183+ >
184+ < Ionicons name = "stop-circle-outline" size = { 15 } color = { colors . error } />
185+ < Text style = { [ styles . cancelText , { color : colors . error } ] } > Cancel job</ Text >
186+ </ TouchableOpacity >
187+ ) : null }
188+ </ TouchableOpacity >
189+ ) ;
190+ } ) ;
191+
89192export default function JobsScreen ( { navigation, route } : Props ) {
90193 const { colors, shadows } = useTheme ( ) ;
91194 const insets = useSafeAreaInsets ( ) ;
@@ -130,11 +233,6 @@ export default function JobsScreen({ navigation, route }: Props) {
130233 ) ;
131234 } , [ cancelJob ] ) ;
132235
133- const statusColor = useCallback (
134- ( variant : StatusVariant ) => statusColorFor ( colors , variant ) ,
135- [ colors ] ,
136- ) ;
137-
138236 const sortedJobs = useMemo ( ( ) => {
139237 return [ ...jobs ] . sort ( ( a , b ) => {
140238 const aT = new Date ( a . started_at ) . getTime ( ) ;
@@ -143,90 +241,28 @@ export default function JobsScreen({ navigation, route }: Props) {
143241 } ) ;
144242 } , [ jobs ] ) ;
145243
146- const renderItem = ( { item } : { item : JobResponse } ) => {
147- const variant = statusVariant ( item . status ) ;
148- const variantColor = statusColor ( variant ) ;
149- const duration = formatDuration ( item . started_at , item . finished_at ) ;
150- const isRunning = variant === 'running' || variant === 'queued' ;
151- const workflowName = workflowNames [ item . workflow_id ] || `Workflow ${ item . workflow_id . substring ( 0 , 8 ) } ` ;
152-
153- return (
154- < TouchableOpacity
155- onPress = { ( ) => navigation . navigate ( 'JobDetail' , { jobId : item . id } ) }
156- activeOpacity = { 0.8 }
157- accessibilityRole = "button"
158- accessibilityLabel = { `Open job ${ workflowName } ` }
159- style = { [
160- styles . card ,
161- shadows . small ,
162- { backgroundColor : colors . cardBg , borderColor : colors . borderLight } ,
163- ] }
164- >
165- < View style = { styles . cardHeader } >
166- < View style = { [ styles . statusPill , { backgroundColor : variantColor + '20' } ] } >
167- < View style = { [ styles . statusDot , { backgroundColor : variantColor } ] } />
168- < Text style = { [ styles . statusText , { color : variantColor } ] } >
169- { item . status }
170- </ Text >
171- </ View >
172- < Text style = { [ styles . timeText , { color : colors . textTertiary } ] } >
173- { formatRelative ( item . started_at ) }
174- </ Text >
175- </ View >
176-
177- < Text style = { [ styles . title , { color : colors . text } ] } numberOfLines = { 1 } >
178- { workflowName }
179- </ Text >
180- < Text style = { [ styles . idText , { color : colors . textTertiary } ] } numberOfLines = { 1 } >
181- Job { item . id }
182- </ Text >
183-
184- < View style = { styles . metaRow } >
185- { duration ? (
186- < View style = { styles . metaItem } >
187- < Ionicons name = "time-outline" size = { 13 } color = { colors . textSecondary } />
188- < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } > { duration } </ Text >
189- </ View >
190- ) : null }
191- { item . job_type ? (
192- < View style = { styles . metaItem } >
193- < Ionicons name = "layers-outline" size = { 13 } color = { colors . textSecondary } />
194- < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } > { item . job_type } </ Text >
195- </ View >
196- ) : null }
197- { typeof item . cost === 'number' ? (
198- < View style = { styles . metaItem } >
199- < Ionicons name = "card-outline" size = { 13 } color = { colors . textSecondary } />
200- < Text style = { [ styles . metaText , { color : colors . textSecondary } ] } >
201- ${ item . cost . toFixed ( 4 ) }
202- </ Text >
203- </ View >
204- ) : null }
205- </ View >
206-
207- { item . error ? (
208- < View style = { [ styles . errorBox , { backgroundColor : colors . error + '14' } ] } >
209- < Ionicons name = "alert-circle-outline" size = { 13 } color = { colors . error } />
210- < Text style = { [ styles . errorText , { color : colors . error } ] } numberOfLines = { 3 } >
211- { item . error }
212- </ Text >
213- </ View >
214- ) : null }
244+ const handleOpen = useCallback (
245+ ( jobId : string ) => {
246+ navigation . navigate ( 'JobDetail' , { jobId } ) ;
247+ } ,
248+ [ navigation ] ,
249+ ) ;
215250
216- { isRunning ? (
217- < TouchableOpacity
218- onPress = { ( ) => handleCancel ( item ) }
219- style = { [ styles . cancelBtn , { borderColor : colors . error } ] }
220- accessibilityRole = "button"
221- accessibilityLabel = "Cancel job"
222- >
223- < Ionicons name = "stop-circle-outline" size = { 15 } color = { colors . error } />
224- < Text style = { [ styles . cancelText , { color : colors . error } ] } > Cancel job</ Text >
225- </ TouchableOpacity >
226- ) : null }
227- </ TouchableOpacity >
228- ) ;
229- } ;
251+ const renderItem = useCallback (
252+ ( { item } : { item : JobResponse } ) => (
253+ < JobCard
254+ job = { item }
255+ workflowName = {
256+ workflowNames [ item . workflow_id ] || `Workflow ${ item . workflow_id . substring ( 0 , 8 ) } `
257+ }
258+ colors = { colors }
259+ shadows = { shadows }
260+ onOpen = { handleOpen }
261+ onCancel = { handleCancel }
262+ />
263+ ) ,
264+ [ workflowNames , colors , shadows , handleOpen , handleCancel ] ,
265+ ) ;
230266
231267 if ( isLoading ) {
232268 return (
@@ -247,7 +283,7 @@ export default function JobsScreen({ navigation, route }: Props) {
247283
248284 < FlatList
249285 data = { sortedJobs }
250- keyExtractor = { ( j ) => j . id }
286+ keyExtractor = { keyExtractor }
251287 renderItem = { renderItem }
252288 contentContainerStyle = { [ styles . list , { paddingBottom : insets . bottom + 24 } ] }
253289 refreshControl = {
0 commit comments