Skip to content

Commit 97fc536

Browse files
authored
Prevent tag-select crash on unresolved tag IDs (dubinc#3703)
1 parent 0059b9a commit 97fc536

1 file changed

Lines changed: 38 additions & 18 deletions

File tree

apps/web/ui/links/link-builder/tag-select.tsx

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Tooltip,
1616
} from "@dub/ui";
1717
import { cn } from "@dub/utils";
18-
import { memo, useEffect, useMemo, useState } from "react";
18+
import { memo, useEffect, useMemo, useRef, useState } from "react";
1919
import { useFormContext, useWatch } from "react-hook-form";
2020
import { toast } from "sonner";
2121
import { mutate } from "swr";
@@ -61,6 +61,16 @@ export const TagSelect = memo(() => {
6161
});
6262
const [debouncedUrl] = useDebounce(url, 500);
6363

64+
const tagResolutionCacheRef = useRef<Map<string, TagProps>>(new Map());
65+
const prevLinkIdRef = useRef<string | undefined>(undefined);
66+
if (linkId !== prevLinkIdRef.current) {
67+
tagResolutionCacheRef.current = new Map();
68+
prevLinkIdRef.current = linkId;
69+
}
70+
for (const t of [...(tags ?? []), ...(availableTags ?? [])]) {
71+
if (t?.id) tagResolutionCacheRef.current.set(t.id, t);
72+
}
73+
6474
const [isOpen, setIsOpen] = useState(false);
6575

6676
const createTag = async (tag: string) => {
@@ -87,15 +97,23 @@ export const TagSelect = memo(() => {
8797
return false;
8898
};
8999

90-
const options = useMemo(
91-
() => availableTags?.map((tag) => getTagOption(tag)),
92-
[availableTags],
93-
);
100+
const options = useMemo(() => {
101+
if (loadingTags || availableTags === undefined) return undefined;
102+
const apiIds = new Set(availableTags.map((t) => t.id));
103+
const notOnCurrentPage = [...tagResolutionCacheRef.current.values()].filter(
104+
(t) => t?.id && !apiIds.has(t.id),
105+
);
106+
return [...availableTags, ...notOnCurrentPage].map((tag) =>
107+
getTagOption(tag),
108+
);
109+
}, [availableTags, loadingTags, tags]);
94110

95-
const selectedTags = useMemo(
96-
() => tags.map((tag) => getTagOption(tag)),
97-
[tags],
98-
);
111+
const selectedTags = useMemo(() => {
112+
const resolved = (tags ?? []).filter(
113+
(tag): tag is TagProps => tag != null && tag.id != null,
114+
);
115+
return resolved.map((tag) => getTagOption(tag));
116+
}, [tags]);
99117

100118
useLinkBuilderKeyboardShortcut("t", () => setIsOpen(true), {
101119
priority: 2,
@@ -172,15 +190,17 @@ export const TagSelect = memo(() => {
172190
selected={selectedTags}
173191
setSelected={(newTags) => {
174192
const selectedIds = newTags.map(({ value }) => value);
175-
setValue(
176-
"tags",
177-
selectedIds.map((id) =>
178-
[...(availableTags || []), ...(tags || [])]?.find(
179-
(t) => t.id === id,
180-
),
181-
),
182-
{ shouldDirty: true },
183-
);
193+
const lookup = new Map<string, TagProps>();
194+
for (const t of [...(availableTags ?? []), ...(tags ?? [])]) {
195+
if (t?.id) lookup.set(t.id, t);
196+
}
197+
for (const t of tagResolutionCacheRef.current.values()) {
198+
if (t?.id && !lookup.has(t.id)) lookup.set(t.id, t);
199+
}
200+
const nextTags = selectedIds
201+
.map((id) => lookup.get(id))
202+
.filter((t): t is TagProps => t != null);
203+
setValue("tags", nextTags, { shouldDirty: true });
184204
setSuggestedTags((tags) =>
185205
tags.filter(({ id }) => !selectedIds.includes(id)),
186206
);

0 commit comments

Comments
 (0)