diff --git a/docs/docs/assets/images/plugin/plugin_attachment_carousel.png b/docs/docs/assets/images/plugin/plugin_attachment_carousel.png deleted file mode 100644 index dfdf7b819522..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_attachment_carousel.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon.png b/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon.png deleted file mode 100644 index 3208659fa010..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon_wrong.png b/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon_wrong.png deleted file mode 100644 index 372142a8a7f0..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_attachment_carousel_icon_wrong.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_vite_dev_site.png b/docs/docs/assets/images/plugin/plugin_vite_dev_site.png deleted file mode 100644 index 589c3310b44a..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_vite_dev_site.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_vite_mantine_carousel.png b/docs/docs/assets/images/plugin/plugin_vite_mantine_carousel.png deleted file mode 100644 index b9166f40bccf..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_vite_mantine_carousel.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_vite_running.png b/docs/docs/assets/images/plugin/plugin_vite_running.png deleted file mode 100644 index abeb11bf36f8..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_vite_running.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_vscode_layout.png b/docs/docs/assets/images/plugin/plugin_vscode_layout.png deleted file mode 100644 index 052f97a9a90a..000000000000 Binary files a/docs/docs/assets/images/plugin/plugin_vscode_layout.png and /dev/null differ diff --git a/docs/docs/assets/images/plugin/plugin_admin_interface.png b/docs/docs/assets/images/plugin/plugin_walkthrough_admin_interface.png similarity index 100% rename from docs/docs/assets/images/plugin/plugin_admin_interface.png rename to docs/docs/assets/images/plugin/plugin_walkthrough_admin_interface.png diff --git a/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel.png b/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel.png new file mode 100644 index 000000000000..6ea1d415277d Binary files /dev/null and b/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel.png differ diff --git a/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel_picsum.png b/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel_picsum.png new file mode 100644 index 000000000000..dd5e537f69b9 Binary files /dev/null and b/docs/docs/assets/images/plugin/plugin_walkthrough_attachment_carousel_picsum.png differ diff --git a/docs/docs/assets/images/plugin/plugin_walkthrough_default.png b/docs/docs/assets/images/plugin/plugin_walkthrough_default.png new file mode 100644 index 000000000000..37f131781091 Binary files /dev/null and b/docs/docs/assets/images/plugin/plugin_walkthrough_default.png differ diff --git a/docs/docs/assets/images/plugin/plugin_walkthrough_icon_default.png b/docs/docs/assets/images/plugin/plugin_walkthrough_icon_default.png new file mode 100644 index 000000000000..05230a7daf29 Binary files /dev/null and b/docs/docs/assets/images/plugin/plugin_walkthrough_icon_default.png differ diff --git a/docs/docs/assets/images/plugin/plugin_interface_integration.png b/docs/docs/assets/images/plugin/plugin_walkthrough_interface_integration.png similarity index 100% rename from docs/docs/assets/images/plugin/plugin_interface_integration.png rename to docs/docs/assets/images/plugin/plugin_walkthrough_interface_integration.png diff --git a/docs/docs/assets/images/plugin/plugin_walkthrough_vscode_layout.png b/docs/docs/assets/images/plugin/plugin_walkthrough_vscode_layout.png new file mode 100644 index 000000000000..1c5f7c5cd5c5 Binary files /dev/null and b/docs/docs/assets/images/plugin/plugin_walkthrough_vscode_layout.png differ diff --git a/docs/docs/plugins/walkthrough.md b/docs/docs/plugins/walkthrough.md index 5efd5351178d..d6039e73ae46 100644 --- a/docs/docs/plugins/walkthrough.md +++ b/docs/docs/plugins/walkthrough.md @@ -9,14 +9,15 @@ A walkthrough showing how to build a part panel plugin. By the end of the walkthrough, you will have created a plugin that adds a new part panel to display an image carousel from the images attached to the current part. -![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_attachment_carousel.png "Attachment Carousel in Inventree panel screenshot") +![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_walkthrough_attachment_carousel.png "Attachment Carousel in Inventree panel screenshot") #### Prerequisites This is a basic walkthrough and not a guide on how to code in Python or React. It is presumed you have the following, -* A running Inventree instance +* A running Inventree instance and/or a [devcontainer](https://docs.inventree.org/en/stable/develop/devcontainer/) * The [Inventree Plugin Creator](https://github.com/inventree/plugin-creator) installed * [Visual Studio Code (VS Code)](https://code.visualstudio.com/) (or an IDE of your choosing) +* [Node Package Manager](https://www.npmjs.com/) installed * Basic Python knowledge * Basic React knowledge * Optional: @@ -37,42 +38,50 @@ After the plugin wizard has launched, enter the name as `Attachment Carousel` an At the plugin structure information questions, -* Select `UserInterfaceMixin` +* Only select `UserInterfaceMixin` and `SettingsMixin` * `Add User Interface Support` -* Select `Custom panel items` -* Select `None` for the `DevOps support` +* Only select `Custom panel items` +* Select `No` for `Enable translation support` +* Select `No` for `Git Integration` ![Plugin Wizard Screenshot showing mixins](../assets/images/plugin/plugin_wizard_mixins.png "Plugin Wizard Screenshot showing mixins") ![Plugin Wizard Screenshot showing items](../assets/images/plugin/plugin_wizard_items.png "Plugin Wizard Screenshot showing items") -After the Plugin Creator has finished you should have a new project that looks like this, +After the Plugin Creator has finished you should have a new project that looks like this (seen here in VS Code), -![VS Code screenshot](../assets/images/plugin/plugin_vscode_layout.png "VS Code screenshot") +![VS Code screenshot](../assets/images/plugin/plugin_walkthrough_vscode_layout.png "VS Code screenshot") #### A Brief Overview of the Environment `attachment_carousel` contains several files, but the main file is `core.py` which is the Python entry point for the plugin. Once created, this folder will also contain the bundled frontend code and any [static file assets](./index.md#static-files). -`frontend/src` contains the frontend React TypeScript code, -* `App.tsx` - The application component -* `main.tsx` - The standalone development entry point (this is only required for testing) -* `Panel.tsx` - The Inventree panel entry point +`frontend/src` contains `Panel.tsx' which is the frontend React TypeScript code. ### Testing the Plugin Environment +To ensure the environment is working as expected, from the terminal navigate to the `frontend` directory and run either the development server (if using the development environment) **or** build the project and install the plugin. -To ensure the environment is working as expected, from the terminal run the development Vite server, +Development Environment ``` Bash +npm install npm run dev ``` -This should launch the local development server, -![Vite screenshot](../assets/images/plugin/plugin_vite_running.png "Vite screenshot") +Generic Build and install -If you browse to the address provided you should see the default plugin website, +``` Bash +npm install +npm run build +``` + +Copy the built `attachment_carousel` diretory to the `inventree-data/plugins` directory and enable it via the admin interface. -![Vite website screenshot](../assets/images/plugin/plugin_vite_dev_site.png "Vite website screenshot") +![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_walkthrough_default.png "Attachment Carousel in Inventree panel screenshot") + +!!! info "Tip" + If you do not see the `Attachment Carousel` panel listed, make sure you have `Enable interface integration` turned on (it is disabled by default) +![Inventree Plugin Settings screenshot](../assets/images/plugin/plugin_walkthrough_interface_integration.png "Inventree Plugin Settings screenshot") ### Creating the Carousel @@ -84,32 +93,46 @@ As the Mantine UI carousel is not added to the plugin project by default, add it npm install @mantine/carousel ``` -Edit `main.tsx` to add the carousel CSS, +Edit `Panel.tsx` to add the carousel references, ``` TypeScript -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' import { MantineProvider } from '@mantine/core' -import "@mantine/core/styles.css"; -import '@mantine/carousel/styles.css'; - -createRoot(document.getElementById('root')!).render( - - - - - , -) +import AttachmentCarousel from './AttachmentCarousel.tsx'; + +// Import for type checking +import { checkPluginVersion, type InvenTreePluginContext } 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/latest/plugins/mixins/ui/#plugin-context + */ +function AttachmentCarouselPanel({context}: {context: InvenTreePluginContext;}) { + console.log(context); + return ( + <> + + + + + ); +} + +// This is the function which is called by InvenTree to render the actual panel component +export function renderAttachmentCarouselPanel(context: InvenTreePluginContext) { + checkPluginVersion(context); + return ; +} ``` -Edit `App.tsx` to add the relevant carousel code, +Create a new file called `AttachmentCarousel.tsx` and add the carousel code, ``` TypeScript +{% raw %} import { Carousel } from '@mantine/carousel'; import { Image, AspectRatio } from '@mantine/core'; -export default function App() { +export default function AttachmentCarousel() { const indicators = true const loop = true @@ -128,55 +151,16 @@ export default function App() { return ( - {slides} + {slides} ); } +{% endraw %} ``` -Run the Vite development environment again and browse to the website. You should now see the Mantine UI carousel. - -![Mantine UI carousel screenshot](../assets/images/plugin/plugin_vite_mantine_carousel.png "Mantine UI carousel screenshot") - -### Adding The Carousel to an Inventree Panel - -As previously mentioned, `Panel.tsx` is the default entry point from the Inventree Python backend. Therefore, change `Panel.tsx` to render our carousel component rather than the example code, - -``` TypeScript -import { MantineProvider } from '@mantine/core'; -import { createRoot } from 'react-dom/client'; -import App from './App'; -import '@mantine/carousel/styles.css'; - -/** - * Render the AttachmentCarouselPanel component. - * - * @param target - The target HTML element to render the panel into - * @param context - The context object to pass to the panel - */ -export function renderAttachmentCarouselPanel(target: HTMLElement, context: any) { - createRoot(target).render( - - - - ); -} -``` - -Generate the Vite bundle via terminal, +As detailed earlier, run/build the plugin. You should now see that the plugin has been update to show a carousel. -``` Bash -vite run build --emptyOutDir -``` - -Depending on how you are running Inventree, [install the plugin](./install.md), enable it, and view the resulting panel, - -![Attachment Carousel Screenshot](../assets/images/plugin/plugin_attachment_carousel_icon_wrong.png "Attachment Carousel Screenshot") - -!!! info "Tip" - If you do not see the `Attachment Carousel` panel listed, make sure you have `Enable interface integration` turned on (it is disabled by default) - -![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_interface_integration.png "Attachment Carousel in Inventree panel screenshot") +![Mantine UI carousel screenshot](../assets/images/plugin/plugin_walkthrough_attachment_carousel_picsum.png "Mantine UI carousel screenshot") ### Connecting the Carousel to Inventree Part Attachments via the Frontend @@ -188,70 +172,96 @@ Install TanStack Query in our project via the terminal npm install @tanstack/react-query ``` -Update `App.tsx` to use TanStack Query and query for all part attachments, +Update `Panel.tsx` to add a TanStack `QueryClientProvider` and send the context to the attachment carousel, +``` diff +import { MantineProvider } from '@mantine/core' +import AttachmentCarousel from './AttachmentCarousel'; ++import { QueryClientProvider } from '@tanstack/react-query'; +... +function AttachmentCarouselPanel({context}: {context: InvenTreePluginContext;}) { +- console.log(context); + return ( + <> ++ + +- ++ + ++ + + ); +} +... +``` !!! info "More Info" - By using the `context` api query we do not need to pass an API key or have the user login to perform the API call, it just happens automatically. + By providing the `context`, we can use the context api query to avoid the need to pass an API key or have the user login to perform the API call, it just happens automatically. + +Update `Attachment_carousel.tsx` to use TanStack Query and query for all part attachments, ``` TypeScript +{% raw %} import { Carousel } from '@mantine/carousel'; import { Image, AspectRatio } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; -export default function Test({ context }: { context: any }) { - - const indicators = true - const loop = true - const vaildAttachmentTypes: string[] = ["jpg", "jpeg"] // Add more items to the array if you want to support more file types. - let imageUrls: string[] = new Array(); - - const { data: attachments } = useQuery({ - queryKey: ["attachment"], - queryFn: async () => { - const response = await context.api?.get('api/attachment/', { - params: { - model_type: context.model, - model_id: context.id, - is_file: true, - limit: 100 - } - }) - return await response.data.results - } - }) +export default function AttachmentCarousel({context}: { context: any }) { - if (attachments) { - attachments.forEach((element: { attachment: string; filename: string; }) => { - if (vaildAttachmentTypes.includes(element.filename.split('.').pop() || "")) { - imageUrls.push(context.globalSettings.getSetting("INVENTREE_BASE_URL") + element.attachment); + const indicators = true + const loop = true + const vaildAttachmentTypes: string[] = ["jpg", "jpeg"] // Add more items to the array if you want to support more file types. + let imageUrls: string[] = new Array(); + + const { data: attachments } = useQuery({ + queryKey: ["attachment"], + queryFn: async () => { + const response = await context.api?.get('api/attachment/', { + params: { + model_type: context.model, + model_id: context.id, + is_file: true, + limit: 100 + } + }) + return await response.data.results } - }); - } - else { - imageUrls = [""]; - } - - const slides = imageUrls.map((url) => ( - - - - )); + }) + + if (attachments) { + attachments.forEach((element: { attachment: string; filename: string; }) => { + if (vaildAttachmentTypes.includes(element.filename.split('.').pop() || "")) { + imageUrls.push(context.globalSettings.getSetting("INVENTREE_BASE_URL") + element.attachment); + } + }); + } + else { + imageUrls = [""]; + } + + const slides = imageUrls.map((url) => ( + + + + )); return ( - {slides} + {slides} ); } +{% endraw %} ``` Upload some appropriately sized images in the specified file type (see code), re-bundle the plugin, and update it in Inventree. You should now see the carousel displaying images from the attachments on the part. Upload images to different parts and see how the carousel changes based on the part you are viewing. + + ### Connecting the Carousel to Inventree Part Attachments via the Backend -An alternative to doing the API query on the frontend is to do it via the backend and supply the image URLs via additional context data. The advantage of doing the query this way is the panel can now be shown or hidden based on if any images are found on the part, and the images could also be checked to ensure they are images and not corrupted etc. The downside is there is more backend processing, and the context variable could become quite large when passed to the frontend. Of course, queries could be done on the backend and frontend and not pass the data via the context. It is for the developer of the plugin to decide which approach is best... +An alternative to doing the API query on the frontend is to do it via the backend and supply the image URLs via additional context data. The advantage of doing the query this way is the panel can now be shown or hidden based on if any images are found on the part, and the images could also be checked to ensure they are images and not corrupted etc. The downsides are, there is more backend processing, the context variable could become quite large when passed to the frontend and the carousel will not automatically see new attachments as the backend is only run when the page is first loaded. Of course, queries could be done on the backend and frontend and not pass the data via the context. It is for the developer of the plugin to decide which approach is best... Back to the walkthrough, open `core.py` in the `attachment_carousel` folder and make the following changes, @@ -282,7 +292,7 @@ Back to the walkthrough, open `core.py` in the `attachment_carousel` folder and + if attachments.count() > 0: + for attachment in attachments: -+ attachments_urls.append(base_url + '/media/' + attachment.attachment.name) ++ attachment_urls.append(base_url + '/media/' + attachment.attachment.name) + if len(attachment_urls) > 0: panels.append({ @@ -294,6 +304,7 @@ Back to the walkthrough, open `core.py` in the `attachment_carousel` folder and 'context': { # Provide additional context data to the panel 'settings': self.get_settings_dict(), +- 'foo': 'bar' + 'attachments': attachment_urls, } }) @@ -303,7 +314,7 @@ Back to the walkthrough, open `core.py` in the `attachment_carousel` folder and Now the backend collects the attachment details and passes them to the frontend, whilst also only displaying the panel if attachments are found. -Next, modify `App.tsx` to use this new information, +Next, modify `AttachmentCarousel.tsx` to use this new information, ``` Diff import { Carousel } from '@mantine/carousel'; @@ -358,6 +369,11 @@ Next, modify `App.tsx` to use this new information, ``` +Again, re-bundle the plugin, and update it in Inventree. + +You should now see the carousel displaying images from the attachments on the part, but this time they are passed via context from the backend. + + ### Changing the Panel Icon Inventree uses [Tabler icons](https://tabler.io/icons) and it is easy to change the panel's icon to something more suitable. Simply find the [Tabler icon](https://tabler.io/icons) you would like and update the icon reference in `core.py`, @@ -378,7 +394,7 @@ panels.append({ }) ``` -![Attachment Carousel in Inventree panel with updated icon screenshot](../assets/images/plugin/plugin_attachment_carousel_icon.png "Attachment Carousel in Inventree panel screenshot updated icon screenshot") +![Attachment Carousel in Inventree panel with updated icon screenshot](../assets/images/plugin/plugin_walkthrough_attachment_carousel.png "Attachment Carousel in Inventree panel screenshot updated icon screenshot") ### Adding Plugin Admin Options @@ -414,7 +430,7 @@ Make the following changes to `core.py`, ... ``` -Edit `App.tsx` to use the new options, +Edit `AttachmentCarousel.tsx` to use the new options, ``` Diff export default function App({ context }: { context: any }) { @@ -428,13 +444,13 @@ export default function App({ context }: { context: any }) { Now you have two options that change the behaviour of the plugin. -![Attachment Carousel Inventree Admin interface screenshot](../assets/images/plugin/plugin_admin_interface.png "Attachment Carousel Inventree Admin interface screenshot") +![Attachment Carousel Inventree Admin interface screenshot](../assets/images/plugin/plugin_walkthrough_admin_interface.png "Attachment Carousel Inventree Admin interface screenshot") -### Using CSS to Enhance the User Experience +### Using CSS to Enhance the End User Experience When `loop` is set to `disabled` the carousel still displays the previous and next buttons for the first and last images. This behaviour can be changed with some CSS. -Create a new file called `App.css` and add the following, +Create a new file called `AttachmentCarousel.css` and add the following, ``` CSS .control { @@ -452,23 +468,47 @@ Create a new file called `App.css` and add the following, } ``` -Vite will automatically bundle all the CSS files in the project to `static/assets/style.css`, but it will not automatically add a reference to the stylesheet. Add the reference manually to `Panel.tsx`, +Update `AttachmentCarousel.tsx` to reference to this stylesheet and the default Mantine Carousel styles, -``` TypeScript +```diff +import { Carousel } from '@mantine/carousel'; +import { Image, AspectRatio } from '@mantine/core'; ++ import '@mantine/carousel/styles.css'; // Import Mantine Carousel styles ++ import './AttachmentCarousel.css'; // Import custom styles for the carousel +... +``` + +Update `AttachmentCarousel.tsx` to use the new styles, + +```diff ... -export function renderAttachmentCarouselPanel(target: HTMLElement, context: any) { +{% raw %} + +-{slides} ++{slides} + +{% endraw %} +... +``` - createRoot(target).render( +Vite will automatically bundle all the CSS files in the project to `static/assets/style.css`, but it will not automatically add a reference to the stylesheet. Add the reference manually to `Panel.tsx`, + +``` diff +... +function AttachmentCarouselPanel({context}: {context: InvenTreePluginContext;}) { + console.log(context); + return ( <> - - - - ++ + + + ); } +... ``` The reference requires the host and the [plugin-name](./index.md##static-files). Rather than statically coding these references, the host reference may be retrieved via the context and the plugin-name may be passed via additional context data. In this example it is passed as the `slug`, as by default the slug is the plugin name. Add this additional context data via `core.py`, @@ -483,7 +523,7 @@ panels.append({ 'context': { # Provide additional context data to the panel 'settings': self.get_settings_dict(), - 'attachments': attachments_info, + 'attachments': attachments_urls, + 'slug': self.SLUG } }) @@ -491,4 +531,4 @@ panels.append({ Update the plugin in Inventree and the walkthrough is complete! -![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_attachment_carousel.png "Attachment Carousel in Inventree panel screenshot") +![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_walkthrough_attachment_carousel.png "Attachment Carousel in Inventree panel screenshot") diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index ead7da69fa5d..578f5e56f0cf 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 387 +INVENTREE_API_VERSION = 388 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v388 -> 2025-08-23 : https://github.com/inventree/InvenTree/pull/10213 + - Disable paging on PurchaseOrderReceive call + v387 -> 2025-08-19 : https://github.com/inventree/InvenTree/pull/10188 - Adds "update_records" field to the DataImportSession API diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 7d6a68ea0644..31205352e19f 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -492,6 +492,7 @@ class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI): queryset = models.PurchaseOrderLineItem.objects.none() serializer_class = serializers.PurchaseOrderReceiveSerializer + pagination_class = None def create(self, request, *args, **kwargs): """Override the create method to handle stock item creation."""