@c-rex/components 0.3.0-build.41 → 0.3.0-build.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/src/autocomplete.tsx +20 -14
- package/src/documents/result-list-item.tsx +2 -1
- package/src/favorites/bookmark-button.tsx +20 -1
- package/src/favorites/favorite-button.tsx +11 -2
- package/src/footer/social-links-block.tsx +1 -0
- package/src/generated/create-client-request.tsx +0 -7
- package/src/info/information-unit-authors-grid-client.tsx +183 -0
- package/src/info/information-unit-metadata-grid-client.tsx +12 -2
- package/src/navbar/navbar.tsx +0 -2
- package/src/restriction-menu/__tests__/restriction-menu-logic.test.ts +148 -0
- package/src/restriction-menu/restriction-menu-item.tsx +13 -7
- package/src/restriction-menu/restriction-menu-logic.ts +51 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +21 -10
- package/src/results/filter-navbar.tsx +2 -1
- package/src/results/filter-sidebar/index.tsx +65 -49
- package/src/results/generic/search-results-client.tsx +1 -1
- package/src/results/information-unit-search-results-card-list.tsx +2 -1
- package/src/results/information-unit-search-results-cards.tsx +4 -3
- package/src/results/information-unit-search-results-table.tsx +6 -4
- package/src/results/pagination.tsx +3 -1
- package/src/stores/search-settings-store.ts +0 -4
- package/src/stores/ui-preferences-store.ts +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/components",
|
|
3
|
-
"version": "0.3.0-build.
|
|
3
|
+
"version": "0.3.0-build.42",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./src/info/information-unit-metadata-grid-client.tsx",
|
|
54
54
|
"import": "./src/info/information-unit-metadata-grid-client.tsx"
|
|
55
55
|
},
|
|
56
|
+
"./information-unit-authors-grid-client": {
|
|
57
|
+
"types": "./src/info/information-unit-authors-grid-client.tsx",
|
|
58
|
+
"import": "./src/info/information-unit-authors-grid-client.tsx"
|
|
59
|
+
},
|
|
56
60
|
"./info-card": {
|
|
57
61
|
"types": "./src/info/info-card.tsx",
|
|
58
62
|
"import": "./src/info/info-card.tsx"
|
|
@@ -145,6 +149,10 @@
|
|
|
145
149
|
"types": "./src/stores/search-settings-store.ts",
|
|
146
150
|
"import": "./src/stores/search-settings-store.ts"
|
|
147
151
|
},
|
|
152
|
+
"./ui-preferences-store": {
|
|
153
|
+
"types": "./src/stores/ui-preferences-store.ts",
|
|
154
|
+
"import": "./src/stores/ui-preferences-store.ts"
|
|
155
|
+
},
|
|
148
156
|
"./article-content": {
|
|
149
157
|
"types": "./src/article/article-content.tsx",
|
|
150
158
|
"import": "./src/article/article-content.tsx"
|
package/src/autocomplete.tsx
CHANGED
|
@@ -36,6 +36,7 @@ export const AutoComplete = ({
|
|
|
36
36
|
const [pkg] = useQueryState("package");
|
|
37
37
|
|
|
38
38
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
39
40
|
const suggestionRequestIdRef = useRef(0);
|
|
40
41
|
const suggestionAbortControllerRef = useRef<AbortController | null>(null);
|
|
41
42
|
const pathname = usePathname();
|
|
@@ -56,18 +57,22 @@ export const AutoComplete = ({
|
|
|
56
57
|
const contentLang = useSearchSettingsStore.getState().language;
|
|
57
58
|
const restrictions = searchParams.get("restrict")
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
if (restrictions.includes("informationSubjects")) {
|
|
60
|
+
const mergedScopes: string[] = [];
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
if (restrictions?.includes("informationSubjects")) {
|
|
63
|
+
const informationSubject = restrictions.split("informationSubjects=")[1]?.split(",") || [];
|
|
64
|
+
mergedScopes.push(...informationSubject.map((subject) => `informationSubjects=${subject}`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (pkg != null) {
|
|
68
|
+
mergedScopes.push(pkg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mergedScopes.length > 0) {
|
|
72
|
+
params.scopes = mergedScopes as unknown as string[];
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
if (contentLang) params.lang = contentLang;
|
|
70
|
-
if (pkg != null) params.scopes = pkg as unknown as string[];
|
|
71
76
|
|
|
72
77
|
const results = await suggestionRequest({ endpoint, prefix, queryParams: params, signal });
|
|
73
78
|
|
|
@@ -168,6 +173,7 @@ export const AutoComplete = ({
|
|
|
168
173
|
>
|
|
169
174
|
<InputGroup variant={embedded ? "embedded" : "default"}>
|
|
170
175
|
<InputGroupInput
|
|
176
|
+
ref={inputRef}
|
|
171
177
|
variant={embedded ? "embedded" : undefined}
|
|
172
178
|
className={inputClass}
|
|
173
179
|
type="text"
|
|
@@ -190,7 +196,11 @@ export const AutoComplete = ({
|
|
|
190
196
|
size="icon-xs"
|
|
191
197
|
variant="ghost"
|
|
192
198
|
aria-label={t("clearSearch")}
|
|
193
|
-
onClick={() =>
|
|
199
|
+
onClick={(e) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
setQuery("")
|
|
202
|
+
inputRef.current?.focus();
|
|
203
|
+
}}
|
|
194
204
|
>
|
|
195
205
|
<X className="size-3" />
|
|
196
206
|
</InputGroupButton>
|
|
@@ -217,11 +227,7 @@ export const AutoComplete = ({
|
|
|
217
227
|
<li
|
|
218
228
|
key={option}
|
|
219
229
|
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
220
|
-
onClick={() =>
|
|
221
|
-
//handleSelect(`"${option}"`)
|
|
222
|
-
// TODO: check if the quotes are necessary
|
|
223
|
-
handleSelect(option)
|
|
224
|
-
}}
|
|
230
|
+
onClick={() => handleSelect(option)}
|
|
225
231
|
>
|
|
226
232
|
{option}
|
|
227
233
|
</li>
|
|
@@ -54,6 +54,7 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
|
|
|
54
54
|
isLoadingDetails = false,
|
|
55
55
|
}) => {
|
|
56
56
|
const t = useTranslations();
|
|
57
|
+
const tTypes = useTranslations("itemTypes");
|
|
57
58
|
|
|
58
59
|
return (
|
|
59
60
|
<div
|
|
@@ -92,7 +93,7 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
|
|
|
92
93
|
</span>
|
|
93
94
|
|
|
94
95
|
<div>
|
|
95
|
-
<Badge>{itemType}</Badge>
|
|
96
|
+
<Badge>{tTypes(itemType.toLowerCase() as any) ?? itemType}</Badge>
|
|
96
97
|
</div>
|
|
97
98
|
|
|
98
99
|
<span className="text-sm block">
|
|
@@ -20,17 +20,20 @@ import {
|
|
|
20
20
|
TableRow,
|
|
21
21
|
} from "@c-rex/ui/table";
|
|
22
22
|
import { FaRegBookmark } from "react-icons/fa6";
|
|
23
|
+
import { FaFile } from "react-icons/fa6";
|
|
23
24
|
import { useFavorites } from "./favorites-context";
|
|
24
25
|
|
|
25
26
|
type BookmarkProps = {
|
|
26
27
|
shortId: string;
|
|
27
28
|
linkPattern?: string;
|
|
29
|
+
documentLinkPattern?: string;
|
|
28
30
|
triggerVariant?: ComponentProps<typeof Button>["variant"];
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const BookmarkButton: FC<BookmarkProps> = ({
|
|
32
34
|
shortId,
|
|
33
35
|
linkPattern = `/topics/{shortId}/pages`,
|
|
36
|
+
documentLinkPattern,
|
|
34
37
|
triggerVariant = "outline"
|
|
35
38
|
}) => {
|
|
36
39
|
const t = useTranslations("bookmarks");
|
|
@@ -76,7 +79,23 @@ export const BookmarkButton: FC<BookmarkProps> = ({
|
|
|
76
79
|
</TableRow>
|
|
77
80
|
)}
|
|
78
81
|
|
|
79
|
-
{isHydrated &&
|
|
82
|
+
{isHydrated && documentLinkPattern && documentLabel && (
|
|
83
|
+
<TableRow className="min-h-12">
|
|
84
|
+
<TableCell>
|
|
85
|
+
<FaFile className="text-primary" />
|
|
86
|
+
</TableCell>
|
|
87
|
+
<TableCell colSpan={2}>
|
|
88
|
+
<Link
|
|
89
|
+
href={documentLinkPattern.replace("{shortId}", shortId).replace("{id}", shortId)}
|
|
90
|
+
className="hover:underline font-medium"
|
|
91
|
+
>
|
|
92
|
+
{documentLabel}
|
|
93
|
+
</Link>
|
|
94
|
+
</TableCell>
|
|
95
|
+
</TableRow>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{isHydrated && visibleMarkersCount === 0 && !documentLinkPattern && (
|
|
80
99
|
<TableRow>
|
|
81
100
|
<TableCell colSpan={3} className="text-center">
|
|
82
101
|
{t("empty")}
|
|
@@ -8,12 +8,14 @@ import { MARKER_COLORS, RESULT_TYPES } from "@c-rex/constants";
|
|
|
8
8
|
import { ResultTypes } from "@c-rex/types";
|
|
9
9
|
import { Loader2 } from "lucide-react";
|
|
10
10
|
import { toast } from "sonner";
|
|
11
|
+
import { useTranslations } from "next-intl";
|
|
11
12
|
|
|
12
13
|
export const FavoriteButton: FC<{
|
|
13
14
|
id: string;
|
|
14
15
|
type: ResultTypes;
|
|
15
16
|
label: string;
|
|
16
17
|
}> = ({ id, type, label }) => {
|
|
18
|
+
const t = useTranslations();
|
|
17
19
|
const addFavoriteTopic = useFavorites((state) => state.favoriteTopic);
|
|
18
20
|
const addFavoriteDocument = useFavorites((state) => state.favoriteDocument);
|
|
19
21
|
const removeFavoriteTopic = useFavorites((state) => state.unfavoriteTopic);
|
|
@@ -22,7 +24,12 @@ export const FavoriteButton: FC<{
|
|
|
22
24
|
const favoriteDocumentList = useFavorites((state) => state.documents);
|
|
23
25
|
const favoriteList = useFavorites((state) => state.favorites);
|
|
24
26
|
const isHydrated = useFavorites((state) => state.isHydrated);
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
const isFavoriteInList = Boolean(favoriteList.find((fav) => fav.id === id));
|
|
29
|
+
const isFavoriteViaDocument = type === RESULT_TYPES.TOPIC
|
|
30
|
+
? Object.values(favoriteDocumentList).some((doc) => doc.topics.some((t) => t.id === id))
|
|
31
|
+
: false;
|
|
32
|
+
const isFavorite = isFavoriteInList || isFavoriteViaDocument;
|
|
26
33
|
|
|
27
34
|
const [documentData, setDocumentData] = useState<{ id: string, label: string } | null>(null);
|
|
28
35
|
const [topicData, setTopicData] = useState<{ id: string, label: string } | null>(null);
|
|
@@ -121,7 +128,7 @@ export const FavoriteButton: FC<{
|
|
|
121
128
|
await addFavorite(id);
|
|
122
129
|
}
|
|
123
130
|
} catch {
|
|
124
|
-
toast.error("
|
|
131
|
+
toast.error(t("favoritesUpdateError"));
|
|
125
132
|
} finally {
|
|
126
133
|
setIsLoading(false);
|
|
127
134
|
}
|
|
@@ -135,6 +142,8 @@ export const FavoriteButton: FC<{
|
|
|
135
142
|
size="icon"
|
|
136
143
|
onClick={handleToggle}
|
|
137
144
|
disabled={showLoading}
|
|
145
|
+
aria-label={isFavorite ? t("favorites") : t("favorites")}
|
|
146
|
+
aria-pressed={!showLoading && isFavorite}
|
|
138
147
|
>
|
|
139
148
|
{showLoading ? (
|
|
140
149
|
<Loader2 className="animate-spin" />
|
|
@@ -25,6 +25,7 @@ export const SocialLinksBlock = ({ links, className, ariaLabel }: SocialLinksBlo
|
|
|
25
25
|
target="_blank"
|
|
26
26
|
rel="noreferrer"
|
|
27
27
|
title={link.label}
|
|
28
|
+
aria-label={link.label}
|
|
28
29
|
className="underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
29
30
|
>
|
|
30
31
|
<SocialIcon label={link.label} className="size-7" />
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-generated from OpenAPI spec (Client-Side)
|
|
3
|
-
* Source: https://staging.c-rex.net/ids/api/swagger/v1/swagger.json
|
|
4
|
-
* Generated: 2025-12-11T16:23:03.635Z
|
|
5
|
-
* Do not edit manually
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
"use client";
|
|
9
2
|
|
|
10
3
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
|
|
5
|
+
import { User, Mail, Link as LinkIcon } from "lucide-react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { cn } from "@c-rex/utils";
|
|
8
|
+
|
|
9
|
+
type Author = {
|
|
10
|
+
name: string;
|
|
11
|
+
photo: string | null;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
role?: string | null;
|
|
14
|
+
emails?: string[];
|
|
15
|
+
urls?: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
title: string;
|
|
20
|
+
vcardUrls: string[];
|
|
21
|
+
vcardFields?: string[];
|
|
22
|
+
embedded?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const AuthorAvatar = ({ author }: { author: Author }) => (
|
|
27
|
+
author.photo ? (
|
|
28
|
+
<img
|
|
29
|
+
src={author.photo}
|
|
30
|
+
alt={author.name}
|
|
31
|
+
className="size-10 rounded-full object-cover shrink-0 border border-border"
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<div className="size-10 rounded-full bg-muted shrink-0 flex items-center justify-center border border-border">
|
|
35
|
+
<User className="size-5 text-muted-foreground" />
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const AuthorEntry = ({ author, fields }: { author: Author, fields: string[] }) => {
|
|
41
|
+
const showPhoto = fields.includes("photo");
|
|
42
|
+
const showName = fields.includes("name");
|
|
43
|
+
const showTitle = fields.includes("title");
|
|
44
|
+
const showRole = fields.includes("role");
|
|
45
|
+
const showEmails = fields.includes("emails");
|
|
46
|
+
const showUrls = fields.includes("urls");
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex items-start gap-3 w-full overflow-hidden">
|
|
50
|
+
{showPhoto && <AuthorAvatar author={author} />}
|
|
51
|
+
<div className="flex flex-col gap-1 min-w-0 flex-1">
|
|
52
|
+
{showName && (
|
|
53
|
+
<span className="text-sm font-semibold text-foreground leading-tight truncate">
|
|
54
|
+
{author.name}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{(showTitle && author.title) || (showRole && author.role) ? (
|
|
59
|
+
<span className="text-xs text-muted-foreground leading-tight truncate">
|
|
60
|
+
{(showTitle && author.title) || (showRole && author.role)}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
|
|
64
|
+
<div className="flex flex-col gap-1 mt-1">
|
|
65
|
+
{showEmails && author.emails?.map((email) => (
|
|
66
|
+
<Link
|
|
67
|
+
key={email}
|
|
68
|
+
href={`mailto:${email}`}
|
|
69
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-primary transition-colors min-w-0"
|
|
70
|
+
title={email}
|
|
71
|
+
>
|
|
72
|
+
<Mail className="size-3.5 shrink-0" />
|
|
73
|
+
<span className="truncate">{email}</span>
|
|
74
|
+
</Link>
|
|
75
|
+
))}
|
|
76
|
+
|
|
77
|
+
{showUrls && author.urls?.map((url) => (
|
|
78
|
+
<Link
|
|
79
|
+
key={url}
|
|
80
|
+
href={url}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-primary transition-colors min-w-0"
|
|
84
|
+
title={url}
|
|
85
|
+
>
|
|
86
|
+
<LinkIcon className="size-3.5 shrink-0" />
|
|
87
|
+
<span className="truncate">{url.replace(/^https?:\/\//, "")}</span>
|
|
88
|
+
</Link>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const InformationUnitAuthorsGridClient = ({
|
|
97
|
+
title,
|
|
98
|
+
vcardUrls,
|
|
99
|
+
vcardFields = ["name"],
|
|
100
|
+
embedded = false,
|
|
101
|
+
className,
|
|
102
|
+
}: Props) => {
|
|
103
|
+
const [authors, setAuthors] = useState<Author[]>([]);
|
|
104
|
+
const [loading, setLoading] = useState(true);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!vcardUrls?.length) {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
|
|
114
|
+
Promise.all(
|
|
115
|
+
vcardUrls.map((url) =>
|
|
116
|
+
fetch(url, { signal: controller.signal })
|
|
117
|
+
.then((r) => r.json())
|
|
118
|
+
.then((vcard) => ({
|
|
119
|
+
name: vcard.fullName || "",
|
|
120
|
+
photo: vcard.photos?.[0]?.source || null,
|
|
121
|
+
title: vcard.titles?.[0] || null,
|
|
122
|
+
role: vcard.roles?.[0] || null,
|
|
123
|
+
emails: vcard.emails?.map((e: any) => e.value) || [],
|
|
124
|
+
urls: vcard.urls?.map((u: any) => u.value) || [],
|
|
125
|
+
}))
|
|
126
|
+
.catch((): null => null)
|
|
127
|
+
)
|
|
128
|
+
).then((results) => {
|
|
129
|
+
setAuthors(results.filter(Boolean) as Author[]);
|
|
130
|
+
setLoading(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return () => controller.abort();
|
|
134
|
+
}, [vcardUrls]);
|
|
135
|
+
|
|
136
|
+
if (loading) {
|
|
137
|
+
return (
|
|
138
|
+
<Card className={cn("!gap-0", className)}>
|
|
139
|
+
<CardHeader className="pb-3">
|
|
140
|
+
<div className="h-6 w-24 bg-muted animate-pulse rounded" />
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent className="space-y-4">
|
|
143
|
+
{[1, 2].map((i) => (
|
|
144
|
+
<div key={i} className="flex items-center gap-3 animate-pulse">
|
|
145
|
+
<div className="size-10 rounded-full bg-muted shrink-0" />
|
|
146
|
+
<div className="space-y-2 flex-1">
|
|
147
|
+
<div className="h-4 w-3/4 bg-muted rounded" />
|
|
148
|
+
<div className="h-3 w-1/2 bg-muted rounded" />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
</CardContent>
|
|
153
|
+
</Card>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (authors.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
const content = (
|
|
160
|
+
<div className="flex flex-col gap-5">
|
|
161
|
+
{authors.map((author, i) => (
|
|
162
|
+
<AuthorEntry key={i} author={author} fields={vcardFields} />
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (embedded) {
|
|
168
|
+
return <div className={cn("p-4", className)}>{content}</div>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Card className={cn("!gap-0", className)}>
|
|
173
|
+
<CardHeader className="pb-3">
|
|
174
|
+
<CardTitle className="text-lg font-heading tracking-tight">
|
|
175
|
+
{title}
|
|
176
|
+
</CardTitle>
|
|
177
|
+
</CardHeader>
|
|
178
|
+
<CardContent className="pb-5">
|
|
179
|
+
{content}
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type Props = {
|
|
28
28
|
title: string;
|
|
29
29
|
linkPattern: string;
|
|
30
|
+
bookmarkDocumentLinkPattern?: string;
|
|
30
31
|
data: CommonItemsModel;
|
|
31
32
|
metadataIncludeProperties?: Array<keyof InformationUnitModel>;
|
|
32
33
|
metadataExcludeProperties?: Array<keyof InformationUnitModel>;
|
|
@@ -115,6 +116,7 @@ export const InformationUnitMetadataGridClient = ({
|
|
|
115
116
|
data,
|
|
116
117
|
embedded = false,
|
|
117
118
|
linkPattern,
|
|
119
|
+
bookmarkDocumentLinkPattern,
|
|
118
120
|
metadataIncludeProperties,
|
|
119
121
|
metadataExcludeProperties,
|
|
120
122
|
showBookmarkButton = false,
|
|
@@ -150,7 +152,10 @@ export const InformationUnitMetadataGridClient = ({
|
|
|
150
152
|
<h4 className="text-sm font-medium">{t("favorites")}</h4>
|
|
151
153
|
</TableCell>
|
|
152
154
|
<TableCell className="min-h-12 pt-3 text-xs text-muted-foreground align-top break-words whitespace-normal">
|
|
153
|
-
<BookmarkButton
|
|
155
|
+
<BookmarkButton
|
|
156
|
+
shortId={data.shortId!}
|
|
157
|
+
documentLinkPattern={bookmarkDocumentLinkPattern}
|
|
158
|
+
/>
|
|
154
159
|
</TableCell>
|
|
155
160
|
</TableRow>
|
|
156
161
|
)}
|
|
@@ -211,7 +216,12 @@ export const InformationUnitMetadataGridClient = ({
|
|
|
211
216
|
<CardHeader>
|
|
212
217
|
<CardTitle className="text-lg flex justify-between items-end">
|
|
213
218
|
{title}
|
|
214
|
-
{showBookmarkButton &&
|
|
219
|
+
{showBookmarkButton && (
|
|
220
|
+
<BookmarkButton
|
|
221
|
+
shortId={data.shortId!}
|
|
222
|
+
documentLinkPattern={bookmarkDocumentLinkPattern}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
215
225
|
</CardTitle>
|
|
216
226
|
</CardHeader>
|
|
217
227
|
{cardContent}
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -4,10 +4,8 @@ import { SignInBtn } from "./sign-in-out-btns";
|
|
|
4
4
|
import { getServerSession } from "next-auth";
|
|
5
5
|
import { SettingsMenu } from "./settings";
|
|
6
6
|
import { UserMenu } from "./user-menu";
|
|
7
|
-
import { SearchInput } from "../search-input";
|
|
8
7
|
import { CrexSDK } from "@c-rex/core/sdk";
|
|
9
8
|
import * as AutocompleteOptions from "../generated/suggestions";
|
|
10
|
-
import { cn } from "@c-rex/utils";
|
|
11
9
|
import { getTranslations } from "next-intl/server";
|
|
12
10
|
import { Button } from "@c-rex/ui/button";
|
|
13
11
|
import { DropdownHoverItem } from "@c-rex/ui/dropdown-hover-item";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolveMenuState } from "../restriction-menu-logic";
|
|
2
|
+
|
|
3
|
+
const BASE = { maxItemsByRow: 7, itemCount: 0 };
|
|
4
|
+
|
|
5
|
+
describe("resolveMenuState – restrict construction", () => {
|
|
6
|
+
it("hierarchy mode (components): prepends hasInformationUnits=true when onlyUsedEntries=true", () => {
|
|
7
|
+
const { restrict } = resolveMenuState({ ...BASE, queryRestrict: [], onlyUsedEntries: true });
|
|
8
|
+
expect(restrict).toEqual(["hasInformationUnits=true"]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("flat mode (informationSubjects): prepends hasInformationUnits=true when onlyUsedEntries=true", () => {
|
|
12
|
+
const { restrict } = resolveMenuState({
|
|
13
|
+
...BASE,
|
|
14
|
+
queryRestrict: ["someOtherFilter=x"],
|
|
15
|
+
onlyUsedEntries: true,
|
|
16
|
+
});
|
|
17
|
+
expect(restrict[0]).toBe("hasInformationUnits=true");
|
|
18
|
+
expect(restrict).toContain("someOtherFilter=x");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("omits hasInformationUnits filter when onlyUsedEntries=false", () => {
|
|
22
|
+
const { restrict } = resolveMenuState({ ...BASE, queryRestrict: [], onlyUsedEntries: false });
|
|
23
|
+
expect(restrict).not.toContain("hasInformationUnits=true");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("passes through existing queryRestrict entries when onlyUsedEntries=false", () => {
|
|
27
|
+
const { restrict } = resolveMenuState({
|
|
28
|
+
...BASE,
|
|
29
|
+
queryRestrict: ["category=foo"],
|
|
30
|
+
onlyUsedEntries: false,
|
|
31
|
+
});
|
|
32
|
+
expect(restrict).toEqual(["category=foo"]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("resolveMenuState – resolvedPageSize", () => {
|
|
37
|
+
it("deferred mode, not loaded: uses maxItemsByRow as page size", () => {
|
|
38
|
+
const { resolvedPageSize } = resolveMenuState({
|
|
39
|
+
...BASE,
|
|
40
|
+
maxItemsByRow: 7,
|
|
41
|
+
fetchMode: "deferred",
|
|
42
|
+
loadAll: false,
|
|
43
|
+
});
|
|
44
|
+
expect(resolvedPageSize).toBe(7);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("deferred mode, loadAll=true: uses 100", () => {
|
|
48
|
+
const { resolvedPageSize } = resolveMenuState({
|
|
49
|
+
...BASE,
|
|
50
|
+
maxItemsByRow: 7,
|
|
51
|
+
fetchMode: "deferred",
|
|
52
|
+
loadAll: true,
|
|
53
|
+
});
|
|
54
|
+
expect(resolvedPageSize).toBe(100);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("all mode: uses 100 regardless of loadAll", () => {
|
|
58
|
+
const { resolvedPageSize } = resolveMenuState({
|
|
59
|
+
...BASE,
|
|
60
|
+
maxItemsByRow: 7,
|
|
61
|
+
fetchMode: "all",
|
|
62
|
+
loadAll: false,
|
|
63
|
+
});
|
|
64
|
+
expect(resolvedPageSize).toBe(100);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("explicit pageSize overrides deferred mode", () => {
|
|
68
|
+
const { resolvedPageSize } = resolveMenuState({
|
|
69
|
+
...BASE,
|
|
70
|
+
maxItemsByRow: 7,
|
|
71
|
+
fetchMode: "deferred",
|
|
72
|
+
loadAll: false,
|
|
73
|
+
explicitPageSize: 25,
|
|
74
|
+
});
|
|
75
|
+
expect(resolvedPageSize).toBe(25);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("resolveMenuState – hasMoreItems", () => {
|
|
80
|
+
it("deferred mode: hasMoreItems=true when itemCount equals resolvedPageSize and not loaded", () => {
|
|
81
|
+
const { hasMoreItems } = resolveMenuState({
|
|
82
|
+
...BASE,
|
|
83
|
+
maxItemsByRow: 7,
|
|
84
|
+
fetchMode: "deferred",
|
|
85
|
+
loadAll: false,
|
|
86
|
+
itemCount: 7,
|
|
87
|
+
});
|
|
88
|
+
expect(hasMoreItems).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("deferred mode: hasMoreItems=false after loadAll=true", () => {
|
|
92
|
+
const { hasMoreItems } = resolveMenuState({
|
|
93
|
+
...BASE,
|
|
94
|
+
maxItemsByRow: 7,
|
|
95
|
+
fetchMode: "deferred",
|
|
96
|
+
loadAll: true,
|
|
97
|
+
itemCount: 7,
|
|
98
|
+
});
|
|
99
|
+
expect(hasMoreItems).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("server-side pagination: hasMoreItems=true when totalItemCount > itemCount", () => {
|
|
103
|
+
const { hasMoreItems } = resolveMenuState({
|
|
104
|
+
...BASE,
|
|
105
|
+
maxItemsByRow: 7,
|
|
106
|
+
fetchMode: "deferred",
|
|
107
|
+
loadAll: false,
|
|
108
|
+
itemCount: 7,
|
|
109
|
+
totalItemCount: 20,
|
|
110
|
+
});
|
|
111
|
+
expect(hasMoreItems).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("server-side pagination: hasMoreItems=false when all items loaded", () => {
|
|
115
|
+
const { hasMoreItems } = resolveMenuState({
|
|
116
|
+
...BASE,
|
|
117
|
+
maxItemsByRow: 7,
|
|
118
|
+
fetchMode: "deferred",
|
|
119
|
+
loadAll: false,
|
|
120
|
+
itemCount: 7,
|
|
121
|
+
totalItemCount: 7,
|
|
122
|
+
});
|
|
123
|
+
expect(hasMoreItems).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("explicit pageSize: hasMoreItems driven only by server totalItemCount", () => {
|
|
127
|
+
const noPagination = resolveMenuState({
|
|
128
|
+
...BASE,
|
|
129
|
+
maxItemsByRow: 7,
|
|
130
|
+
fetchMode: "deferred",
|
|
131
|
+
loadAll: false,
|
|
132
|
+
explicitPageSize: 25,
|
|
133
|
+
itemCount: 25,
|
|
134
|
+
});
|
|
135
|
+
expect(noPagination.hasMoreItems).toBe(false);
|
|
136
|
+
|
|
137
|
+
const withMore = resolveMenuState({
|
|
138
|
+
...BASE,
|
|
139
|
+
maxItemsByRow: 7,
|
|
140
|
+
fetchMode: "deferred",
|
|
141
|
+
loadAll: false,
|
|
142
|
+
explicitPageSize: 25,
|
|
143
|
+
itemCount: 25,
|
|
144
|
+
totalItemCount: 50,
|
|
145
|
+
});
|
|
146
|
+
expect(withMore.hasMoreItems).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -31,6 +31,7 @@ export const RestrictionNavigationItem: FC<Props> = ({
|
|
|
31
31
|
history: "push",
|
|
32
32
|
});
|
|
33
33
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
34
|
+
const searchNavigationPending = useSearchNavigationStore((state) => state.pending);
|
|
34
35
|
|
|
35
36
|
const { restrictionValue, shouldRemoveRestrictParam } = getRestrictionValue({
|
|
36
37
|
shortId,
|
|
@@ -49,6 +50,8 @@ export const RestrictionNavigationItem: FC<Props> = ({
|
|
|
49
50
|
<Button
|
|
50
51
|
variant={selected ? "default" : "outline"}
|
|
51
52
|
rounded="full"
|
|
53
|
+
aria-pressed={selected}
|
|
54
|
+
disabled={searchNavigationPending}
|
|
52
55
|
onClick={() => {
|
|
53
56
|
startSearchNavigation();
|
|
54
57
|
if (shouldRemoveRestrictParam) {
|
|
@@ -82,6 +85,7 @@ export const RestrictionDropdownItem: FC<Props> = ({
|
|
|
82
85
|
history: "push",
|
|
83
86
|
});
|
|
84
87
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
88
|
+
const searchNavigationPending = useSearchNavigationStore((state) => state.pending);
|
|
85
89
|
|
|
86
90
|
const { restrictionValue, shouldRemoveRestrictParam } = getRestrictionValue({
|
|
87
91
|
shortId,
|
|
@@ -97,6 +101,8 @@ export const RestrictionDropdownItem: FC<Props> = ({
|
|
|
97
101
|
<TooltipTrigger asChild>
|
|
98
102
|
<Button
|
|
99
103
|
variant={selected ? "default" : "ghost"}
|
|
104
|
+
aria-pressed={selected}
|
|
105
|
+
disabled={searchNavigationPending}
|
|
100
106
|
onClick={() => {
|
|
101
107
|
if (onClick) {
|
|
102
108
|
onClick();
|
|
@@ -141,16 +147,16 @@ function getRestrictionValue({
|
|
|
141
147
|
|
|
142
148
|
if (currentRestrict && multipleSelection) {
|
|
143
149
|
if (selected) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (restrictionsLength === 1) {
|
|
150
|
+
const eqIndex = currentRestrict.indexOf("=");
|
|
151
|
+
if (eqIndex === -1) {
|
|
147
152
|
shouldRemoveRestrictParam = true;
|
|
148
153
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (/^[^=]+=$/.test(restrictParam)) {
|
|
154
|
+
const fieldPrefix = currentRestrict.slice(0, eqIndex + 1);
|
|
155
|
+
const ids = currentRestrict.slice(eqIndex + 1).split(",").filter((id) => id !== shortId);
|
|
156
|
+
if (ids.length === 0) {
|
|
153
157
|
shouldRemoveRestrictParam = true;
|
|
158
|
+
} else {
|
|
159
|
+
restrictParam = fieldPrefix + ids.join(",");
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
} else {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type FetchMode = "all" | "deferred";
|
|
2
|
+
|
|
3
|
+
export type ResolvedMenuState = {
|
|
4
|
+
restrict: string[];
|
|
5
|
+
resolvedPageSize: number;
|
|
6
|
+
hasMoreItems: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function resolveMenuState({
|
|
10
|
+
queryRestrict = [],
|
|
11
|
+
onlyUsedEntries = true,
|
|
12
|
+
fetchMode = "deferred",
|
|
13
|
+
loadAll = false,
|
|
14
|
+
maxItemsByRow,
|
|
15
|
+
explicitPageSize,
|
|
16
|
+
itemCount,
|
|
17
|
+
totalItemCount,
|
|
18
|
+
}: {
|
|
19
|
+
queryRestrict?: string[];
|
|
20
|
+
onlyUsedEntries?: boolean;
|
|
21
|
+
fetchMode?: FetchMode;
|
|
22
|
+
loadAll?: boolean;
|
|
23
|
+
maxItemsByRow: number;
|
|
24
|
+
explicitPageSize?: number;
|
|
25
|
+
itemCount: number;
|
|
26
|
+
totalItemCount?: number;
|
|
27
|
+
}): ResolvedMenuState {
|
|
28
|
+
const restrict = onlyUsedEntries
|
|
29
|
+
? ["hasInformationUnits=true", ...queryRestrict]
|
|
30
|
+
: queryRestrict;
|
|
31
|
+
|
|
32
|
+
const resolvedPageSize = explicitPageSize
|
|
33
|
+
? explicitPageSize
|
|
34
|
+
: fetchMode === "deferred" && !loadAll
|
|
35
|
+
? Math.max(maxItemsByRow, 1)
|
|
36
|
+
: 100;
|
|
37
|
+
|
|
38
|
+
const hasMoreFromServer =
|
|
39
|
+
typeof totalItemCount === "number" ? totalItemCount > itemCount : false;
|
|
40
|
+
const allItemsLoaded =
|
|
41
|
+
typeof totalItemCount === "number" && itemCount >= totalItemCount;
|
|
42
|
+
const hasMoreItems =
|
|
43
|
+
hasMoreFromServer ||
|
|
44
|
+
(!explicitPageSize &&
|
|
45
|
+
!allItemsLoaded &&
|
|
46
|
+
fetchMode === "deferred" &&
|
|
47
|
+
!loadAll &&
|
|
48
|
+
itemCount >= resolvedPageSize);
|
|
49
|
+
|
|
50
|
+
return { restrict, resolvedPageSize, hasMoreItems };
|
|
51
|
+
}
|
|
@@ -6,6 +6,7 @@ import { FC, ReactNode, useMemo, useState } from "react";
|
|
|
6
6
|
import * as ComponentOptions from "../generated/client-components";
|
|
7
7
|
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
8
8
|
import { RestrictionSelectionMenu } from "./restriction-selection-menu";
|
|
9
|
+
import { resolveMenuState } from "./restriction-menu-logic";
|
|
9
10
|
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
10
11
|
|
|
11
12
|
type GenericRequestData = {
|
|
@@ -62,18 +63,21 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
62
63
|
}) => {
|
|
63
64
|
const [loadAll, setLoadAll] = useState(false);
|
|
64
65
|
const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
|
|
65
|
-
const queryRestrict = queryParams?.Restrict || [];
|
|
66
|
-
const restrict = onlyUsedEntries ? ["hasInformationUnits=true", ...queryRestrict] : queryRestrict;
|
|
66
|
+
const queryRestrict = useMemo(() => queryParams?.Restrict || [], [queryParams?.Restrict]);
|
|
67
67
|
const explicitPageSize =
|
|
68
68
|
Number.isFinite(Number(queryParams?.PageSize)) && Number(queryParams?.PageSize) > 0
|
|
69
69
|
? Number(queryParams?.PageSize)
|
|
70
70
|
: undefined;
|
|
71
71
|
const maxItemsByRow = useMemo(() => Math.max(...Object.values(itemsByRow)), [itemsByRow]);
|
|
72
|
-
const resolvedPageSize = useMemo(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
const { restrict, resolvedPageSize } = useMemo(() => resolveMenuState({
|
|
73
|
+
queryRestrict,
|
|
74
|
+
onlyUsedEntries,
|
|
75
|
+
fetchMode,
|
|
76
|
+
loadAll,
|
|
77
|
+
maxItemsByRow,
|
|
78
|
+
explicitPageSize,
|
|
79
|
+
itemCount: 0,
|
|
80
|
+
}), [queryRestrict, onlyUsedEntries, fetchMode, loadAll, maxItemsByRow, explicitPageSize]);
|
|
77
81
|
const requestedFields = Array.isArray(queryParams?.Fields) ? queryParams.Fields : undefined;
|
|
78
82
|
const resolvedFields = useMemo(() => {
|
|
79
83
|
const baseFields = requestedFields && requestedFields.length > 0 ? requestedFields : ["labels"];
|
|
@@ -102,9 +106,16 @@ export const TaxonomyRestrictionMenu: FC<TaxonomyRestrictionMenuProps> = ({
|
|
|
102
106
|
if (!data) return null;
|
|
103
107
|
|
|
104
108
|
const itemCount = data.items?.length || 0;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const { hasMoreItems } = resolveMenuState({
|
|
110
|
+
queryRestrict,
|
|
111
|
+
onlyUsedEntries,
|
|
112
|
+
fetchMode,
|
|
113
|
+
loadAll,
|
|
114
|
+
maxItemsByRow,
|
|
115
|
+
explicitPageSize,
|
|
116
|
+
itemCount,
|
|
117
|
+
totalItemCount: data.pageInfo?.totalItemCount,
|
|
118
|
+
});
|
|
108
119
|
|
|
109
120
|
return (
|
|
110
121
|
<RestrictionSelectionMenu
|
|
@@ -47,6 +47,7 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
|
|
|
47
47
|
const { setIsMobileFiltersOpen } = useFilterSidebarState();
|
|
48
48
|
const restrictionList = useRestrictionStore((state) => state.restrictionList);
|
|
49
49
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
50
|
+
const searchNavigationPending = useSearchNavigationStore((state) => state.pending);
|
|
50
51
|
const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
|
|
51
52
|
const [params, setParams] = useQueryStates({
|
|
52
53
|
language: parseAsString.withDefault(useSearchSettingsStore.getState().language || ''),
|
|
@@ -207,7 +208,7 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
return (
|
|
210
|
-
<div className=
|
|
211
|
+
<div className={`pb-4 flex flex-wrap gap-1 transition-opacity duration-150${searchNavigationPending ? " opacity-60 pointer-events-none" : ""}`}>
|
|
211
212
|
{hasMobileFilters && (
|
|
212
213
|
<Button
|
|
213
214
|
size="sm"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { FC,
|
|
3
|
+
import { FC, useMemo, useState } from "react";
|
|
4
4
|
import { useLocale, useTranslations } from 'next-intl'
|
|
5
|
-
import { Check, ChevronDown, ChevronRight } from "lucide-react"
|
|
5
|
+
import { Check, ChevronDown, ChevronRight, Loader2 } from "lucide-react"
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
|
|
7
7
|
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
8
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@c-rex/ui/sheet";
|
|
@@ -76,6 +76,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
76
76
|
shallow: false,
|
|
77
77
|
});
|
|
78
78
|
const startSearchNavigation = useSearchNavigationStore((state) => state.start);
|
|
79
|
+
const searchNavigationPending = useSearchNavigationStore((state) => state.pending);
|
|
79
80
|
const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
|
|
80
81
|
|
|
81
82
|
const filteredTags = useMemo(() => {
|
|
@@ -105,6 +106,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
const onClickHandler = (key: string, item: FilterItem) => {
|
|
109
|
+
if (!item.active && item.hits === 0) return;
|
|
108
110
|
startSearchNavigation();
|
|
109
111
|
if (item.active) {
|
|
110
112
|
setParams(removeFilterItem(key, item, params.filter))
|
|
@@ -171,20 +173,24 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
171
173
|
|
|
172
174
|
return (
|
|
173
175
|
<>
|
|
174
|
-
{visibleItems.map((item) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
176
|
+
{visibleItems.map((item) => {
|
|
177
|
+
const isDisabled = !item.active && item.hits === 0;
|
|
178
|
+
return (
|
|
179
|
+
<SidebarMenuSubItem key={item.shortId}>
|
|
180
|
+
<SidebarMenuSubButton
|
|
181
|
+
className={isDisabled ? "!py-1 opacity-40 cursor-not-allowed" : "cursor-pointer !py-1"}
|
|
182
|
+
data-taxonomy-id={item.taxonomyId}
|
|
183
|
+
data-taxonomy-short-id={item.shortId}
|
|
184
|
+
isActive={item.active}
|
|
185
|
+
aria-disabled={isDisabled}
|
|
186
|
+
onClick={() => onClickHandler(key, item)}
|
|
187
|
+
>
|
|
188
|
+
{item.label} ({item.hits}/{item.total})
|
|
189
|
+
{item.active && <Check className="ml-2" />}
|
|
190
|
+
</SidebarMenuSubButton>
|
|
191
|
+
</SidebarMenuSubItem>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
188
194
|
{(hasMoreItems || canShowLess) && (
|
|
189
195
|
<SidebarMenuSubItem>
|
|
190
196
|
<div className="flex items-center gap-3 px-2">
|
|
@@ -222,20 +228,24 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
222
228
|
|
|
223
229
|
return (
|
|
224
230
|
<>
|
|
225
|
-
{visibleItems.map((item) =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
{visibleItems.map((item) => {
|
|
232
|
+
const isDisabled = !item.active && item.hits === 0;
|
|
233
|
+
return (
|
|
234
|
+
<SidebarMenuSubItem key={item.shortId}>
|
|
235
|
+
<SidebarMenuSubButton
|
|
236
|
+
className={isDisabled ? "!py-1 opacity-40 cursor-not-allowed" : "cursor-pointer !py-1"}
|
|
237
|
+
data-taxonomy-id={item.taxonomyId}
|
|
238
|
+
data-taxonomy-short-id={item.shortId}
|
|
239
|
+
isActive={item.active}
|
|
240
|
+
aria-disabled={isDisabled}
|
|
241
|
+
onClick={() => onClickHandler(facetKey, item)}
|
|
242
|
+
>
|
|
243
|
+
{item.label} ({item.hits}/{item.total})
|
|
244
|
+
{item.active && <Check className="ml-2" />}
|
|
245
|
+
</SidebarMenuSubButton>
|
|
246
|
+
</SidebarMenuSubItem>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
239
249
|
{(hasMoreItems || canShowLess) && (
|
|
240
250
|
<SidebarMenuSubItem>
|
|
241
251
|
<div className="flex items-center gap-3 px-2">
|
|
@@ -298,7 +308,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
298
308
|
<span className={toggleSlotClass} aria-hidden="true" />
|
|
299
309
|
)}
|
|
300
310
|
<SidebarMenuSubButton
|
|
301
|
-
className={item.isStructural ? "!py-1
|
|
311
|
+
className={item.isStructural ? "!py-1 cursor-default text-muted-foreground" : "cursor-pointer !py-1"}
|
|
302
312
|
data-taxonomy-id={item.taxonomyId}
|
|
303
313
|
data-taxonomy-short-id={item.shortId}
|
|
304
314
|
isActive={item.active}
|
|
@@ -320,7 +330,7 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
320
330
|
};
|
|
321
331
|
|
|
322
332
|
const content = (
|
|
323
|
-
<SidebarContent className=
|
|
333
|
+
<SidebarContent className={`!gap-1.5 p-1.5 capitalize${searchNavigationPending ? " opacity-60 pointer-events-none" : ""}`} suppressHydrationWarning>
|
|
324
334
|
{Object.entries(filteredTags).map(([key, value]) => {
|
|
325
335
|
const items = value as ResolvedFilterItem[];
|
|
326
336
|
const hasTaxonomy = Boolean(resolveFacetTaxonomy(key, facetTaxonomies));
|
|
@@ -337,21 +347,21 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
337
347
|
<div
|
|
338
348
|
key={key}
|
|
339
349
|
data-filter-group-key={key}
|
|
340
|
-
className="
|
|
350
|
+
className="rounded-md border border-border/40 overflow-hidden bg-background/50"
|
|
341
351
|
>
|
|
342
352
|
{!hasSectionLabels && (
|
|
343
353
|
<Collapsible defaultOpen className="py-0 group/collapsible">
|
|
344
|
-
<SidebarGroup className="!p-
|
|
345
|
-
<SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-
|
|
346
|
-
<CollapsibleTrigger className="!h-
|
|
354
|
+
<SidebarGroup className="!p-0">
|
|
355
|
+
<SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-xs font-bold bg-muted/30">
|
|
356
|
+
<CollapsibleTrigger className="!h-7 capitalize rounded-none px-2">
|
|
347
357
|
{propertyLabel}
|
|
348
358
|
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
|
349
359
|
</CollapsibleTrigger>
|
|
350
360
|
</SidebarGroupLabel>
|
|
351
361
|
<CollapsibleContent>
|
|
352
|
-
<SidebarGroupContent>
|
|
353
|
-
<SidebarMenu className="!gap-0
|
|
354
|
-
<SidebarMenuSub className="!ml-
|
|
362
|
+
<SidebarGroupContent className="pb-1">
|
|
363
|
+
<SidebarMenu className="!gap-0">
|
|
364
|
+
<SidebarMenuSub className="!ml-1.5 !pl-1 !py-0.5 !gap-0">
|
|
355
365
|
{!hierarchicalFacet && renderFacetItems(key, items)}
|
|
356
366
|
{hierarchicalFacet && visibleRoots.map((item) => renderHierarchyNode(key, item, hierarchy!.children, 0))}
|
|
357
367
|
{hierarchicalFacet && (hasMoreRoots || canShowLessRoots) && (
|
|
@@ -411,22 +421,22 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
411
421
|
<Collapsible
|
|
412
422
|
defaultOpen
|
|
413
423
|
key={`${key}:${sectionLabel}`}
|
|
414
|
-
className="py-0 group/collapsible"
|
|
424
|
+
className="py-0 group/collapsible border-t border-border/30 first:border-t-0"
|
|
415
425
|
>
|
|
416
|
-
<SidebarGroup className="!p-
|
|
426
|
+
<SidebarGroup className="!p-0">
|
|
417
427
|
<SidebarGroupLabel
|
|
418
428
|
asChild
|
|
419
|
-
className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-
|
|
429
|
+
className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-xs font-bold bg-muted/30"
|
|
420
430
|
>
|
|
421
|
-
<CollapsibleTrigger className="!h-
|
|
431
|
+
<CollapsibleTrigger className="!h-7 rounded-none px-2">
|
|
422
432
|
{sectionLabel}
|
|
423
433
|
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
|
424
434
|
</CollapsibleTrigger>
|
|
425
435
|
</SidebarGroupLabel>
|
|
426
436
|
<CollapsibleContent>
|
|
427
|
-
<SidebarGroupContent>
|
|
428
|
-
<SidebarMenu className="!gap-0
|
|
429
|
-
<SidebarMenuSub className="!ml-
|
|
437
|
+
<SidebarGroupContent className="pb-1">
|
|
438
|
+
<SidebarMenu className="!gap-0">
|
|
439
|
+
<SidebarMenuSub className="!ml-1.5 !pl-1 !py-0.5 !gap-0">
|
|
430
440
|
{shouldRenderSectionHierarchy && !hasNestedGroups &&
|
|
431
441
|
visibleSectionRoots.map((item) => renderHierarchyNode(key, item, sectionHierarchy!.children, 0))}
|
|
432
442
|
{shouldRenderSectionHierarchy && !hasNestedGroups && (hasMoreSectionRoots || canShowLessSectionRoots) && (
|
|
@@ -500,7 +510,10 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
500
510
|
className="w-[calc(100vw-2rem)] max-w-sm overflow-y-auto !px-2 !pt-6 lg:hidden"
|
|
501
511
|
>
|
|
502
512
|
<SheetHeader className="justify-center items-end font-bold">
|
|
503
|
-
<SheetTitle
|
|
513
|
+
<SheetTitle className="flex items-center gap-2">
|
|
514
|
+
{t("filter.filters")}
|
|
515
|
+
{searchNavigationPending && <Loader2 className="size-3.5 animate-spin text-muted-foreground" />}
|
|
516
|
+
</SheetTitle>
|
|
504
517
|
<span className="text-xs text-muted-foreground leading-5">
|
|
505
518
|
{totalItemCount} {t("results.results")}
|
|
506
519
|
</span>
|
|
@@ -509,9 +522,12 @@ const FilterSidebar: FC<FilterSidebarProps> = ({
|
|
|
509
522
|
</SheetContent>
|
|
510
523
|
</Sheet>
|
|
511
524
|
|
|
512
|
-
<div className="hidden w-
|
|
525
|
+
<div className="hidden w-56 rounded-md border bg-sidebar pb-2 lg:block lg:w-72">
|
|
513
526
|
<SidebarHeader className="justify-center items-end font-bold">
|
|
514
|
-
|
|
527
|
+
<span className="flex items-center gap-2">
|
|
528
|
+
{t("filter.filters")}
|
|
529
|
+
{searchNavigationPending && <Loader2 className="size-3.5 animate-spin text-muted-foreground" />}
|
|
530
|
+
</span>
|
|
515
531
|
<span className="text-xs text-muted-foreground leading-5">
|
|
516
532
|
{totalItemCount} {t("results.results")}
|
|
517
533
|
</span>
|
|
@@ -184,7 +184,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
|
|
|
184
184
|
excludeProperties={facetExcludeProperties}
|
|
185
185
|
/>
|
|
186
186
|
|
|
187
|
-
<div className=
|
|
187
|
+
<div className={`flex-1 flex flex-col gap-4 transition-opacity duration-150${isLoading ? " opacity-60 pointer-events-none" : ""}`}>
|
|
188
188
|
{Number(data.pageInfo.pageCount) > 1 && (
|
|
189
189
|
<Pagination pageInfo={data.pageInfo} className="pt-0" />
|
|
190
190
|
)}
|
|
@@ -34,6 +34,7 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
|
|
|
34
34
|
descriptionFragmentSubjectIds = [],
|
|
35
35
|
}) => {
|
|
36
36
|
const t = useTranslations();
|
|
37
|
+
const tTypes = useTranslations("itemTypes");
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<div className="flex-1">
|
|
@@ -91,7 +92,7 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
|
|
|
91
92
|
</span>
|
|
92
93
|
|
|
93
94
|
<div>
|
|
94
|
-
<Badge>{itemType}</Badge>
|
|
95
|
+
<Badge>{tTypes(itemType.toLowerCase() as any) ?? itemType}</Badge>
|
|
95
96
|
</div>
|
|
96
97
|
<span className="text-sm">
|
|
97
98
|
<HtmlRendition
|
|
@@ -5,7 +5,7 @@ import { FC, useEffect, useRef, useState } from "react";
|
|
|
5
5
|
import Link from "next/link";
|
|
6
6
|
import { cn, formatDateToLocale } from "@c-rex/utils";
|
|
7
7
|
import { Badge } from "@c-rex/ui/badge";
|
|
8
|
-
import { useLocale } from "next-intl";
|
|
8
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
9
9
|
import { TopicsResponseItem } from "@c-rex/interfaces";
|
|
10
10
|
import { Card } from "@c-rex/ui/card";
|
|
11
11
|
import { useQueryState } from "nuqs";
|
|
@@ -28,6 +28,7 @@ const InformationUnitSearchResultCard: FC<{
|
|
|
28
28
|
index: number;
|
|
29
29
|
query: string | null;
|
|
30
30
|
}> = ({ item, index, query }) => {
|
|
31
|
+
const t = useTranslations();
|
|
31
32
|
const locale = useLocale();
|
|
32
33
|
const date = formatDateToLocale(item.created!, locale);
|
|
33
34
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
@@ -170,8 +171,8 @@ const InformationUnitSearchResultCard: FC<{
|
|
|
170
171
|
</div>
|
|
171
172
|
|
|
172
173
|
{!item.disabled && (
|
|
173
|
-
<Link href={item.link} className="absolute inset-0">
|
|
174
|
-
<span className="sr-only">
|
|
174
|
+
<Link href={item.link} className="absolute inset-0" aria-label={t("results.viewArticle")}>
|
|
175
|
+
<span className="sr-only">{t("results.viewArticle")}</span>
|
|
175
176
|
</Link>
|
|
176
177
|
)}
|
|
177
178
|
</Card>
|
|
@@ -18,7 +18,9 @@ const IconsToFileExtension: Record<string, React.ReactNode> = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const InformationUnitSearchResultsTable: FC<InformationUnitSearchResultsTableProps> = ({ items }) => {
|
|
21
|
-
const t = useTranslations("results")
|
|
21
|
+
const t = useTranslations("results");
|
|
22
|
+
const tRoot = useTranslations();
|
|
23
|
+
const tTypes = useTranslations("itemTypes");
|
|
22
24
|
const [query] = useQueryState("search");
|
|
23
25
|
|
|
24
26
|
|
|
@@ -55,7 +57,7 @@ const InformationUnitSearchResultsTable: FC<InformationUnitSearchResultsTablePro
|
|
|
55
57
|
|
|
56
58
|
<div className="w-4/5 md:w-1/5 p-2">
|
|
57
59
|
<Badge variant="secondary">
|
|
58
|
-
{item.type}
|
|
60
|
+
{tTypes(item.type.toLowerCase() as any) ?? item.type}
|
|
59
61
|
</Badge>
|
|
60
62
|
</div>
|
|
61
63
|
|
|
@@ -78,12 +80,12 @@ const InformationUnitSearchResultsTable: FC<InformationUnitSearchResultsTablePro
|
|
|
78
80
|
<DropdownMenuContent>
|
|
79
81
|
<DropdownMenuItem>
|
|
80
82
|
<a href={item.files[fileKey].view} target="_blank" rel="noreferrer" className="flex items-center">
|
|
81
|
-
<Eye className="mr-2" />
|
|
83
|
+
<Eye className="mr-2" /> {tRoot("open")}
|
|
82
84
|
</a>
|
|
83
85
|
</DropdownMenuItem>
|
|
84
86
|
<DropdownMenuItem>
|
|
85
87
|
<a href={item.files[fileKey].download} target="_blank" rel="noreferrer" className="flex items-center">
|
|
86
|
-
<CloudDownload className="mr-2" />
|
|
88
|
+
<CloudDownload className="mr-2" /> {tRoot("download")}
|
|
87
89
|
</a>
|
|
88
90
|
</DropdownMenuItem>
|
|
89
91
|
</DropdownMenuContent>
|
|
@@ -14,6 +14,7 @@ import { usePathname, useSearchParams } from "next/navigation";
|
|
|
14
14
|
import { ResultContainerPageInfoModel } from "@c-rex/interfaces";
|
|
15
15
|
import { useTranslations } from "next-intl";
|
|
16
16
|
import { cn } from "@c-rex/utils";
|
|
17
|
+
import { useSearchNavigationStore } from "../stores/search-navigation-store";
|
|
17
18
|
|
|
18
19
|
interface PaginationProps {
|
|
19
20
|
pageInfo: ResultContainerPageInfoModel;
|
|
@@ -67,6 +68,7 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
|
67
68
|
const t = useTranslations("results");
|
|
68
69
|
const pathname = usePathname();
|
|
69
70
|
const searchParams = useSearchParams();
|
|
71
|
+
const searchNavigationPending = useSearchNavigationStore((state) => state.pending);
|
|
70
72
|
|
|
71
73
|
const pageNumber = pageInfo.pageNumber || 1;
|
|
72
74
|
const pageCount = pageInfo.pageCount || 1;
|
|
@@ -78,7 +80,7 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
|
78
80
|
const search = currentSearch.length > 0 ? `?${currentSearch}` : "";
|
|
79
81
|
|
|
80
82
|
return (
|
|
81
|
-
<div className={cn("flex flex-col gap-3 py-4", className)}>
|
|
83
|
+
<div className={cn("flex flex-col gap-3 py-4 transition-opacity duration-150", searchNavigationPending ? "opacity-60 pointer-events-none" : "", className)}>
|
|
82
84
|
<PaginationUI className="py-4 items-center justify-center sm:justify-between">
|
|
83
85
|
<span className="hidden sm:block text-sm text-muted-foreground">
|
|
84
86
|
{t("paginationResults", {
|
|
@@ -25,8 +25,6 @@ export type SearchSettingsState = {
|
|
|
25
25
|
wildcard: WildCardType,
|
|
26
26
|
operator: OperatorType,
|
|
27
27
|
like: boolean,
|
|
28
|
-
facetExcludeProperties?: string[],
|
|
29
|
-
metadataExcludeProperties?: string[],
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export type SearchSettingsStore = SearchSettingsState & {
|
|
@@ -38,8 +36,6 @@ export const defaultSearchSettings: SearchSettingsState = {
|
|
|
38
36
|
wildcard: WILD_CARD_OPTIONS.BOTH,
|
|
39
37
|
operator: OPERATOR_OPTIONS.OR,
|
|
40
38
|
like: false,
|
|
41
|
-
facetExcludeProperties: [],
|
|
42
|
-
metadataExcludeProperties: [],
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
/**
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { persist, createJSONStorage, StateStorage } from "zustand/middleware";
|
|
3
|
+
|
|
4
|
+
const cookieStorage: StateStorage = {
|
|
5
|
+
getItem: (name: string): string | null => {
|
|
6
|
+
if (typeof document === "undefined") return null;
|
|
7
|
+
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
|
|
8
|
+
return match ? decodeURIComponent(match[2] as string) : null;
|
|
9
|
+
},
|
|
10
|
+
setItem: (name: string, value: string): void => {
|
|
11
|
+
if (typeof document === "undefined") return;
|
|
12
|
+
const maxAge = 60 * 60 * 24 * 365;
|
|
13
|
+
document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;
|
|
14
|
+
},
|
|
15
|
+
removeItem: (name: string): void => {
|
|
16
|
+
if (typeof document === "undefined") return;
|
|
17
|
+
document.cookie = `${name}=;path=/;max-age=0`;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type UiPreferencesState = {
|
|
22
|
+
facetExcludeProperties: string[];
|
|
23
|
+
metadataExcludeProperties: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UiPreferencesStore = UiPreferencesState & {
|
|
27
|
+
updatePreferences: (prefs: Partial<UiPreferencesState>) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const defaultUiPreferences: UiPreferencesState = {
|
|
31
|
+
facetExcludeProperties: [],
|
|
32
|
+
metadataExcludeProperties: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function readLegacyValues(): Partial<UiPreferencesState> {
|
|
36
|
+
if (typeof document === "undefined") return {};
|
|
37
|
+
try {
|
|
38
|
+
const match = document.cookie.match(new RegExp(`(^| )c-rex-search-settings=([^;]+)`));
|
|
39
|
+
if (!match) return {};
|
|
40
|
+
const parsed = JSON.parse(decodeURIComponent(match[2] as string));
|
|
41
|
+
const state = parsed?.state || {};
|
|
42
|
+
const result: Partial<UiPreferencesState> = {};
|
|
43
|
+
if (Array.isArray(state.facetExcludeProperties)) {
|
|
44
|
+
result.facetExcludeProperties = state.facetExcludeProperties;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(state.metadataExcludeProperties)) {
|
|
47
|
+
result.metadataExcludeProperties = state.metadataExcludeProperties;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const useUiPreferencesStore = create<UiPreferencesStore>()(
|
|
56
|
+
persist(
|
|
57
|
+
(set, get) => ({
|
|
58
|
+
...defaultUiPreferences,
|
|
59
|
+
updatePreferences: (prefs) => set((state) => ({ ...state, ...prefs })),
|
|
60
|
+
}),
|
|
61
|
+
{
|
|
62
|
+
name: "c-rex-ui-preferences",
|
|
63
|
+
storage: createJSONStorage(() => cookieStorage),
|
|
64
|
+
onRehydrateStorage: () => (state) => {
|
|
65
|
+
if (!state) return;
|
|
66
|
+
const isNew =
|
|
67
|
+
state.facetExcludeProperties.length === 0 &&
|
|
68
|
+
state.metadataExcludeProperties.length === 0;
|
|
69
|
+
if (isNew) {
|
|
70
|
+
const legacy = readLegacyValues();
|
|
71
|
+
if (Object.keys(legacy).length > 0) {
|
|
72
|
+
state.updatePreferences(legacy);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read UI preferences from cookie on the server.
|
|
82
|
+
* Falls back to legacy `c-rex-search-settings` cookie for migration.
|
|
83
|
+
*/
|
|
84
|
+
export function getUiPreferencesFromCookie(
|
|
85
|
+
newCookieValue: string | undefined,
|
|
86
|
+
legacyCookieValue?: string | undefined
|
|
87
|
+
): UiPreferencesState {
|
|
88
|
+
if (newCookieValue) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(newCookieValue);
|
|
91
|
+
const state = parsed?.state || {};
|
|
92
|
+
const result: UiPreferencesState = { ...defaultUiPreferences };
|
|
93
|
+
if (Array.isArray(state.facetExcludeProperties)) {
|
|
94
|
+
result.facetExcludeProperties = state.facetExcludeProperties;
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(state.metadataExcludeProperties)) {
|
|
97
|
+
result.metadataExcludeProperties = state.metadataExcludeProperties;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
result.facetExcludeProperties.length === 0 &&
|
|
101
|
+
result.metadataExcludeProperties.length === 0 &&
|
|
102
|
+
legacyCookieValue
|
|
103
|
+
) {
|
|
104
|
+
return migrateFromLegacy(legacyCookieValue);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
} catch {
|
|
108
|
+
// fall through to legacy
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (legacyCookieValue) {
|
|
112
|
+
return migrateFromLegacy(legacyCookieValue);
|
|
113
|
+
}
|
|
114
|
+
return { ...defaultUiPreferences };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function migrateFromLegacy(cookieValue: string): UiPreferencesState {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(cookieValue);
|
|
120
|
+
const state = parsed?.state || {};
|
|
121
|
+
return {
|
|
122
|
+
facetExcludeProperties: Array.isArray(state.facetExcludeProperties) ? state.facetExcludeProperties : [],
|
|
123
|
+
metadataExcludeProperties: Array.isArray(state.metadataExcludeProperties) ? state.metadataExcludeProperties : [],
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return { ...defaultUiPreferences };
|
|
127
|
+
}
|
|
128
|
+
}
|