diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 90bd51dfd7..747ef01e37 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,14 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 261
+INVENTREE_API_VERSION = 262
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
-261 - 2024-09;26 : https://github.com/inventree/InvenTree/pull/8184
+262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
+ - Tweak permission requirements for uninstalling plugins via API
+
+261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
- Fixes for BuildOrder API serializers
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py
index ff1e301357..ab7626e2af 100644
--- a/src/backend/InvenTree/plugin/api.py
+++ b/src/backend/InvenTree/plugin/api.py
@@ -21,11 +21,11 @@ from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import (
CreateAPI,
ListAPI,
+ RetrieveDestroyAPI,
RetrieveUpdateAPI,
- RetrieveUpdateDestroyAPI,
UpdateAPI,
)
-from InvenTree.permissions import IsSuperuser
+from InvenTree.permissions import IsSuperuser, IsSuperuserOrReadOnly
from plugin import registry
from plugin.base.action.api import ActionPluginView
from plugin.base.barcodes.api import barcode_api_urls
@@ -143,7 +143,7 @@ class PluginList(ListAPI):
search_fields = ['key', 'name']
-class PluginDetail(RetrieveUpdateDestroyAPI):
+class PluginDetail(RetrieveDestroyAPI):
"""API detail endpoint for PluginConfig object.
get:
@@ -158,6 +158,7 @@ class PluginDetail(RetrieveUpdateDestroyAPI):
queryset = PluginConfig.objects.all()
serializer_class = PluginSerializers.PluginConfigSerializer
+ permission_classes = [IsSuperuserOrReadOnly]
lookup_field = 'key'
lookup_url_kwarg = 'plugin'
diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py
index 5f16bac369..ce3555720c 100644
--- a/src/backend/InvenTree/plugin/plugin.py
+++ b/src/backend/InvenTree/plugin/plugin.py
@@ -438,3 +438,11 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
self.package = package
# endregion
+
+ def plugin_static_file(self, *args):
+ """Construct a path to a static file within the plugin directory."""
+ import os
+
+ from django.conf import settings
+
+ return '/' + os.path.join(settings.STATIC_URL, 'plugins', self.SLUG, *args)
diff --git a/src/frontend/src/tables/plugin/PluginListTable.tsx b/src/frontend/src/tables/plugin/PluginListTable.tsx
index 3fc1a3e746..2f18762736 100644
--- a/src/frontend/src/tables/plugin/PluginListTable.tsx
+++ b/src/frontend/src/tables/plugin/PluginListTable.tsx
@@ -17,7 +17,8 @@ import {
IconHelpCircle,
IconInfoCircle,
IconPlaylistAdd,
- IconRefresh
+ IconRefresh,
+ IconTrash
} from '@tabler/icons-react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -352,9 +353,10 @@ export default function PluginListTable() {
{
hidden:
record.is_builtin != false ||
- record.is_installed != true ||
+ !record.is_installed ||
record.active != false,
title: t`Activate`,
+ tooltip: t`Activate selected plugin`,
color: 'green',
icon: ,
onClick: () => {
@@ -364,11 +366,9 @@ export default function PluginListTable() {
}
},
{
- hidden:
- record.active != true ||
- record.is_package != true ||
- !record.package_name,
+ hidden: !record.active || !record.is_package || !record.package_name,
title: t`Update`,
+ tooltip: t`Update selected plugin`,
color: 'blue',
icon: ,
onClick: () => {
@@ -377,10 +377,37 @@ export default function PluginListTable() {
}
},
{
- hidden: record.is_installed != false,
- title: t`Delete`,
+ // Uninstall an installed plugin
+ // Must be inactive, not a builtin, not a sample, and installed as a package
+ hidden:
+ !user.isSuperuser() ||
+ record.active ||
+ record.is_builtin ||
+ record.is_sample ||
+ !record.is_installed ||
+ !record.is_package,
+ title: t`Uninstall`,
+ tooltip: t`Uninstall selected plugin`,
color: 'red',
icon: ,
+ onClick: () => {
+ setSelectedPlugin(record.key);
+ uninstallPluginModal.open();
+ }
+ },
+ {
+ // Delete a plugin configuration
+ // Must be inactive, not a builtin, not a sample, and not installed (i.e. no matching plugin)
+ hidden:
+ record.active ||
+ record.is_builtin ||
+ record.is_sample ||
+ record.is_installed ||
+ !user.isSuperuser(),
+ title: t`Delete`,
+ tooltip: t`Delete selected plugin configuration`,
+ color: 'red',
+ icon: ,
onClick: () => {
setSelectedPlugin(record.key);
deletePluginModal.open();
@@ -436,7 +463,7 @@ export default function PluginListTable() {
const uninstallPluginModal = useEditApiFormModal({
title: t`Uninstall Plugin`,
url: ApiEndpoints.plugin_uninstall,
- // pathParams: { key: selectedPlugin },
+ pathParams: { key: selectedPlugin },
fetchInitialData: false,
timeout: 30000,
fields: {
@@ -460,15 +487,14 @@ export default function PluginListTable() {
const deletePluginModal = useDeleteApiFormModal({
url: ApiEndpoints.plugin_list,
- pathParams: { key: selectedPlugin },
+ pk: selectedPlugin,
+ fetchInitialData: false,
title: t`Delete Plugin`,
preFormWarning: t`Deleting this plugin configuration will remove all associated settings and data. Are you sure you want to delete this plugin?`,
table: table
});
const reloadPlugins = useCallback(() => {
- console.log('reloadPlugins:');
-
api
.post(apiUrl(ApiEndpoints.plugin_reload), {
full_reload: true,