diff --git a/README.md b/README.md index e8e62f6bd..229e6f880 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,9 @@ server { Now ensure that storybook is running via `npm run storybook`. Finally you can access it under `{your-domain/storybook}` + +## Documentation Overview + +- [Installation](./doc/01_Installation.md) +- [Getting started with your first plugin](./doc/05_Plugins/README.md) +- [SDK Overview](./doc/07_SDK_Overview/README.md) diff --git a/doc/05_Plugins/01_Register_a_tab_for_a_folder_asset.md b/doc/05_Plugins/01_Register_a_tab_for_a_folder_asset.md new file mode 100644 index 000000000..cbd5c0e2a --- /dev/null +++ b/doc/05_Plugins/01_Register_a_tab_for_a_folder_asset.md @@ -0,0 +1,64 @@ +# How to register a new tab for a folder asset + +In this guide we want to add a new tab for a folder asset. Let's take our [basic plugin](./README.md) as starting point. + +First of all let's create a new module that takes care of the registration of a new tab. + +`./modules/asset/folder-tab-extension.tsx`: +``` typescript +import React from 'react' +import { type AbstractModule, Icon, type FolderTabManager, container, serviceIds } from 'pimcore-studio-ui' +import { MyFirstTabComponent } from './components/my-first-tab-component'; + +export const FolderTabExtension: AbstractModule = { + onInit: (): void => { + const tabManager = container.get(serviceIds['Asset/Editor/FolderTabManager']) + + tabManager.register({ + children: , + icon: , + key: 'my-first-tab-component', + label: '1. tab component' + }) + } +} +``` + +In our example we are importing the `MyFirstTabComponent` as the content for our new registered tab. Let's create it: + +`./modules/asset/components/my-first-tab-component.tsx`: +``` typescript +import React from 'react'; + +export const MyFirstTabComponent = (): React.JSX.Element => { + return ( +
+

My First Tab

+

This is a simple tab component.

+
+ ) +} +``` + +And last but not least we have to register our newly created module: + +`./main.ts` +``` typescript +import { Pimcore } from 'pimcore-studio-ui' +import { FolderTabExtension } from './modules/assets/folder-tab-extension' + +Pimcore.pluginSystem.registerPlugin({ + name: 'pimcore-plugin', + + // register modules here + onStartup: ({ moduleSystem }): void => { + moduleSystem.registerModule(FolderTabExtension) + } +}) +``` + +That's it! Our new tab is now rendered in our Pimcore Studio UI. + + + + diff --git a/doc/05_Plugins/02_Adding_custom_icons.md b/doc/05_Plugins/02_Adding_custom_icons.md new file mode 100644 index 000000000..6551f0965 --- /dev/null +++ b/doc/05_Plugins/02_Adding_custom_icons.md @@ -0,0 +1,56 @@ +# How to add custom icons + +In this guide we want add a completely new icon to Pimcore Studio UI. Let's take our [Folder tab plugin](./01_Register_a_tab_for_a_folder_asset.md) as starting point. + +We could use any kind of icon library like Font Awesome or comparable. But let's assume we want to add a custom one: + +`./icons/my-rocket.inline.svg` +``` html + + + +``` + +In the current version of our plugin we are rendering a already known camera icon of Pimcore Studio. Let's replace it with our rocket: + +`./modules/asset/folder-tab-extension.tsx`: +``` typescript +import React from 'react' +import { type AbstractModule Icon, type FolderTabManager, container, serviceIds, type IconLibrary } from 'pimcore-studio-ui' +import { MyFirstTabComponent } from './components/my-first-tab-component'; +import MyRocket from './../../icons/my-rocket.inline.svg' + +export const FolderTabExtension: AbstractModule = { + onInit: (): void => { + const iconLibrary = container.get(serviceIds.iconLibrary) + + // register your new icon here + iconLibrary.register({ + name: 'my-rocket', + component: MyRocket + }) + + const tabManager = container.get(serviceIds['Asset/Editor/FolderTabManager']) + + tabManager.register({ + children: , + icon: , // tell the tab manager to use our new icon + key: 'my-first-tab-component', + label: '1. tab component' + }) + } +} +``` + +We are using `@svgr/webpack` to directly convert a loaded svg file to a react component. So please make sure that you have it installed like mentioned in our [basic plugin](./README.md) example. Also, TypeScript does not know how to handle our svg-import, yet. Let's add a simple declaration file for that: + +`./types/svg.d.ts` +``` typescript +declare module '*.svg' { + import type React from 'react' + const SVG: React.FC> + export default SVG +} +``` + +That's it! Compile your plugin and take a look at our newly added icon! diff --git a/doc/05_Plugins/03_Add_your_first_widget.md b/doc/05_Plugins/03_Add_your_first_widget.md new file mode 100644 index 000000000..9646c5a35 --- /dev/null +++ b/doc/05_Plugins/03_Add_your_first_widget.md @@ -0,0 +1,81 @@ +# How to add your first widget + +In this guide we want to add a new bottom widget when a user clicks a button. Let's take our [Folder tab plugin](./01_Register_a_tab_for_a_folder_asset.md) as starting point. + +A widget is just a simple react component. So, let's create one: + +`./modules/asset/widgets/my-first-widget.tsx` +``` typescript +import React from 'react'; + +export const MyFirstWidget = (): React.JSX.Element => { + return ( +
+

My First Widget

+

This is a simple widget component.

+
+ ); +} +``` + +Now, with the react component defined - we have to tell Pimcore Studio to handle it as a widget: + +`./modules/asset/folder-tab-extension.tsx`: +``` typescript +import React from 'react' +import { type AbstractModule Icon, type FolderTabManager, container, serviceIds, WidgetRegistry } from 'pimcore-studio-ui' +import { MyFirstTabComponent } from './components/my-first-tab-component'; +import { MyFirstWidget } from './widgets/my-first-widget'; + +export const FolderTabExtension: AbstractModule = { + onInit: (): void => { + // registration of our new widget + const widgetManager = container.get(serviceIds.widgetManager) + + widgetManager.registerWidget({ + name: 'my-first-widget', + component: MyFirstWidget + }) + + const tabManager = container.get(serviceIds['Asset/Editor/FolderTabManager']) + + tabManager.register({ + children: , + icon: , + key: 'my-first-tab-component', + label: '1. tab component' + }) + } +} +``` + +Last but not least we have to open up the widget via button click. For that let's extend our tab component: + +`./modules/asset/components/my-first-tab-component.tsx`: +``` typescript +import React from 'react'; +import { Button } from 'antd'; +import { useWidgetManager } from 'pimcore-studio-ui'; + +export const MyFirstTabComponent = (): React.JSX.Element => { + const widgetManager = useWidgetManager(); + + function onClick(): void { + widgetManager.openBottomWidget({ + name: 'My first widget', + component: 'my-first-widget', + }); + } + + return ( +
+

My First Tab

+

This is a simple tab component.

+ + +
+ ); +} +``` + +That's it! We can now open up our first widget via button click. diff --git a/doc/05_Plugins/README.md b/doc/05_Plugins/README.md new file mode 100644 index 000000000..ecb09990f --- /dev/null +++ b/doc/05_Plugins/README.md @@ -0,0 +1,217 @@ +# Plugin system + +The Pimcore Studio plugin system allows developers to extend the functionality of Pimcore Studio by creating custom plugins. + +Plugins can add new features, modify existing functionality, or integrate with external systems. They can provide additional tools, widgets, or settings that enhance the user experience within Pimcore Studio. + +## Getting started + +To begin, ensure you have a basic [Pimcore Bundle](https://pimcore.com/docs/platform/Pimcore/Extending_Pimcore/Bundle_Developers_Guide/). For this guide, let’s call our plugin `PimcoreStudioUiDemoPluginBundle`. + +Since there’s no npm package available yet, follow the local development guide to install the Pimcore Studio UI as one of your frontend dependencies. + +With our dependency in place we should now setup our bundling process. we recommend using [Symfony Encore](https://symfony.com/doc/current/frontend/encore/installation.html). Later on, you’ll need a generated manifest from it. + +``` javascript +const Encore = require('@symfony/webpack-encore'); +const webpack = require('webpack'); +const path = require('path'); +// Install this package for live reloading during development +const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); + +// Manually configure the runtime environment if not already configured yet by the "encore" command. +// It's useful when you use tools that rely on webpack.config.js file. +if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); +} + +Encore + // directory where compiled assets will be stored + .setOutputPath(path.resolve(__dirname, 'public', 'build')) + // public path used by the web server to access the output path + .setPublicPath('/bundles/pimcorestudiouidemoplugin/build') + + /* + * ENTRY CONFIG + * + * Each entry will result in one JavaScript file (e.g. app.js) + * and one CSS file (e.g. app.css) if your JavaScript imports CSS. + */ + .addEntry('main', path.resolve(__dirname, 'assets', 'js', 'src', 'main.ts')) + + // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. + .splitEntryChunks() + + // will require an extra script tag for runtime.js + // but, you probably want this, unless you're building a single-page app + .disableSingleRuntimeChunk() + + /* + * FEATURE CONFIG + * + * Enable & configure other features below. For a full + * list of features, see: + * https://symfony.com/doc/current/frontend.html#adding-more-features + */ + .cleanupOutputBeforeBuild() + .enableBuildNotifications() + .enableSourceMaps(!Encore.isProduction()) + // enables hashed filenames (e.g. app.abc123.css) + .enableVersioning(Encore.isProduction()) + + // uncomment if you use TypeScript + .enableTypeScriptLoader() + + // uncomment if you use React + .enableReactPreset() + + // install @svgr/webpack for this rule + // You will be able to directly inline SVGs after importing them + .addRule({ + test: /\inline\.svg$/i, + use: [{ + loader: '@svgr/webpack', + options: { + icon: true, + typescript: true + } + }], + }) + + // Dev-server for local development with live-reloading + .configureDevServerOptions(options => { + options.host = '0.0.0.0'; + options.hot = true; + options.port = 3030; + options.allowedHosts = 'all'; + }) + + // Important! Reference this vendor-manifest in your build. + // It will take care of injecting Ant-Design, React, etc. without the need to bundle it in your plugin. + .addPlugin(new webpack.DllReferencePlugin({ + context: __dirname, + manifest: path.join(__dirname, 'node_modules', 'pimcore-studio-ui', 'dist', 'vendor', 'vendor-manifest.json') + })) +; + +if (!Encore.isDevServer()) { + // only needed for CDN's or sub-directory deploy + Encore + .setManifestKeyPrefix('bundles/pimcorestudiouidemoplugin/build') + ; +} + +// Additional dev-server configuration +if (Encore.isDevServer()) { + Encore + .setOutputPath('public/build/') + .setPublicPath('/build') + .addPlugin(new ReactRefreshPlugin()) + ; +} + +let config = Encore.getWebpackConfig(); + +// Exclude inline SVGs for package "@svgr/webpack" from the default encore rule +config.module.rules.forEach(rule => { + if (rule.test.toString().includes('|svg|')) { + rule.exclude = /\.inline\.svg$/ + } +}) + +module.exports = config; +``` + +Update your package.json with the following commands, to run Symfony Encore: + +``` json +"scripts": { + "dev-server": "encore dev-server --port 3030 --hot", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress" +}, + +``` + +We are now able to bundle files. So, let's create our main entry point `main.ts`. + +``` typescript +import { Pimcore } from 'pimcore-studio-ui' + +Pimcore.pluginSystem.registerPlugin({ + name: 'pimcore-demo-plugin', + + // Register and overwrite services here + onInit: ({ container }): void => { + console.log('Init my plugin') + }, + + // register modules here + onStartup: ({ moduleSystem }): void => { + console.log('Start up my plugin') + } +}) + +``` + +This will register a simple plugin in our Pimcore Studio UI. + +Now that the main entry point is in place, it’s time to bundle our files. Simply run: + +``` +npm run build +``` + +When the command is finished you should have a few new files in your `./public/build` directory. Most important for us is the `entrypoint.json`, because we still have to tell Pimcore where it will find our generated frontend files. For that we should use the `PimcoreBundleStudioUiInterface` in our Pimcore bundle: + +``` PHP +getPath() . '/public/build/entrypoints.json']; + } + + public function getWebpackEntryPoints(): array + { + return ['main']; + } +} +``` +Now just ensure that our bundle is installed. And finally we should see our `console.log()` in the browser console when we access our Pimcore Studio in the browser. + +### Further guides + +- [How to register a new tab for a folder asset](./01_Register_a_tab_for_a_folder_asset.md) +- [How to add a custom icon](./02_Adding_custom_icons.md) +- [How to add your first widget](03_Add_your_first_widget.md) + +## Local development + +For local development we have to create a local package of the Pimcore Studio UI first. For that we simply have to navigate to our package.json and run the following commands: + +``` +npm install +npm run build +npm pack --pack-destination ~ +``` + +This will create a installable tar file in our home directory. We can now install the package directly in our Bundle via: + +``` +npm install ~/package-name.tgz +``` diff --git a/doc/07_SDK_Overview/README.md b/doc/07_SDK_Overview/README.md new file mode 100644 index 000000000..e8a443df2 --- /dev/null +++ b/doc/07_SDK_Overview/README.md @@ -0,0 +1,120 @@ +# SDK Overview + +The Pimcore Studio UI includes an SDK that allows you to extend specific features out of the box. +There are some essential parts that we want to explain in more detail. + +## Concepts + +### Plugin Architecture + +Overview how a plugin would be integrated in the Pimcore Studio Core lifecycle: + +![Plugin architecture](./../img/plugin-architecture.jpg) + +### Plugins + +Plugins empower you to enhance the Pimcore Studio UI by integrating methods directly into its lifecycle. One crucial method is onInit, which enables you to register new services or override existing ones. For instance, you can completely replace the default tab manager for folder assets, thereby customizing the behavior of your registered tabs: + +``` typescript +onInit: ({ container }): void => { + container.rebind(serviceIds['Asset/Editor/FolderTabManager']).to(ExtendedFolderTabManager).inSingletonScope() +}, +``` + +The `onInit`-method is triggered early in the lifecycle. It processes even the modules registered within Pimcore Studio itself through your extended service. + +Additionally, there’s the `onStartup`-method, which ensures that all your registered modules execute at the appropriate time. + +``` typescript +onStartup: ({ moduleSystem }): void => { + moduleSystem.registerModule(FolderTabExtension) +} +``` + +#### Source + +- [Plugin system](https://github.com/pimcore/studio-ui-bundle/blob/1.x/assets/js/src/core/app/plugin-system/plugin-system.ts) + +### Modules + +Modules consist of code snippets executed immediately after initializing all services from the Studio UI Core and Plugins. They run before the initial app render, enabling you to leverage existing services (such as a tab manager) to provide additional configuration for React components during rendering. For instance, you could register a new tab for a folder asset: + +``` typescript +export const ImageSliderModule: AbstractModule = { + onInit: (): void => { + const tabManager = container.get(serviceIds['Asset/Editor/FolderTabManager']) + + tabManager.register({ + children: , + icon: , + key: 'image-gallery', + label: 'Image Gallery' + }) + } +} +``` + +#### Source + +- [Module system](https://github.com/pimcore/studio-ui-bundle/blob/1.x/assets/js/src/core/app/module-system/module-system.ts) + +### Services + +Think of services as straightforward objects that assist with specific tasks. For instance, they can help you create a new tab: + +``` typescript +const tabManager = container.get(serviceIds['Asset/Editor/FolderTabManager']) + +tabManager.register({ + children: , + icon: , + key: 'image-gallery', + label: 'Image Gallery' +}) +``` + +To maximize the benefits of services, we’ve introduced a service container using [Inversify](https://github.com/inversify/InversifyJS). This container is created early in the application lifecycle, ensuring accessibility throughout the entire app. + +#### source + +- [Service container](https://github.com/pimcore/studio-ui-bundle/blob/1.x/assets/js/src/core/app/depency-injection/index.ts) +- [Core services](https://github.com/pimcore/studio-ui-bundle/blob/1.x/assets/js/src/core/app/config/services/index.ts) + +### Components + +Pimcore Studio UI also offers React components to simplify your work. If you’re curious about how they function, explore our Storybook. + +#### Source + +- [Component overview](https://github.com/pimcore/studio-ui-bundle/tree/1.x/assets/js/src/core/components) + +### Hooks + +In this context, hooks refer to [React custom hooks](https://react.dev/learn/reusing-logic-with-custom-hooks). There are numerous hooks available, including one that facilitates interaction with the widget manager: + +``` typescript +export const MyFirstTabComponent = (): React.JSX.Element => { + const widgetManager = useWidgetManager(); + + function onClick(): void { + widgetManager.openBottomWidget({ + name: 'My first widget', + component: 'my-first-widget', + }); + } + + return ( +
+

My First Tab

+

This is a simple tab component.

+ + +
+ ); +} +``` + +#### Source + +- [Widget manager hooks](https://github.com/pimcore/studio-ui-bundle/tree/1.x/assets/js/src/core/modules/widget-manager/hooks) + diff --git a/doc/img/plugin-architecture.jpg b/doc/img/plugin-architecture.jpg new file mode 100644 index 000000000..fc9520f05 Binary files /dev/null and b/doc/img/plugin-architecture.jpg differ