diff --git a/package.json b/package.json index 185a4c88..7fb663ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qoretechnologies/reqore", - "version": "0.36.1", + "version": "0.36.2", "description": "ReQore is a highly theme-able and modular UI library for React", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/components/InternalPopover/index.tsx b/src/components/InternalPopover/index.tsx index 41f4a633..aed794e5 100644 --- a/src/components/InternalPopover/index.tsx +++ b/src/components/InternalPopover/index.tsx @@ -59,8 +59,8 @@ const StyledPopoverArrow = styled.div<{ theme: IReqoreTheme }>` const StyledPopoverWrapper = styled.div<{ theme: IReqoreTheme }>` animation: 0.2s ${fadeIn} ease-out; - max-width: ${({ maxWidth = '80vw' }) => maxWidth}; - max-height: ${({ maxHeight = '80vh' }) => maxHeight}; + max-width: ${({ maxWidth }) => maxWidth}; + max-height: ${({ maxHeight }) => maxHeight}; z-index: 999999; border-radius: ${RADIUS_FROM_SIZE.normal}px; border: ${({ flat, noWrapper, ...rest }: any) => diff --git a/src/components/KeyValueTable/index.tsx b/src/components/KeyValueTable/index.tsx index 8f36a60a..409848d5 100644 --- a/src/components/KeyValueTable/index.tsx +++ b/src/components/KeyValueTable/index.tsx @@ -1,7 +1,9 @@ import { keys } from 'lodash'; import { useMemo } from 'react'; +import { SIZE_TO_PX } from '../../constants/sizes'; import { TReqoreIntent } from '../../constants/theme'; import { IReqoreIconName } from '../../types/icons'; +import { IReqoreButtonProps } from '../Button'; import { IReqorePanelProps } from '../Panel'; import ReqoreTable, { IReqoreTableColumn, IReqoreTableProps, IReqoreTableRowData } from '../Table'; import { IReqoreTableValueProps, ReqoreTableValue } from '../Table/value'; @@ -21,6 +23,7 @@ export interface IReqoreKeyValueTableProps | 'height' | 'fill' | 'filterable' + | 'exportable' | 'filter' | 'onFilterChange' | 'filterProps' @@ -36,6 +39,10 @@ export interface IReqoreKeyValueTableProps keyAlign?: IReqoreTableColumn['align']; maxKeyWidth?: number; + sortable?: boolean; + + rowActions?: (key: string, value: TReqoreKeyValueTableValue) => IReqoreButtonProps[]; + valueLabel?: string; valueIcon?: IReqoreIconName; valueAlign?: IReqoreTableColumn['align']; @@ -61,6 +68,8 @@ export const ReqoreKeyValueTable = ({ keyAlign = 'left', valueAlign = 'left', defaultValueFilter, + sortable, + rowActions, ...rest }: IReqoreKeyValueTableProps) => { const { columns, items } = useMemo(() => { @@ -68,6 +77,7 @@ export const ReqoreKeyValueTable = ({ { dataId: 'tableKey', grow: 1, + sortable, width: maxKeyWidth < 150 ? maxKeyWidth : 150, pin: 'left', hideable: false, @@ -87,6 +97,7 @@ export const ReqoreKeyValueTable = ({ { dataId: 'value', grow: 3, + sortable, hideable: false, filterable: true, pinnable: false, @@ -100,12 +111,34 @@ export const ReqoreKeyValueTable = ({ }, cell: { + tooltip: (value) => ({ + content: JSON.stringify(value), + noArrow: true, + useTargetWidth: true, + }), content: (data) => valueRenderer?.(data, ReqoreTableValue) || , }, }, ]; + if (rowActions) { + columns.push({ + dataId: 'actions', + hideable: false, + pin: 'right', + header: { + icon: 'SettingsLine', + }, + width: SIZE_TO_PX[rest.size || 'normal'] * 2, + align: 'center', + cell: { + padded: 'none', + actions: (data) => rowActions(data.tableKey, data.value), + }, + }); + } + let items: IReqoreTableRowData[] = []; keys(data).map((key) => { @@ -123,6 +156,15 @@ export const ReqoreKeyValueTable = ({ columns={columns} data={items} {...rest} + sort={ + sortable + ? { + by: 'tableKey', + thenBy: 'value', + direction: 'asc', + } + : undefined + } className={`${rest.className || ''} reqore-key-value-table`} /> ); diff --git a/src/components/Message/index.tsx b/src/components/Message/index.tsx index 67f69faf..991a6999 100644 --- a/src/components/Message/index.tsx +++ b/src/components/Message/index.tsx @@ -52,6 +52,7 @@ export interface IReqoreMessageProps onFinish?: () => any; flat?: boolean; hasShadow?: boolean; + margin?: 'top' | 'bottom' | 'both' | 'none'; } export interface IReqoreNotificationStyle extends IReqoreMessageProps { diff --git a/src/components/Notifications/notification.tsx b/src/components/Notifications/notification.tsx index a7d5b17c..e42b30fa 100644 --- a/src/components/Notifications/notification.tsx +++ b/src/components/Notifications/notification.tsx @@ -61,6 +61,7 @@ export interface IReqoreNotificationStyle extends IWithReqoreOpaque { minimal?: boolean; size?: TSizes; asMessage?: boolean; + margin?: 'top' | 'bottom' | 'both' | 'none'; } const timeoutAnimation = keyframes` @@ -84,6 +85,15 @@ export const StyledReqoreNotification = styled(StyledEffect) css` + margin-top: ${margin === 'top' || margin === 'both' + ? `${PADDING_FROM_SIZE[size]}px` + : undefined}; + margin-bottom: ${margin === 'bottom' || margin === 'both' + ? `${PADDING_FROM_SIZE[size]}px` + : undefined}; + `}; + // Do not fade in the component if it's a message ${({ asMessage }) => { if (asMessage) { @@ -95,7 +105,8 @@ export const StyledReqoreNotification = styled(StyledEffect) (asMessage ? undefined : `10px`)}; + margin-top: ${({ asMessage, size }) => + asMessage ? undefined : `${PADDING_FROM_SIZE[size]}px`}; } ${({ diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index a0de0ca1..ba5913ea 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -31,6 +31,8 @@ const Popover = memo( }: IReqorePopoverProps) => { const [ref, setRef] = useState(undefined); + console.log(rest); + const popoverData: IPopoverControls = usePopover({ targetElement: ref, ...rest }); useEffect(() => { @@ -38,6 +40,7 @@ const Popover = memo( }, [popoverData]); if (isReqoreComponent) { + console.log(Component); return ( useMount(() => { targetRef.current?.addEventListener('wheel', (e) => { - if (e.deltaY) { - e.preventDefault(); + // Only scroll if the element is scrollable + if (refs[type].current?.scrollHeight > refs[type].current?.clientHeight) { + if (e.deltaY) { + e.preventDefault(); - const currentScroll = refs[type].current?.scrollTop + e.deltaY; + const currentScroll = refs[type].current?.scrollTop + e.deltaY; - onScrollChange?.(currentScroll > 0); + onScrollChange?.(currentScroll > 0); - refs[type].current?.scrollTo({ top: currentScroll }); + refs[type].current?.scrollTo({ top: currentScroll }); - if (type === 'left') { - refs.main.current?.scrollTo({ top: refs.main.current?.scrollTop + e.deltaY }); - refs.right.current?.scrollTo({ top: refs.right.current?.scrollTop + e.deltaY }); - } else if (type === 'main') { - refs.left.current?.scrollTo({ top: refs.left.current?.scrollTop + e.deltaY }); - refs.right.current?.scrollTo({ top: refs.right.current?.scrollTop + e.deltaY }); - } else if (type === 'right') { - refs.main.current?.scrollTo({ top: refs.main.current?.scrollTop + e.deltaY }); - refs.left.current?.scrollTo({ top: refs.left.current?.scrollTop + e.deltaY }); + if (type === 'left') { + refs.main.current?.scrollTo({ top: refs.main.current?.scrollTop + e.deltaY }); + refs.right.current?.scrollTo({ top: refs.right.current?.scrollTop + e.deltaY }); + } else if (type === 'main') { + refs.left.current?.scrollTo({ top: refs.left.current?.scrollTop + e.deltaY }); + refs.right.current?.scrollTo({ top: refs.right.current?.scrollTop + e.deltaY }); + } else if (type === 'right') { + refs.main.current?.scrollTo({ top: refs.main.current?.scrollTop + e.deltaY }); + refs.left.current?.scrollTo({ top: refs.left.current?.scrollTop + e.deltaY }); + } } - } - if (e.deltaX && type === 'main') { - refs.header.current?.scrollTo({ left: refs.main.current?.scrollLeft + e.deltaX }); + if (e.deltaX && type === 'main') { + refs.header.current?.scrollTo({ left: refs.main.current?.scrollLeft + e.deltaX }); + } } }); }); diff --git a/src/components/Table/cell.tsx b/src/components/Table/cell.tsx index 2833e723..8344da5b 100644 --- a/src/components/Table/cell.tsx +++ b/src/components/Table/cell.tsx @@ -1,9 +1,13 @@ import { lighten, rgba } from 'polished'; +import { forwardRef } from 'react'; import styled, { css } from 'styled-components'; import { IReqoreTableColumn } from '.'; import { TEXT_FROM_SIZE } from '../../constants/sizes'; import { changeLightness, getReadableColorFrom } from '../../helpers/colors'; import { alignToFlexAlign } from '../../helpers/utils'; +import { useCombinedRefs } from '../../hooks/useCombinedRefs'; +import { useTooltip } from '../../hooks/useTooltip'; +import { IWithReqoreTooltip } from '../../types/global'; import { TReqoreColor } from '../Effect'; import { IReqoreTableCellStyle } from './row'; @@ -11,7 +15,8 @@ export interface IReqoreCustomTableBodyCellProps extends IReqoreTableBodyCellPro export interface IReqoreCustomTableBodyCell extends React.FC {} export interface IReqoreTableBodyCellProps extends Partial, - React.HTMLAttributes { + React.HTMLAttributes, + IWithReqoreTooltip { children?: React.ReactNode; padded?: IReqoreTableColumn['cell']['padded']; } @@ -118,6 +123,12 @@ export const StyledTableCell = styled.div` }} `; -export const ReqoreTableBodyCell = (props: IReqoreTableBodyCellProps) => { - return ; -}; +export const ReqoreTableBodyCell = forwardRef( + (props: IReqoreTableBodyCellProps, ref) => { + const { targetRef } = useCombinedRefs(ref); + + useTooltip(targetRef.current, props.tooltip); + + return ; + } +); diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 530e876e..61b127ec 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -3,10 +3,17 @@ import { size as count, isArray } from 'lodash'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useMeasure, useUpdateEffect } from 'react-use'; import styled, { css } from 'styled-components'; -import { ReqoreMessage, ReqorePaginationContainer, ReqorePanel, ReqoreVerticalSpacer } from '../..'; +import { + ReqoreMessage, + ReqorePaginationContainer, + ReqorePanel, + ReqoreVerticalSpacer, + useReqoreTheme, +} from '../..'; import { TReqorePaginationType, getPagingObjectFromType } from '../../constants/paging'; import { TABLE_SIZE_TO_PX, TSizes } from '../../constants/sizes'; import { IReqoreTheme, TReqoreIntent } from '../../constants/theme'; +import ReqoreThemeProvider from '../../containers/ThemeProvider'; import { useQueryWithDelay } from '../../hooks/useQueryWithDelay'; import { IReqoreIntent, IReqoreTooltip } from '../../types/global'; import { IReqoreButtonProps, TReqoreBadge } from '../Button'; @@ -183,7 +190,6 @@ const ReqoreTable = ({ selected, onSelectedChange, selectToggleTooltip, - customTheme, onRowClick, striped, selectedRowIntent = 'info', @@ -222,6 +228,7 @@ const ReqoreTable = ({ const [_internalColumns, setColumns] = useState(columns); const [zoom, setZoom] = useState(sizeToZoom[size]); const [showExportModal, setShowExportModal] = useState<'full' | 'current' | undefined>(undefined); + const theme = useReqoreTheme('main', rest.customTheme, intent); const [wrapperRef, sizes] = useMeasure(); const { query, preQuery, setQuery, setPreQuery } = useQueryWithDelay( @@ -554,6 +561,7 @@ const ReqoreTable = ({ if (exportable) { moreActions = [ + ...moreActions, ...getExportActions((type) => setShowExportModal(type)), { divider: true, line: true }, ]; @@ -561,6 +569,7 @@ const ReqoreTable = ({ if (zoomable) { moreActions = [ + ...moreActions, ...getZoomActions('reqore-table', zoom, setZoom, true), { divider: true, line: true }, ]; @@ -703,47 +712,49 @@ const ReqoreTable = ({ getContentRef={wrapperRef} badge={badge} > - - items={transformedData} - type={ - pagingOptions - ? { - ...pagingOptions, - onPageChange: () => { - if (!pagingOptions.infinite) { - handleScrollToTop(); - } - }, - } - : undefined - } - > - {(pagedData) => ( - <> - {showExportModal && ( - setShowExportModal(undefined)} - /> - )} - - {renderTable('left', pagedData)} - {renderTable('main', pagedData)} - {renderTable('right', pagedData)} - - {count(pagedData) === 0 ? ( - <> - - - {emptyMessage} - - - ) : null} - - )} - + + + items={transformedData} + type={ + pagingOptions + ? { + ...pagingOptions, + onPageChange: () => { + if (!pagingOptions.infinite) { + handleScrollToTop(); + } + }, + } + : undefined + } + > + {(pagedData) => ( + <> + {showExportModal && ( + setShowExportModal(undefined)} + /> + )} + + {renderTable('left', pagedData)} + {renderTable('main', pagedData)} + {renderTable('right', pagedData)} + + {count(pagedData) === 0 ? ( + <> + + + {emptyMessage} + + + ) : null} + + )} + + ); diff --git a/src/components/Table/row.tsx b/src/components/Table/row.tsx index c9b53f50..2e8d55e1 100644 --- a/src/components/Table/row.tsx +++ b/src/components/Table/row.tsx @@ -3,7 +3,7 @@ import { isFunction, isString } from 'lodash'; import React, { ReactElement, useState } from 'react'; import styled, { css } from 'styled-components'; import { IReqoreTableColumn, IReqoreTableData, IReqoreTableRowClick } from '.'; -import { ReqoreButton, ReqoreControlGroup, ReqorePopover } from '../..'; +import { ReqoreButton, ReqoreControlGroup } from '../..'; import { SIZE_TO_PX, TSizes } from '../../constants/sizes'; import { IReqoreTheme, TReqoreIntent } from '../../constants/theme'; import { IReqoreTooltip } from '../../types/global'; @@ -200,48 +200,44 @@ const ReqoreTableRow = ({ : {}; return ( - ) => { - if (cell?.onClick) { - e.stopPropagation(); - cell.onClick(data[index]); - } else if (onRowClick) { - e.stopPropagation(); - onRowClick(data[index]); - } else if (selectable && data[index]._selectId) { - // Otherwise select the row if selectable - onSelectClick(data[index]._selectId!); - } - }, - className: 'reqore-table-cell', - } as IReqoreTableCellStyle - } - {...tooltip} + {...({ + width: resizedWidth || width, + minWidth, + maxWidth, + grow, + align, + size, + striped, + tooltip, + padded: cell?.padded, + disabled: data[index]._disabled, + selected: !!isSelected, + selectedIntent: selectedRowIntent, + flat, + even: index % 2 === 0 ? true : false, + intent: cell?.intent || data[index]._intent || intent, + hovered: isHovered, + interactive: !!cell?.onClick || !!onRowClick, + interactiveCell: !!cell?.onClick, + onClick: (e: React.MouseEvent) => { + if (cell?.onClick) { + e.stopPropagation(); + cell.onClick(data[index]); + } else if (onRowClick) { + e.stopPropagation(); + onRowClick(data[index]); + } else if (selectable && data[index]._selectId) { + // Otherwise select the row if selectable + onSelectClick(data[index]._selectId!); + } + }, + className: 'reqore-table-cell', + } as IReqoreTableCellStyle)} > {renderContent(cell, data[index], dataId, align)} - + ); } ); diff --git a/src/constants/sizes.ts b/src/constants/sizes.ts index 6fc33782..3e1f869f 100644 --- a/src/constants/sizes.ts +++ b/src/constants/sizes.ts @@ -110,11 +110,11 @@ export const TEXT_FROM_SIZE = { }; export const ICON_FROM_SIZE = { - tiny: 11, - small: 14, - normal: 17, - big: 20, - huge: 23, + tiny: 13, + small: 17, + normal: 20, + big: 26, + huge: 33, }; export const PADDING_FROM_SIZE = { diff --git a/src/stories/KeyValueTable/KeyValueTable.stories.tsx b/src/stories/KeyValueTable/KeyValueTable.stories.tsx index 583be62a..cec51c20 100644 --- a/src/stories/KeyValueTable/KeyValueTable.stories.tsx +++ b/src/stories/KeyValueTable/KeyValueTable.stories.tsx @@ -3,6 +3,7 @@ import ReqoreIcon from '../../components/Icon'; import { IReqoreKeyValueTableProps, ReqoreKeyValueTable } from '../../components/KeyValueTable'; import { IReqoreTableRowData, TReqoreTableColumnContent } from '../../components/Table'; import { TReqorePaginationType } from '../../constants/paging'; +import { Exportable as ExportableTable } from '../Table/Table.stories'; import { StoryMeta } from '../utils'; import { CustomIntentArg, FlatArg, IntentArg, SizeArg, argManager } from '../utils/args'; @@ -112,6 +113,23 @@ export const Striped: Story = { }, }; +export const Exportable: Story = { + args: { + exportable: true, + zoomable: true, + }, + play: async ({ canvasElement, ...rest }) => { + // @ts-ignore + await ExportableTable.play({ canvasElement, ...rest }); + }, +}; + +export const Sortable: Story = { + args: { + sortable: true, + }, +}; + export const Filterable: Story = { args: { filterable: true, @@ -139,6 +157,29 @@ export const AllFiltersActive: Story = { }, }; +export const WithActions: Story = { + args: { + rowActions: () => [ + { + icon: 'EditLine', + }, + { + icon: 'DeleteBinLine', + intent: 'danger', + }, + ], + zoomable: true, + }, +}; + +export const CustomTheme: Story = { + args: { + customTheme: { main: '#ff0000' }, + flat: false, + transparent: false, + }, +}; + export const NoDataMessage: Story = { args: { filterable: true, diff --git a/src/stories/Message/Message.stories.tsx b/src/stories/Message/Message.stories.tsx index f9dd36c4..ccf945ad 100644 --- a/src/stories/Message/Message.stories.tsx +++ b/src/stories/Message/Message.stories.tsx @@ -109,6 +109,31 @@ export const CustomTheme: Story = { }, }; +export const WithMargin: Story = { + render: () => ( + <> + + In to am attended desirous raptures declared diverted confined at. Collected instantly + remaining up certainly to necessary as. Over walk dull into son boy door went new. At or + happiness commanded daughters as. + + + In to am attended desirous raptures declared diverted confined at. + + + In to am attended desirous raptures declared diverted confined at. Collected instantly + remaining up certainly to necessary as. Over walk dull into son boy door went new. At or + happiness commanded daughters as. + + + In to am attended desirous raptures declared diverted confined at. Collected instantly + remaining up certainly to necessary as. Over walk dull into son boy door went new. At or + happiness commanded daughters as. + + + ), +}; + export const Effect: Story = { render: Template,