@c-rex/components 0.3.0-build.40 → 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/favorites/favorites-context.tsx +4 -0
- 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-hierarchy.ts +1 -1
- 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 +5 -4
- 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
|
@@ -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);
|
|
@@ -67,7 +68,7 @@ const InformationUnitSearchResultCard: FC<{
|
|
|
67
68
|
fetch(url)
|
|
68
69
|
.then(r => r.json())
|
|
69
70
|
.then(vcard => ({ name: vcard.fullName || "", photo: vcard.photos?.[0]?.source || null }))
|
|
70
|
-
.catch(() => null)
|
|
71
|
+
.catch((): { name: string; photo: string | null } | null => null)
|
|
71
72
|
)
|
|
72
73
|
).then(results => {
|
|
73
74
|
setCardData(prev => ({
|
|
@@ -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
|
/**
|