@c-rex/components 0.3.0-build.29 → 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.
@@ -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
+ };
@@ -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 { RestrictionMenuContainer } from "./restriction-menu/restriction-menu-container";
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 RestrictionMenuContainer>) => ReactNode;
13
- } & Partial<ComponentProps<typeof NavBar>> & Partial<ComponentProps<typeof RestrictionMenuContainer>>;
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 RestrictionMenuContainer> = {
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
- <RestrictionMenuContainer {...restrictionMenuProps} />
51
+ <TaxonomyRestrictionMenu {...restrictionMenuProps} />
52
52
  )}
53
53
  </div>
54
54
  )}
@@ -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
+ };
@@ -1,117 +1,4 @@
1
- "use client";
2
-
3
- import { DomainEntityModel } from "@c-rex/interfaces";
4
- import { FC, ReactNode, useMemo, useState } from "react"
5
- import * as ComponentOptions from "../generated/client-components";
6
- import { Skeleton } from "@c-rex/ui/skeleton";
7
- import { RestrictionMenu } from "./restriction-menu";
8
-
9
- type GenericRequestData = {
10
- items?: DomainEntityModel[];
11
- pageInfo?: {
12
- totalItemCount?: number;
13
- };
14
- } | null | undefined;
15
- type GenericQueryParams = Record<string, unknown> & { Restrict?: string[]; PageSize?: number; Fields?: string[] };
16
- type GenericRequestProps = {
17
- queryParams?: GenericQueryParams;
18
- children: (props: { data?: GenericRequestData; isLoading?: boolean }) => ReactNode;
19
- };
20
- type RestrictionMenuFetchMode = "all" | "deferred";
21
-
22
- type Props = {
23
- restrictField: string
24
- navigationMenuListClassName?: string
25
- itemsToRender?: number
26
- requestType: keyof typeof ComponentOptions
27
- onlyUsedEntries?: boolean
28
- enableHierarchy?: boolean
29
- fetchMode?: RestrictionMenuFetchMode
30
- showAllWhenEmpty?: boolean
31
- queryParams?: GenericQueryParams
32
- };
33
-
34
- export const RestrictionMenuContainer: FC<Props> = ({
35
- queryParams,
36
- restrictField,
37
- itemsToRender = 7,
38
- requestType,
39
- onlyUsedEntries = true,
40
- enableHierarchy = false,
41
- fetchMode = "deferred",
42
- showAllWhenEmpty = true,
43
- navigationMenuListClassName = "items-center justify-between flex-row",
44
- }) => {
45
- const [loadAll, setLoadAll] = useState(false);
46
- const RequestComponent = ComponentOptions[requestType] as unknown as FC<GenericRequestProps>;
47
- const queryRestrict = queryParams?.Restrict || [];
48
- const restrict = onlyUsedEntries ? ["hasInformationUnits=true", ...queryRestrict] : queryRestrict;
49
- const explicitPageSize =
50
- Number.isFinite(Number(queryParams?.PageSize)) && Number(queryParams?.PageSize) > 0
51
- ? Number(queryParams?.PageSize)
52
- : undefined;
53
- const resolvedPageSize = useMemo(() => {
54
- if (explicitPageSize) return explicitPageSize;
55
- // TODO(UI/Gabriel): `deferred` currently behaves like "initial subset + load all on first More click".
56
- // Keep this behavior for now; planned follow-up is incremental loading from inside the opened More dropdown.
57
- if (fetchMode === "deferred" && !loadAll) return Math.max(itemsToRender, 1);
58
- return 100;
59
- }, [explicitPageSize, fetchMode, itemsToRender, loadAll]);
60
- const requestedFields = Array.isArray(queryParams?.Fields) ? queryParams.Fields : undefined;
61
- const resolvedFields = useMemo(() => {
62
- const baseFields = requestedFields && requestedFields.length > 0 ? requestedFields : ["labels"];
63
- if (!enableHierarchy) return baseFields;
64
- const withParents = new Set(baseFields);
65
- withParents.add("parents");
66
- return Array.from(withParents);
67
- }, [enableHierarchy, requestedFields]);
68
-
69
- return (
70
- <RequestComponent
71
- queryParams={{
72
- ...queryParams,
73
- Fields: resolvedFields,
74
- Links: true,
75
- Sort: ["labels.value"],
76
- PageSize: resolvedPageSize,
77
- Restrict: restrict,
78
- }}
79
- >
80
- {({ data, isLoading }) => {
81
-
82
- if (isLoading) return (
83
- <div className="flex justify-between">
84
- <Skeleton className="w-12 h-9 rounded-full" />
85
- {Array(itemsToRender).fill(0).map((_, index) => (
86
- <Skeleton key={`skeleton-${index}`} className="w-28 h-9 rounded-full" />
87
- ))}
88
- <Skeleton className="w-20 h-9 rounded-full" />
89
- </div>
90
- )
91
-
92
- if (!data) return null;
93
-
94
- const itemCount = data.items?.length || 0;
95
- const totalItemCount = data.pageInfo?.totalItemCount;
96
- const hasMoreFromServer = typeof totalItemCount === "number" ? totalItemCount > itemCount : false;
97
- const hasMoreItems = hasMoreFromServer || (explicitPageSize ? false : (fetchMode === "deferred" && !loadAll && itemCount >= resolvedPageSize));
98
-
99
- return (
100
- <RestrictionMenu
101
- restrictField={restrictField}
102
- items={data.items || []}
103
- enableHierarchy={enableHierarchy}
104
- hasMoreItems={hasMoreItems}
105
- showAllWhenEmpty={showAllWhenEmpty}
106
- onRequestMore={() => {
107
- if (fetchMode === "deferred" && !explicitPageSize) {
108
- setLoadAll(true);
109
- }
110
- }}
111
- navigationMenuListClassName={navigationMenuListClassName}
112
- />
113
- )
114
- }}
115
- </RequestComponent>
116
- );
117
- };
1
+ export {
2
+ TaxonomyRestrictionMenu as RestrictionMenuContainer,
3
+ type TaxonomyRestrictionMenuProps as RestrictionMenuContainerProps,
4
+ } from "./taxonomy-restriction-menu";