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

[UI] Copy cells expansion (#11410)

* Prevent copy button if copy value is null

* Add "link" columns to order tables

* Support copy for default column types

* Tweak padding to avoid flickering issues

* Refactor IPNColumn

* Adjust visual styling

* Copy for SKU and MPN columns

* Add more copy columns

* More tweaks

* Tweak playwright testing

* Further cleanup

* More copy cols
This commit is contained in:
Oliver
2026-02-24 11:44:14 +11:00
committed by GitHub
parent 8a2adda1e1
commit 449bd4e5a0
26 changed files with 142 additions and 122 deletions

View File

@@ -1,6 +1,7 @@
import { t } from '@lingui/core/macro';
import {
ActionIcon,
type ActionIconVariant,
Button,
type DefaultMantineColor,
type FloatingPosition,
@@ -22,7 +23,8 @@ export function CopyButton({
tooltipPosition,
content,
size,
color = 'gray'
color = 'gray',
variant = 'transparent'
}: Readonly<{
value: any;
label?: string;
@@ -32,6 +34,7 @@ export function CopyButton({
content?: JSX.Element;
size?: MantineSize;
color?: DefaultMantineColor;
variant?: ActionIconVariant;
}>) {
const ButtonComponent = label ? Button : ActionIcon;
@@ -51,7 +54,7 @@ export function CopyButton({
e.preventDefault();
copy();
}}
variant='transparent'
variant={copied ? 'transparent' : (variant ?? 'transparent')}
size={size ?? 'sm'}
>
{copied ? (

View File

@@ -107,6 +107,18 @@ export function PartColumn(props: PartColumnProps): TableColumn {
};
}
export function IPNColumn(props: TableColumnProps): TableColumn {
return {
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
switchable: true,
title: t`IPN`,
copyable: true,
...props
};
}
export type StockColumnProps = TableColumnProps & {
nullMessage?: string | ReactNode;
};
@@ -448,6 +460,7 @@ export function DescriptionColumn(props: TableColumnProps): TableColumn {
sortable: false,
switchable: true,
minWidth: '200px',
copyable: true,
...props
};
}
@@ -457,6 +470,8 @@ export function LinkColumn(props: TableColumnProps): TableColumn {
accessor: 'link',
sortable: false,
defaultVisible: false,
copyable: true,
copyAccessor: props.accessor ?? 'link',
render: (record: any) => {
const url = resolveItem(record, props.accessor ?? 'link');
@@ -490,6 +505,7 @@ export function ReferenceColumn(props: TableColumnProps): TableColumn {
title: t`Reference`,
sortable: true,
switchable: true,
copyable: true,
...props
};
}
@@ -664,6 +680,7 @@ export function DateColumn(props: TableColumnProps): TableColumn {
formatDate(resolveItem(record, props.accessor ?? 'date'), {
showTime: props.extra?.showTime
}),
copyable: true,
...props
};
}

View File

@@ -20,19 +20,30 @@ export function CopyableCell({
return (
<Group
gap='xs'
gap={0}
p={0}
wrap='nowrap'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
justify='space-between'
align='center'
>
{children}
{isHovered && value != null && (
<span
style={{ position: 'relative' }}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<CopyButton value={value} />
<div
style={{
position: 'absolute',
right: 0,
transform: 'translateY(-50%)'
}}
>
<CopyButton value={value} variant={'default'} />
</div>
</span>
)}
</Group>

View File

@@ -285,7 +285,9 @@ export function InvenTreeTable<T extends Record<string, any>>({
}
const copyValue = rawCopyValue == null ? '' : String(rawCopyValue);
return <CopyableCell value={copyValue}>{content}</CopyableCell>;
if (!!copyValue) {
return <CopyableCell value={copyValue}>{content}</CopyableCell>;
}
};
}

View File

@@ -44,6 +44,7 @@ import {
BooleanColumn,
CategoryColumn,
DescriptionColumn,
IPNColumn,
NoteColumn,
ReferenceColumn
} from '../ColumnRenderers';
@@ -129,12 +130,9 @@ export function BomTable({
);
}
},
{
accessor: 'sub_part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN'
},
IPNColumn({
accessor: 'sub_part_detail.IPN'
}),
CategoryColumn({
accessor: 'category_detail',
defaultVisible: false,

View File

@@ -15,6 +15,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
PartColumn,
ReferenceColumn
} from '../ColumnRenderers';
@@ -40,11 +41,9 @@ export function UsedInTable({
title: t`Assembly`,
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
sortable: false,
title: t`IPN`
},
IPNColumn({
sortable: false
}),
{
accessor: 'part_detail.revision',
title: t`Revision`,

View File

@@ -22,6 +22,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DecimalColumn,
IPNColumn,
LocationColumn,
PartColumn,
ReferenceColumn,
@@ -99,14 +100,9 @@ export default function BuildAllocatedStockTable({
hidden: !showPartInfo,
switchable: false
}),
{
accessor: 'part_detail.IPN',
ordering: 'IPN',
hidden: !showPartInfo,
title: t`IPN`,
sortable: true,
switchable: true
},
IPNColumn({
hidden: !showPartInfo
}),
{
hidden: !showPartInfo,
accessor: 'bom_reference',
@@ -119,7 +115,9 @@ export default function BuildAllocatedStockTable({
title: t`Batch Code`,
sortable: false,
switchable: true,
render: (record: any) => record?.stock_item_detail?.batch
render: (record: any) => record?.stock_item_detail?.batch,
copyable: true,
copyAccessor: 'stock_item_detail.batch'
},
DecimalColumn({
accessor: 'stock_item_detail.quantity',

View File

@@ -44,6 +44,7 @@ import {
CategoryColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LocationColumn,
PartColumn,
RenderPartColumn
@@ -91,7 +92,9 @@ export function BuildLineSubTable({
},
{
accessor: 'stock_item_detail.batch',
title: t`Batch`
title: t`Batch`,
copyable: true,
copyAccessor: 'stock_item_detail.batch'
},
LocationColumn({
accessor: 'location_detail'
@@ -332,12 +335,7 @@ export default function BuildLineTable({
);
}
}),
{
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
title: t`IPN`
},
IPNColumn({}),
CategoryColumn({
accessor: 'category_detail',
defaultVisible: false,

View File

@@ -18,6 +18,8 @@ import {
CreationDateColumn,
DateColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
@@ -79,13 +81,7 @@ export function BuildOrderTable({
PartColumn({
switchable: false
}),
{
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
switchable: true,
title: t`IPN`
},
IPNColumn({}),
{
accessor: 'part_detail.revision',
title: t`Revision`,
@@ -150,7 +146,8 @@ export function BuildOrderTable({
ordering: 'issued_by',
title: t`Issued By`
}),
ResponsibleColumn({})
ResponsibleColumn({}),
LinkColumn({})
];
}, [parentBuildId, globalSettings]);

View File

@@ -16,6 +16,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
PartColumn,
ProjectCodeColumn,
StatusColumn
@@ -56,10 +57,7 @@ export default function PartSalesAllocationsTable({
PartColumn({
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`
},
IPNColumn({}),
ProjectCodeColumn({
accessor: 'order_detail.project_code_detail'
}),

View File

@@ -41,6 +41,7 @@ import {
CategoryColumn,
DefaultLocationColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn
} from '../ColumnRenderers';
@@ -56,17 +57,17 @@ function partTableColumns(): TableColumn[] {
part: '',
accessor: 'name'
}),
{
accessor: 'IPN',
sortable: true
},
IPNColumn({
accessor: 'IPN'
}),
{
accessor: 'revision',
sortable: true
},
{
accessor: 'units',
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
CategoryColumn({

View File

@@ -288,7 +288,8 @@ export default function PartTestResultTable({
accessor: 'batch',
title: t`Batch Code`,
sortable: true,
switchable: true
switchable: true,
copyable: true
},
LocationColumn({
accessor: 'location_detail'

View File

@@ -3,7 +3,7 @@ import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { type ReactNode, useMemo } from 'react';
import { CompanyColumn, PartColumn } from '../ColumnRenderers';
import { CompanyColumn, IPNColumn, PartColumn } from '../ColumnRenderers';
import ParametricDataTable from '../general/ParametricDataTable';
export default function ManufacturerPartParametricTable({
@@ -16,12 +16,9 @@ export default function ManufacturerPartParametricTable({
PartColumn({
switchable: false
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: false,
switchable: true
},
IPNColumn({
sortable: false
}),
{
accessor: 'manufacturer',
sortable: true,
@@ -32,7 +29,8 @@ export default function ManufacturerPartParametricTable({
{
accessor: 'MPN',
title: t`MPN`,
sortable: true
sortable: true,
copyable: true
}
];
}, []);

View File

@@ -25,6 +25,7 @@ import { useUserState } from '../../states/UserState';
import {
CompanyColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn
} from '../ColumnRenderers';
@@ -86,13 +87,7 @@ export function ManufacturerPartTable({
PartColumn({
switchable: !!partId
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
{
accessor: 'manufacturer',
sortable: true,
@@ -103,7 +98,8 @@ export function ManufacturerPartTable({
{
accessor: 'MPN',
title: t`MPN`,
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
LinkColumn({})

View File

@@ -251,7 +251,8 @@ export function PurchaseOrderLineItemTable({
ordering: 'MPN',
title: t`Manufacturer Code`,
sortable: true,
defaultVisible: false
defaultVisible: false,
copyable: true
},
CurrencyColumn({
accessor: 'purchase_price',

View File

@@ -19,6 +19,7 @@ import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
@@ -120,7 +121,8 @@ export function PurchaseOrderTable({
)
},
{
accessor: 'supplier_reference'
accessor: 'supplier_reference',
copyable: true
},
LineItemsProgressColumn({}),
StatusColumn({ model: ModelType.purchaseorder }),
@@ -150,7 +152,8 @@ export function PurchaseOrderTable({
});
}
},
ResponsibleColumn({})
ResponsibleColumn({}),
LinkColumn({})
];
}, []);

View File

@@ -27,7 +27,8 @@ export default function SupplierPartParametricTable({
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
sortable: true,
copyable: true
}
];
}, []);

View File

@@ -32,6 +32,7 @@ import {
CompanyColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
NoteColumn,
PartColumn
@@ -92,13 +93,7 @@ export function SupplierPartTable({
switchable: !!partId,
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
{
accessor: 'supplier',
sortable: true,
@@ -109,7 +104,8 @@ export function SupplierPartTable({
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
{
@@ -124,7 +120,9 @@ export function SupplierPartTable({
accessor: 'MPN',
sortable: true,
title: t`MPN`,
render: (record: any) => record?.manufacturer_part_detail?.MPN
render: (record: any) => record?.manufacturer_part_detail?.MPN,
copyable: true,
copyAccessor: 'manufacturer_part_detail.MPN'
},
BooleanColumn({
accessor: 'primary',

View File

@@ -19,6 +19,7 @@ import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
@@ -123,7 +124,8 @@ export function ReturnOrderTable({
)
},
{
accessor: 'customer_reference'
accessor: 'customer_reference',
copyable: true
},
DescriptionColumn({}),
LineItemsProgressColumn({}),
@@ -154,7 +156,8 @@ export function ReturnOrderTable({
currency: record.order_currency || record.customer_detail?.currency
});
}
}
},
LinkColumn({})
];
}, []);

View File

@@ -29,6 +29,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
LocationColumn,
PartColumn,
ReferenceColumn,
@@ -130,13 +131,9 @@ export default function SalesOrderAllocationTable({
accessor: 'part_detail.description',
hidden: showPartInfo != true
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
hidden: showPartInfo != true,
sortable: true,
ordering: 'IPN'
},
IPNColumn({
hidden: showPartInfo != true
}),
{
accessor: 'serial',
title: t`Serial Number`,
@@ -149,7 +146,9 @@ export default function SalesOrderAllocationTable({
title: t`Batch Code`,
sortable: true,
switchable: true,
render: (record: any) => record?.item_detail?.batch
render: (record: any) => record?.item_detail?.batch,
copyable: true,
copyAccessor: 'item_detail.batch'
},
{
accessor: 'available',

View File

@@ -47,6 +47,7 @@ import {
DateColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
ProjectCodeColumn,
RenderPartColumn
@@ -94,13 +95,7 @@ export default function SalesOrderLineItemTable({
);
}
},
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
DescriptionColumn({
accessor: 'part_detail.description'
}),

View File

@@ -142,7 +142,8 @@ export default function SalesOrderShipmentTable({
accessor: 'order_detail.reference',
title: t`Sales Order`,
hidden: !showOrderInfo,
sortable: false
sortable: false,
copyable: true
},
StatusColumn({
switchable: true,
@@ -155,7 +156,8 @@ export default function SalesOrderShipmentTable({
accessor: 'reference',
title: t`Shipment Reference`,
switchable: false,
sortable: true
sortable: true,
copyable: true
},
{
accessor: 'allocated_items',
@@ -193,14 +195,14 @@ export default function SalesOrderShipmentTable({
title: t`Delivery Date`
}),
{
accessor: 'tracking_number'
accessor: 'tracking_number',
copyable: true
},
{
accessor: 'invoice_number'
accessor: 'invoice_number',
copyable: true
},
LinkColumn({
accessor: 'link'
})
LinkColumn({})
];
}, [showOrderInfo]);

View File

@@ -20,6 +20,7 @@ import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
@@ -146,7 +147,8 @@ export function SalesOrderTable({
},
{
accessor: 'customer_reference',
title: t`Customer Reference`
title: t`Customer Reference`,
copyable: true
},
DescriptionColumn({}),
LineItemsProgressColumn({}),
@@ -190,7 +192,8 @@ export function SalesOrderTable({
currency: record.order_currency || record.customer_detail?.currency
});
}
}
},
LinkColumn({})
];
}, []);

View File

@@ -24,6 +24,7 @@ import { useUserState } from '../../states/UserState';
import {
DateColumn,
DescriptionColumn,
IPNColumn,
LocationColumn,
PartColumn,
StatusColumn,
@@ -59,12 +60,7 @@ function stockItemTableColumns({
accessor: 'part',
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN'
},
IPNColumn({}),
{
accessor: 'part_detail.revision',
title: t`Revision`,
@@ -83,7 +79,8 @@ function stockItemTableColumns({
StatusColumn({ model: ModelType.stockitem }),
{
accessor: 'batch',
sortable: true
sortable: true,
copyable: true
},
LocationColumn({
hidden: !showLocation,
@@ -101,13 +98,15 @@ function stockItemTableColumns({
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true,
defaultVisible: false
defaultVisible: false,
copyable: true
},
{
accessor: 'MPN',
title: t`Manufacturer Part`,
sortable: true,
defaultVisible: false
defaultVisible: false,
copyable: true
},
{
accessor: 'purchase_price',

View File

@@ -27,6 +27,7 @@ import { useTable } from '../../hooks/UseTable';
import {
DateColumn,
DescriptionColumn,
IPNColumn,
PartColumn,
StockColumn
} from '../ColumnRenderers';
@@ -238,14 +239,10 @@ export function StockTrackingTable({
switchable: true,
hidden: !partId
}),
{
title: t`IPN`,
accessor: 'part_detail.IPN',
sortable: true,
IPNColumn({
defaultVisible: false,
switchable: true,
hidden: !partId
},
}),
StockColumn({
title: t`Stock Item`,
accessor: 'item_detail',

View File

@@ -31,7 +31,7 @@ test('Purchasing - Index', async ({ browser }) => {
// Clearing the filters, more orders should be visible
await clearTableFilters(page);
await page.getByText(/1 - 1\d \/ 1\d/).waitFor();
await page.getByText(/1 - \d\d \/ \d\d/).waitFor();
// Suppliers tab
await loadTab(page, 'Suppliers');
@@ -49,9 +49,11 @@ test('Purchasing - Index', async ({ browser }) => {
// Check for expected values
await clearTableFilters(page);
await page
.getByRole('textbox', { name: 'table-search-input' })
.fill('R_100K_0402');
await page.getByText('R_100K_0402_1%').first().waitFor();
await page.getByRole('cell', { name: 'RR05P100KDTR-ND' }).first().waitFor();
await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).first().waitFor();
// Manufacturers tab
await loadTab(page, 'Manufacturers');