@@ -15,7 +15,7 @@ import {
15
15
json ,
16
16
redirectDocument ,
17
17
} from "@remix-run/server-runtime" ;
18
- import { useState } from "react" ;
18
+ import { useMemo , useState } from "react" ;
19
19
import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
20
20
import { z } from "zod" ;
21
21
import { InlineCode } from "~/components/code/InlineCode" ;
@@ -199,6 +199,43 @@ export default function Page() {
199
199
const project = useProject ( ) ;
200
200
const environment = useEnvironment ( ) ;
201
201
202
+ // Add isFirst and isLast to each environment variable
203
+ // They're set based on if they're the first or last time that `key` has been seen in the list
204
+ const groupedEnvironmentVariables = useMemo ( ( ) => {
205
+ // Create a map to track occurrences of each key
206
+ const keyOccurrences = new Map < string , number > ( ) ;
207
+
208
+ // First pass: count total occurrences of each key
209
+ environmentVariables . forEach ( ( variable ) => {
210
+ keyOccurrences . set ( variable . key , ( keyOccurrences . get ( variable . key ) || 0 ) + 1 ) ;
211
+ } ) ;
212
+
213
+ // Second pass: add isFirstTime, isLastTime, and occurrences flags
214
+ const seenKeys = new Set < string > ( ) ;
215
+ const currentOccurrences = new Map < string , number > ( ) ;
216
+
217
+ return environmentVariables . map ( ( variable ) => {
218
+ // Track current occurrence number for this key
219
+ const currentCount = ( currentOccurrences . get ( variable . key ) || 0 ) + 1 ;
220
+ currentOccurrences . set ( variable . key , currentCount ) ;
221
+
222
+ const totalOccurrences = keyOccurrences . get ( variable . key ) || 1 ;
223
+ const isFirstTime = ! seenKeys . has ( variable . key ) ;
224
+ const isLastTime = currentCount === totalOccurrences ;
225
+
226
+ if ( isFirstTime ) {
227
+ seenKeys . add ( variable . key ) ;
228
+ }
229
+
230
+ return {
231
+ ...variable ,
232
+ isFirstTime,
233
+ isLastTime,
234
+ occurences : totalOccurrences ,
235
+ } ;
236
+ } ) ;
237
+ } , [ environmentVariables ] ) ;
238
+
202
239
return (
203
240
< PageContainer >
204
241
< NavBar >
@@ -245,47 +282,79 @@ export default function Page() {
245
282
</ TableRow >
246
283
</ TableHeader >
247
284
< TableBody >
248
- { environmentVariables . length > 0 ? (
249
- environmentVariables . map ( ( variable ) => (
250
- < TableRow key = { `${ variable . id } -${ variable . environment . id } ` } >
251
- < TableCell >
252
- < CopyableText value = { variable . key } className = "font-mono" />
253
- </ TableCell >
254
- < TableCell >
255
- { variable . isSecret ? (
256
- < SimpleTooltip
257
- button = {
258
- < div className = "flex items-center gap-x-1.5" >
259
- < LockClosedIcon className = "size-3 text-text-dimmed" />
260
- < span className = "text-sm text-text-dimmed" > Secret</ span >
261
- </ div >
262
- }
263
- content = "This variable is secret and cannot be revealed."
264
- />
265
- ) : (
266
- < ClipboardField
267
- secure = { ! revealAll }
268
- value = { variable . value }
269
- variant = { "secondary/small" }
270
- fullWidth = { true }
271
- />
272
- ) }
273
- </ TableCell >
274
-
275
- < TableCell >
276
- < EnvironmentCombo environment = { variable . environment } className = "text-sm" />
277
- </ TableCell >
278
- < TableCellMenu
279
- isSticky
280
- hiddenButtons = {
281
- < >
282
- < EditEnvironmentVariablePanel variable = { variable } revealAll = { revealAll } />
283
- < DeleteEnvironmentVariableButton variable = { variable } />
284
- </ >
285
+ { groupedEnvironmentVariables . length > 0 ? (
286
+ groupedEnvironmentVariables . map ( ( variable ) => {
287
+ let cellClassName = "" ;
288
+ let borderedCellClassName = "" ;
289
+
290
+ if ( variable . occurences > 1 ) {
291
+ cellClassName = "py-1" ;
292
+ borderedCellClassName =
293
+ "relative after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright" ;
294
+ if ( variable . isLastTime ) {
295
+ cellClassName = "pt-1 pb-2" ;
296
+ borderedCellClassName = "" ;
297
+ } else if ( variable . isFirstTime ) {
298
+ cellClassName = "pt-2 pb-1" ;
299
+ }
300
+ } else {
301
+ cellClassName = "py-2" ;
302
+ }
303
+
304
+ return (
305
+ < TableRow
306
+ key = { `${ variable . id } -${ variable . environment . id } ` }
307
+ className = {
308
+ variable . isLastTime ? "after:bg-charcoal-600" : "after:bg-transparent"
285
309
}
286
- />
287
- </ TableRow >
288
- ) )
310
+ >
311
+ < TableCell className = { cellClassName } >
312
+ { variable . isFirstTime ? (
313
+ < CopyableText value = { variable . key } className = "font-mono" />
314
+ ) : null }
315
+ </ TableCell >
316
+ < TableCell
317
+ className = { cn ( cellClassName , borderedCellClassName , "after:left-3" ) }
318
+ >
319
+ { variable . isSecret ? (
320
+ < SimpleTooltip
321
+ button = {
322
+ < div className = "flex items-center gap-x-1.5" >
323
+ < LockClosedIcon className = "size-3 text-text-dimmed" />
324
+ < span className = "text-sm text-text-dimmed" > Secret</ span >
325
+ </ div >
326
+ }
327
+ content = "This variable is secret and cannot be revealed."
328
+ />
329
+ ) : (
330
+ < ClipboardField
331
+ secure = { ! revealAll }
332
+ value = { variable . value }
333
+ variant = { "secondary/small" }
334
+ fullWidth = { true }
335
+ />
336
+ ) }
337
+ </ TableCell >
338
+
339
+ < TableCell className = { cn ( cellClassName , borderedCellClassName ) } >
340
+ < EnvironmentCombo environment = { variable . environment } className = "text-sm" />
341
+ </ TableCell >
342
+ < TableCellMenu
343
+ className = { cn ( cellClassName , borderedCellClassName ) }
344
+ isSticky
345
+ hiddenButtons = {
346
+ < >
347
+ < EditEnvironmentVariablePanel
348
+ variable = { variable }
349
+ revealAll = { revealAll }
350
+ />
351
+ < DeleteEnvironmentVariableButton variable = { variable } />
352
+ </ >
353
+ }
354
+ />
355
+ </ TableRow >
356
+ ) ;
357
+ } )
289
358
) : (
290
359
< TableRow >
291
360
< TableCell colSpan = { 4 } >
0 commit comments