2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-25 16:17:58 +00:00

feat: add copyable cells feature with CopyButton component (#11406)

This commit is contained in:
Tin Pham
2026-02-23 00:59:22 -08:00
committed by GitHub
parent 33c57887ad
commit edc639d55c
5 changed files with 77 additions and 2 deletions

View File

@@ -99,6 +99,8 @@ export type TableState = {
* @param cellsStyle - The style of the cells in the column
* @param extra - Extra data to pass to the render function
* @param noContext - Disable context menu for this column
* @param copyable - Enable copy button on hover (uses accessor to get value, or custom function)
* @param copyAccessor - Custom accessor path for copy value (defaults to column accessor)
*/
export type TableColumnProps<T = any> = {
accessor?: string;
@@ -123,6 +125,8 @@ export type TableColumnProps<T = any> = {
extra?: any;
noContext?: boolean;
style?: MantineStyleProp;
copyable?: boolean | ((record: T) => string);
copyAccessor?: string;
};
/**

View File

@@ -46,7 +46,11 @@ export function CopyButton({
<ButtonComponent
disabled={disabled}
color={copied ? 'teal' : color}
onClick={copy}
onClick={(e: any) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
variant='transparent'
size={size ?? 'sm'}
>

View File

@@ -0,0 +1,40 @@
import { Group } from '@mantine/core';
import { useState } from 'react';
import { CopyButton } from '../components/buttons/CopyButton';
/**
* A wrapper component that adds a copy button to cell content on hover
* This component is used to make table cells copyable without adding visual clutter
*
* @param children - The cell content to render
* @param value - The value to copy when the copy button is clicked
*/
export function CopyableCell({
children,
value
}: Readonly<{
children: React.ReactNode;
value: string;
}>) {
const [isHovered, setIsHovered] = useState(false);
return (
<Group
gap='xs'
wrap='nowrap'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
justify='space-between'
>
{children}
{isHovered && value != null && (
<span
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<CopyButton value={value} />
</span>
)}
</Group>
);
}

View File

@@ -33,6 +33,7 @@ import { hashString } from '../functions/tables';
import { useLocalState } from '../states/LocalState';
import { useUserSettingsState } from '../states/SettingsStates';
import { useStoredTableState } from '../states/StoredTableState';
import { CopyableCell } from './CopyableCell';
import InvenTreeTableHeader from './InvenTreeTableHeader';
const ACTIONS_COLUMN_ACCESSOR: string = '--actions--';
@@ -264,11 +265,36 @@ export function InvenTreeTable<T extends Record<string, any>>({
? false
: (tableState.hiddenColumns?.includes(col.accessor) ?? false);
// Wrap the render function with CopyableCell if copyable is enabled
const originalRender = col.render;
let wrappedRender = originalRender;
if (col.copyable) {
wrappedRender = (record: any, index?: number) => {
const content =
originalRender?.(record, index) ??
resolveItem(record, col.accessor);
// Determine the value to copy, ensuring it is always a string
let rawCopyValue: unknown;
if (typeof col.copyable === 'function') {
rawCopyValue = col.copyable(record);
} else {
const accessor = col.copyAccessor ?? col.accessor;
rawCopyValue = resolveItem(record, accessor);
}
const copyValue = rawCopyValue == null ? '' : String(rawCopyValue);
return <CopyableCell value={copyValue}>{content}</CopyableCell>;
};
}
return {
...col,
hidden: hidden,
resizable: col.resizable ?? true,
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`,
render: wrappedRender,
cellsStyle: (record: any, index: number) => {
const width = (col as any).minWidth ?? 100;
return {

View File

@@ -237,7 +237,8 @@ export function PurchaseOrderLineItemTable({
title: t`Supplier Code`,
switchable: false,
sortable: true,
ordering: 'SKU'
ordering: 'SKU',
copyable: true
},
LinkColumn({
accessor: 'supplier_part_detail.link',