Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/admin-ui/src/Dialog/components/DialogHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type DialogProps } from "../Dialog.js";
import { DialogTitle } from "./DialogTitle.js";
import { DialogDescription } from "./DialogDescription.js";

const dialogHeaderVariants = cva(["flex flex-col gap-sm", "text-neutral-primary", "sm:text-left"], {
const dialogHeaderVariants = cva(["flex flex-col gap-xs", "text-neutral-primary", "sm:text-left"], {
variants: {
size: {
sm: "pt-md pb-md-extra px-md-extra mr-xl",
Expand Down
3 changes: 2 additions & 1 deletion packages/admin-ui/src/DropdownMenu/DropdownMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ export const Default: Story = {
icon={<Link.Icon label="Link 1" element={<LinkIcon />} />}
/>
<Link
text={"Link 2"}
text={"Link 2 (target: _blank)"}
to={"#link-2"}
icon={<Link.Icon label="Link 2" element={<LinkIcon />} />}
target={"_blank"}
/>
<Link
text={"Link 3"}
Expand Down
56 changes: 55 additions & 1 deletion packages/admin-ui/src/ScrollArea/ScrollArea.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-webpack5";
import { ScrollArea, ScrollBar } from "./ScrollArea.js";
import { ScrollArea, ScrollBar, ScrollPosition } from "./ScrollArea.js";
import React from "react";
import { Heading } from "~/Heading/index.js";
import { Text } from "~/Text/index.js";
Expand Down Expand Up @@ -89,3 +89,57 @@ export const HorizontalScrolling: Story = {
);
}
};

export const WithScrollPositionTracking: Story = {
render: () => {
const [position, setPosition] = React.useState<ScrollPosition | null>(null);
const [loadMoreTriggered, setLoadMoreTriggered] = React.useState(false);

const handleScrollPositionChange = React.useCallback(
(pos: ScrollPosition) => {
setPosition(pos);

// Trigger load more when scrolled 90% down.
if (pos.top >= 0.9 && !loadMoreTriggered) {
setLoadMoreTriggered(true);
console.log("Load more triggered at position:", pos);
} else if (pos.top < 0.9) {
setLoadMoreTriggered(false);
}
},
[loadMoreTriggered]
);

return (
<div className="space-y-4">
<ScrollArea
className="h-72 w-48 rounded-md border border-neutral-dimmed"
onScrollPositionChange={handleScrollPositionChange}
>
<div className="p-4">
<Heading level={6} className="mb-4">
Tags
</Heading>
{tags.map(tag => (
<div key={tag}>
<Text className="text-sm">{tag}</Text>
<Separator className="my-2" />
</div>
))}
</div>
</ScrollArea>

{position && (
<div className="rounded-md border border-neutral-dimmed p-4 space-y-2">
<Text className="font-semibold">Scroll Position:</Text>
<Text className="text-sm">Top: {(position.top * 100).toFixed(1)}%</Text>
<Text className="text-sm">ScrollTop: {position.scrollTop}px</Text>
<Text className="text-sm">
Load More Triggered: {loadMoreTriggered ? "Yes" : "No"}
</Text>
</div>
)}
</div>
);
}
};
57 changes: 56 additions & 1 deletion packages/admin-ui/src/ScrollArea/ScrollArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,73 @@ import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "~/utils.js";

export interface ScrollPosition {
top: number;
left: number;
scrollTop: number;
scrollLeft: number;
scrollHeight: number;
scrollWidth: number;
clientHeight: number;
clientWidth: number;
}

interface ScrollAreaProps
extends Omit<React.ComponentProps<typeof ScrollAreaPrimitive.Root>, "onScroll"> {
onScrollPositionChange?: (position: ScrollPosition) => void;
onScroll?: (position: ScrollPosition) => void;
}

function ScrollArea({
className,
children,
onScrollPositionChange,
onScroll,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
}: ScrollAreaProps) {
const viewportRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
const viewport = viewportRef.current;
if (!viewport || (!onScrollPositionChange && !onScroll)) {
return;
}

// The Viewport component itself is the scrollable element.
const handleScroll = () => {
const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } =
viewport;

const position: ScrollPosition = {
top: scrollHeight > clientHeight ? scrollTop / (scrollHeight - clientHeight) : 0,
left: scrollWidth > clientWidth ? scrollLeft / (scrollWidth - clientWidth) : 0,
scrollTop,
scrollLeft,
scrollHeight,
scrollWidth,
clientHeight,
clientWidth
};

onScrollPositionChange?.(position);
onScroll?.(position);
};

// Call handleScroll initially to provide initial position.
handleScroll();

viewport.addEventListener("scroll", handleScroll);
return () => viewport.removeEventListener("scroll", handleScroll);
}, [onScrollPositionChange, onScroll]);

return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
Expand Down
4 changes: 2 additions & 2 deletions packages/admin-ui/src/Sidebar/components/SidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const SidebarContent = ({ className, children, ...props }: React.ComponentProps<
const isExpanded = state === "expanded";

if (isExpanded) {
// Extract dir prop to avoid type conflict with ScrollArea
// Extract dir and onScroll props to avoid type conflicts with ScrollArea.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { dir, ...restProps } = props;
const { dir, onScroll, ...restProps } = props;
return (
<ScrollArea
data-sidebar="content"
Expand Down
1 change: 0 additions & 1 deletion packages/app-headless-cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"raw.macro": "^0.4.2",
"react": "18.2.0",
"react-butterfiles": "^1.3.3",
"react-custom-scrollbars": "^4.2.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import * as GQL from "~/admin/viewsGraphql.js";
import type { ListCmsModelsQueryResponse } from "~/admin/viewsGraphql.js";
import * as GQL from "~/admin/viewsGraphql.js";
import { withoutBeingDeletedModels } from "~/admin/viewsGraphql.js";
import type {
BindComponentRenderProp,
Expand All @@ -11,31 +11,14 @@ import type {
import { Options } from "./Options.js";
import { useReferences } from "../hooks/useReferences.js";
import { Entry } from "./Entry.js";
import { Container } from "./Container.js";
import { ReferencesDialog } from "./ReferencesDialog.js";
import { useModelFieldGraphqlContext, useQuery } from "~/admin/hooks/index.js";
import { useSnackbar } from "@webiny/app-admin";
import type { CmsReferenceValue } from "~/admin/plugins/fieldRenderers/ref/components/types.js";
import { parseIdentifier } from "@webiny/utils";
import { Entries } from "./Entries.js";
import { NewReferencedEntryDialog } from "~/admin/plugins/fieldRenderers/ref/components/NewReferencedEntryDialog.js";
import {
FormComponentErrorMessage,
FormComponentLabel,
OverlayLoader,
Text
} from "@webiny/admin-ui";

const getRecordCountMessage = (count: number) => {
switch (count) {
case 0:
return "no records selected";
case 1:
return "1 record selected";
default:
return `${count} records selected`;
}
};
import { FormComponentErrorMessage, FormComponentLabel } from "@webiny/admin-ui";

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

const {
entries,
loading: loadingEntries,
loadMore
} = useReferences({
const { entries, loadMore } = useReferences({
values,
perPage: 10,
requestContext
Expand Down Expand Up @@ -211,10 +190,6 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
[values]
);

const loading = loadingEntries || loadingModels;

const message = getRecordCountMessage(values.length);

const { validation } = bind;
const { isValid: validationIsValid, message: validationMessage } = validation || {};
const invalid = useMemo(() => validationIsValid === false, [validationIsValid]);
Expand All @@ -223,10 +198,8 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
<>
<div className={"flex items-center justify-between"}>
<FormComponentLabel text={field.label} invalid={invalid} />
<Text size={"sm"}>({message})</Text>
</div>
<Container className={"webiny_ref-field-container"}>
{loading && <OverlayLoader size={"md"} />}
<div className={"webiny_ref-field-container"}>
<Entries entries={entries} loadMore={loadMore}>
{(entry, index) => {
const isFirst = index === 0;
Expand All @@ -251,8 +224,10 @@ export const AdvancedMultipleReferenceField = (props: AdvancedMultipleReferenceF
);
}}
</Entries>
</Container>
</div>
<FormComponentErrorMessage text={validationMessage} invalid={invalid} />
{values.length > 0 && <div className="mb-md" />}

<Options
models={models}
onNewRecord={onNewRecord}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { Options } from "./Options.js";
import { useReferences } from "../hooks/useReferences.js";
import { Entry } from "./Entry.js";
import { ReferencesDialog } from "./ReferencesDialog.js";
import { NoEntries } from "./NoEntries.js";
import { Container } from "./Container.js";
import { useQuery, useModelFieldGraphqlContext } from "~/admin/hooks/index.js";
import type { ListCmsModelsQueryResponse } from "~/admin/viewsGraphql.js";
import * as GQL from "~/admin/viewsGraphql.js";
Expand Down Expand Up @@ -172,21 +170,20 @@ export const AdvancedSingleReferenceField = (props: AdvancedSingleReferenceField
return (
<>
<FormComponentLabel text={field.label} invalid={invalid} />
<Container className={"webiny_ref-field-container"}>
<div className={"webiny_ref-field-container"}>
{loading && <OverlayLoader size={"md"} />}
{initialValue ? (
{initialValue && (
<Entry
model={initialValue.model}
placement="singleRefField"
index={0}
entry={initialValue.entry}
onRemove={onRemove}
/>
) : (
<NoEntries text={"No record found"} />
)}
</Container>
</div>
<FormComponentErrorMessage text={validationMessage} invalid={invalid} />
{initialValue && <div className="mb-md" />}
<Options
models={models}
onNewRecord={onNewRecord}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useCallback } from "react";
import debounce from "lodash/debounce.js";
import type { CmsReferenceContentEntry } from "~/admin/plugins/fieldRenderers/ref/components/types.js";
import { Scrollbar } from "@webiny/admin-ui";
import type { positionValues as PositionValues } from "react-custom-scrollbars";
import { NoEntries } from "~/admin/plugins/fieldRenderers/ref/advanced/components/NoEntries.js";
import { ScrollArea } from "@webiny/admin-ui";

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

const loadMoreOnScroll = useCallback(
debounce((position: PositionValues) => {
debounce(position => {
if (position.top <= 0.9) {
return;
}
Expand All @@ -24,21 +22,17 @@ export const Entries = (props: EntriesProps) => {
[entries, loadMore]
);

if (entries.length === 0) {
return <NoEntries text={"No records found"} />;
}

return (
<div style={{ height: "260px" }} className={"w-full overflow-x-hidden overflow-y-hidden"}>
<Scrollbar data-testid="advanced-ref-field-entries" onScrollFrame={loadMoreOnScroll}>
<ScrollArea
className={"max-h-[404px] w-full flex flex-col gap-md"}
data-testid="advanced-ref-field-entries"
onScroll={loadMoreOnScroll}
>
<div className={"flex flex-col gap-md"}>
{entries.map((entry, index) => {
return (
<div className={"mb-sm w-full"} key={`entry-${entry.id}`}>
{children(entry, index)}
</div>
);
return <div key={`entry-${entry.id}`}>{children(entry, index)}</div>;
})}
</Scrollbar>
</div>
</div>
</ScrollArea>
);
};
Loading
Loading