2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 04:56:45 +00:00

[PUI] subscribe (#8272)

* Add "starred" icon to part category table

* Edit "starred" status for PartCategory in PUI

* Edit "starred" state of part in PUI

* Playground fix
This commit is contained in:
Oliver 2024-10-12 11:55:23 +11:00 committed by GitHub
parent 7443d21854
commit 8aade768c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 103 additions and 35 deletions

View File

@ -270,7 +270,7 @@ class CategoryDetail(CategoryMixin, CustomRetrieveUpdateDestroyAPI):
if 'starred' in data: if 'starred' in data:
starred = str2bool(data.get('starred', False)) 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) response = super().update(request, *args, **kwargs)
@ -1423,7 +1423,9 @@ class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
if 'starred' in data: if 'starred' in data:
starred = str2bool(data.get('starred', False)) 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) response = super().update(request, *args, **kwargs)

View File

@ -288,11 +288,10 @@ class PartCategory(InvenTree.models.InvenTreeTree):
def get_subscribers(self, include_parents=True): def get_subscribers(self, include_parents=True):
"""Return a list of users who subscribe to this PartCategory.""" """Return a list of users who subscribe to this PartCategory."""
cats = self.get_ancestors(include_self=True)
subscribers = set() subscribers = set()
if include_parents: if include_parents:
cats = self.get_ancestors(include_self=True)
queryset = PartCategoryStar.objects.filter(category__in=cats) queryset = PartCategoryStar.objects.filter(category__in=cats)
else: else:
queryset = PartCategoryStar.objects.filter(category=self) 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.""" """Returns True if the specified user subscribes to this category."""
return user in self.get_subscribers(**kwargs) 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.""" """Set the "subscription" status of this PartCategory against the specified user."""
if not user: if not user:
return return
if self.is_starred_by(user) == status: if self.is_starred_by(user, **kwargs) == status:
return return
if status: if status:
@ -1451,13 +1450,13 @@ class Part(
"""Return True if the specified user subscribes to this part.""" """Return True if the specified user subscribes to this part."""
return user in self.get_subscribers(**kwargs) 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.""" """Set the "subscription" status of this Part against the specified user."""
if not user: if not user:
return return
# Already subscribed? # Already subscribed?
if self.is_starred_by(user) == status: if self.is_starred_by(user, **kwargs) == status:
return return
if status: if status:

View File

@ -61,7 +61,14 @@ export function usePartFields({
salable: {}, salable: {},
virtual: {}, virtual: {},
locked: {}, 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 // Additional fields for creation
@ -111,6 +118,10 @@ export function usePartFields({
delete fields['default_expiry']; delete fields['default_expiry'];
} }
if (create) {
delete fields['starred'];
}
return fields; return fields;
}, [create, settings]); }, [create, settings]);
} }
@ -118,7 +129,12 @@ export function usePartFields({
/** /**
* Construct a set of fields for creating / editing a PartCategory instance * Construct a set of fields for creating / editing a PartCategory instance
*/ */
export function partCategoryFields(): ApiFormFieldSet { export function partCategoryFields({
create
}: {
create?: boolean;
}): ApiFormFieldSet {
let fields: ApiFormFieldSet = useMemo(() => {
let fields: ApiFormFieldSet = { let fields: ApiFormFieldSet = {
parent: { parent: {
description: t`Parent part category`, description: t`Parent part category`,
@ -133,11 +149,25 @@ export function partCategoryFields(): ApiFormFieldSet {
}, },
default_keywords: {}, default_keywords: {},
structural: {}, structural: {},
starred: {
field_type: 'boolean',
label: t`Subscribed`,
description: t`Subscribe to notifications for this category`,
disabled: false,
required: false
},
icon: { icon: {
field_type: 'icon' field_type: 'icon'
} }
}; };
if (create) {
delete fields['starred'];
}
return fields;
}, [create]);
return fields; return fields;
} }

View File

@ -3,6 +3,7 @@ import {
Icon123, Icon123,
IconArrowBigDownLineFilled, IconArrowBigDownLineFilled,
IconArrowMerge, IconArrowMerge,
IconBell,
IconBinaryTree2, IconBinaryTree2,
IconBookmarks, IconBookmarks,
IconBox, IconBox,
@ -159,6 +160,8 @@ const icons = {
issue: IconBrandTelegram, issue: IconBrandTelegram,
complete: IconCircleCheck, complete: IconCircleCheck,
deliver: IconTruckDelivery, deliver: IconTruckDelivery,
bell: IconBell,
notification: IconBell,
// Part Icons // Part Icons
active: IconCheck, active: IconCheck,

View File

@ -27,9 +27,10 @@ import {
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
// Generate some example forms using the modal API forms interface // Generate some example forms using the modal API forms interface
const fields = partCategoryFields();
function ApiFormsPlayground() { function ApiFormsPlayground() {
const fields = partCategoryFields({});
const editCategory = useEditApiFormModal({ const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list, url: ApiEndpoints.category_list,
pk: 2, pk: 2,

View File

@ -109,6 +109,12 @@ export default function CategoryDetail() {
label: t`Parent Category`, label: t`Parent Category`,
model: ModelType.partcategory, model: ModelType.partcategory,
hidden: !category?.parent hidden: !category?.parent
},
{
type: 'boolean',
name: 'starred',
icon: 'notification',
label: t`Subscribed`
} }
]; ];
@ -165,7 +171,7 @@ export default function CategoryDetail() {
url: ApiEndpoints.category_list, url: ApiEndpoints.category_list,
pk: id, pk: id,
title: t`Edit Part Category`, title: t`Edit Part Category`,
fields: partCategoryFields(), fields: partCategoryFields({}),
onFormSuccess: refreshInstance onFormSuccess: refreshInstance
}); });

View File

@ -358,6 +358,12 @@ export default function PartDetail() {
type: 'boolean', type: 'boolean',
name: 'virtual', name: 'virtual',
label: t`Virtual Part` label: t`Virtual Part`
},
{
type: 'boolean',
name: 'starred',
label: t`Subscribed`,
icon: 'bell'
} }
]; ];

View File

@ -3,7 +3,7 @@
*/ */
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Anchor, Group, Skeleton, Text, Tooltip } from '@mantine/core'; 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 { YesNoButton } from '../components/buttons/YesNoButton';
import { Thumbnail } from '../components/images/Thumbnail'; import { Thumbnail } from '../components/images/Thumbnail';
@ -42,6 +42,11 @@ export function PartColumn({
<IconLock size={16} /> <IconLock size={16} />
</Tooltip> </Tooltip>
)} )}
{part?.starred && (
<Tooltip label={t`You are subscribed to notifications for this part`}>
<IconBell size={16} color="green" />
</Tooltip>
)}
</Group> </Group>
</Group> </Group>
) : ( ) : (

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro'; 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 { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
@ -36,10 +37,21 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
sortable: true, sortable: true,
switchable: false, switchable: false,
render: (record: any) => ( render: (record: any) => (
<Group gap="xs"> <Group gap="xs" wrap="nowrap" justify="space-between">
<Group gap="xs" wrap="nowrap">
{record.icon && <ApiIcon name={record.icon} />} {record.icon && <ApiIcon name={record.icon} />}
{record.name} {record.name}
</Group> </Group>
<Group gap="xs" justify="flex-end" wrap="nowrap">
{record.starred && (
<Tooltip
label={t`You are subscribed to notifications for this category`}
>
<IconBell color="green" size={16} />
</Tooltip>
)}
</Group>
</Group>
) )
}, },
DescriptionColumn({}), DescriptionColumn({}),
@ -81,10 +93,12 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
]; ];
}, []); }, []);
const newCategoryFields = partCategoryFields({ create: true });
const newCategory = useCreateApiFormModal({ const newCategory = useCreateApiFormModal({
url: ApiEndpoints.category_list, url: ApiEndpoints.category_list,
title: t`New Part Category`, title: t`New Part Category`,
fields: partCategoryFields(), fields: newCategoryFields,
focus: 'name', focus: 'name',
initialData: { initialData: {
parent: parentId parent: parentId
@ -96,11 +110,13 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
const [selectedCategory, setSelectedCategory] = useState<number>(-1); const [selectedCategory, setSelectedCategory] = useState<number>(-1);
const editCategoryFields = partCategoryFields({ create: false });
const editCategory = useEditApiFormModal({ const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list, url: ApiEndpoints.category_list,
pk: selectedCategory, pk: selectedCategory,
title: t`Edit Part Category`, title: t`Edit Part Category`,
fields: partCategoryFields(), fields: editCategoryFields,
onFormSuccess: (record: any) => table.updateRecord(record) onFormSuccess: (record: any) => table.updateRecord(record)
}); });