2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-12 03:28:37 +00:00

Global part lock (#11995)

* Add new global setting

* Update lock checks

* Bug fix for task comparison

* Update user interface

* Hide locked field if locking not enabled

* Update existing unit tests

* Update docs
This commit is contained in:
Oliver
2026-05-24 07:41:52 +10:00
committed by GitHub
parent 8e7465dd24
commit 7d61203be8
12 changed files with 174 additions and 37 deletions
+5
View File
@@ -188,6 +188,11 @@ export function usePartFields({
delete fields['default_expiry'];
}
// Remove "locked" field if locking not enabled
if (!globalSettings.isSet('PART_ENABLE_LOCKING')) {
delete fields['locked'];
}
if (create) {
delete fields['starred'];
}
@@ -212,6 +212,7 @@ export default function SystemSettings() {
'PART_ALLOW_DUPLICATE_IPN',
'PART_ALLOW_EDIT_IPN',
'PART_ALLOW_DELETE_FROM_ASSEMBLY',
'PART_ENABLE_LOCKING',
'PART_ENABLE_REVISION',
'PART_REVISION_ASSEMBLY_ONLY',
'PART_SHOW_RELATED',
+29 -17
View File
@@ -202,6 +202,11 @@ export default function PartDetail() {
refetchOnMount: true
});
const lockingEnabled = useMemo(
() => globalSettings.isSet('PART_ENABLE_LOCKING'),
[globalSettings]
);
const revisionsEnabled = useMemo(
() => globalSettings.isSet('PART_ENABLE_REVISION'),
[globalSettings]
@@ -808,7 +813,12 @@ export default function PartDetail() {
icon: <IconTestPipe />,
hidden: !part.testable,
content: part?.pk ? (
<PartTestTemplateTable partId={part?.pk} partLocked={part.locked} />
<PartTestTemplateTable
partId={part?.pk}
partLocked={
globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked
}
/>
) : (
<Skeleton />
)
@@ -836,7 +846,7 @@ export default function PartDetail() {
icon: <IconListDetails />,
content: (
<>
{part.locked && (
{lockingEnabled && part.locked && (
<Alert
title={t`Part is Locked`}
color='orange'
@@ -849,7 +859,7 @@ export default function PartDetail() {
<ParameterTable
modelType={ModelType.part}
modelId={part?.pk}
allowEdit={part?.locked != true}
allowEdit={!lockingEnabled || part?.locked != true}
/>
</>
)
@@ -1149,20 +1159,22 @@ export default function PartDetail() {
<PageDetail
title={`${t`Part`}: ${part.full_name}`}
icon={
<ActionIcon
aria-label='part-lock-icon'
variant='transparent'
disabled={!user.hasChangeRole(UserRoles.part)}
onClick={() => {
api
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
locked: !part.locked
})
.then(refreshInstance);
}}
>
{part?.locked ? <IconLock /> : <IconLockOpen />}
</ActionIcon>
lockingEnabled ? (
<ActionIcon
aria-label='part-lock-icon'
variant='transparent'
disabled={!user.hasChangeRole(UserRoles.part)}
onClick={() => {
api
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
locked: !part.locked
})
.then(refreshInstance);
}}
>
{part?.locked ? <IconLock /> : <IconLockOpen />}
</ActionIcon>
) : undefined
}
subtitle={part.description}
imageUrl={part.image}
+3 -1
View File
@@ -52,6 +52,8 @@ export function RenderPartColumn({
part: any;
full_name?: boolean;
}) {
const globalSettings = useGlobalSettingsState.getState();
if (!part) {
return <Skeleton />;
}
@@ -69,7 +71,7 @@ export function RenderPartColumn({
<IconExclamationCircle color='red' size={16} />
</Tooltip>
)}
{part?.locked && (
{globalSettings.isSet('PART_ENABLE_LOCKING') && part?.locked && (
<Tooltip label={t`Part is Locked`}>
<IconLock size={16} />
</Tooltip>
+22 -14
View File
@@ -40,7 +40,10 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useImporterState } from '../../states/ImporterState';
import { useUserSettingsState } from '../../states/SettingsStates';
import {
useGlobalSettingsState,
useUserSettingsState
} from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState';
import {
BooleanColumn,
@@ -91,6 +94,13 @@ export function BomTable({
const [isEditing, setIsEditing] = useState<boolean>(false);
const globalSettings = useGlobalSettingsState();
const isLocked = useMemo(
() => globalSettings.isSet('PART_ENABLE_LOCKING') && (partLocked ?? false),
[globalSettings, partLocked]
);
const userSettings = useUserSettingsState();
const tableColumns: TableColumn[] = useMemo(() => {
@@ -605,16 +615,14 @@ export function BomTable({
title: t`Validate BOM Line`,
color: 'green',
hidden:
partLocked ||
record.validated ||
!user.hasChangeRole(UserRoles.bom),
isLocked || record.validated || !user.hasChangeRole(UserRoles.bom),
icon: <IconCircleCheck />,
onClick: () => {
validateBomItem(record);
}
},
RowEditAction({
hidden: partLocked || !user.hasChangeRole(UserRoles.bom),
hidden: isLocked || !user.hasChangeRole(UserRoles.bom),
onClick: () => {
setSelectedBomItem(record);
editBomItem.open();
@@ -623,7 +631,7 @@ export function BomTable({
{
title: t`Edit Substitutes`,
color: 'blue',
hidden: partLocked || !user.hasAddRole(UserRoles.bom),
hidden: isLocked || !user.hasAddRole(UserRoles.bom),
icon: <IconSwitch3 />,
onClick: () => {
setSelectedBomItem(record);
@@ -631,7 +639,7 @@ export function BomTable({
}
},
RowDeleteAction({
hidden: partLocked || !user.hasDeleteRole(UserRoles.bom),
hidden: isLocked || !user.hasDeleteRole(UserRoles.bom),
onClick: () => {
setSelectedBomItem(record);
deleteBomItem.open();
@@ -639,7 +647,7 @@ export function BomTable({
})
];
},
[isEditing, partId, partLocked, user]
[isEditing, partId, isLocked, user]
);
const tableActions = useMemo(() => {
@@ -649,7 +657,7 @@ export function BomTable({
tooltip={t`Add BOM Items`}
position='bottom-start'
icon={<IconPlus />}
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.bom)}
hidden={!isEditing || isLocked || !user.hasAddRole(UserRoles.bom)}
actions={[
{
name: t`Add BOM Item`,
@@ -667,7 +675,7 @@ export function BomTable({
/>,
<ActionButton
key='edit-bom'
hidden={partLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
hidden={isLocked || !user.hasChangeRole(UserRoles.bom) || isEditing}
tooltip={t`Edit BOM`}
icon={<IconEdit />}
onClick={() => {
@@ -686,7 +694,7 @@ export function BomTable({
}}
/>
];
}, [isEditing, partLocked, user]);
}, [isEditing, isLocked, user]);
// Row expansion (for displaying subassemblies)
const rowExpansion = subassemblyRowExpansion({ table: table });
@@ -699,7 +707,7 @@ export function BomTable({
{deleteBomItem.modal}
{editSubstitutes.modal}
<Stack gap='xs'>
{partLocked && (
{isLocked && (
<Alert
title={t`Part is Locked`}
color='orange'
@@ -730,9 +738,9 @@ export function BomTable({
modelField: 'sub_part',
onCellClick: () => {},
rowActions: isEditing ? rowActions : undefined,
enableSelection: isEditing && !partLocked,
enableSelection: isEditing && !isLocked,
enableBulkDelete:
isEditing && !partLocked && user.hasDeleteRole(UserRoles.bom),
isEditing && !isLocked && user.hasDeleteRole(UserRoles.bom),
enableDownload: true,
rowExpansion: isEditing ? undefined : rowExpansion
}}