Skip to content

Commit c7e4d99

Browse files
authored
feat: new ref field (#4878)
1 parent e67f00b commit c7e4d99

File tree

24 files changed

+332
-324
lines changed

24 files changed

+332
-324
lines changed

packages/admin-ui/src/Dialog/components/DialogHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type DialogProps } from "../Dialog.js";
44
import { DialogTitle } from "./DialogTitle.js";
55
import { DialogDescription } from "./DialogDescription.js";
66

7-
const dialogHeaderVariants = cva(["flex flex-col gap-sm", "text-neutral-primary", "sm:text-left"], {
7+
const dialogHeaderVariants = cva(["flex flex-col gap-xs", "text-neutral-primary", "sm:text-left"], {
88
variants: {
99
size: {
1010
sm: "pt-md pb-md-extra px-md-extra mr-xl",

packages/admin-ui/src/DropdownMenu/DropdownMenu.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ export const Default: Story = {
100100
icon={<Link.Icon label="Link 1" element={<LinkIcon />} />}
101101
/>
102102
<Link
103-
text={"Link 2"}
103+
text={"Link 2 (target: _blank)"}
104104
to={"#link-2"}
105105
icon={<Link.Icon label="Link 2" element={<LinkIcon />} />}
106+
target={"_blank"}
106107
/>
107108
<Link
108109
text={"Link 3"}

packages/admin-ui/src/ScrollArea/ScrollArea.stories.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react-webpack5";
2-
import { ScrollArea, ScrollBar } from "./ScrollArea.js";
2+
import { ScrollArea, ScrollBar, ScrollPosition } from "./ScrollArea.js";
33
import React from "react";
44
import { Heading } from "~/Heading/index.js";
55
import { Text } from "~/Text/index.js";
@@ -89,3 +89,57 @@ export const HorizontalScrolling: Story = {
8989
);
9090
}
9191
};
92+
93+
export const WithScrollPositionTracking: Story = {
94+
render: () => {
95+
const [position, setPosition] = React.useState<ScrollPosition | null>(null);
96+
const [loadMoreTriggered, setLoadMoreTriggered] = React.useState(false);
97+
98+
const handleScrollPositionChange = React.useCallback(
99+
(pos: ScrollPosition) => {
100+
setPosition(pos);
101+
102+
// Trigger load more when scrolled 90% down.
103+
if (pos.top >= 0.9 && !loadMoreTriggered) {
104+
setLoadMoreTriggered(true);
105+
console.log("Load more triggered at position:", pos);
106+
} else if (pos.top < 0.9) {
107+
setLoadMoreTriggered(false);
108+
}
109+
},
110+
[loadMoreTriggered]
111+
);
112+
113+
return (
114+
<div className="space-y-4">
115+
<ScrollArea
116+
className="h-72 w-48 rounded-md border border-neutral-dimmed"
117+
onScrollPositionChange={handleScrollPositionChange}
118+
>
119+
<div className="p-4">
120+
<Heading level={6} className="mb-4">
121+
Tags
122+
</Heading>
123+
{tags.map(tag => (
124+
<div key={tag}>
125+
<Text className="text-sm">{tag}</Text>
126+
<Separator className="my-2" />
127+
</div>
128+
))}
129+
</div>
130+
</ScrollArea>
131+
132+
{position && (
133+
<div className="rounded-md border border-neutral-dimmed p-4 space-y-2">
134+
<Text className="font-semibold">Scroll Position:</Text>
135+
<Text className="text-sm">Top: {(position.top * 100).toFixed(1)}%</Text>
136+
<Text className="text-sm">ScrollTop: {position.scrollTop}px</Text>
137+
<Text className="text-sm">
138+
Load More Triggered: {loadMoreTriggered ? "Yes" : "No"}
139+
</Text>
140+
</div>
141+
)}
142+
</div>
143+
);
144+
}
145+
};

packages/admin-ui/src/ScrollArea/ScrollArea.tsx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,73 @@ import * as React from "react";
22
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
33
import { cn } from "~/utils.js";
44

5+
export interface ScrollPosition {
6+
top: number;
7+
left: number;
8+
scrollTop: number;
9+
scrollLeft: number;
10+
scrollHeight: number;
11+
scrollWidth: number;
12+
clientHeight: number;
13+
clientWidth: number;
14+
}
15+
16+
interface ScrollAreaProps
17+
extends Omit<React.ComponentProps<typeof ScrollAreaPrimitive.Root>, "onScroll"> {
18+
onScrollPositionChange?: (position: ScrollPosition) => void;
19+
onScroll?: (position: ScrollPosition) => void;
20+
}
21+
522
function ScrollArea({
623
className,
724
children,
25+
onScrollPositionChange,
26+
onScroll,
827
...props
9-
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
28+
}: ScrollAreaProps) {
29+
const viewportRef = React.useRef<HTMLDivElement>(null);
30+
31+
React.useEffect(() => {
32+
const viewport = viewportRef.current;
33+
if (!viewport || (!onScrollPositionChange && !onScroll)) {
34+
return;
35+
}
36+
37+
// The Viewport component itself is the scrollable element.
38+
const handleScroll = () => {
39+
const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } =
40+
viewport;
41+
42+
const position: ScrollPosition = {
43+
top: scrollHeight > clientHeight ? scrollTop / (scrollHeight - clientHeight) : 0,
44+
left: scrollWidth > clientWidth ? scrollLeft / (scrollWidth - clientWidth) : 0,
45+
scrollTop,
46+
scrollLeft,
47+
scrollHeight,
48+
scrollWidth,
49+
clientHeight,
50+
clientWidth
51+
};
52+
53+
onScrollPositionChange?.(position);
54+
onScroll?.(position);
55+
};
56+
57+
// Call handleScroll initially to provide initial position.
58+
handleScroll();
59+
60+
viewport.addEventListener("scroll", handleScroll);
61+
return () => viewport.removeEventListener("scroll", handleScroll);
62+
}, [onScrollPositionChange, onScroll]);
63+
1064
return (
1165
<ScrollAreaPrimitive.Root
1266
data-slot="scroll-area"
1367
className={cn("relative", className)}
1468
{...props}
1569
>
1670
<ScrollAreaPrimitive.Viewport
71+
ref={viewportRef}
1772
data-slot="scroll-area-viewport"
1873
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
1974
>

packages/admin-ui/src/Sidebar/components/SidebarContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ const SidebarContent = ({ className, children, ...props }: React.ComponentProps<
88
const isExpanded = state === "expanded";
99

1010
if (isExpanded) {
11-
// Extract dir prop to avoid type conflict with ScrollArea
11+
// Extract dir and onScroll props to avoid type conflicts with ScrollArea.
1212
// eslint-disable-next-line @typescript-eslint/no-unused-vars
13-
const { dir, ...restProps } = props;
13+
const { dir, onScroll, ...restProps } = props;
1414
return (
1515
<ScrollArea
1616
data-sidebar="content"

packages/app-headless-cms/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"raw.macro": "^0.4.2",
6666
"react": "18.2.0",
6767
"react-butterfiles": "^1.3.3",
68-
"react-custom-scrollbars": "^4.2.1",
6968
"react-dnd": "^16.0.1",
7069
"react-dnd-html5-backend": "^16.0.1",
7170
"react-dom": "18.2.0",

packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/advanced/components/AdvancedMultipleReferenceField.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useCallback, useEffect, useMemo, useState } from "react";
2-
import * as GQL from "~/admin/viewsGraphql.js";
32
import type { ListCmsModelsQueryResponse } from "~/admin/viewsGraphql.js";
3+
import * as GQL from "~/admin/viewsGraphql.js";
44
import { withoutBeingDeletedModels } from "~/admin/viewsGraphql.js";
55
import type {
66
BindComponentRenderProp,
@@ -11,31 +11,14 @@ import type {
1111
import { Options } from "./Options.js";
1212
import { useReferences } from "../hooks/useReferences.js";
1313
import { Entry } from "./Entry.js";
14-
import { Container } from "./Container.js";
1514
import { ReferencesDialog } from "./ReferencesDialog.js";
1615
import { useModelFieldGraphqlContext, useQuery } from "~/admin/hooks/index.js";
1716
import { useSnackbar } from "@webiny/app-admin";
1817
import type { CmsReferenceValue } from "~/admin/plugins/fieldRenderers/ref/components/types.js";
1918
import { parseIdentifier } from "@webiny/utils";
2019
import { Entries } from "./Entries.js";
2120
import { NewReferencedEntryDialog } from "~/admin/plugins/fieldRenderers/ref/components/NewReferencedEntryDialog.js";
22-
import {
23-
FormComponentErrorMessage,
24-
FormComponentLabel,
25-
OverlayLoader,
26-
Text
27-
} from "@webiny/admin-ui";
28-
29-
const getRecordCountMessage = (count: number) => {
30-
switch (count) {
31-
case 0:
32-
return "no records selected";
33-
case 1:
34-
return "1 record selected";
35-
default:
36-
return `${count} records selected`;
37-
}
38-
};
21+
import { FormComponentErrorMessage, FormComponentLabel } from "@webiny/admin-ui";
3922

4023
interface AdvancedMultipleReferenceFieldProps extends CmsModelFieldRendererProps {
4124
bind: BindComponentRenderProp<CmsReferenceValue[] | undefined | null>;
@@ -105,11 +88,7 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
10588
setLinkEntryDialogModel(null);
10689
}, []);
10790

108-
const {
109-
entries,
110-
loading: loadingEntries,
111-
loadMore
112-
} = useReferences({
91+
const { entries, loadMore } = useReferences({
11392
values,
11493
perPage: 10,
11594
requestContext
@@ -211,10 +190,6 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
211190
[values]
212191
);
213192

214-
const loading = loadingEntries || loadingModels;
215-
216-
const message = getRecordCountMessage(values.length);
217-
218193
const { validation } = bind;
219194
const { isValid: validationIsValid, message: validationMessage } = validation || {};
220195
const invalid = useMemo(() => validationIsValid === false, [validationIsValid]);
@@ -223,10 +198,8 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
223198
<>
224199
<div className={"flex items-center justify-between"}>
225200
<FormComponentLabel text={field.label} invalid={invalid} />
226-
<Text size={"sm"}>({message})</Text>
227201
</div>
228-
<Container className={"webiny_ref-field-container"}>
229-
{loading && <OverlayLoader size={"md"} />}
202+
<div className={"webiny_ref-field-container"}>
230203
<Entries entries={entries} loadMore={loadMore}>
231204
{(entry, index) => {
232205
const isFirst = index === 0;
@@ -251,8 +224,10 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
251224
);
252225
}}
253226
</Entries>
254-
</Container>
227+
</div>
255228
<FormComponentErrorMessage text={validationMessage} invalid={invalid} />
229+
{values.length > 0 && <div className="mb-md" />}
230+
256231
<Options
257232
models={models}
258233
onNewRecord={onNewRecord}

packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/advanced/components/AdvancedSingleReferenceField.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { Options } from "./Options.js";
99
import { useReferences } from "../hooks/useReferences.js";
1010
import { Entry } from "./Entry.js";
1111
import { ReferencesDialog } from "./ReferencesDialog.js";
12-
import { NoEntries } from "./NoEntries.js";
13-
import { Container } from "./Container.js";
1412
import { useQuery, useModelFieldGraphqlContext } from "~/admin/hooks/index.js";
1513
import type { ListCmsModelsQueryResponse } from "~/admin/viewsGraphql.js";
1614
import * as GQL from "~/admin/viewsGraphql.js";
@@ -172,21 +170,20 @@ export const AdvancedSingleReferenceField = (props: AdvancedSingleReferenceField
172170
return (
173171
<>
174172
<FormComponentLabel text={field.label} invalid={invalid} />
175-
<Container className={"webiny_ref-field-container"}>
173+
<div className={"webiny_ref-field-container"}>
176174
{loading && <OverlayLoader size={"md"} />}
177-
{initialValue ? (
175+
{initialValue && (
178176
<Entry
179177
model={initialValue.model}
180178
placement="singleRefField"
181179
index={0}
182180
entry={initialValue.entry}
183181
onRemove={onRemove}
184182
/>
185-
) : (
186-
<NoEntries text={"No record found"} />
187183
)}
188-
</Container>
184+
</div>
189185
<FormComponentErrorMessage text={validationMessage} invalid={invalid} />
186+
{initialValue && <div className="mb-md" />}
190187
<Options
191188
models={models}
192189
onNewRecord={onNewRecord}
Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import React, { useCallback } from "react";
22
import debounce from "lodash/debounce.js";
33
import type { CmsReferenceContentEntry } from "~/admin/plugins/fieldRenderers/ref/components/types.js";
4-
import { Scrollbar } from "@webiny/admin-ui";
5-
import type { positionValues as PositionValues } from "react-custom-scrollbars";
6-
import { NoEntries } from "~/admin/plugins/fieldRenderers/ref/advanced/components/NoEntries.js";
4+
import { ScrollArea } from "@webiny/admin-ui";
75

86
interface EntriesProps {
97
entries: CmsReferenceContentEntry[];
@@ -15,7 +13,7 @@ export const Entries = (props: EntriesProps) => {
1513
const { entries, children, loadMore } = props;
1614

1715
const loadMoreOnScroll = useCallback(
18-
debounce((position: PositionValues) => {
16+
debounce(position => {
1917
if (position.top <= 0.9) {
2018
return;
2119
}
@@ -24,21 +22,17 @@ export const Entries = (props: EntriesProps) => {
2422
[entries, loadMore]
2523
);
2624

27-
if (entries.length === 0) {
28-
return <NoEntries text={"No records found"} />;
29-
}
30-
3125
return (
32-
<div style={{ height: "260px" }} className={"w-full overflow-x-hidden overflow-y-hidden"}>
33-
<Scrollbar data-testid="advanced-ref-field-entries" onScrollFrame={loadMoreOnScroll}>
26+
<ScrollArea
27+
className={"max-h-[404px] w-full flex flex-col gap-md"}
28+
data-testid="advanced-ref-field-entries"
29+
onScroll={loadMoreOnScroll}
30+
>
31+
<div className={"flex flex-col gap-md"}>
3432
{entries.map((entry, index) => {
35-
return (
36-
<div className={"mb-sm w-full"} key={`entry-${entry.id}`}>
37-
{children(entry, index)}
38-
</div>
39-
);
33+
return <div key={`entry-${entry.id}`}>{children(entry, index)}</div>;
4034
})}
41-
</Scrollbar>
42-
</div>
35+
</div>
36+
</ScrollArea>
4337
);
4438
};

0 commit comments

Comments
 (0)