[PUI] Fix for hovering in parameteric part table (#7933)

* Fix for hovering in parameteric part table

- Visual glitches are now gone

* Prevent editing of template field
This commit is contained in:
Oliver 2024-08-20 13:13:36 +10:00 committed by GitHub
parent 7cbaeb159e
commit dbe12c2c53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 55 deletions

View File

@ -141,7 +141,11 @@ export function partCategoryFields(): ApiFormFieldSet {
return fields; return fields;
} }
export function usePartParameterFields(): ApiFormFieldSet { export function usePartParameterFields({
editTemplate
}: {
editTemplate?: boolean;
}): ApiFormFieldSet {
// Valid field choices // Valid field choices
const [choices, setChoices] = useState<any[]>([]); const [choices, setChoices] = useState<any[]>([]);
@ -156,6 +160,7 @@ export function usePartParameterFields(): ApiFormFieldSet {
disabled: true disabled: true
}, },
template: { template: {
disabled: editTemplate == false,
onValueChange: (value: any, record: any) => { onValueChange: (value: any, record: any) => {
// Adjust the type of the "data" field based on the selected template // Adjust the type of the "data" field based on the selected template
if (record?.checkbox) { if (record?.checkbox) {
@ -195,5 +200,5 @@ export function usePartParameterFields(): ApiFormFieldSet {
} }
} }
}; };
}, [fieldType, choices]); }, [editTemplate, fieldType, choices]);
} }

View File

@ -27,6 +27,7 @@ import {
IconCornerUpRightDouble, IconCornerUpRightDouble,
IconCurrencyDollar, IconCurrencyDollar,
IconDots, IconDots,
IconEdit,
IconExclamationCircle, IconExclamationCircle,
IconExternalLink, IconExternalLink,
IconFileUpload, IconFileUpload,
@ -109,6 +110,7 @@ const icons = {
units: IconRulerMeasure, units: IconRulerMeasure,
keywords: IconTag, keywords: IconTag,
status: IconInfoCircle, status: IconInfoCircle,
edit: IconEdit,
info: IconInfoCircle, info: IconInfoCircle,
exclamation: IconExclamationCircle, exclamation: IconExclamationCircle,
details: IconInfoCircle, details: IconInfoCircle,

View File

@ -81,7 +81,7 @@ const defaultPageSize: number = 25;
* @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format) * @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format)
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions * @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked * @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
* @param onCellClick : (event: any, record: any, recordIndex: number, column: any, columnIndex: number) => void - Callback function when a cell is clicked * @param onCellClick : (event: any, record: any, index: number, column: any, columnIndex: number) => void - Callback function when a cell is clicked
* @param modelType: ModelType - The model type for the table * @param modelType: ModelType - The model type for the table
*/ */
export type InvenTreeTableProps<T = any> = { export type InvenTreeTableProps<T = any> = {

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core'; import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react'; import { IconInfoCircle } from '@tabler/icons-react';
import { ReactNode } from 'react'; import { ReactNode, useMemo } from 'react';
import { InvenTreeIcon, InvenTreeIconType } from '../functions/icons'; import { InvenTreeIcon, InvenTreeIconType } from '../functions/icons';
@ -23,12 +23,26 @@ export function TableHoverCard({
icon?: InvenTreeIconType; icon?: InvenTreeIconType;
iconColor?: string; iconColor?: string;
}) { }) {
// If no extra information presented, just return the raw value const extraItems: ReactNode = useMemo(() => {
if (!extra) { if (Array.isArray(extra)) {
return value; if (extra.length == 0) {
} return null;
}
if (Array.isArray(extra) && extra.length == 0) { return (
<Stack gap="xs">
{extra.map((item, idx) => (
<div key={t`item-${idx}`}>{item}</div>
))}
</Stack>
);
} else {
return extra;
}
}, [extra]);
// If no extra information presented, just return the raw value
if (!extraItems) {
return value; return value;
} }
@ -50,7 +64,7 @@ export function TableHoverCard({
<Text fw="bold">{title}</Text> <Text fw="bold">{title}</Text>
</Group> </Group>
<Divider /> <Divider />
{extra} {extraItems}
</Stack> </Stack>
</HoverCard.Dropdown> </HoverCard.Dropdown>
</HoverCard> </HoverCard>

View File

@ -1,9 +1,9 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ActionIcon, Group, Tooltip } from '@mantine/core'; import { Group } from '@mantine/core';
import { useHover } from '@mantine/hooks'; import { useHover } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react'; import { ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { YesNoButton } from '../../components/buttons/YesNoButton'; import { YesNoButton } from '../../components/buttons/YesNoButton';
@ -13,6 +13,8 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { usePartParameterFields } from '../../forms/PartForms'; import { usePartParameterFields } from '../../forms/PartForms';
import { cancelEvent } from '../../functions/events'; import { cancelEvent } from '../../functions/events';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -30,28 +32,32 @@ import { TableHoverCard } from '../TableHoverCard';
function ParameterCell({ function ParameterCell({
record, record,
template, template,
canEdit, canEdit
onEdit
}: { }: {
record: any; record: any;
template: any; template: any;
canEdit: boolean; canEdit: boolean;
onEdit: () => void;
}) { }) {
const { hovered, ref } = useHover(); const { hovered, ref } = useHover();
// Find matching template parameter // Find matching template parameter
let parameter = record.parameters?.find( const parameter = useMemo(() => {
(p: any) => p.template == template.pk return record.parameters?.find((p: any) => p.template == template.pk);
); }, [record.parameters, template]);
let extra: any[] = []; let extra: any[] = [];
let value: any = parameter?.data; // Format the value for display
const value: ReactNode = useMemo(() => {
let v: any = parameter?.data;
if (template?.checkbox && value != undefined) { // Handle boolean values
value = <YesNoButton value={parameter.data} />; if (template?.checkbox && v != undefined) {
} v = <YesNoButton value={parameter.data} />;
}
return v;
}, [parameter, template]);
if ( if (
template.units && template.units &&
@ -62,30 +68,21 @@ function ParameterCell({
extra.push(`${parameter.data_numeric} [${template.units}]`); extra.push(`${parameter.data_numeric} [${template.units}]`);
} }
const handleClick = useCallback((event: any) => { if (hovered && canEdit) {
cancelEvent(event); extra.push(t`Click to edit`);
onEdit(); }
}, []);
return ( return (
<div> <div>
<Group grow ref={ref} justify="space-between"> <Group grow ref={ref} justify="space-between">
<Group grow style={{ flex: 1 }}> <Group grow>
<TableHoverCard <TableHoverCard
value={value ?? '-'} value={value ?? '-'}
extra={extra} extra={extra}
icon={hovered && canEdit ? 'edit' : 'info'}
title={t`Internal Units`} title={t`Internal Units`}
/> />
</Group> </Group>
{hovered && canEdit && (
<div style={{ flex: 0 }}>
<Tooltip label={t`Edit parameter`}>
<ActionIcon size="xs" onClick={handleClick} variant="transparent">
<IconEdit />
</ActionIcon>
</Tooltip>
</div>
)}
</Group> </Group>
</div> </div>
); );
@ -98,6 +95,7 @@ export default function ParametricPartTable({
}) { }) {
const table = useTable('parametric-parts'); const table = useTable('parametric-parts');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const categoryParmeters = useQuery({ const categoryParmeters = useQuery({
queryKey: ['category-parameters', categoryId], queryKey: ['category-parameters', categoryId],
@ -118,7 +116,9 @@ export default function ParametricPartTable({
const [selectedTemplate, setSelectedTemplate] = useState<number>(0); const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
const [selectedParameter, setSelectedParameter] = useState<number>(0); const [selectedParameter, setSelectedParameter] = useState<number>(0);
const partParameterFields: ApiFormFieldSet = usePartParameterFields(); const partParameterFields: ApiFormFieldSet = usePartParameterFields({
editTemplate: false
});
const addParameter = useCreateApiFormModal({ const addParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
@ -195,26 +195,25 @@ export default function ParametricPartTable({
record={record} record={record}
template={template} template={template}
canEdit={user.hasChangeRole(UserRoles.part)} canEdit={user.hasChangeRole(UserRoles.part)}
onEdit={() => {
setSelectedTemplate(template.pk);
setSelectedPart(record.pk);
let parameter = record.parameters?.find(
(p: any) => p.template == template.pk
);
if (parameter) {
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
addParameter.open();
}
}}
/> />
) )
}; };
}); });
}, [user, categoryParmeters.data]); }, [user, categoryParmeters.data]);
const onParameterClick = useCallback((template: number, part: any) => {
setSelectedTemplate(template);
setSelectedPart(part.pk);
let parameter = part.parameters?.find((p: any) => p.template == template);
if (parameter) {
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
addParameter.open();
}
}, []);
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@ -268,14 +267,28 @@ export default function ParametricPartTable({
columns={tableColumns} columns={tableColumns}
props={{ props={{
enableDownload: false, enableDownload: false,
tableFilters: tableFilters,
params: { params: {
category: categoryId, category: categoryId,
cascade: true, cascade: true,
category_detail: true, category_detail: true,
parameters: true parameters: true
}, },
modelType: ModelType.part, onCellClick: ({ event, record, index, column, columnIndex }) => {
tableFilters: tableFilters cancelEvent(event);
// Is this a "parameter" cell?
if (column?.accessor?.toString()?.startsWith('parameter_')) {
const col = column as any;
onParameterClick(col.extra.template, record);
} else {
// Navigate through to the part detail page
if (record?.pk) {
const url = getDetailUrl(ModelType.part, record.pk);
navigateToLink(url, navigate, event);
}
}
}
}} }}
/> />
</> </>

View File

@ -105,7 +105,7 @@ export function PartParameterTable({
]; ];
}, [partId]); }, [partId]);
const partParameterFields: ApiFormFieldSet = usePartParameterFields(); const partParameterFields: ApiFormFieldSet = usePartParameterFields({});
const newParameter = useCreateApiFormModal({ const newParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,