Skip to content
Open
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
52 changes: 41 additions & 11 deletions client/src/features/searchV2/components/MemberListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,25 @@ export default function MemberListRow({ members }: MemberListRowProps) {
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const measuredWidths = useRef<number[]>([]);
const [visibleCount, setVisibleCount] = useState(members.length);
// When the first member does not fit at full width, constrain it so the name can truncate.
const [truncateMaxWidth, setTruncateMaxWidth] = useState<number | null>(null);

const measureItemWidths = useCallback(() => {
for (let i = 0; i < members.length; i++) {
const el = itemRefs.current[i];
if (el) {
measuredWidths.current[i] = el.offsetWidth;
}
if (!el) continue;
// Sum children so a truncated item still reports its natural width
// (the item's own scrollWidth collapses to maxWidth when truncating).
const avatar = el.children[0] as HTMLElement | undefined;
const name = el.children[1] as HTMLElement | undefined;
const innerGap =
avatar && name
? parseFloat(
getComputedStyle(el).columnGap || getComputedStyle(el).gap,
) || 0
: 0;
measuredWidths.current[i] =
(avatar?.offsetWidth ?? 0) + innerGap + (name?.scrollWidth ?? 0);
}
}, [members.length]);

Expand Down Expand Up @@ -78,20 +90,33 @@ export default function MemberListRow({ members }: MemberListRowProps) {
count++;
}

setVisibleCount(Math.max(1, count));
if (count === 0 && total > 0) {
// Always show at least one member; cap width so text-truncate
const reservedForBadge =
total > 1 ? OVERFLOW_BADGE_WIDTH + MEMBER_GAP : 0;
setVisibleCount(1);
setTruncateMaxWidth(Math.max(0, containerWidth - reservedForBadge));
return;
}

setVisibleCount(count);
setTruncateMaxWidth(null);
}, [members.length]);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

measureItemWidths();
const updateVisibleMembers = () => {
measureItemWidths();
recalculate();
};

// TODO: fix react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect
recalculate();
updateVisibleMembers();

const observer = new ResizeObserver(() => {
recalculate();
updateVisibleMembers();
});
observer.observe(container);

Expand All @@ -112,9 +137,12 @@ export default function MemberListRow({ members }: MemberListRowProps) {
"overflow-hidden",
"position-relative",
)}
data-cy="member-list-row"
>
{members.map((member, index) => {
const isHidden = index >= visibleCount;
const shouldTruncate =
!isHidden && truncateMaxWidth != null && index === 0;
return (
<div
key={member.id}
Expand All @@ -125,7 +153,7 @@ export default function MemberListRow({ members }: MemberListRowProps) {
"align-items-center",
"d-flex",
"gap-1",
isHidden ? "flex-shrink-0" : "overflow-hidden",
shouldTruncate ? "overflow-hidden" : "flex-shrink-0",
)}
style={
isHidden
Expand All @@ -134,11 +162,13 @@ export default function MemberListRow({ members }: MemberListRowProps) {
position: "absolute",
pointerEvents: "none",
}
: { minWidth: 0 }
: shouldTruncate
? { maxWidth: truncateMaxWidth, minWidth: 0 }
: undefined
}
>
<UserAvatar namespace={member.namespace ?? ""} />
<span className="text-truncate">
<span className={cx("text-truncate", "min-w-0")}>
{member.first_name} {member.last_name}
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ function SearchFilterContent({
visualization={visualization}
type={filter.allowSelectMany ? "checkbox" : "radio"}
>
{element.icon}
{element.label}
{element.quantity !== undefined ? (
<Badge className="ms-1">{element.quantity}</Badge>
Expand Down
44 changes: 29 additions & 15 deletions client/src/features/searchV2/components/SearchResultRecap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import cx from "classnames";
import { ReactNode, useMemo } from "react";

import { getFilterValueLabel } from "~/features/searchV2/contextSearch.utils";
import useAppSelector from "../../../utils/customHooks/useAppSelector.hook";
import { useGetSearchQueryQuery } from "../api/searchV2Api.api";
import {
Expand All @@ -33,51 +34,64 @@ import { selectSearchApiQuery } from "../searchV2.slice";

export default function SearchResultRecap() {
const state = useAppSelector(({ searchV2 }) => searchV2);
const {
contentType,
created,
directMember,
keywords,
query,
role,
visibility,
} = state;
const apiQuery = useAppSelector(selectSearchApiQuery);
const { data, isFetching } = useGetSearchQueryQuery({ params: apiQuery });
const total = data?.pagingInfo.totalResult;

const filters = useMemo(() => {
const parts: ReactNode[] = [];
if (state.contentType) {
if (contentType) {
parts.push(
<span key="type">
{FILTER_CONTENT.label}: {state.contentType}
{FILTER_CONTENT.label}:{" "}
{getFilterValueLabel([contentType], FILTER_CONTENT.allowedValues)}
</span>,
);
}
if (state.visibility) {
if (visibility) {
parts.push(
<span key="visibility">
{FILTER_VISIBILITY.label}: {state.visibility}
{FILTER_VISIBILITY.label}:{" "}
{getFilterValueLabel([visibility], FILTER_VISIBILITY.allowedValues)}
</span>,
);
}
if (state.role) {
if (role) {
parts.push(
<span key="role">
{FILTER_MY_ROLE.label}: {state.role}
{FILTER_MY_ROLE.label}:{" "}
{getFilterValueLabel(role.split(","), FILTER_MY_ROLE.allowedValues)}
</span>,
);
}
if (state.keywords) {
if (keywords) {
parts.push(
<span key="keywords">
{FILTER_KEYWORD.label}: {state.keywords}
{FILTER_KEYWORD.label}: {keywords}
</span>,
);
}
if (state.directMember) {
if (directMember) {
parts.push(
<span key="member">
{FILTER_MEMBER.label}: {state.directMember}
{FILTER_MEMBER.label}: {directMember}
</span>,
);
}
if (state.created) {
if (created) {
parts.push(
<span key="created">
{FILTER_DATE.label}: {state.created}
{FILTER_DATE.label}:{" "}
{getFilterValueLabel([created], FILTER_DATE.allowedValues)}
</span>,
);
}
Expand All @@ -91,7 +105,7 @@ export default function SearchResultRecap() {
))}
</>
) : null;
}, [state]);
}, [contentType, created, directMember, keywords, role, visibility]);

return (
<p className="mb-0">
Expand All @@ -102,10 +116,10 @@ export default function SearchResultRecap() {
{total ? total : "No"} {total && total > 1 ? "results" : "result"}
</span>
)}
{state.query && (
{query && (
<>
{" "}
for <span className="fw-semibold">{`"${state.query}"`}</span>
for <span className="fw-semibold">{`"${query}"`}</span>
</>
)}
{filters && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function SearchResultListItem({ item }: SearchResultListItemProps) {
to={url}
data-cy="search-list-item"
>
<Row className="g-2">
<Row className={cx("g-2", "flex-nowrap")}>
<Col xs="auto">
<h5>
<SearchResultListItemIcon item={item} />
Expand Down
80 changes: 24 additions & 56 deletions client/src/features/searchV2/contextSearch.constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,39 +51,23 @@ export const FILTER_CONTENT: EnumFilter = {
allowedValues: [
{
value: "Project",
label: (
<>
<Folder className={cx("bi", "me-1")} />
Project
</>
),
label: "Project",
icon: <Folder className={cx("bi", "me-1")} />,
},
{
value: "DataConnector",
label: (
<>
<Database className={cx("bi", "me-1")} />
Data
</>
),
label: "Data",
icon: <Database className={cx("bi", "me-1")} />,
},
{
value: "User",
label: (
<>
<Person className={cx("bi", "me-1")} />
User
</>
),
label: "User",
icon: <Person className={cx("bi", "me-1")} />,
},
{
value: "Group",
label: (
<>
<People className={cx("bi", "me-1")} />
Group
</>
),
label: "Group",
icon: <People className={cx("bi", "me-1")} />,
},
],
allowSelectMany: false,
Expand All @@ -102,21 +86,13 @@ export const FILTER_CONTENT_NAMESPACE: EnumFilter = {
allowedValues: [
{
value: "Project",
label: (
<>
<Folder className={cx("bi", "me-1")} />
Project
</>
),
icon: <Folder className={cx("bi", "me-1")} />,
label: "Project",
},
{
value: "DataConnector",
label: (
<>
<Database className={cx("bi", "me-1")} />
Data
</>
),
icon: <Database className={cx("bi", "me-1")} />,
label: "Data",
},
],
allowSelectMany: false,
Expand Down Expand Up @@ -167,21 +143,13 @@ export const FILTER_VISIBILITY: EnumFilter = {
{ value: "", label: "Any visibility" },
{
value: "public",
label: (
<>
<Globe className={cx("bi", "me-1")} />
Public
</>
),
icon: <Globe className={cx("bi", "me-1")} />,
label: "Public",
},
{
value: "private",
label: (
<>
<Lock className={cx("bi", "me-1")} />
Private
</>
),
icon: <Lock className={cx("bi", "me-1")} />,
label: "Private",
},
],
allowSelectMany: false,
Expand All @@ -201,15 +169,15 @@ export const FILTER_MY_ROLE: EnumFilter = {
allowedValues: [
{
value: "owner",
label: <>Owner</>,
label: "Owner",
},
{
value: "editor",
label: <>Editor</>,
label: "Editor",
},
{
value: "viewer",
label: <>Viewer</>,
label: "Viewer",
},
],
allowSelectMany: true,
Expand All @@ -236,27 +204,27 @@ export const FILTER_DATE: EnumFilter = {
allowedValues: [
{
value: "",
label: <>All</>,
label: "All",
id: "any",
},
{
value: ">today-7d",
label: <>Last week</>,
label: "Last week",
id: "last-week",
},
{
value: ">today-31d",
label: <>Last month</>,
label: "Last month",
id: "last-month",
},
{
value: ">today-90d",
label: <>Last 90 days</>,
label: "Last 90 days",
id: "last-90-days",
},
{
value: "<today-90d",
label: <>Older than 90 days</>,
label: "Older than 90 days",
id: "older-than-90-days",
},
],
Expand Down
5 changes: 3 additions & 2 deletions client/src/features/searchV2/contextSearch.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export type GroupSearchEntity = Exclude<
{ type: "Group" | "User" }
>;

interface FilterValue {
label: ReactNode;
export interface FilterValue {
label: string | ReactNode;
icon?: ReactNode;
quantity?: number;
value: string;
id?: string;
Expand Down
Loading
Loading