diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py
index 4bb10ecf60..5197911efd 100644
--- a/src/backend/InvenTree/part/api.py
+++ b/src/backend/InvenTree/part/api.py
@@ -270,7 +270,7 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
if 'starred' in data:
starred = str2bool(data.get('starred', False))
- self.get_object().set_starred(request.user, starred)
+ self.get_object().set_starred(request.user, starred, include_parents=False)
response = super().update(request, *args, **kwargs)
@@ -1423,7 +1423,9 @@ class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
if 'starred' in data:
starred = str2bool(data.get('starred', False))
- self.get_object().set_starred(request.user, starred)
+ self.get_object().set_starred(
+ request.user, starred, include_variants=False, include_categories=False
+ )
response = super().update(request, *args, **kwargs)
diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py
index 5088093468..6e20eba45e 100644
--- a/src/backend/InvenTree/part/models.py
+++ b/src/backend/InvenTree/part/models.py
@@ -288,11 +288,10 @@ class PartCategory(InvenTree.models.InvenTreeTree):
def get_subscribers(self, include_parents=True):
"""Return a list of users who subscribe to this PartCategory."""
- cats = self.get_ancestors(include_self=True)
-
subscribers = set()
if include_parents:
+ cats = self.get_ancestors(include_self=True)
queryset = PartCategoryStar.objects.filter(category__in=cats)
else:
queryset = PartCategoryStar.objects.filter(category=self)
@@ -306,12 +305,12 @@ class PartCategory(InvenTree.models.InvenTreeTree):
"""Returns True if the specified user subscribes to this category."""
return user in self.get_subscribers(**kwargs)
- def set_starred(self, user, status: bool) -> None:
+ def set_starred(self, user, status: bool, **kwargs) -> None:
"""Set the "subscription" status of this PartCategory against the specified user."""
if not user:
return
- if self.is_starred_by(user) == status:
+ if self.is_starred_by(user, **kwargs) == status:
return
if status:
@@ -1451,13 +1450,13 @@ class Part(
"""Return True if the specified user subscribes to this part."""
return user in self.get_subscribers(**kwargs)
- def set_starred(self, user, status):
+ def set_starred(self, user, status, **kwargs):
"""Set the "subscription" status of this Part against the specified user."""
if not user:
return
# Already subscribed?
- if self.is_starred_by(user) == status:
+ if self.is_starred_by(user, **kwargs) == status:
return
if status:
diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx
index 7146695231..54a30cf3f9 100644
--- a/src/frontend/src/forms/PartForms.tsx
+++ b/src/frontend/src/forms/PartForms.tsx
@@ -61,7 +61,14 @@ export function usePartFields({
salable: {},
virtual: {},
locked: {},
- active: {}
+ active: {},
+ starred: {
+ field_type: 'boolean',
+ label: t`Subscribed`,
+ description: t`Subscribe to notifications for this part`,
+ disabled: false,
+ required: false
+ }
};
// Additional fields for creation
@@ -111,6 +118,10 @@ export function usePartFields({
delete fields['default_expiry'];
}
+ if (create) {
+ delete fields['starred'];
+ }
+
return fields;
}, [create, settings]);
}
@@ -118,25 +129,44 @@ export function usePartFields({
/**
* Construct a set of fields for creating / editing a PartCategory instance
*/
-export function partCategoryFields(): ApiFormFieldSet {
- let fields: ApiFormFieldSet = {
- parent: {
- description: t`Parent part category`,
- required: false
- },
- name: {},
- description: {},
- default_location: {
- filters: {
- structural: false
+export function partCategoryFields({
+ create
+}: {
+ create?: boolean;
+}): ApiFormFieldSet {
+ let fields: ApiFormFieldSet = useMemo(() => {
+ let fields: ApiFormFieldSet = {
+ parent: {
+ description: t`Parent part category`,
+ required: false
+ },
+ name: {},
+ description: {},
+ default_location: {
+ filters: {
+ structural: false
+ }
+ },
+ default_keywords: {},
+ structural: {},
+ starred: {
+ field_type: 'boolean',
+ label: t`Subscribed`,
+ description: t`Subscribe to notifications for this category`,
+ disabled: false,
+ required: false
+ },
+ icon: {
+ field_type: 'icon'
}
- },
- default_keywords: {},
- structural: {},
- icon: {
- field_type: 'icon'
+ };
+
+ if (create) {
+ delete fields['starred'];
}
- };
+
+ return fields;
+ }, [create]);
return fields;
}
diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx
index 192bdbaf62..38f64de3be 100644
--- a/src/frontend/src/functions/icons.tsx
+++ b/src/frontend/src/functions/icons.tsx
@@ -3,6 +3,7 @@ import {
Icon123,
IconArrowBigDownLineFilled,
IconArrowMerge,
+ IconBell,
IconBinaryTree2,
IconBookmarks,
IconBox,
@@ -159,6 +160,8 @@ const icons = {
issue: IconBrandTelegram,
complete: IconCircleCheck,
deliver: IconTruckDelivery,
+ bell: IconBell,
+ notification: IconBell,
// Part Icons
active: IconCheck,
diff --git a/src/frontend/src/pages/Index/Playground.tsx b/src/frontend/src/pages/Index/Playground.tsx
index 5df5702c0d..b596f4ecaf 100644
--- a/src/frontend/src/pages/Index/Playground.tsx
+++ b/src/frontend/src/pages/Index/Playground.tsx
@@ -27,9 +27,10 @@ import {
} from '../../hooks/UseForm';
// Generate some example forms using the modal API forms interface
-const fields = partCategoryFields();
function ApiFormsPlayground() {
+ const fields = partCategoryFields({});
+
const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list,
pk: 2,
diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx
index b75402c376..b1a4757a91 100644
--- a/src/frontend/src/pages/part/CategoryDetail.tsx
+++ b/src/frontend/src/pages/part/CategoryDetail.tsx
@@ -109,6 +109,12 @@ export default function CategoryDetail() {
label: t`Parent Category`,
model: ModelType.partcategory,
hidden: !category?.parent
+ },
+ {
+ type: 'boolean',
+ name: 'starred',
+ icon: 'notification',
+ label: t`Subscribed`
}
];
@@ -165,7 +171,7 @@ export default function CategoryDetail() {
url: ApiEndpoints.category_list,
pk: id,
title: t`Edit Part Category`,
- fields: partCategoryFields(),
+ fields: partCategoryFields({}),
onFormSuccess: refreshInstance
});
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 9ad5008541..7053e3b89e 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -358,6 +358,12 @@ export default function PartDetail() {
type: 'boolean',
name: 'virtual',
label: t`Virtual Part`
+ },
+ {
+ type: 'boolean',
+ name: 'starred',
+ label: t`Subscribed`,
+ icon: 'bell'
}
];
diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx
index a02a3d6fe8..0f53d26b79 100644
--- a/src/frontend/src/tables/ColumnRenderers.tsx
+++ b/src/frontend/src/tables/ColumnRenderers.tsx
@@ -3,7 +3,7 @@
*/
import { t } from '@lingui/macro';
import { Anchor, Group, Skeleton, Text, Tooltip } from '@mantine/core';
-import { IconExclamationCircle, IconLock } from '@tabler/icons-react';
+import { IconBell, IconExclamationCircle, IconLock } from '@tabler/icons-react';
import { YesNoButton } from '../components/buttons/YesNoButton';
import { Thumbnail } from '../components/images/Thumbnail';
@@ -42,6 +42,11 @@ export function PartColumn({
)}
+ {part?.starred && (
+
+
+
+ )}
) : (
diff --git a/src/frontend/src/tables/part/PartCategoryTable.tsx b/src/frontend/src/tables/part/PartCategoryTable.tsx
index ee127f4578..a6eba9bef8 100644
--- a/src/frontend/src/tables/part/PartCategoryTable.tsx
+++ b/src/frontend/src/tables/part/PartCategoryTable.tsx
@@ -1,5 +1,6 @@
import { t } from '@lingui/macro';
-import { Group } from '@mantine/core';
+import { Group, Tooltip } from '@mantine/core';
+import { IconBell } from '@tabler/icons-react';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
@@ -36,9 +37,20 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
sortable: true,
switchable: false,
render: (record: any) => (
-
- {record.icon && }
- {record.name}
+
+
+ {record.icon && }
+ {record.name}
+
+
+ {record.starred && (
+
+
+
+ )}
+
)
},
@@ -81,10 +93,12 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
];
}, []);
+ const newCategoryFields = partCategoryFields({ create: true });
+
const newCategory = useCreateApiFormModal({
url: ApiEndpoints.category_list,
title: t`New Part Category`,
- fields: partCategoryFields(),
+ fields: newCategoryFields,
focus: 'name',
initialData: {
parent: parentId
@@ -96,11 +110,13 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
const [selectedCategory, setSelectedCategory] = useState(-1);
+ const editCategoryFields = partCategoryFields({ create: false });
+
const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list,
pk: selectedCategory,
title: t`Edit Part Category`,
- fields: partCategoryFields(),
+ fields: editCategoryFields,
onFormSuccess: (record: any) => table.updateRecord(record)
});