Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion plugin_creator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ def default_values() -> dict:


def gather_info(context: dict) -> dict:
"""Gather project information from the user."""
"""Gather project information from the user.

There are some conventions regarding naming:

- Plugin title: The human-readable name of the plugin (e.g., "Custom Plugin").
- Plugin name: The Python class name for the plugin (e.g., "CustomPlugin").
- Plugin slug: A URL-friendly version of the plugin title (e.g., "custom-plugin").
- Package name: The Python package name for the plugin (e.g., "custom_plugin").
- Distribution name: The name used for the Python package distribution (e.g., "inventree-custom-plugin").
"""

info("Enter project information:")

Expand All @@ -55,6 +64,15 @@ def gather_info(context: dict) -> dict:
context['plugin_slug'] = context['plugin_title'].replace(" ", "-").lower()
context['package_name'] = context['plugin_slug'].replace("-", "_")

# Convert the package slug to a distribution name
# e.g. 'custom-plugin' -> 'inventree-custom-plugin'
pkg = context['plugin_slug']

if not pkg.startswith("inventree-"):
pkg = f"inventree-{pkg}"

context['distribution_name'] = pkg

success(f"Generating plugin '{context['package_name']}' - {context['plugin_description']}")

info("Enter author information:")
Expand Down Expand Up @@ -133,6 +151,7 @@ def cleanup(plugin_dir: str, context: dict) -> None:

devops.cleanup_devops_files(context['ci_support'], plugin_dir)

# Remove frontend code entirely if not enabled
if context['frontend']['enabled']:
frontend.update_frontend(
plugin_dir,
Expand All @@ -141,6 +160,9 @@ def cleanup(plugin_dir: str, context: dict) -> None:
else:
frontend.remove_frontend(plugin_dir)

# Cleanup mixins
mixins.cleanup_mixins(plugin_dir, context)

if context["git_support"]:
devops.git_init(plugin_dir)

Expand Down
33 changes: 31 additions & 2 deletions plugin_creator/mixins.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
"""InvenTree plugin mixin selection."""

import os

import questionary
from questionary.prompts.common import Choice

from .helpers import info


def available_mixins() -> list:
"""Return a list of available plugin mixin classes."""

# TODO: Support the commented out mixins

return [
# 'APICallMixin',
# 'ActionMixin',
# 'APICallMixin',
# 'AppMixin',
# 'BarcodeMixin',
# 'BulkNotificationMethod',
# 'CurrencyExchangeMixin',
'EventMixin',
# 'DataExportMixin',
# 'IconPackMixin',
# 'LabelPrintingMixin',
'LocateMixin',
# 'MailMixin',
# 'NavigationMixin',
'ReportMixin',
'ScheduleMixin',
'SettingsMixin',
# 'SupplierBarcodeMixin',
# 'UrlsMixin',
'UrlsMixin',
'UserInterfaceMixin',
'ValidationMixin',
]
Expand All @@ -48,3 +54,26 @@ def get_mixins() -> list:
"Select plugin mixins",
choices=choices
).ask()


def cleanup_mixins(plugin_dir: str, context: dict) -> list:
"""Post-build step to remove certain files based on selected mixins."""

mixins = context['plugin_mixins']['mixin_list']

src_dir = os.path.join(
plugin_dir,
context['package_name'],
)

if "UrlsMixin" not in mixins:
# Remove files associated with the UrlsMixin
for filename in [
'serializers.py',
'views.py',
]:
path = os.path.join(src_dir, filename)

if os.path.exists(path):
info("Removing:", filename)
os.remove(path)
2 changes: 2 additions & 0 deletions plugin_creator/template/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"plugin_name": "MyCustomPlugin",
"plugin_slug": "my-custom-plugin",
"package_name": "my_custom_plugin",
"distribution_name": "inventree-my-custom-plugin",
"plugin_version": "0.1.0",
"plugin_mixins": {
"mixin_list": [
Expand All @@ -12,6 +13,7 @@
"ScheduleMixin",
"SettingsMixin",
"UserInterfaceMixin",
"UrlsMixin",
"ValidationMixin"
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "{{ cookiecutter.plugin_slug }}",
"name": "{{ cookiecutter.distribution_name }}",
"private": true,
"version": "0.0.0",
"version": "{{ cookiecutter.plugin_version }}",
"type": "module",
"scripts": {
"lint": "npx @biomejs/biome check ./src",
Expand All @@ -12,13 +12,17 @@
},
"dependencies": {
"@inventreedb/ui": "latest",
"@tabler/icons-react": "^3.31.0"
"@mantine/charts": "{{ cookiecutter.frontend.mantine_version }}",
"@mantine/dates": "{{ cookiecutter.frontend.mantine_version }}",
"@mantine/hooks": "{{ cookiecutter.frontend.mantine_version }}",
"react": "{{ cookiecutter.frontend.react_version }}",
"react-dom": "{{ cookiecutter.frontend.react_version }}",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.60.5"
},
"peerDependencies": {
"@mantine/core": "{{ cookiecutter.frontend.mantine_version }}",
"@mantine/notifications": "{{ cookiecutter.frontend.mantine_version }}",
"react": "{{ cookiecutter.frontend.react_version }}",
"react-dom": "{{ cookiecutter.frontend.react_version }}"
"@mantine/notifications": "{{ cookiecutter.frontend.mantine_version }}"
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useCallback, useMemo, useState } from 'react';
import { Alert, Button, Group, Stack, Text, Title } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useCallback, useMemo, useState } from 'react';
{% if "UrlsMixin" in cookiecutter.plugin_mixins.mixin_list -%}
import { useQuery } from '@tanstack/react-query';
{%- endif %}

// Import for type checking
import { checkPluginVersion, type InvenTreePluginContext } from '@inventreedb/ui';
Expand All @@ -9,7 +12,7 @@ import { ApiEndpoints, apiUrl, ModelType } from '@inventreedb/ui';
/**
* Render a custom panel with the provided context.
* Refer to the InvenTree documentation for the context interface
* https://docs.inventree.org/en/stable/extend/plugins/ui/#plugin-context
* https://docs.inventree.org/en/latest/plugins/mixins/ui/#plugin-context
*/
function {{ cookiecutter.plugin_name }}Panel({
context
Expand All @@ -30,6 +33,22 @@ function {{ cookiecutter.plugin_name }}Panel({
return JSON.stringify(data, null, 2);
}, [context.instance]);

{% if "UrlsMixin" in cookiecutter.plugin_mixins.mixin_list -%}
// Fetch API data from the example API endpoint
// It will re-fetch when the partId changes
const apiQuery = useQuery(
{
queryKey: ['apiData', partId],
queryFn: async() => {
const url = `/plugin/{{ cookiecutter.plugin_slug }}/example/`;

return context.api.get(url).then((response) => response.data).catch(() => {});
}
},
context.queryClient,
);
{%- endif %}

// Custom form to edit the selected part
const editPartForm = context.forms.edit({
url: apiUrl(ApiEndpoints.part_list, partId),
Expand Down Expand Up @@ -68,7 +87,7 @@ function {{ cookiecutter.plugin_name }}Panel({
<>
{editPartForm.modal}
<Stack gap="xs">
<Title order={3}>{{ cookiecutter.plugin_title }}</Title>
<Title c={context.theme.primaryColor} order={3}>{{ cookiecutter.plugin_title }}</Title>
<Text>
This is a custom panel for the {{ cookiecutter.plugin_name }} plugin.
</Text>
Expand All @@ -93,6 +112,25 @@ function {{ cookiecutter.plugin_name }}Panel({
No instance data available
</Alert>
)}
{% if "UrlsMixin" in cookiecutter.plugin_mixins.mixin_list -%}
{apiQuery.isFetched && apiQuery.data && (
<Alert color="green" title="API Query Data">
{apiQuery.isFetching || apiQuery.isLoading ? (
<Text>Loading...</Text>
) : (
<Stack gap='xs'>
<Text>Part Count: {apiQuery.data.part_count}</Text>
<Text>Today: {apiQuery.data.today}</Text>
<Text>Random Text: {apiQuery.data.random_text}</Text>
<Button
disabled={apiQuery.isFetching || apiQuery.isLoading}
onClick={() => apiQuery.refetch()}>
Reload Data
</Button>
</Stack>
)}
</Alert>
)}{%- endif %}
</Stack>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
long_description = f.read()

setuptools.setup(
name="{{ cookiecutter.package_name }}",
name="{{ cookiecutter.distribution_name }}",
version={{ cookiecutter.package_name }}.PLUGIN_VERSION,
author="{{ cookiecutter.author_name }}",
{%- if cookiecutter.author_email %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ class {{ cookiecutter.plugin_name }}(InvenTreePlugin):
{%- if cookiecutter.plugin_mixins.mixin_list %}
{% if "ScheduleMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Scheduled tasks (from ScheduleMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/schedule/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/schedule/
SCHEDULED_TASKS = {
# Define your scheduled tasks here...
}
{%- endif %}
{% if "SettingsMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Plugin settings (from SettingsMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/settings/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/settings/
SETTINGS = {
# Define your plugin settings here...
'CUSTOM_VALUE': {
Expand All @@ -61,7 +61,7 @@ class {{ cookiecutter.plugin_name }}(InvenTreePlugin):
{%- endif %}
{% if "EventMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Respond to InvenTree events (from EventMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/event/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/event/
def wants_process_event(self, event: str) -> bool:
"""Return True if the plugin wants to process the given event."""
# Example: only process the 'create part' event
Expand All @@ -75,7 +75,7 @@ def process_event(self, event: str, *args, **kwargs) -> None:
{%- endif %}
{% if "LocateMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Perform custom locate operations (from LocateMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/locate/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/locate/
def locate_stock_item(self, item_id: int):
"""Attempt to locate a particular StockItem."""
...
Expand All @@ -86,7 +86,7 @@ def locate_stock_location(self, location_id: int):
{%- endif %}
{% if "ReportMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Custom report context (from ReportMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/report/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/report/
def add_label_context(self, label_instance, model_instance, request, context, **kwargs):
"""Add custom context data to a label rendering context."""

Expand All @@ -103,12 +103,25 @@ def report_callback(self, template, instance, report, request, **kwargs):
"""Callback function called after a report is generated."""
...

{%- endif -%}

{%- endif %}
{% if "UrlsMixin" in cookiecutter.plugin_mixins.mixin_list %}
# Custom URL endpoints (from UrlsMixin)
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/urls/
def setup_urls(self):
"""Configure custom URL endpoints for this plugin."""
from django.urls import path
from .views import ExampleView

return [
# Provide path to a simple custom view - replace this with your own views
path('example/', ExampleView.as_view(), name='example-view'),
]

{%- endif %}
{% if "UserInterfaceMixin" in cookiecutter.plugin_mixins.mixin_list %}

# User interface elements (from UserInterfaceMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/ui/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/ui/
{% if cookiecutter.frontend.features.panel %}
# Custom UI panels
def get_ui_panels(self, request, context: dict, **kwargs):
Expand All @@ -135,6 +148,7 @@ def get_ui_panels(self, request, context: dict, **kwargs):

return panels
{% endif %}

{% if cookiecutter.frontend.features.dashboard -%}
# Custom dashboard items
def get_ui_dashboard_items(self, request, context: dict, **kwargs):
Expand Down Expand Up @@ -168,7 +182,7 @@ def get_ui_dashboard_items(self, request, context: dict, **kwargs):
{%- if "ValidationMixin" in cookiecutter.plugin_mixins.mixin_list %}

# Custom data validation (from ValidationMixin)
# Ref: https://docs.inventree.org/en/stable/extend/plugins/validation/
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/validation/
def validate_model_deletion(self, instance, **kwargs):
"""Run custom validation when a model instance is being deleted."""
...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""API serializers for the {{ cookiecutter.plugin_name }} plugin.

In practice, you would define your custom serializers here.

Ref: https://www.django-rest-framework.org/api-guide/serializers/
"""

from rest_framework import serializers


class ExampleSerializer(serializers.Serializer):
"""Example serializer for the {{ cookiecutter.plugin_name }} plugin.

This simply demonstrates how to create a serializer,
with a few example fields of different types.
"""

class Meta:
"""Meta options for this serializer."""
fields = [
'random_text',
'part_count',
'today',
]

random_text = serializers.CharField(
max_length=100,
required=True,
label="Random Text",
help_text="A text field containing randomly generated data."
)

part_count = serializers.IntegerField(
label="Number of Parts",
help_text="Total number of parts in the InvenTree database.",
)

today = serializers.DateField(
required=False,
label="Today",
help_text="The current date.",
)
Loading