Skip to content

Commit a179380

Browse files
authored
Collection block bug fixes (#1852)
- Navigate the row-actions relation icon to the relation's `entityId` rather than its `id`, matching LinkableRelationChip. The previous URL pointed at a non-existent entity. Lookup is deferred until the popover opens, so list/gallery views (which mount the component per row) don't pay an O(rows × relations) scan on every render. - Keep the row-actions popover open while its nested space selector is active. The selector's content is portaled outside the outer popover's DOM, so opening it fired `onFocusOutside` on the outer Content and unmounted both. Suppress the outer's dismiss handlers while the nested popover is open and close it explicitly on selection. - `SelectSpaceAsPopover` gains optional controlled `open`/`onOpenChange` props (used by the row-actions wrapper above). The props are typed as a discriminated union so passing `open` without `onOpenChange` is a type error — otherwise Radix could end up stuck open with no updater.
1 parent 8ae8659 commit a179380

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

apps/web/design-system/select-space-dialog.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ import * as React from 'react';
44

55
import { SelectSpace } from './select-space';
66

7-
type SelectEntityAsPopoverProps = {
7+
type SelectEntityAsPopoverBaseProps = {
88
trigger: React.ReactNode;
99
entityId: string;
1010
spaceId?: string;
1111
verified?: boolean;
1212
onDone: (result: { id: string; name: string | null; space?: string; verified?: boolean }) => void;
1313
};
1414

15-
export function SelectSpaceAsPopover({ trigger, onDone, entityId, spaceId, verified }: SelectEntityAsPopoverProps) {
15+
// Either both `open` and `onOpenChange` are provided (controlled) or neither is
16+
// (uncontrolled). Passing `open` without `onOpenChange` would leave Radix unable
17+
// to dismiss the popover.
18+
type SelectEntityAsPopoverProps = SelectEntityAsPopoverBaseProps &
19+
({ open?: undefined; onOpenChange?: undefined } | { open: boolean; onOpenChange: (open: boolean) => void });
20+
21+
export function SelectSpaceAsPopover({
22+
trigger,
23+
onDone,
24+
entityId,
25+
spaceId,
26+
verified,
27+
open,
28+
onOpenChange,
29+
}: SelectEntityAsPopoverProps) {
1630
return (
17-
<Popover.Root>
31+
<Popover.Root open={open} onOpenChange={onOpenChange}>
1832
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
1933

2034
<Popover.Portal>

apps/web/partials/blocks/table/collection-row-actions.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export function CollectionRowActions({
4646
openedWithMainViewEditing = false,
4747
}: CollectionRowActionsProps) {
4848
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
49+
// The space-selector is a nested popover. Its content is portaled outside this
50+
// popover's DOM, so opening it would normally fire `onFocusOutside` /
51+
// `onInteractOutside` on the outer popover and immediately unmount the nested
52+
// popover along with us. Track its open state and suppress the outer's
53+
// dismiss handlers while it's active.
54+
const [isSpacePopoverOpen, setIsSpacePopoverOpen] = useState(false);
4955
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
5056
const { storage } = useMutate();
5157
const { blockEntity } = useDataBlock();
@@ -68,6 +74,21 @@ export function CollectionRowActions({
6874
}
6975
};
7076

77+
// A relation has two IDs: `id` (its own identifier, used for update/delete) and
78+
// `entityId` (the entity that represents the relation, used for navigation —
79+
// same pattern as LinkableRelationChip). Look up the collection-item relation by
80+
// its id and navigate to its entityId so the link lands on the relation entity
81+
// page, not on a non-existent entity at /space/.../$relation.id.
82+
//
83+
// Gate the scan on `isPopoverOpen`: in list/gallery views `CollectionRowActions`
84+
// is mounted per row (CSS-hidden until hover), so scanning every render would be
85+
// O(rows × relations). Radix only mounts `Popover.Content` (where the link
86+
// lives) while open, so the lookup only needs to be correct for the open row.
87+
const collectionItemRelation = isPopoverOpen
88+
? blockEntity?.relations.find(r => r.id === relationId)
89+
: undefined;
90+
const relationEntityId = collectionItemRelation?.entityId ?? relationId;
91+
7192
return (
7293
<div className="flex shrink-0 flex-nowrap items-center gap-0.5">
7394
{relationId && (
@@ -106,13 +127,20 @@ export function CollectionRowActions({
106127
event.preventDefault();
107128
event.stopPropagation();
108129
}}
130+
onFocusOutside={event => {
131+
if (isSpacePopoverOpen) event.preventDefault();
132+
}}
133+
onInteractOutside={event => {
134+
if (isSpacePopoverOpen) event.preventDefault();
135+
}}
109136
onMouseEnter={() => {
110137
if (closeTimeoutRef.current) {
111138
clearTimeout(closeTimeoutRef.current);
112139
closeTimeoutRef.current = null;
113140
}
114141
}}
115142
onMouseLeave={() => {
143+
if (isSpacePopoverOpen) return;
116144
setIsPopoverOpen(false);
117145
}}
118146
>
@@ -121,15 +149,17 @@ export function CollectionRowActions({
121149
entityId={EntityId(entityId)}
122150
spaceId={spaceId}
123151
verified={verified}
152+
open={isSpacePopoverOpen}
153+
onOpenChange={setIsSpacePopoverOpen}
124154
onDone={result => {
125155
if (!relationId) return;
126156
onLinkEntry(relationId, result, verified);
157+
setIsSpacePopoverOpen(false);
127158
}}
128159
trigger={
129160
<button
130161
type="button"
131162
className="inline-flex items-center p-1"
132-
onMouseDown={e => e.preventDefault()}
133163
>
134164
<span className="inline-flex size-[12px] items-center justify-center rounded-sm border group-hover:border-grey-03 group-hover:text-grey-03 hover:border-text! hover:text-text!">
135165
{space ? (
@@ -145,7 +175,7 @@ export function CollectionRowActions({
145175
/>
146176
)}
147177
<PrefetchLink
148-
href={`/space/${currentSpaceId}/${relationId}`}
178+
href={`/space/${currentSpaceId}/${relationEntityId}`}
149179
className="p-1 group-hover:text-grey-03 hover:text-text!"
150180
>
151181
<RelationSmall />

0 commit comments

Comments
 (0)