From 65d01964fdb099414900a58d6ef658c4dded770b Mon Sep 17 00:00:00 2001 From: Petar Vukmirovic Date: Mon, 13 Oct 2025 16:59:31 +0200 Subject: [PATCH 1/6] Introduce documentation for Custom Blob Document API --- .../apidocs/studio-pro-11/extensibility-api/web/_index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/_index.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/_index.md index fe6b31c59d8..c8c045dd91c 100644 --- a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/_index.md +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/_index.md @@ -47,3 +47,4 @@ Below is a list of how-tos for you to begin with: * [How to Open Documents](/apidocs-mxsdk/apidocs/web-extensibility-api-11/editor-api/) * [How to Exchange Information Between Active Views](/apidocs-mxsdk/apidocs/web-extensibility-api-11/message-passing-api/) * [How to Show Version Control Information](/apidocs-mxsdk/apidocs/web-extensibility-api-11/version-control-api/) +* [How to Introduce a New Document Type](/apidocs-mxsdk/apidocs/web-extensibility-api-11/custom-blob-document-api/) From 5afacfec25729191bb9a2f4ae67d697abd2131a0 Mon Sep 17 00:00:00 2001 From: Petar Vukmirovic Date: Mon, 13 Oct 2025 16:59:46 +0200 Subject: [PATCH 2/6] Add untracked file --- .../custom-blob-document-api.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md new file mode 100644 index 00000000000..f7c2ebbfa47 --- /dev/null +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md @@ -0,0 +1,270 @@ +--- +title: "Register New Document Types with Corresponding Editor" +linktitle: "Introduce New Document Types" +url: /apidocs-mxsdk/apidocs/web-extensibility-api-11/custom-blob-document-api/ +--- + +## Introduction + +This how-to describes how to introduce a new document type and provide a custom editor to allow users to edit new documents of the introduced type. + +## Prerequisites + +Before starting this how-to, make sure you have completed the following prerequisites: + +* [Get Started with the Web Extensibility API](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/). + +## Custom Document Model + +Studio Pro provides you with possibility to extend its metamodel by introducing your own document types. These new documents are allowed to store arbitrary data, +which must be serializable as strings. If an editor, which is a user-defined UI component, is registered for a document, new document will appear in UI as any other +built-in document type (e.g., constants, Java Actions or pages). In particular, it will appear in New Document and Find Advanced dialogs, context menus for adding +new documents, App explorer and other UI elements that show Studio Pro documents. Custom editors can be registered as either tabs or modal dialogs. + +## Registering a New Document Type + +To start with registering a new document type, generate a new extension called `myextension`, as described in [getting started guide](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/). +We will explain which files of the generated extension to edit and then explain what the edits achieve. + +First, replace the contents of `src/main/index.ts` with: + +```typescript {hl_lines=["8-24"]} +import { IComponent, getStudioProApi } from "@mendix/extensions-api"; +import { personDarkThemeIcon, personDocumentType, personLightThemeIcon } from "../model/constants"; +import { PersonInfo } from "../model/PersonInfo"; + +export const component: IComponent = { + async loaded(componentContext) { + const studioPro = getStudioProApi(componentContext); + await studioPro.app.model.customBlobDocuments.registerDocumentType({ + type: personDocumentType, + readableTypeName: 'Person', + defaultContent: { + firstName: '', + lastName: '', + age: 0, + email: '' + } + }); + await studioPro.ui.editors.registerEditorForCustomDocument({ + documentType: personDocumentType, + editorEntryPoint: 'editor', + editorKind: 'tab', + iconLight: personLightThemeIcon, + iconDark: personDarkThemeIcon + }) + + } +} +``` + +Then add a new file `src/model/contants.ts` with contents: + +```typescript +export const personDocumentType = 'myextension.Person'; +export const personLightThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAABKElEQVRIDd2Vyw3CMBBEAxIUAWVQBxIcKIBiuNAAFVAIV2iAA2cKoAGYF9nIctaxscIBRhrZ2Z3d9T9N8++YaoIb8ShexYcjfWz40FRhraib+MwQDdpijKXci7nEsZ8YYrOoSe6LEdsLpurFtW1yudiskjXPFSaHufGciFxwqZ9cLcJNWXnjAK2Zi7NdOsKcjlwdcIlygaV+crUIl8jbwnauj5F4CY2ujw0fmiTCAndDtXC2g+HzNq8JJVau9m2Jl+CkKAYxEbfi2ZE+Nnxo4jjeqQ5Sx3QnZThTH4gNX5yc7/cx9WLavovGKJfizJG+NXKSJy+afO2raI3oE1vyqaAA+OpjRwHWtqYIMdZekdMEUy15/NBkl8WsICMbz4ng2A3+y1TOH8ALNqHxhf/P+xwAAAAASUVORK5CYII='; +export const personDarkThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAWdJREFUSIm1ljFuwkAQRd/giFTkABS5gMsolBRcIFBwCOTGNUfgDtDRJ9yDioaCKlJ8B0dYmyLjZGLtrh0Jj7SyNPP3f894dtbinHP0aIM+yQHuYkERuQdegDnwBIw1VABH4BV4c86VQRIXMGABXADXsi7AIsjjIR4AG0NwAnIgBUa6UvWdDG4DDLoI1OQlkAFJJMtEMWUtEhXQstTksxCxR2hmRP6UCwMamppnXcnN/sx8k6FPYGlqHixLRCAx32RZ++05mOtz65y7Btsu3I1XYNvgwmZwJty1XbNINYOzL4MxgIg8/Pftjb1bLmgZFSJSiAgiMvHEJhorYhxWoAY+Gt9RnyvP3lUDY/f+ipr67fmuX258U6ACPoEd8Kxrp74KmBp8rhz7H58JetsUWCtRcwZVwLqtTTsdNM3kAHzoOtg3V0z8oCmov1FhwP0NO93U77g2Qje5cETJvHaLKzMqcAvr/a/iC+JcVEP5CMhEAAAAAElFTkSuQmCC'; +``` + +and another file `src/model/PersonInfo.ts` in the same directory with contents + +```typescript +export type PersonInfo = { + firstName: string; + lastName: string; + age: number; + email: string; +} +``` + +Then rename the file `src/ui/index.tsx` to `src/ui/editor.tsx` and paste the following contents into it: + +```typescript {hl_lines=["16-22", "24-35", "37-43"]} +import React, { StrictMode, useCallback, useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { getStudioProApi, IComponent, StudioProApi } from "@mendix/extensions-api"; +import type { PersonInfo } from "../model/PersonInfo"; + +function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) { + const {studioPro,documentId} = input; + const [person, setPerson] = useState({ + firstName: "", + lastName: "", + age: 0, + email: "", + }); + const [documentVersion, setDocumentVersion] = useState(0); // Used to trigger re-fetching the document + + useEffect(() => { + studioPro.app.model.customBlobDocuments.addEventListener("documentsChanged", ({ documents }) => { + if (documents.some(doc => doc.id === documentId)) { + setDocumentVersion(v => v + 1); // Trigger re-fetch of the document + } + }); + }, [studioPro]); + + useEffect(() => { + studioPro.app.model.customBlobDocuments + .getDocumentById(documentId) + .then(documentFromModel => { + if (documentFromModel && !("error" in documentFromModel)) { + setPerson(documentFromModel.document.contents); + } + }) + .catch(err => { + studioPro.ui.messageBoxes.show("error", "Error loading document", "Details: " + err?.message || err); + }); + }, [studioPro, documentId, documentVersion]); + + const savePerson = useCallback(async () => { + try { + await studioPro.app.model.customBlobDocuments.updateDocumentContent(documentId, person) + } catch (error) { + studioPro.ui.messageBoxes.show("error", "Error saving document", "Details: " + ((error as {message?: string})?.message || error)); + } + }, [studioPro, documentId, person]); + + const labelStyle = { display: 'inline-block', width: '300px' }; + + return ( +
+

Person Editor

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +export const component: IComponent = { + async loaded(componentContext, args: { documentId: string; }) { + const studioPro = getStudioProApi(componentContext); + createRoot(document.getElementById("root")!).render( + + + + ); + } +}; +``` + +Lastly, we need to update the build instructions and manifest. To do so, replace the contents of `build-extension.mjs` with + +```javascript {hl_lines=["16-19"]} +import * as esbuild from 'esbuild' +import {copyToAppPlugin, copyManifestPlugin, commonConfig} from "./build.helpers.mjs" +import parseArgs from "minimist" + +const outDir = `dist/myextension` +const appDir = "/Users/petar.vukmirovic/Mendix/App-Guide-Test-Blob" +const extensionDirectoryName = "extensions" + +const entryPoints = [ + { + in: 'src/main/index.ts', + out: 'main' + } +] + +entryPoints.push({ + in: 'src/ui/editor.tsx', + out: 'editor' +}) + +const args = parseArgs(process.argv.slice(2)) +const buildContext = await esbuild.context({ + ...commonConfig, + outdir: outDir, + plugins: [copyManifestPlugin(outDir), copyToAppPlugin(appDir, outDir, extensionDirectoryName)], + entryPoints +}) + +if('watch' in args) { + await buildContext.watch(); +} +else { + await buildContext.rebuild(); + await buildContext.dispose(); +} +``` + +and the contents of `manifest.json` with + +```json {hl_lines=["6"]} +{ + "mendixComponent": { + "entryPoints": { + "main": "main.js", + "ui": { + "editor": "editor.js" + } + } + } +} +``` + +### Walking through the example code + +In `src/main/index.ts`, we are first registering a new document type. When a new document type is registered, you can perform all +CRUD operations on this document type, but it will not yet be shown in the UI since no editor for it has yet been registered. Note +that you can optionally provide `readableTypeName` which will be shown instead of full type name in every user-facing context such +as in logs and in the Studio Pro UI. You can optionally customize the (de)serialization of the document contents to string, which +defaults to `JSON.stringify` and `JSON.parse`. The next call to `studioPro` API registers editor for our document type. It registers +`editor` entry point of our extension as the editor that will be shown when user interacts with the document in StudioPro (for example +through App Explorer or Find Results). This editor will be shown as a tab, but you can also register editors to be shown as +modal dialogs. Lastly, icons for both light and dark theme are registered. Those icons will be shown in all UI elements where +document icon is needed. + +The first highlighted block of code in `src/ui/editor.tsx` listens for changes in documents to make sure that the last version of currently +active document is shown. Note that the document can be changed outside currently open editor by calls to custom blob document API, +as well as by some Studio Pro operations such as undoing changes. In the next highlighted block, we fetch the document contents +whenever new document is open or updated. Lastly, we make sure that the current changes can be saved. + +We also highlighted changes that need to be made to `build-extension.mjs` and `manifest.json` to make sure that `editor` entry point +is properly built and loaded. + +## Extensibility Feedback + +If you would like to provide us with additional feedback, you can complete a small [survey](https://survey.alchemer.eu/s3/90801191/Extensibility-Feedback). + +Any feedback is appreciated. \ No newline at end of file From 841e4b05a47fb27b47a1eb0e0e5b149490930143 Mon Sep 17 00:00:00 2001 From: Petar Vukmirovic Date: Tue, 14 Oct 2025 09:40:51 +0200 Subject: [PATCH 3/6] Fix the grammar and spelling --- .../custom-blob-document-api.md | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md index f7c2ebbfa47..7e44c92987b 100644 --- a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md @@ -6,7 +6,7 @@ url: /apidocs-mxsdk/apidocs/web-extensibility-api-11/custom-blob-document-api/ ## Introduction -This how-to describes how to introduce a new document type and provide a custom editor to allow users to edit new documents of the introduced type. +This how-to describes how to introduce a new document type and provide a custom editor so users can edit documents of that type. ## Prerequisites @@ -16,15 +16,12 @@ Before starting this how-to, make sure you have completed the following prerequi ## Custom Document Model -Studio Pro provides you with possibility to extend its metamodel by introducing your own document types. These new documents are allowed to store arbitrary data, -which must be serializable as strings. If an editor, which is a user-defined UI component, is registered for a document, new document will appear in UI as any other -built-in document type (e.g., constants, Java Actions or pages). In particular, it will appear in New Document and Find Advanced dialogs, context menus for adding -new documents, App explorer and other UI elements that show Studio Pro documents. Custom editors can be registered as either tabs or modal dialogs. +Studio Pro lets you extend its metamodel by introducing custom document types. These documents can store arbitrary data that must be serializable as strings. If an editor (a user-defined UI component) is registered for a document type, documents of that type will appear in the UI like any other built-in document type (for example, constants, Java Actions, or pages). In particular, they will appear in the New Document and Find Advanced dialogs, context menus for adding documents, the App Explorer, and other UI elements that show Studio Pro documents. Custom editors can be registered to appear either as tabs or as modal dialogs. ## Registering a New Document Type -To start with registering a new document type, generate a new extension called `myextension`, as described in [getting started guide](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/). -We will explain which files of the generated extension to edit and then explain what the edits achieve. +To register a new document type, generate a new extension called `myextension` as described in the [Getting started guide](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/). +Below we explain which files of the generated extension to edit and what those edits achieve. First, replace the contents of `src/main/index.ts` with: @@ -58,7 +55,7 @@ export const component: IComponent = { } ``` -Then add a new file `src/model/contants.ts` with contents: +Then add a new file `src/model/constants.ts` with the following contents: ```typescript export const personDocumentType = 'myextension.Person'; @@ -77,7 +74,7 @@ export type PersonInfo = { } ``` -Then rename the file `src/ui/index.tsx` to `src/ui/editor.tsx` and paste the following contents into it: +Rename the file `src/ui/index.tsx` to `src/ui/editor.tsx` and paste the following contents into it: ```typescript {hl_lines=["16-22", "24-35", "37-43"]} import React, { StrictMode, useCallback, useEffect, useState } from "react"; @@ -101,7 +98,7 @@ function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) { setDocumentVersion(v => v + 1); // Trigger re-fetch of the document } }); - }, [studioPro]); + }, [studioPro, documentId]); useEffect(() => { studioPro.app.model.customBlobDocuments @@ -188,7 +185,7 @@ export const component: IComponent = { }; ``` -Lastly, we need to update the build instructions and manifest. To do so, replace the contents of `build-extension.mjs` with +Lastly, update the build instructions and manifest. To do so, replace the contents of `build-extension.mjs` with ```javascript {hl_lines=["16-19"]} import * as esbuild from 'esbuild' @@ -228,7 +225,7 @@ else { } ``` -and the contents of `manifest.json` with +and replace the contents of `manifest.json` with ```json {hl_lines=["6"]} { @@ -245,26 +242,16 @@ and the contents of `manifest.json` with ### Walking through the example code -In `src/main/index.ts`, we are first registering a new document type. When a new document type is registered, you can perform all -CRUD operations on this document type, but it will not yet be shown in the UI since no editor for it has yet been registered. Note -that you can optionally provide `readableTypeName` which will be shown instead of full type name in every user-facing context such -as in logs and in the Studio Pro UI. You can optionally customize the (de)serialization of the document contents to string, which -defaults to `JSON.stringify` and `JSON.parse`. The next call to `studioPro` API registers editor for our document type. It registers -`editor` entry point of our extension as the editor that will be shown when user interacts with the document in StudioPro (for example -through App Explorer or Find Results). This editor will be shown as a tab, but you can also register editors to be shown as -modal dialogs. Lastly, icons for both light and dark theme are registered. Those icons will be shown in all UI elements where -document icon is needed. +In `src/main/index.ts`, we first register a document type. When a document type is registered, you can perform all CRUD operations on it, but it won't appear in the UI until an editor is registered for it. Note that you can optionally provide `readableTypeName`, which will be shown instead of the full type name in user-facing contexts such as logs and the Studio Pro UI. You can also customize serialization of the document contents to a string; by default the API uses `JSON.stringify` for serialization and `JSON.parse` for deserialization. -The first highlighted block of code in `src/ui/editor.tsx` listens for changes in documents to make sure that the last version of currently -active document is shown. Note that the document can be changed outside currently open editor by calls to custom blob document API, -as well as by some Studio Pro operations such as undoing changes. In the next highlighted block, we fetch the document contents -whenever new document is open or updated. Lastly, we make sure that the current changes can be saved. +The next call to the Studio Pro API registers an editor for our document type. It registers the `editor` entry point of our extension as the editor that will be shown when a user interacts with the document in Studio Pro (for example, through the App Explorer or Find Results). This editor will be shown as a tab, but you can also register editors to be shown as modal dialogs. Finally, icons for both the light and dark themes are registered; those icons appear wherever a document icon is needed. -We also highlighted changes that need to be made to `build-extension.mjs` and `manifest.json` to make sure that `editor` entry point -is properly built and loaded. +The first highlighted block of code in `src/ui/editor.tsx` listens for changes to documents to ensure that the most recent version of the currently active document is shown. Note that the document can be changed outside the currently open editor by calls to the custom blob document API, or by some Studio Pro operations such as undo. In the next highlighted block, we fetch the document contents whenever a new document is opened or an existing document is updated. Finally, we provide a way to save changes. + +We also highlighted the changes needed in `build-extension.mjs` and `manifest.json` to ensure that the `editor` entry point is built and loaded properly. ## Extensibility Feedback If you would like to provide us with additional feedback, you can complete a small [survey](https://survey.alchemer.eu/s3/90801191/Extensibility-Feedback). -Any feedback is appreciated. \ No newline at end of file +Any feedback is appreciated. From e2098da155b271eee406a888ad543b2679d3e671 Mon Sep 17 00:00:00 2001 From: quinntracy Date: Tue, 14 Oct 2025 14:35:18 +0200 Subject: [PATCH 4/6] Review --- .../custom-blob-document-api.md | 446 +++++++++--------- 1 file changed, 232 insertions(+), 214 deletions(-) diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md index 7e44c92987b..a00b3c39f6b 100644 --- a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md @@ -1,12 +1,12 @@ --- -title: "Register New Document Types with Corresponding Editor" +title: "Register New Document Types With a Corresponding Editor" linktitle: "Introduce New Document Types" url: /apidocs-mxsdk/apidocs/web-extensibility-api-11/custom-blob-document-api/ --- ## Introduction -This how-to describes how to introduce a new document type and provide a custom editor so users can edit documents of that type. +This how-to describes how to introduce a new document type and set up a custom editor that allows users to edit documents of that type. ## Prerequisites @@ -16,239 +16,257 @@ Before starting this how-to, make sure you have completed the following prerequi ## Custom Document Model -Studio Pro lets you extend its metamodel by introducing custom document types. These documents can store arbitrary data that must be serializable as strings. If an editor (a user-defined UI component) is registered for a document type, documents of that type will appear in the UI like any other built-in document type (for example, constants, Java Actions, or pages). In particular, they will appear in the New Document and Find Advanced dialogs, context menus for adding documents, the App Explorer, and other UI elements that show Studio Pro documents. Custom editors can be registered to appear either as tabs or as modal dialogs. +Studio Pro allows you to extend its metamodel by adding custom document types. These documents can store arbitrary data that can be serialized as strings. If you register an editor (a user-defined UI component) for a specific document type, documents of that type will be displayed in the UI alongside any other built-in document type, such as constants, Java Actions, and pages. Specifically, they will appear in the **New Document** and **Find Advanced** dialogs, context menus for adding documents, the App Explorer, and other UI elements that show Studio Pro documents. You can register custom editors to appear either as tabs or as modal dialogs. ## Registering a New Document Type -To register a new document type, generate a new extension called `myextension` as described in the [Getting started guide](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/). -Below we explain which files of the generated extension to edit and what those edits achieve. - -First, replace the contents of `src/main/index.ts` with: - -```typescript {hl_lines=["8-24"]} -import { IComponent, getStudioProApi } from "@mendix/extensions-api"; -import { personDarkThemeIcon, personDocumentType, personLightThemeIcon } from "../model/constants"; -import { PersonInfo } from "../model/PersonInfo"; - -export const component: IComponent = { - async loaded(componentContext) { - const studioPro = getStudioProApi(componentContext); - await studioPro.app.model.customBlobDocuments.registerDocumentType({ - type: personDocumentType, - readableTypeName: 'Person', - defaultContent: { - firstName: '', - lastName: '', - age: 0, - email: '' - } - }); - await studioPro.ui.editors.registerEditorForCustomDocument({ - documentType: personDocumentType, - editorEntryPoint: 'editor', - editorKind: 'tab', - iconLight: personLightThemeIcon, - iconDark: personDarkThemeIcon - }) +To register a new document type, follow the steps below: + +1. Generate a new extension named `myextension` as described in the [Get Started](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/) guide. +2. Replace the contents of `src/main/index.ts` with the code below: + + ```typescript {hl_lines=["8-24"]} + import { IComponent, getStudioProApi } from "@mendix/extensions-api"; + import { personDarkThemeIcon, personDocumentType, personLightThemeIcon } from "../model/constants"; + import { PersonInfo } from "../model/PersonInfo"; + + export const component: IComponent = { + async loaded(componentContext) { + const studioPro = getStudioProApi(componentContext); + await studioPro.app.model.customBlobDocuments.registerDocumentType({ + type: personDocumentType, + readableTypeName: 'Person', + defaultContent: { + firstName: '', + lastName: '', + age: 0, + email: '' + } + }); + await studioPro.ui.editors.registerEditorForCustomDocument({ + documentType: personDocumentType, + editorEntryPoint: 'editor', + editorKind: 'tab', + iconLight: personLightThemeIcon, + iconDark: personDarkThemeIcon + }) + } } -} -``` - -Then add a new file `src/model/constants.ts` with the following contents: - -```typescript -export const personDocumentType = 'myextension.Person'; -export const personLightThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAABKElEQVRIDd2Vyw3CMBBEAxIUAWVQBxIcKIBiuNAAFVAIV2iAA2cKoAGYF9nIctaxscIBRhrZ2Z3d9T9N8++YaoIb8ShexYcjfWz40FRhraib+MwQDdpijKXci7nEsZ8YYrOoSe6LEdsLpurFtW1yudiskjXPFSaHufGciFxwqZ9cLcJNWXnjAK2Zi7NdOsKcjlwdcIlygaV+crUIl8jbwnauj5F4CY2ujw0fmiTCAndDtXC2g+HzNq8JJVau9m2Jl+CkKAYxEbfi2ZE+Nnxo4jjeqQ5Sx3QnZThTH4gNX5yc7/cx9WLavovGKJfizJG+NXKSJy+afO2raI3oE1vyqaAA+OpjRwHWtqYIMdZekdMEUy15/NBkl8WsICMbz4ng2A3+y1TOH8ALNqHxhf/P+xwAAAAASUVORK5CYII='; -export const personDarkThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAWdJREFUSIm1ljFuwkAQRd/giFTkABS5gMsolBRcIFBwCOTGNUfgDtDRJ9yDioaCKlJ8B0dYmyLjZGLtrh0Jj7SyNPP3f894dtbinHP0aIM+yQHuYkERuQdegDnwBIw1VABH4BV4c86VQRIXMGABXADXsi7AIsjjIR4AG0NwAnIgBUa6UvWdDG4DDLoI1OQlkAFJJMtEMWUtEhXQstTksxCxR2hmRP6UCwMamppnXcnN/sx8k6FPYGlqHixLRCAx32RZ++05mOtz65y7Btsu3I1XYNvgwmZwJty1XbNINYOzL4MxgIg8/Pftjb1bLmgZFSJSiAgiMvHEJhorYhxWoAY+Gt9RnyvP3lUDY/f+ipr67fmuX258U6ACPoEd8Kxrp74KmBp8rhz7H58JetsUWCtRcwZVwLqtTTsdNM3kAHzoOtg3V0z8oCmov1FhwP0NO93U77g2Qje5cETJvHaLKzMqcAvr/a/iC+JcVEP5CMhEAAAAAElFTkSuQmCC'; -``` - -and another file `src/model/PersonInfo.ts` in the same directory with contents - -```typescript -export type PersonInfo = { - firstName: string; - lastName: string; - age: number; - email: string; -} -``` - -Rename the file `src/ui/index.tsx` to `src/ui/editor.tsx` and paste the following contents into it: - -```typescript {hl_lines=["16-22", "24-35", "37-43"]} -import React, { StrictMode, useCallback, useEffect, useState } from "react"; -import { createRoot } from "react-dom/client"; -import { getStudioProApi, IComponent, StudioProApi } from "@mendix/extensions-api"; -import type { PersonInfo } from "../model/PersonInfo"; - -function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) { - const {studioPro,documentId} = input; - const [person, setPerson] = useState({ - firstName: "", - lastName: "", - age: 0, - email: "", - }); - const [documentVersion, setDocumentVersion] = useState(0); // Used to trigger re-fetching the document - - useEffect(() => { - studioPro.app.model.customBlobDocuments.addEventListener("documentsChanged", ({ documents }) => { - if (documents.some(doc => doc.id === documentId)) { - setDocumentVersion(v => v + 1); // Trigger re-fetch of the document - } + ``` + +3. Add a new file `src/model/constants.ts` with the following contents: + + ```typescript + export const personDocumentType = 'myextension.Person'; + export const personLightThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAABKElEQVRIDd2Vyw3CMBBEAxIUAWVQBxIcKIBiuNAAFVAIV2iAA2cKoAGYF9nIctaxscIBRhrZ2Z3d9T9N8++YaoIb8ShexYcjfWz40FRhraib+MwQDdpijKXci7nEsZ8YYrOoSe6LEdsLpurFtW1yudiskjXPFSaHufGciFxwqZ9cLcJNWXnjAK2Zi7NdOsKcjlwdcIlygaV+crUIl8jbwnauj5F4CY2ujw0fmiTCAndDtXC2g+HzNq8JJVau9m2Jl+CkKAYxEbfi2ZE+Nnxo4jjeqQ5Sx3QnZThTH4gNX5yc7/cx9WLavovGKJfizJG+NXKSJy+afO2raI3oE1vyqaAA+OpjRwHWtqYIMdZekdMEUy15/NBkl8WsICMbz4ng2A3+y1TOH8ALNqHxhf/P+xwAAAAASUVORK5CYII='; + export const personDarkThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAWdJREFUSIm1ljFuwkAQRd/giFTkABS5gMsolBRcIFBwCOTGNUfgDtDRJ9yDioaCKlJ8B0dYmyLjZGLtrh0Jj7SyNPP3f894dtbinHP0aIM+yQHuYkERuQdegDnwBIw1VABH4BV4c86VQRIXMGABXADXsi7AIsjjIR4AG0NwAnIgBUa6UvWdDG4DDLoI1OQlkAFJJMtEMWUtEhXQstTksxCxR2hmRP6UCwMamppnXcnN/sx8k6FPYGlqHixLRCAx32RZ++05mOtz65y7Btsu3I1XYNvgwmZwJty1XbNINYOzL4MxgIg8/Pftjb1bLmgZFSJSiAgiMvHEJhorYhxWoAY+Gt9RnyvP3lUDY/f+ipr67fmuX258U6ACPoEd8Kxrp74KmBp8rhz7H58JetsUWCtRcwZVwLqtTTsdNM3kAHzoOtg3V0z8oCmov1FhwP0NO93U77g2Qje5cETJvHaLKzMqcAvr/a/iC+JcVEP5CMhEAAAAAElFTkSuQmCC'; + ``` + +4. Add another file `src/model/PersonInfo.ts` in the same directory: + + ```typescript + export type PersonInfo = { + firstName: string; + lastName: string; + age: number; + email: string; + } + ``` + +5. Rename the `src/ui/index.tsx` file to `src/ui/editor.tsx` and paste the following contents into it: + + ```typescript {hl_lines=["16-22", "24-35", "37-43"]} + import React, { StrictMode, useCallback, useEffect, useState } from "react"; + import { createRoot } from "react-dom/client"; + import { getStudioProApi, IComponent, StudioProApi } from "@mendix/extensions-api"; + import type { PersonInfo } from "../model/PersonInfo"; + + function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) { + const {studioPro,documentId} = input; + const [person, setPerson] = useState({ + firstName: "", + lastName: "", + age: 0, + email: "", }); - }, [studioPro, documentId]); - - useEffect(() => { - studioPro.app.model.customBlobDocuments - .getDocumentById(documentId) - .then(documentFromModel => { - if (documentFromModel && !("error" in documentFromModel)) { - setPerson(documentFromModel.document.contents); + const [documentVersion, setDocumentVersion] = useState(0); // Used to trigger re-fetching the document + + useEffect(() => { + studioPro.app.model.customBlobDocuments.addEventListener("documentsChanged", ({ documents }) => { + if (documents.some(doc => doc.id === documentId)) { + setDocumentVersion(v => v + 1); // Trigger re-fetch of the document } - }) - .catch(err => { - studioPro.ui.messageBoxes.show("error", "Error loading document", "Details: " + err?.message || err); }); - }, [studioPro, documentId, documentVersion]); - - const savePerson = useCallback(async () => { - try { - await studioPro.app.model.customBlobDocuments.updateDocumentContent(documentId, person) - } catch (error) { - studioPro.ui.messageBoxes.show("error", "Error saving document", "Details: " + ((error as {message?: string})?.message || error)); - } - }, [studioPro, documentId, person]); - - const labelStyle = { display: 'inline-block', width: '300px' }; - - return ( -
-

Person Editor

-
- -
-
- -
-
- -
-
- -
-
- + }, [studioPro, documentId]); + + useEffect(() => { + studioPro.app.model.customBlobDocuments + .getDocumentById(documentId) + .then(documentFromModel => { + if (documentFromModel && !("error" in documentFromModel)) { + setPerson(documentFromModel.document.contents); + } + }) + .catch(err => { + studioPro.ui.messageBoxes.show("error", "Error loading document", "Details: " + err?.message || err); + }); + }, [studioPro, documentId, documentVersion]); + + const savePerson = useCallback(async () => { + try { + await studioPro.app.model.customBlobDocuments.updateDocumentContent(documentId, person) + } catch (error) { + studioPro.ui.messageBoxes.show("error", "Error saving document", "Details: " + ((error as {message?: string})?.message || error)); + } + }, [studioPro, documentId, person]); + + const labelStyle = { display: 'inline-block', width: '300px' }; + + return ( +
+

Person Editor

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- ); -} - -export const component: IComponent = { - async loaded(componentContext, args: { documentId: string; }) { - const studioPro = getStudioProApi(componentContext); - createRoot(document.getElementById("root")!).render( - - - ); } -}; -``` -Lastly, update the build instructions and manifest. To do so, replace the contents of `build-extension.mjs` with - -```javascript {hl_lines=["16-19"]} -import * as esbuild from 'esbuild' -import {copyToAppPlugin, copyManifestPlugin, commonConfig} from "./build.helpers.mjs" -import parseArgs from "minimist" + export const component: IComponent = { + async loaded(componentContext, args: { documentId: string; }) { + const studioPro = getStudioProApi(componentContext); + createRoot(document.getElementById("root")!).render( + + + + ); + } + }; + ``` + +6. Update the build instructions and manifest by replacing the contents of `build-extension.mjs` with the code below: + + ```javascript {hl_lines=["16-19"]} + import * as esbuild from 'esbuild' + import {copyToAppPlugin, copyManifestPlugin, commonConfig} from "./build.helpers.mjs" + import parseArgs from "minimist" + + const outDir = `dist/myextension` + const appDir = "/Users/petar.vukmirovic/Mendix/App-Guide-Test-Blob" + const extensionDirectoryName = "extensions" + + const entryPoints = [ + { + in: 'src/main/index.ts', + out: 'main' + } + ] + + entryPoints.push({ + in: 'src/ui/editor.tsx', + out: 'editor' + }) + + const args = parseArgs(process.argv.slice(2)) + const buildContext = await esbuild.context({ + ...commonConfig, + outdir: outDir, + plugins: [copyManifestPlugin(outDir), copyToAppPlugin(appDir, outDir, extensionDirectoryName)], + entryPoints + }) + + if('watch' in args) { + await buildContext.watch(); + } + else { + await buildContext.rebuild(); + await buildContext.dispose(); + } + ``` -const outDir = `dist/myextension` -const appDir = "/Users/petar.vukmirovic/Mendix/App-Guide-Test-Blob" -const extensionDirectoryName = "extensions" +7. Replace the contents of `manifest.json` with the code below: -const entryPoints = [ + ```json {hl_lines=["6"]} { - in: 'src/main/index.ts', - out: 'main' - } -] - -entryPoints.push({ - in: 'src/ui/editor.tsx', - out: 'editor' -}) - -const args = parseArgs(process.argv.slice(2)) -const buildContext = await esbuild.context({ - ...commonConfig, - outdir: outDir, - plugins: [copyManifestPlugin(outDir), copyToAppPlugin(appDir, outDir, extensionDirectoryName)], - entryPoints -}) - -if('watch' in args) { - await buildContext.watch(); -} -else { - await buildContext.rebuild(); - await buildContext.dispose(); -} -``` - -and replace the contents of `manifest.json` with - -```json {hl_lines=["6"]} -{ - "mendixComponent": { - "entryPoints": { - "main": "main.js", - "ui": { - "editor": "editor.js" - } + "mendixComponent": { + "entryPoints": { + "main": "main.js", + "ui": { + "editor": "editor.js" + } + } + } } - } -} -``` + ``` + +## Explanation + +### Register the Document Type + +In `src/main/index.ts`, you begin by registering a new document type. Once registered, you can perform all CRUD operations on it. However, will not appear in the UI until an editor is also registered. + +Optionally, you can provide a `readableTypeName` to display a user-friendly name in logs and the Studio Pro UI instead of the full type name. You can also customize serialization of the document contents to a string. By default, the API uses `JSON.stringify` for serialization and `JSON.parse` for deserialization. + +### Register the Editor + +Make a call to the Studio Pro API to register an editor for the document type. This does the following: + +* It registers the `editor` entry point of the extension to the document type, so the editor is shown when users interacts with the document in Studio Pro (for example, through the App Explorer or **Find Results**). +* This editor is shown as a tab, but you can also configure it to be shown as a modal dialog. +* Icons for both the light and dark themes are registered; these icons appear wherever a document icon is needed. + +### Changes in the Editor -### Walking through the example code +In `src/ui/editor.tsx`, the first highlighted block of code listens for changes to documents to ensure the most recent version of the currently active document is shown. Note that the document can be changed outside the currently open editor, either by calls to the custom blob document API, or by Studio Pro operations like **Undo**. -In `src/main/index.ts`, we first register a document type. When a document type is registered, you can perform all CRUD operations on it, but it won't appear in the UI until an editor is registered for it. Note that you can optionally provide `readableTypeName`, which will be shown instead of the full type name in user-facing contexts such as logs and the Studio Pro UI. You can also customize serialization of the document contents to a string; by default the API uses `JSON.stringify` for serialization and `JSON.parse` for deserialization. +In the next highlighted block, document contents are fetched whenever a new document is opened or an existing document is updated. -The next call to the Studio Pro API registers an editor for our document type. It registers the `editor` entry point of our extension as the editor that will be shown when a user interacts with the document in Studio Pro (for example, through the App Explorer or Find Results). This editor will be shown as a tab, but you can also register editors to be shown as modal dialogs. Finally, icons for both the light and dark themes are registered; those icons appear wherever a document icon is needed. +We then provide a way to save changes. -The first highlighted block of code in `src/ui/editor.tsx` listens for changes to documents to ensure that the most recent version of the currently active document is shown. Note that the document can be changed outside the currently open editor by calls to the custom blob document API, or by some Studio Pro operations such as undo. In the next highlighted block, we fetch the document contents whenever a new document is opened or an existing document is updated. Finally, we provide a way to save changes. +### Update Build and Manifest Files -We also highlighted the changes needed in `build-extension.mjs` and `manifest.json` to ensure that the `editor` entry point is built and loaded properly. +Highlight the necessary changes in `build-extension.mjs` and `manifest.json` to ensure the `editor` entry point is built and loaded properly. ## Extensibility Feedback From 03ffa26399b2e9f6e0f61f1d2dc58177fe9280dc Mon Sep 17 00:00:00 2001 From: Petar Vukmirovic Date: Tue, 14 Oct 2025 15:53:58 +0200 Subject: [PATCH 5/6] Another small changes pass --- .../custom-blob-document-api.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md index a00b3c39f6b..dd4e40517f7 100644 --- a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md @@ -185,7 +185,7 @@ To register a new document type, follow the steps below: }; ``` -6. Update the build instructions and manifest by replacing the contents of `build-extension.mjs` with the code below: +6. Update the build instructions by replacing the contents of `build-extension.mjs` with the code below: ```javascript {hl_lines=["16-19"]} import * as esbuild from 'esbuild' @@ -229,14 +229,14 @@ To register a new document type, follow the steps below: ```json {hl_lines=["6"]} { - "mendixComponent": { + "mendixComponent": { "entryPoints": { - "main": "main.js", - "ui": { + "main": "main.js", + "ui": { "editor": "editor.js" + } } - } - } + } } ``` @@ -244,17 +244,19 @@ To register a new document type, follow the steps below: ### Register the Document Type -In `src/main/index.ts`, you begin by registering a new document type. Once registered, you can perform all CRUD operations on it. However, will not appear in the UI until an editor is also registered. +In `src/main/index.ts`, you begin by registering a new document type. Once registered, you can perform all CRUD operations on it. However, it will not appear in the UI until an editor is also registered. Optionally, you can provide a `readableTypeName` to display a user-friendly name in logs and the Studio Pro UI instead of the full type name. You can also customize serialization of the document contents to a string. By default, the API uses `JSON.stringify` for serialization and `JSON.parse` for deserialization. ### Register the Editor -Make a call to the Studio Pro API to register an editor for the document type. This does the following: +The next call to the Studio Pro API in `src/main/index.ts` registers an editor for the document type. This does the following: -* It registers the `editor` entry point of the extension to the document type, so the editor is shown when users interacts with the document in Studio Pro (for example, through the App Explorer or **Find Results**). +* It registers the `editor` entry point of the extension to the document type, so the editor is shown when users interacts with the document in Studio Pro (for example, through the **App Explorer** or **Find Results**). * This editor is shown as a tab, but you can also configure it to be shown as a modal dialog. * Icons for both the light and dark themes are registered; these icons appear wherever a document icon is needed. +* Note that this editor will behave like editors for other, built-in, document types. For example, `studioPro.ui.editors.editDocument` call will open the registered editor for +custom documents. ### Changes in the Editor @@ -266,7 +268,7 @@ We then provide a way to save changes. ### Update Build and Manifest Files -Highlight the necessary changes in `build-extension.mjs` and `manifest.json` to ensure the `editor` entry point is built and loaded properly. +Highlighted text in `build-extension.mjs` and `manifest.json` shows changes necessary to ensure the `editor` entry point is built and loaded properly. ## Extensibility Feedback From f012f6e87c181fae90872228f201b65da414c33b Mon Sep 17 00:00:00 2001 From: Quinn Tracy <142489060+quinntracy@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:03:46 +0200 Subject: [PATCH 6/6] Small edit --- .../web/web-extensions-howtos/custom-blob-document-api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md index dd4e40517f7..59bc2d8d57e 100644 --- a/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md +++ b/content/en/docs/apidocs-mxsdk/apidocs/studio-pro-11/extensibility-api/web/web-extensions-howtos/custom-blob-document-api.md @@ -255,8 +255,7 @@ The next call to the Studio Pro API in `src/main/index.ts` registers an editor f * It registers the `editor` entry point of the extension to the document type, so the editor is shown when users interacts with the document in Studio Pro (for example, through the **App Explorer** or **Find Results**). * This editor is shown as a tab, but you can also configure it to be shown as a modal dialog. * Icons for both the light and dark themes are registered; these icons appear wherever a document icon is needed. -* Note that this editor will behave like editors for other, built-in, document types. For example, `studioPro.ui.editors.editDocument` call will open the registered editor for -custom documents. +* Note that this editor will behave like editors for other, built-in document types. For example, the `studioPro.ui.editors.editDocument` call will open the registered editor for custom documents. ### Changes in the Editor