Skip to content

Commit 8c2ca01

Browse files
committed
lift tag row dialogs into provider
1 parent e51fcfa commit 8c2ca01

3 files changed

Lines changed: 266 additions & 169 deletions

File tree

src/features/dashboard/templates/tags/table-cells.tsx

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import { MiddleTruncate } from '@/ui/primitives/middle-truncate'
1717
import { BuildLink } from './build-link'
1818
import { DEFAULT_TAG_NAME } from './constants'
1919
import TagDeleteDialog from './delete-dialog'
20-
import ReassignTagDialog from './reassign-dialog'
21-
import RollbackTagDialog from './rollback-dialog'
20+
import { useTagDialog } from './tag-dialog-provider'
2221
import type { TagGroup } from './types'
2322

2423
export interface TagTableMeta {
@@ -59,10 +58,9 @@ export function BuildLinkCell(ctx: CellContext<TagGroup, unknown>) {
5958
export function ActionsCell(ctx: CellContext<TagGroup, unknown>) {
6059
const { row } = ctx
6160
const { teamSlug, templateId, templateName } = getMeta(ctx)
61+
const { actions } = useTagDialog()
6262
const [menuOpen, setMenuOpen] = useState(false)
6363
const [deleteOpen, setDeleteOpen] = useState(false)
64-
const [rollbackOpen, setRollbackOpen] = useState(false)
65-
const [reassignOpen, setReassignOpen] = useState(false)
6664

6765
const group = row.original
6866
const isDefaultTag = group.tag === DEFAULT_TAG_NAME
@@ -83,7 +81,7 @@ export function ActionsCell(ctx: CellContext<TagGroup, unknown>) {
8381
aria-label={`Reassign tag ${group.tag} to a different build`}
8482
onClick={(e) => {
8583
e.stopPropagation()
86-
setReassignOpen(true)
84+
actions.openReassign(group)
8785
}}
8886
>
8987
Reassign
@@ -99,7 +97,9 @@ export function ActionsCell(ctx: CellContext<TagGroup, unknown>) {
9997
}
10098
onClick={(e) => {
10199
e.stopPropagation()
102-
setRollbackOpen(true)
100+
if (previousAssignment) {
101+
actions.openRollback(group, previousAssignment, 'tags-tab')
102+
}
103103
}}
104104
>
105105
Rollback
@@ -143,31 +143,6 @@ export function ActionsCell(ctx: CellContext<TagGroup, unknown>) {
143143
templateId={templateId}
144144
templateName={templateName}
145145
/>
146-
147-
{previousAssignment && (
148-
<RollbackTagDialog
149-
open={rollbackOpen}
150-
onOpenChange={setRollbackOpen}
151-
tag={group.tag}
152-
currentBuildId={group.primaryAssignment.buildId}
153-
targetBuildId={previousAssignment.buildId}
154-
teamSlug={teamSlug}
155-
templateId={templateId}
156-
templateName={templateName}
157-
surface="tags-tab"
158-
/>
159-
)}
160-
161-
<ReassignTagDialog
162-
open={reassignOpen}
163-
onOpenChange={setReassignOpen}
164-
tag={group.tag}
165-
currentBuildId={group.primaryAssignment.buildId}
166-
teamSlug={teamSlug}
167-
templateId={templateId}
168-
templateName={templateName}
169-
surface="tags-tab"
170-
/>
171146
</div>
172147
)
173148
}

src/features/dashboard/templates/tags/table.tsx

Lines changed: 116 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
useEffect,
2424
useMemo,
2525
useRef,
26-
useState,
2726
} from 'react'
2827
import { PROTECTED_URLS } from '@/configs/urls'
2928
import type { TemplateTagAssignment } from '@/core/modules/templates/models'
@@ -47,7 +46,6 @@ import { TAGS_PAGE_LIMIT } from './constants'
4746
import TagsEmpty from './empty'
4847
import TagsHeader from './header'
4948
import { TagHistoryRow } from './history/tag-history-row'
50-
import RollbackTagDialog from './rollback-dialog'
5149
import { useTagTableStore } from './stores/table-store'
5250
import {
5351
fallbackData,
@@ -58,6 +56,7 @@ import {
5856
trackTagTableInteraction,
5957
useTagColumns,
6058
} from './table-config'
59+
import { TagDialogProvider, useTagDialog } from './tag-dialog-provider'
6160
import type { TagGroup } from './types'
6261

6362
const ESTIMATED_ROW_HEIGHT_PX = 44
@@ -219,152 +218,145 @@ export default function TagsTable({ teamSlug, templateId }: TagsTableProps) {
219218
}, [fetchNextPage, activeSearch, serverSort])
220219

221220
return (
222-
<div className="flex flex-col gap-6 h-full min-h-0">
223-
<TagsHeader
224-
teamSlug={teamSlug}
225-
templateId={templateId}
226-
templateName={templateName}
227-
total={countData?.total}
228-
hasSearch={activeSearch !== undefined}
229-
searchInvalid={searchInvalid}
230-
/>
231-
<div
232-
ref={scrollRef}
233-
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden -mx-8 px-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
234-
>
235-
<DataTable className="w-full">
236-
<DataTableHeader className="sticky top-0 z-30 bg-bg border-b-0 mb-px">
237-
{table.getHeaderGroups().map((headerGroup) => (
238-
<DataTableRow
239-
key={headerGroup.id}
240-
className="border-b-0 flex items-center gap-6 -mx-8 px-8 w-[calc(100%+64px)]"
241-
>
242-
{headerGroup.headers.map((header) => (
243-
<DataTableHead
244-
key={header.id}
245-
header={header}
246-
sorting={sorting.find((s) => s.id === header.id)?.desc}
247-
align={header.id === 'actions' ? 'right' : 'left'}
248-
className={cn(
249-
'h-auto px-0',
250-
header.id === 'tag' && 'flex-1 min-w-0',
251-
header.id === 'assignedAt' && 'w-[178px] shrink-0',
252-
header.id === 'actions' &&
253-
'w-[203px] max-sm:w-4 shrink-0 justify-end'
254-
)}
255-
style={
256-
header.id === 'tag' ? undefined : { width: undefined }
257-
}
258-
>
259-
<span>
260-
{header.isPlaceholder
261-
? null
262-
: flexRender(
263-
header.column.columnDef.header,
264-
header.getContext()
265-
)}
266-
</span>
267-
</DataTableHead>
268-
))}
269-
</DataTableRow>
270-
))}
271-
</DataTableHeader>
272-
273-
{showLoader && (
274-
<div className="h-[35svh] w-full flex items-center justify-center">
275-
<Loader variant="slash" size="lg" />
276-
</div>
277-
)}
221+
<TagDialogProvider
222+
teamSlug={teamSlug}
223+
templateId={templateId}
224+
templateName={templateName}
225+
>
226+
<div className="flex flex-col gap-6 h-full min-h-0">
227+
<TagsHeader
228+
teamSlug={teamSlug}
229+
templateId={templateId}
230+
templateName={templateName}
231+
total={countData?.total}
232+
hasSearch={activeSearch !== undefined}
233+
searchInvalid={searchInvalid}
234+
/>
235+
<div
236+
ref={scrollRef}
237+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden -mx-8 px-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
238+
>
239+
<DataTable className="w-full">
240+
<DataTableHeader className="sticky top-0 z-30 bg-bg border-b-0 mb-px">
241+
{table.getHeaderGroups().map((headerGroup) => (
242+
<DataTableRow
243+
key={headerGroup.id}
244+
className="border-b-0 flex items-center gap-6 -mx-8 px-8 w-[calc(100%+64px)]"
245+
>
246+
{headerGroup.headers.map((header) => (
247+
<DataTableHead
248+
key={header.id}
249+
header={header}
250+
sorting={sorting.find((s) => s.id === header.id)?.desc}
251+
align={header.id === 'actions' ? 'right' : 'left'}
252+
className={cn(
253+
'h-auto px-0',
254+
header.id === 'tag' && 'flex-1 min-w-0',
255+
header.id === 'assignedAt' && 'w-[178px] shrink-0',
256+
header.id === 'actions' &&
257+
'w-[203px] max-sm:w-4 shrink-0 justify-end'
258+
)}
259+
style={
260+
header.id === 'tag' ? undefined : { width: undefined }
261+
}
262+
>
263+
<span>
264+
{header.isPlaceholder
265+
? null
266+
: flexRender(
267+
header.column.columnDef.header,
268+
header.getContext()
269+
)}
270+
</span>
271+
</DataTableHead>
272+
))}
273+
</DataTableRow>
274+
))}
275+
</DataTableHeader>
276+
277+
{showLoader && (
278+
<div className="h-[35svh] w-full flex items-center justify-center">
279+
<Loader variant="slash" size="lg" />
280+
</div>
281+
)}
278282

279-
{showEmpty && <TagsEmpty hasSearch={activeSearch !== undefined} />}
283+
{showEmpty && <TagsEmpty hasSearch={activeSearch !== undefined} />}
280284

281-
{hasData && (
282-
<div
283-
className={cn(
284-
'relative',
285-
showFilterRefetchingOverlay && 'opacity-70 transition-opacity'
286-
)}
287-
style={{ height: `${virtualizer.getTotalSize()}px` }}
288-
>
289-
{virtualItems.map((virtualRow) => {
290-
const row = rows[virtualRow.index]
291-
if (!row) return null
292-
return (
293-
<div
294-
key={virtualRow.key}
295-
data-index={virtualRow.index}
296-
ref={(node) => virtualizer.measureElement(node)}
297-
className={cn(
298-
'absolute left-0 right-0',
299-
'hover:z-20 focus-within:z-10',
300-
'border-b border-stroke/80',
301-
'has-[button[aria-haspopup=menu][data-state=open]]:z-10'
302-
)}
303-
style={{
304-
transform: `translateY(${
305-
virtualRow.start - HEADER_SCROLL_MARGIN_PX
306-
}px)`,
307-
}}
308-
>
309-
<GroupSection
310-
row={row}
311-
teamSlug={teamSlug}
312-
templateId={templateId}
313-
templateName={templateName}
314-
/>
315-
</div>
316-
)
317-
})}
318-
</div>
319-
)}
285+
{hasData && (
286+
<div
287+
className={cn(
288+
'relative',
289+
showFilterRefetchingOverlay && 'opacity-70 transition-opacity'
290+
)}
291+
style={{ height: `${virtualizer.getTotalSize()}px` }}
292+
>
293+
{virtualItems.map((virtualRow) => {
294+
const row = rows[virtualRow.index]
295+
if (!row) return null
296+
return (
297+
<div
298+
key={virtualRow.key}
299+
data-index={virtualRow.index}
300+
ref={(node) => virtualizer.measureElement(node)}
301+
className={cn(
302+
'absolute left-0 right-0',
303+
'hover:z-20 focus-within:z-10',
304+
'border-b border-stroke/80',
305+
'has-[button[aria-haspopup=menu][data-state=open]]:z-10'
306+
)}
307+
style={{
308+
transform: `translateY(${
309+
virtualRow.start - HEADER_SCROLL_MARGIN_PX
310+
}px)`,
311+
}}
312+
>
313+
<GroupSection
314+
row={row}
315+
teamSlug={teamSlug}
316+
templateId={templateId}
317+
/>
318+
</div>
319+
)
320+
})}
321+
</div>
322+
)}
320323

321-
{hasNextPage && (
322-
<div className="flex w-full items-center justify-center py-3">
323-
<LoadMoreButton
324-
isLoading={isFetchingNextPage}
325-
onLoadMore={handleLoadMore}
326-
/>
327-
</div>
328-
)}
329-
</DataTable>
324+
{hasNextPage && (
325+
<div className="flex w-full items-center justify-center py-3">
326+
<LoadMoreButton
327+
isLoading={isFetchingNextPage}
328+
onLoadMore={handleLoadMore}
329+
/>
330+
</div>
331+
)}
332+
</DataTable>
333+
</div>
330334
</div>
331-
</div>
335+
</TagDialogProvider>
332336
)
333337
}
334338

335339
interface GroupSectionProps {
336340
row: Row<TagGroup>
337341
teamSlug: string
338342
templateId: string
339-
templateName: string
340343
}
341344

342-
function GroupSection({
343-
row,
344-
teamSlug,
345-
templateId,
346-
templateName,
347-
}: GroupSectionProps) {
345+
function GroupSection({ row, teamSlug, templateId }: GroupSectionProps) {
348346
'use no memo'
349347

350348
const canExpand = row.getCanExpand()
351349
const isExpanded = row.getIsExpanded()
352350
const dataState = isExpanded ? 'open' : 'closed'
353351
const shouldReduceMotion = useReducedMotion()
354352

355-
const [rollbackRequest, setRollbackRequest] = useState<{
356-
target: TemplateTagAssignment
357-
currentBuildId: string
358-
} | null>(null)
353+
const { actions } = useTagDialog()
359354

360355
const handleRequestRowRollback = useCallback(
361356
(target: TemplateTagAssignment) => {
362-
setRollbackRequest({
363-
target,
364-
currentBuildId: row.original.primaryAssignment.buildId,
365-
})
357+
actions.openRollback(row.original, target, 'history-row')
366358
},
367-
[row.original.primaryAssignment.buildId]
359+
[actions, row.original]
368360
)
369361

370362
const toggle = () => {
@@ -496,20 +488,6 @@ function GroupSection({
496488
</motion.div>
497489
)}
498490
</AnimatePresence>
499-
500-
<RollbackTagDialog
501-
open={rollbackRequest !== null}
502-
onOpenChange={(next) => {
503-
if (!next) setRollbackRequest(null)
504-
}}
505-
tag={row.original.tag}
506-
currentBuildId={rollbackRequest?.currentBuildId ?? ''}
507-
targetBuildId={rollbackRequest?.target.buildId ?? ''}
508-
teamSlug={teamSlug}
509-
templateId={templateId}
510-
templateName={templateName}
511-
surface="history-row"
512-
/>
513491
</div>
514492
)
515493
}

0 commit comments

Comments
 (0)