All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.js.components.OrderableTable.tsx Maven / Gradle / Ivy

There is a newer version: 1.0.6
Show newest version
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

import ClayButton, {ClayButtonWithIcon} from '@clayui/button';
import ClayDropDown, {ClayDropDownWithItems} from '@clayui/drop-down';
import ClayEmptyState from '@clayui/empty-state';
import ClayIcon from '@clayui/icon';
import ClayLayout from '@clayui/layout';
import ClayTable from '@clayui/table';
import classNames from 'classnames';
import {ManagementToolbar} from 'frontend-js-components-web';
import fuzzy from 'fuzzy';
import React, {useEffect, useRef, useState} from 'react';
import {DndProvider, useDrag, useDrop} from 'react-dnd';
import {HTML5Backend} from 'react-dnd-html5-backend';

import {FUZZY_OPTIONS} from '../utils/constants';
import Search from './Search';

import '../../css/components/OrderableTable.scss';

const ROW_DRAGGABLE = 'rowDraggable';

interface IAction {
	icon: string;
	label: string;
	onClick: Function;
}

interface IContentRendererProps {
	item: any;
	query: string;
}

interface IContentRenderer {
	component: React.FC;
	textMatch?: Function;
}

interface IField {
	contentRenderer?: IContentRenderer;
	headingTitle?: boolean;
	label: string;
	name: string;
}

const Row = ({
	actions,
	fields,
	index,
	item,
	onDragCrossover,
	onDrop,
	query,
}: {
	actions?: Array;
	fields: Array;
	index: number;
	item: any;
	onDragCrossover: Function;
	onDrop: Function;
	query: string;
}) => {
	const tableRowRef = useRef(null);

	const [{isDragging}, dragRef] = useDrag({
		collect: (monitor) => ({
			isDragging: monitor.isDragging(),
		}),
		item: {
			index,
			type: ROW_DRAGGABLE,
		},
	});

	const onBlur = () => {
		const currentRow = tableRowRef?.current;

		if (currentRow) {
			const dragging = currentRow.classList.contains('dragging');

			if (dragging) {
				currentRow.classList.remove('dragging');
				onDrop();
			}
		}
	};

	const onKeyDown = (event: React.KeyboardEvent) => {
		const currentRow = tableRowRef?.current;

		if (currentRow) {
			const dragging = currentRow.classList.contains('dragging');

			if (event.key === 'Enter') {
				if (!dragging) {
					currentRow.classList.add('dragging');

					const draggedIndex = index;
					const targetIndex = index;

					onDragCrossover({draggedIndex, targetIndex});
				}
				else {
					currentRow.classList.remove('dragging');
					onDrop();
				}
			}
			else if (event.key === 'ArrowDown' && dragging) {
				const draggedIndex = index;
				const targetIndex = index + 1;

				onDragCrossover({draggedIndex, targetIndex});
			}
			else if (event.key === 'ArrowUp' && dragging) {
				const draggedIndex = index;
				const targetIndex = index - 1;

				if (targetIndex >= 0) {
					onDragCrossover({draggedIndex, targetIndex});
				}
			}
			else if (
				(event.key === 'Escape' || event.key === 'Tab') &&
				dragging
			) {
				currentRow.classList.remove('dragging');
				onDrop();
			}
		}
	};

	const [, dropRef] = useDrop({
		accept: ROW_DRAGGABLE,
		hover(item: {index: number; type: string}, monitor) {
			if (!tableRowRef.current || !onDragCrossover) {
				return;
			}

			const draggedIndex = item.index;
			const targetIndex = index;

			if (draggedIndex === targetIndex) {
				return;
			}

			const targetSize = tableRowRef.current.getBoundingClientRect();
			const targetCenter = (targetSize.bottom - targetSize.top) / 2;

			const draggedOffset: {
				x: number;
				y: number;
			} | null = monitor.getClientOffset();

			if (!draggedOffset) {
				return;
			}

			const draggedTop = draggedOffset.y - targetSize.top;

			if (
				(draggedIndex < targetIndex && draggedTop < targetCenter) ||
				(draggedIndex > targetIndex && draggedTop > targetCenter)
			) {
				return;
			}

			onDragCrossover({draggedIndex, targetIndex});

			item.index = targetIndex;
		},
	});

	dragRef(dropRef(tableRowRef));

	return (
		
			
				{tableRowRef?.current?.classList.contains('dragging') ? (
					
						{Liferay.Language.get(
							'use-up-and-down-arrows-to-move-the-field-and-press-enter-to-place-it-in-desired-position'
						)}
					
				) : null}

				
			

			{fields.map((field) => {
				if (field.contentRenderer) {
					const Component = field.contentRenderer
						.component as React.FC;

					return (
						
							
						
					);
				}

				const itemFieldValue = String(item[field.name]);

				const fuzzyMatch = fuzzy.match(
					query,
					itemFieldValue,
					FUZZY_OPTIONS
				);

				return (
					
						{fuzzyMatch ? (
							
						) : (
							{itemFieldValue}
						)}
					
				);
			})}

			{actions && (
				
					
								

								
									{Liferay.Language.get('actions')}
								
							
						}
					>
						
							{actions.map(({icon, label, onClick}) => (
								
										onClick({
											item,
										})
									}
								>
									{icon && (
										
											
										
									)}

									{label}
								
							))}
						
					
				
			)}
		
	);
};

const Table = ({
	actions,
	fields,
	items,
	onDragCrossover,
	onDrop,
	query,
}: {
	actions?: Array;
	fields: Array;
	items: Array;
	onDragCrossover: Function;
	onDrop: Function;
	query: string;
}) => {
	const [, dropRef] = useDrop({
		accept: ROW_DRAGGABLE,
		drop() {
			onDrop();
		},
	});

	return (
		
			
				
					

					{fields.map((field) => (
						
							{field.label}
						
					))}

					{actions && }
				
			

			
				{items.map((item, index) => (
					
				))}
			
		
	);
};

interface IOrderableTableProps {
	actions?: Array;
	className?: string;
	creationMenuItems?: React.ComponentProps<
		typeof ClayDropDownWithItems
	>['items'];
	creationMenuLabel?: string;
	fields: Array;
	items: Array;
	noItemsButtonLabel: string;
	noItemsDescription: string;
	noItemsTitle: string;
	onOrderChange: (args: {order: string}) => void;
	title?: string;
}

const OrderableTable = ({
	actions,
	className,
	creationMenuItems,
	creationMenuLabel = Liferay.Language.get('new'),
	fields,
	items: initialItems,
	noItemsButtonLabel,
	noItemsDescription,
	noItemsTitle,
	onOrderChange,
	title,
}: IOrderableTableProps) => {
	const [items, setItems] = useState(initialItems);
	const [order, setOrder] = useState(
		initialItems.map((item) => item.id).join(',')
	);
	const [query, setQuery] = useState('');

	useEffect(() => setItems(initialItems), [initialItems]);

	const onSearch = (query: string) => {
		setQuery(query);

		const regexp = new RegExp(query, 'i');

		setItems(
			query
				? initialItems.filter((item) =>
						fields.some((field) => {
							if (field.contentRenderer?.textMatch) {
								return String(
									field.contentRenderer.textMatch(item)
								).match(regexp);
							}

							return String(item[field.name]).match(regexp);
						})
					) || []
				: initialItems
		);
	};

	return (
		
			{title && (
				
					

{title}

)} {creationMenuItems?.length && ( {creationMenuItems.length > 1 ? ( } /> ) : ( )} )} {items.length ? ( { const orderedItems = [...items]; if (draggedIndex !== targetIndex) { orderedItems.splice(draggedIndex, 1); orderedItems.splice( targetIndex, 0, items[draggedIndex] ); } setItems(orderedItems); }} onDrop={() => { const newOrder = items .map((item) => item.id) .join(','); if (newOrder !== order) { setOrder(newOrder); onOrderChange({order: newOrder}); } }} query={query} /> ) : query ? ( ) : ( {creationMenuItems?.length && (creationMenuItems.length > 1 ? ( {noItemsButtonLabel} } /> ) : ( {noItemsButtonLabel} ))} )} ); }; export default OrderableTable;