Skip to content

Commit

Permalink
Fixes #2986 - Multiple UI Updates (#3165)
Browse files Browse the repository at this point in the history
* UI fixes on organisation pages

* Added TSDoc for Truncated Text

* Added Debouncer

* Update src/components/OrgListCard/OrgListCard.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Added code rabbit suggestions

* Fixed test error

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
AceHunterr and coderabbitai[bot] authored Jan 7, 2025
1 parent 3b168ae commit ef5a206
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 25 deletions.
21 changes: 21 additions & 0 deletions src/assets/css/app.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
2 changes: 2 additions & 0 deletions src/components/EventCalendar/EventHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function eventHeader({
id="dropdown-basic"
className={styles.dropdown}
data-testid="selectViewType"
style={{ width: '100%' }}
>
{viewType}
</Dropdown.Toggle>
Expand Down Expand Up @@ -100,6 +101,7 @@ function eventHeader({
id="dropdown-basic"
className={styles.dropdown}
data-testid="eventType"
style={{ width: '100%' }}
>
{t('eventType')}
</Dropdown.Toggle>
Expand Down
21 changes: 12 additions & 9 deletions src/components/OrgListCard/OrgListCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import TruncatedText from './TruncatedText';
// import {useState} from 'react';
import FlaskIcon from 'assets/svgs/flask.svg?react';
import Button from 'react-bootstrap/Button';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -94,17 +96,18 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element {
<h4 className={`${styles.orgName} fw-semibold`}>{name}</h4>
</Tooltip>
{/* Description of the organization */}
<h6 className={`${styles.orgdesc} fw-semibold`}>
<span>{userData?.organizations[0].description}</span>
</h6>
<div className={`${styles.orgdesc} fw-semibold`}>
<TruncatedText
text={userData?.organizations[0]?.description || ''}
/>
</div>

{/* Display the organization address if available */}
{address && address.city && (
{address?.city && (
<div className={styles.address}>
<h6 className="text-secondary">
<span className="address-line">{address.line1}, </span>
<span className="address-line">{address.city}, </span>
<span className="address-line">{address.countryCode}</span>
</h6>
<TruncatedText
text={`${address?.line1}, ${address?.city}, ${address?.countryCode}`}
/>
</div>
)}
{/* Display the number of admins and members */}
Expand Down
80 changes: 80 additions & 0 deletions src/components/OrgListCard/TruncatedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useState, useEffect, useRef } from 'react';
import useDebounce from './useDebounce';

/**
* Props for the `TruncatedText` component.
*
* Includes the text to be displayed and an optional maximum width override.
*/
interface InterfaceTruncatedTextProps {
/** The full text to display. It may be truncated if it exceeds the maximum width. */
text: string;
/** Optional: Override the maximum width for truncation. */
maxWidthOverride?: number;
}

/**
* A React functional component that displays text and truncates it with an ellipsis (`...`)
* if the text exceeds the available width or the `maxWidthOverride` value.
*
* The component adjusts the truncation dynamically based on the available space
* or the `maxWidthOverride` value. It also listens for window resize events to reapply truncation.
*
* @param props - The props for the component.
* @returns A heading element (`<h6>`) containing the truncated or full text.
*
* @example
* ```tsx
* <TruncatedText text="This is a very long text" maxWidthOverride={150} />
* ```
*/
const TruncatedText: React.FC<InterfaceTruncatedTextProps> = ({
text,
maxWidthOverride,
}) => {
const [truncatedText, setTruncatedText] = useState<string>('');
const textRef = useRef<HTMLHeadingElement>(null);

const { debouncedCallback, cancel } = useDebounce(() => {
truncateText();
}, 100);

/**
* Truncate the text based on the available width or the `maxWidthOverride` value.
*/
const truncateText = (): void => {
const element = textRef.current;
if (element) {
const maxWidth = maxWidthOverride || element.offsetWidth;
const fullText = text;

const computedStyle = getComputedStyle(element);
const fontSize = parseFloat(computedStyle.fontSize);
const charPerPx = 0.065 + fontSize * 0.002;
const maxChars = Math.floor(maxWidth * charPerPx);

setTruncatedText(
fullText.length > maxChars
? `${fullText.slice(0, maxChars - 3)}...`
: fullText,
);
}
};

useEffect(() => {
truncateText();
window.addEventListener('resize', debouncedCallback);
return () => {
cancel();
window.removeEventListener('resize', debouncedCallback);
};
}, [text, maxWidthOverride, debouncedCallback, cancel]);

return (
<h6 ref={textRef} className="text-secondary">
{truncatedText}
</h6>
);
};

export default TruncatedText;
42 changes: 42 additions & 0 deletions src/components/OrgListCard/useDebounce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRef, useCallback } from 'react';

/**
* A custom React hook for debouncing a callback function.
* It delays the execution of the callback until after a specified delay has elapsed
* since the last time the debounced function was invoked.
*
* @param callback - The function to debounce.
* @param delay - The delay in milliseconds to wait before invoking the callback.
* @returns An object with the `debouncedCallback` function and a `cancel` method to clear the timeout.
*/
function useDebounce<T extends (...args: unknown[]) => void>(
callback: T,
delay: number,
): { debouncedCallback: (...args: Parameters<T>) => void; cancel: () => void } {
const timeoutRef = useRef<number | undefined>();

/**
* The debounced version of the provided callback function.
* This function resets the debounce timer on each call, ensuring the callback
* is invoked only after the specified delay has elapsed without further calls.
*
* @param args - The arguments to pass to the callback when invoked.
*/
const debouncedCallback = useCallback(
(...args: Parameters<T>) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
callback(...args);
}, delay);
},
[callback, delay],
);

const cancel = useCallback(() => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
}, []);

return { debouncedCallback, cancel };
}

export default useDebounce;
1 change: 1 addition & 0 deletions src/components/UsersTableItem/UsersTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const UsersTableItem = (props: Props): JSX.Element => {
<td>{user.user.email}</td>
<td>
<Button
className="btn btn-success"
onClick={() => setShowJoinedOrganizations(true)}
data-testid={`showJoinedOrgsBtn${user.user._id}`}
>
Expand Down
3 changes: 3 additions & 0 deletions src/screens/OrgList/OrgList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,9 @@ function orgList(): JSX.Element {
)}
</div>
</div>

{/* Text Infos for list */}

{!isLoading &&
(!orgsData?.organizationsConnection ||
orgsData.organizationsConnection.length === 0) &&
Expand Down Expand Up @@ -485,6 +487,7 @@ function orgList(): JSX.Element {
<div
className={`${styles.orgImgContainer} shimmer`}
></div>

<div className={styles.content}>
<h5 className="shimmer" title="Org name"></h5>
<h6 className="shimmer" title="Location"></h6>
Expand Down
56 changes: 45 additions & 11 deletions src/screens/OrganizationPeople/OrganizationPeople.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,52 @@ function organizationPeople(): JSX.Element {
headerAlign: 'center',
headerClassName: `${styles.tableHeader}`,
sortable: false,

renderCell: (params: GridCellParams) => {
return params.row?.image ? (
<img
src={params.row?.image}
alt="avatar"
className={styles.TableImage}
/>
) : (
<Avatar
avatarStyle={styles.TableImage}
name={`${params.row.firstName} ${params.row.lastName}`}
/>
// Fallback to a fixed width if computedWidth is unavailable
const columnWidth = params.colDef.computedWidth || 150;
const imageSize = Math.min(columnWidth * 0.6, 60); // Max size 40px, responsive scaling

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
}}
>
{params.row?.image ? (
<img
src={params.row?.image}
alt="avatar"
style={{
width: `${imageSize}px`,
height: `${imageSize}px`,
borderRadius: '50%',
objectFit: 'cover',
}}
/>
) : (
<div
style={{
width: `${imageSize}px`,
height: `${imageSize}px`,
fontSize: `${imageSize * 0.4}px`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
backgroundColor: '#ccc',
}}
>
<Avatar
name={`${params.row.firstName} ${params.row.lastName}`}
/>
</div>
)}
</div>
);
},
},
Expand Down
Loading

0 comments on commit ef5a206

Please sign in to comment.