@c-rex/components 0.3.0-build.28 → 0.3.0-build.30
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 +34 -2
- package/src/carousel/carousel.tsx +70 -74
- package/src/carousel/information-unit-carousel-item.tsx +85 -0
- package/src/directoryNodes/directory-tree-context.tsx +3 -1
- package/src/documents/description-preview.tsx +132 -0
- package/src/documents/result-list-item.tsx +256 -0
- package/src/documents/result-list.tsx +13 -132
- package/src/info/information-unit-fragment-ids.ts +28 -0
- package/src/info/information-unit-metadata-grid-client.tsx +368 -0
- package/src/info/information-unit-preview-image.tsx +77 -0
- package/src/page-wrapper.tsx +5 -5
- package/src/renditions/file-download.tsx +5 -3
- package/src/renditions/html-client.tsx +99 -0
- package/src/renditions/image/container.tsx +19 -13
- package/src/renditions/image/rendition.tsx +6 -1
- package/src/restriction-menu/restriction-menu-container.tsx +4 -117
- package/src/restriction-menu/restriction-menu.tsx +4 -381
- package/src/restriction-menu/restriction-selection-menu.tsx +383 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +114 -0
- package/src/results/generic/search-results-client.tsx +258 -0
- package/src/results/generic/table-result-list.tsx +5 -3
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
|
|
7
|
+
import { Table, TableBody, TableCell, TableRow } from "@c-rex/ui/table";
|
|
8
|
+
import { Flag } from "../icons/flag-icon";
|
|
9
|
+
import { BookmarkButton } from "../favorites/bookmark-button";
|
|
10
|
+
import { renderMetadataDisplayValues } from "./shared";
|
|
11
|
+
import { InformationUnitsGetAllClient } from "../generated/client-components";
|
|
12
|
+
import type {
|
|
13
|
+
CommonItemsModel,
|
|
14
|
+
InformationUnitModel,
|
|
15
|
+
LiteralModel,
|
|
16
|
+
ObjectRefModel,
|
|
17
|
+
RenditionModel,
|
|
18
|
+
} from "@c-rex/interfaces";
|
|
19
|
+
import {
|
|
20
|
+
INFORMATION_UNIT_PROPERTY_PRESENTATION,
|
|
21
|
+
type InformationUnitPropertyKey,
|
|
22
|
+
} from "@c-rex/services/metadata-presentation-config";
|
|
23
|
+
import { resolveMetadataDisplayProperties } from "@c-rex/services/metadata-view-profile";
|
|
24
|
+
import {
|
|
25
|
+
extractCountryCodeFromLanguage,
|
|
26
|
+
getFileRenditionGroups,
|
|
27
|
+
resolvePreferredLanguage,
|
|
28
|
+
sortAndDeduplicateLanguages,
|
|
29
|
+
} from "@c-rex/utils";
|
|
30
|
+
|
|
31
|
+
type MetadataDisplayRow = {
|
|
32
|
+
key: InformationUnitPropertyKey;
|
|
33
|
+
label: string;
|
|
34
|
+
labelSource: "translationKey" | "direct";
|
|
35
|
+
values: string[];
|
|
36
|
+
valueType?: "text" | "language" | "rendition";
|
|
37
|
+
renditions?: RenditionModel[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Props = {
|
|
41
|
+
title: string;
|
|
42
|
+
linkPattern: string;
|
|
43
|
+
data: CommonItemsModel;
|
|
44
|
+
metadataIncludeProperties?: Array<keyof InformationUnitModel>;
|
|
45
|
+
metadataExcludeProperties?: Array<keyof InformationUnitModel>;
|
|
46
|
+
showBookmarkButton?: boolean;
|
|
47
|
+
showFileRenditions?: boolean;
|
|
48
|
+
embedded?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const resolveLiteralLabel = (labels?: LiteralModel[] | null, uiLanguage?: string): string | undefined => {
|
|
52
|
+
if (!labels || labels.length === 0) return undefined;
|
|
53
|
+
const language = uiLanguage || "en";
|
|
54
|
+
const preferred = resolvePreferredLanguage(
|
|
55
|
+
labels.map((item) => item.language || ""),
|
|
56
|
+
language
|
|
57
|
+
);
|
|
58
|
+
if (preferred) {
|
|
59
|
+
const exact = labels.find((item) => item.language === preferred)?.value;
|
|
60
|
+
if (exact) return exact;
|
|
61
|
+
}
|
|
62
|
+
return labels.find((item) => item.value)?.value || undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const resolveObjectRefLabel = (item: ObjectRefModel, uiLanguage: string): string | undefined => {
|
|
66
|
+
return resolveLiteralLabel(item.labels || [], uiLanguage) || item.shortId || item.id || undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const asObjectRefArray = (value: unknown): ObjectRefModel[] => {
|
|
70
|
+
if (!Array.isArray(value)) return [];
|
|
71
|
+
return value.filter((item): item is ObjectRefModel => Boolean(item && typeof item === "object"));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const asLiteralArray = (value: unknown): LiteralModel[] => {
|
|
75
|
+
if (!Array.isArray(value)) return [];
|
|
76
|
+
return value.filter((item): item is LiteralModel => Boolean(item && typeof item === "object"));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const buildMetadataDisplayRows = (
|
|
80
|
+
data: CommonItemsModel,
|
|
81
|
+
uiLanguage: string,
|
|
82
|
+
includeProperties: InformationUnitPropertyKey[]
|
|
83
|
+
): MetadataDisplayRow[] => {
|
|
84
|
+
const preferredTitle = resolveLiteralLabel(data.titles || [], uiLanguage) || resolveLiteralLabel(data.labels || [], uiLanguage);
|
|
85
|
+
const rows: MetadataDisplayRow[] = [];
|
|
86
|
+
|
|
87
|
+
for (const key of includeProperties) {
|
|
88
|
+
const config = INFORMATION_UNIT_PROPERTY_PRESENTATION[key];
|
|
89
|
+
if (!config?.metadataDisplay?.supported) continue;
|
|
90
|
+
|
|
91
|
+
if (key === "labels") continue;
|
|
92
|
+
|
|
93
|
+
if (key === "titles") {
|
|
94
|
+
if (preferredTitle) {
|
|
95
|
+
rows.push({
|
|
96
|
+
key,
|
|
97
|
+
label: key,
|
|
98
|
+
labelSource: "translationKey",
|
|
99
|
+
values: [preferredTitle],
|
|
100
|
+
valueType: "text",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const value = (data as InformationUnitModel)[key];
|
|
107
|
+
if (value == null) continue;
|
|
108
|
+
|
|
109
|
+
if (key === "languages") {
|
|
110
|
+
const languages = sortAndDeduplicateLanguages((value as string[]) || []);
|
|
111
|
+
if (languages.length > 0) {
|
|
112
|
+
rows.push({
|
|
113
|
+
key,
|
|
114
|
+
label: key,
|
|
115
|
+
labelSource: "translationKey",
|
|
116
|
+
values: languages,
|
|
117
|
+
valueType: "language",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.valueKind === "scalar") {
|
|
124
|
+
rows.push({
|
|
125
|
+
key,
|
|
126
|
+
label: key,
|
|
127
|
+
labelSource: "translationKey",
|
|
128
|
+
values: [String(value)],
|
|
129
|
+
valueType: "text",
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (config.valueKind === "stringArray") {
|
|
135
|
+
const values = Array.from(new Set((value as string[]).map((item) => String(item)).filter(Boolean)));
|
|
136
|
+
if (values.length > 0) {
|
|
137
|
+
rows.push({
|
|
138
|
+
key,
|
|
139
|
+
label: key,
|
|
140
|
+
labelSource: "translationKey",
|
|
141
|
+
values,
|
|
142
|
+
valueType: "text",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (config.valueKind === "literalArray") {
|
|
149
|
+
const preferred = resolveLiteralLabel(asLiteralArray(value), uiLanguage);
|
|
150
|
+
if (preferred) {
|
|
151
|
+
rows.push({
|
|
152
|
+
key,
|
|
153
|
+
label: key,
|
|
154
|
+
labelSource: "translationKey",
|
|
155
|
+
values: [preferred],
|
|
156
|
+
valueType: "text",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (config.valueKind === "objectRef") {
|
|
163
|
+
const label = resolveObjectRefLabel(value as ObjectRefModel, uiLanguage);
|
|
164
|
+
if (label) {
|
|
165
|
+
rows.push({
|
|
166
|
+
key,
|
|
167
|
+
label: key,
|
|
168
|
+
labelSource: "translationKey",
|
|
169
|
+
values: [label],
|
|
170
|
+
valueType: "text",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (config.valueKind === "objectRefArray") {
|
|
177
|
+
const refs = asObjectRefArray(value);
|
|
178
|
+
if (refs.length === 0) continue;
|
|
179
|
+
|
|
180
|
+
const values = Array.from(
|
|
181
|
+
new Set(
|
|
182
|
+
refs
|
|
183
|
+
.map((ref) => resolveObjectRefLabel(ref, uiLanguage))
|
|
184
|
+
.filter((label): label is string => Boolean(label))
|
|
185
|
+
)
|
|
186
|
+
).sort((a, b) => a.localeCompare(b));
|
|
187
|
+
|
|
188
|
+
if (values.length === 0) continue;
|
|
189
|
+
|
|
190
|
+
rows.push({
|
|
191
|
+
key,
|
|
192
|
+
label: key,
|
|
193
|
+
labelSource: "translationKey",
|
|
194
|
+
values,
|
|
195
|
+
valueType: "text",
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (config.valueKind === "renditionArray") {
|
|
201
|
+
const renditions = Array.isArray(value)
|
|
202
|
+
? value.filter((item): item is RenditionModel => Boolean(item && typeof item === "object"))
|
|
203
|
+
: [];
|
|
204
|
+
if (renditions.length === 0) continue;
|
|
205
|
+
if (getFileRenditionGroups({ renditions }).length === 0) continue;
|
|
206
|
+
|
|
207
|
+
rows.push({
|
|
208
|
+
key,
|
|
209
|
+
label: "files",
|
|
210
|
+
labelSource: "translationKey",
|
|
211
|
+
values: [],
|
|
212
|
+
valueType: "rendition",
|
|
213
|
+
renditions,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return rows;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const normalizeVersionItems = (items: CommonItemsModel[], uiLanguage: string) => {
|
|
222
|
+
const uniqueByShortId = new Map<string, { shortId: string; language: string }>();
|
|
223
|
+
|
|
224
|
+
items.forEach((item) => {
|
|
225
|
+
const shortId = item.shortId?.trim();
|
|
226
|
+
if (!shortId) return;
|
|
227
|
+
const language = resolvePreferredLanguage(item.languages, uiLanguage);
|
|
228
|
+
if (!language) return;
|
|
229
|
+
uniqueByShortId.set(shortId, { shortId, language });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return Array.from(uniqueByShortId.values()).sort((a, b) => {
|
|
233
|
+
const languageOrder = a.language.localeCompare(b.language);
|
|
234
|
+
if (languageOrder !== 0) return languageOrder;
|
|
235
|
+
return a.shortId.localeCompare(b.shortId);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const AvailableInRow = ({
|
|
240
|
+
versionOfShortId,
|
|
241
|
+
currentShortId,
|
|
242
|
+
linkPattern,
|
|
243
|
+
}: {
|
|
244
|
+
versionOfShortId?: string | null;
|
|
245
|
+
currentShortId?: string | null;
|
|
246
|
+
linkPattern: string;
|
|
247
|
+
}) => {
|
|
248
|
+
const t = useTranslations();
|
|
249
|
+
const locale = useLocale();
|
|
250
|
+
|
|
251
|
+
if (!versionOfShortId) return null;
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<InformationUnitsGetAllClient
|
|
255
|
+
queryParams={{
|
|
256
|
+
Restrict: [`versionOf.shortId=${versionOfShortId}`],
|
|
257
|
+
Fields: ["shortId", "languages", "labels", "versionOf"],
|
|
258
|
+
PageNumber: 1,
|
|
259
|
+
PageSize: 200,
|
|
260
|
+
Sort: ["languages"],
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{({ data }) => {
|
|
264
|
+
const versions = normalizeVersionItems(data?.items || [], locale)
|
|
265
|
+
.filter((item) => item.shortId !== currentShortId);
|
|
266
|
+
|
|
267
|
+
if (versions.length === 0) return null;
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<TableRow className="min-h-12">
|
|
271
|
+
<TableCell className="font-medium w-28 pl-4">
|
|
272
|
+
<h4 className="text-sm font-medium">{t("availableIn")}</h4>
|
|
273
|
+
</TableCell>
|
|
274
|
+
<TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
|
|
275
|
+
{versions.map((item) => (
|
|
276
|
+
<span className="w-8 block border" key={item.shortId}>
|
|
277
|
+
<Link
|
|
278
|
+
href={linkPattern.replace("{shortId}", item.shortId)}
|
|
279
|
+
title={item.language}
|
|
280
|
+
>
|
|
281
|
+
<Flag countryCode={extractCountryCodeFromLanguage(item.language)} />
|
|
282
|
+
</Link>
|
|
283
|
+
</span>
|
|
284
|
+
))}
|
|
285
|
+
</TableCell>
|
|
286
|
+
</TableRow>
|
|
287
|
+
);
|
|
288
|
+
}}
|
|
289
|
+
</InformationUnitsGetAllClient>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const InformationUnitMetadataGridClient = ({
|
|
294
|
+
title,
|
|
295
|
+
data,
|
|
296
|
+
embedded = false,
|
|
297
|
+
linkPattern,
|
|
298
|
+
metadataIncludeProperties,
|
|
299
|
+
metadataExcludeProperties,
|
|
300
|
+
showBookmarkButton = false,
|
|
301
|
+
showFileRenditions = true,
|
|
302
|
+
}: Props) => {
|
|
303
|
+
const t = useTranslations();
|
|
304
|
+
const locale = useLocale();
|
|
305
|
+
|
|
306
|
+
const metadataRows = useMemo(() => {
|
|
307
|
+
const includeProperties = resolveMetadataDisplayProperties({
|
|
308
|
+
includeProperties: metadataIncludeProperties as string[] | undefined,
|
|
309
|
+
excludeProperties: showFileRenditions
|
|
310
|
+
? (metadataExcludeProperties as string[] | undefined)
|
|
311
|
+
: [...(metadataExcludeProperties || []), "renditions"],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return buildMetadataDisplayRows(data, locale, includeProperties);
|
|
315
|
+
}, [data, locale, metadataExcludeProperties, metadataIncludeProperties, showFileRenditions]);
|
|
316
|
+
|
|
317
|
+
const cardContent = (
|
|
318
|
+
<CardContent className="space-y-3 !p-0">
|
|
319
|
+
<Table className="table-fixed">
|
|
320
|
+
<TableBody>
|
|
321
|
+
{showBookmarkButton && (
|
|
322
|
+
<TableRow className="min-h-12">
|
|
323
|
+
<TableCell className="font-medium w-32 pl-4 align-top">
|
|
324
|
+
<h4 className="text-sm font-medium">{t("favorites")}</h4>
|
|
325
|
+
</TableCell>
|
|
326
|
+
<TableCell className="min-h-12 pt-3 text-xs text-muted-foreground align-top break-words whitespace-normal">
|
|
327
|
+
<BookmarkButton shortId={data.shortId!} />
|
|
328
|
+
</TableCell>
|
|
329
|
+
</TableRow>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{metadataRows.map((row) => (
|
|
333
|
+
<TableRow key={`${row.key}:${row.label}`} className="min-h-12">
|
|
334
|
+
<TableCell className="font-medium w-32 pl-4 align-top">
|
|
335
|
+
<h4 className="text-sm font-medium capitalize">
|
|
336
|
+
{row.labelSource === "translationKey" ? t(row.label) : row.label}
|
|
337
|
+
</h4>
|
|
338
|
+
</TableCell>
|
|
339
|
+
<TableCell className="min-h-12 text-xs text-muted-foreground align-top break-words whitespace-normal [&_*]:break-words">
|
|
340
|
+
{renderMetadataDisplayValues(row, locale)}
|
|
341
|
+
</TableCell>
|
|
342
|
+
</TableRow>
|
|
343
|
+
))}
|
|
344
|
+
|
|
345
|
+
<AvailableInRow
|
|
346
|
+
versionOfShortId={data.versionOf?.shortId}
|
|
347
|
+
currentShortId={data.shortId}
|
|
348
|
+
linkPattern={linkPattern}
|
|
349
|
+
/>
|
|
350
|
+
</TableBody>
|
|
351
|
+
</Table>
|
|
352
|
+
</CardContent>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (embedded) return cardContent;
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<Card className="!p-0 !gap-0">
|
|
359
|
+
<CardHeader>
|
|
360
|
+
<CardTitle className="text-lg flex justify-between items-end">
|
|
361
|
+
{title}
|
|
362
|
+
{showBookmarkButton && <BookmarkButton shortId={data.shortId!} />}
|
|
363
|
+
</CardTitle>
|
|
364
|
+
</CardHeader>
|
|
365
|
+
{cardContent}
|
|
366
|
+
</Card>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CommonItemsModel } from "@c-rex/interfaces";
|
|
4
|
+
import { ImageRenditionContainer } from "../renditions/image/container";
|
|
5
|
+
import { DocumentsGetByIdClient } from "../generated/client-components";
|
|
6
|
+
import { resolveInformationUnitPreviewFragmentShortId } from "./information-unit-fragment-ids";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
item?: (CommonItemsModel & { informationUnits?: CommonItemsModel["informationUnits"] }) | null;
|
|
10
|
+
imageFragmentSubjectIds?: string[];
|
|
11
|
+
loadImage?: boolean;
|
|
12
|
+
emptyImageStyle?: string;
|
|
13
|
+
skeletonStyle?: string;
|
|
14
|
+
imageStyle?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const InformationUnitPreviewImage = ({
|
|
18
|
+
item,
|
|
19
|
+
imageFragmentSubjectIds = [],
|
|
20
|
+
loadImage = true,
|
|
21
|
+
emptyImageStyle,
|
|
22
|
+
skeletonStyle,
|
|
23
|
+
imageStyle,
|
|
24
|
+
}: Props) => {
|
|
25
|
+
const initialFragmentShortId = loadImage
|
|
26
|
+
? resolveInformationUnitPreviewFragmentShortId(item, imageFragmentSubjectIds)
|
|
27
|
+
: undefined;
|
|
28
|
+
|
|
29
|
+
if (!loadImage) {
|
|
30
|
+
return (
|
|
31
|
+
<ImageRenditionContainer
|
|
32
|
+
emptyImageStyle={emptyImageStyle}
|
|
33
|
+
skeletonStyle={skeletonStyle}
|
|
34
|
+
imageStyle={imageStyle}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (initialFragmentShortId || !item?.shortId) {
|
|
40
|
+
return (
|
|
41
|
+
<ImageRenditionContainer
|
|
42
|
+
fragmentShortId={initialFragmentShortId}
|
|
43
|
+
emptyImageStyle={emptyImageStyle}
|
|
44
|
+
skeletonStyle={skeletonStyle}
|
|
45
|
+
imageStyle={imageStyle}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<DocumentsGetByIdClient
|
|
52
|
+
pathParams={{ id: item.shortId }}
|
|
53
|
+
queryParams={{
|
|
54
|
+
// TODO(IDS): Temporary generic fallback for information-unit preview images.
|
|
55
|
+
// Remove this per-card detail lookup once list-based teaser/search requests can
|
|
56
|
+
// safely switch back to embedded `informationUnits` without causing unacceptable
|
|
57
|
+
// IDS load. Keep this in sync with all callers that intentionally removed the
|
|
58
|
+
// embedded relation from their list requests, especially `apps/cdp/app/page.tsx`.
|
|
59
|
+
Fields: ["informationUnits"],
|
|
60
|
+
Embed: ["informationUnits"],
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{({ data }) => {
|
|
64
|
+
const fragmentShortId = resolveInformationUnitPreviewFragmentShortId(data, imageFragmentSubjectIds);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ImageRenditionContainer
|
|
68
|
+
fragmentShortId={fragmentShortId}
|
|
69
|
+
emptyImageStyle={emptyImageStyle}
|
|
70
|
+
skeletonStyle={skeletonStyle}
|
|
71
|
+
imageStyle={imageStyle}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}}
|
|
75
|
+
</DocumentsGetByIdClient>
|
|
76
|
+
);
|
|
77
|
+
};
|
package/src/page-wrapper.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComponentProps, ReactNode } from "react";
|
|
2
2
|
import { NavBar } from './navbar/navbar';
|
|
3
3
|
import { MultiSidebarProvider } from "@c-rex/ui/sidebar";
|
|
4
|
-
import {
|
|
4
|
+
import { TaxonomyRestrictionMenu } from "./restriction-menu/taxonomy-restriction-menu";
|
|
5
5
|
import { Footer } from "./footer/footer";
|
|
6
6
|
|
|
7
7
|
type Props = {
|
|
@@ -9,8 +9,8 @@ type Props = {
|
|
|
9
9
|
showRestrictMenu?: boolean;
|
|
10
10
|
showFooter?: boolean;
|
|
11
11
|
renderFooter?: () => ReactNode;
|
|
12
|
-
renderRestrictionMenu?: (props: ComponentProps<typeof
|
|
13
|
-
} & Partial<ComponentProps<typeof NavBar>> & Partial<ComponentProps<typeof
|
|
12
|
+
renderRestrictionMenu?: (props: ComponentProps<typeof TaxonomyRestrictionMenu>) => ReactNode;
|
|
13
|
+
} & Partial<ComponentProps<typeof NavBar>> & Partial<ComponentProps<typeof TaxonomyRestrictionMenu>>;
|
|
14
14
|
|
|
15
15
|
export const PageWrapper = ({
|
|
16
16
|
children,
|
|
@@ -30,7 +30,7 @@ export const PageWrapper = ({
|
|
|
30
30
|
navigationMenuListClassName,
|
|
31
31
|
...props
|
|
32
32
|
}: Props) => {
|
|
33
|
-
const restrictionMenuProps: ComponentProps<typeof
|
|
33
|
+
const restrictionMenuProps: ComponentProps<typeof TaxonomyRestrictionMenu> = {
|
|
34
34
|
restrictField: restrictField ?? "informationSubjects",
|
|
35
35
|
requestType: requestType ?? "InformationSubjectsGetAllClient",
|
|
36
36
|
itemsToRender,
|
|
@@ -48,7 +48,7 @@ export const PageWrapper = ({
|
|
|
48
48
|
{showRestrictMenu && (
|
|
49
49
|
<div className="flex-1 container pt-6">
|
|
50
50
|
{renderRestrictionMenu ? renderRestrictionMenu(restrictionMenuProps) : (
|
|
51
|
-
<
|
|
51
|
+
<TaxonomyRestrictionMenu {...restrictionMenuProps} />
|
|
52
52
|
)}
|
|
53
53
|
</div>
|
|
54
54
|
)}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { FC } from "react";
|
|
2
4
|
import { RenditionModel } from "@c-rex/interfaces";
|
|
3
|
-
import {
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
4
6
|
import {
|
|
5
7
|
DropdownMenu,
|
|
6
8
|
DropdownMenuContent,
|
|
@@ -18,11 +20,11 @@ interface FileDownloadDropdown {
|
|
|
18
20
|
buttonVariant?: "ghost" | "outline";
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export const FileDownloadDropdown: FC<FileDownloadDropdown> =
|
|
23
|
+
export const FileDownloadDropdown: FC<FileDownloadDropdown> = ({
|
|
22
24
|
renditions,
|
|
23
25
|
buttonVariant = "ghost",
|
|
24
26
|
}) => {
|
|
25
|
-
const t =
|
|
27
|
+
const t = useTranslations();
|
|
26
28
|
|
|
27
29
|
if (renditions == null || renditions.length == 0) return null;
|
|
28
30
|
const groups = getFileRenditionGroups({ renditions });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { FragmentsGetByIdClient } from "../generated/client-components";
|
|
5
|
+
import type { RenditionModel } from "@c-rex/interfaces";
|
|
6
|
+
import { RenderArticle } from "../render-article";
|
|
7
|
+
|
|
8
|
+
type HtmlRenditionClientProps = {
|
|
9
|
+
htmlFormats?: string[];
|
|
10
|
+
fragmentShortId?: string;
|
|
11
|
+
renditions?: RenditionModel[] | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const EMPTY = <div>No rendition available</div>;
|
|
15
|
+
|
|
16
|
+
const findHtmlViewUrl = (
|
|
17
|
+
renditions?: RenditionModel[] | null,
|
|
18
|
+
htmlFormats: string[] = ["application/xhtml+xml", "application/html", "text/html"]
|
|
19
|
+
) => {
|
|
20
|
+
if (!renditions || renditions.length === 0) return undefined;
|
|
21
|
+
const allowed = new Set(htmlFormats.map((item) => item.toLowerCase()));
|
|
22
|
+
const rendition = renditions.find((item) => allowed.has((item.format || "").toLowerCase()));
|
|
23
|
+
return rendition?.links?.find((item) => item.rel === "view")?.href;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const HtmlFromRenditions = ({
|
|
27
|
+
renditions,
|
|
28
|
+
htmlFormats,
|
|
29
|
+
}: {
|
|
30
|
+
renditions?: RenditionModel[] | null;
|
|
31
|
+
htmlFormats?: string[];
|
|
32
|
+
}) => {
|
|
33
|
+
const [htmlContent, setHtmlContent] = useState<string | null>(null);
|
|
34
|
+
const [hasError, setHasError] = useState(false);
|
|
35
|
+
const viewUrl = useMemo(() => findHtmlViewUrl(renditions, htmlFormats), [htmlFormats, renditions]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
if (!viewUrl) {
|
|
40
|
+
setHtmlContent(null);
|
|
41
|
+
setHasError(false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setHasError(false);
|
|
46
|
+
setHtmlContent(null);
|
|
47
|
+
|
|
48
|
+
fetch(viewUrl)
|
|
49
|
+
.then((res) => res.text())
|
|
50
|
+
.then((html) => {
|
|
51
|
+
if (cancelled) return;
|
|
52
|
+
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
53
|
+
setHtmlContent(parsed.body?.innerHTML || "");
|
|
54
|
+
})
|
|
55
|
+
.catch(() => {
|
|
56
|
+
if (cancelled) return;
|
|
57
|
+
setHasError(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
63
|
+
}, [viewUrl]);
|
|
64
|
+
|
|
65
|
+
if (!viewUrl || hasError) return EMPTY;
|
|
66
|
+
if (htmlContent == null) return <div className="text-muted-foreground text-sm">Loading content...</div>;
|
|
67
|
+
|
|
68
|
+
return <RenderArticle htmlContent={htmlContent} />;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const HtmlRenditionClient = ({
|
|
72
|
+
fragmentShortId,
|
|
73
|
+
htmlFormats = ["application/xhtml+xml", "application/html", "text/html"],
|
|
74
|
+
renditions,
|
|
75
|
+
}: HtmlRenditionClientProps) => {
|
|
76
|
+
if (renditions !== undefined) {
|
|
77
|
+
return <HtmlFromRenditions renditions={renditions} htmlFormats={htmlFormats} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!fragmentShortId) return EMPTY;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<FragmentsGetByIdClient
|
|
84
|
+
pathParams={{ id: fragmentShortId }}
|
|
85
|
+
queryParams={{
|
|
86
|
+
Fields: ["titles", "renditions"],
|
|
87
|
+
Embed: ["renditions"],
|
|
88
|
+
Links: true,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{({ data, isLoading }) => {
|
|
92
|
+
if (isLoading && !data) {
|
|
93
|
+
return <div className="text-muted-foreground text-sm">Loading content...</div>;
|
|
94
|
+
}
|
|
95
|
+
return <HtmlFromRenditions renditions={data?.renditions} htmlFormats={htmlFormats} />;
|
|
96
|
+
}}
|
|
97
|
+
</FragmentsGetByIdClient>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -12,12 +12,14 @@ interface ImageContainerProps {
|
|
|
12
12
|
emptyImageStyle?: string;
|
|
13
13
|
skeletonStyle?: string;
|
|
14
14
|
imageStyle?: string;
|
|
15
|
+
containerStyle?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export const ImageRenditionContainer: FC<ImageContainerProps> = ({
|
|
18
19
|
fragmentShortId,
|
|
19
20
|
emptyImageStyle,
|
|
20
21
|
imageStyle,
|
|
22
|
+
containerStyle,
|
|
21
23
|
imageFormats = ["image/svg+xml", "image/gif", "image/png", "image/jpeg", "image/jpg"],
|
|
22
24
|
skeletonStyle
|
|
23
25
|
}) => {
|
|
@@ -35,23 +37,27 @@ export const ImageRenditionContainer: FC<ImageContainerProps> = ({
|
|
|
35
37
|
isLoading ? (
|
|
36
38
|
<Skeleton className={cn("w-full h-full", skeletonStyle)} />
|
|
37
39
|
) : (
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
<div className={cn("w-full h-full flex items-start justify-center overflow-hidden", containerStyle)}>
|
|
41
|
+
<ImageRendition
|
|
42
|
+
key={data?.shortId}
|
|
43
|
+
items={data ? [data] : []}
|
|
44
|
+
formats={imageFormats}
|
|
45
|
+
emptyImageStyle={emptyImageStyle}
|
|
46
|
+
imageStyle={imageStyle}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
45
49
|
)
|
|
46
50
|
)}
|
|
47
51
|
</FragmentsGetByIdClient>
|
|
48
52
|
) : (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
<div className={cn("w-full h-full flex items-start justify-center overflow-hidden", containerStyle)}>
|
|
54
|
+
<ImageRendition
|
|
55
|
+
items={[]}
|
|
56
|
+
formats={imageFormats}
|
|
57
|
+
emptyImageStyle={emptyImageStyle}
|
|
58
|
+
imageStyle={imageStyle}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
55
61
|
)
|
|
56
62
|
)
|
|
57
63
|
}
|
|
@@ -51,6 +51,11 @@ export const ImageRendition: FC<ImageRenditionProps> = ({
|
|
|
51
51
|
"Document image";
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
<img
|
|
54
|
+
<img
|
|
55
|
+
src={src}
|
|
56
|
+
alt={alt}
|
|
57
|
+
loading="eager"
|
|
58
|
+
className={cn("max-w-full", imageStyle)}
|
|
59
|
+
/>
|
|
55
60
|
);
|
|
56
61
|
}
|