Skip to content

Commit 5afacfe

Browse files
Petar VukmirovicPetar Vukmirovic
authored andcommitted
Add untracked file
1 parent 65d0196 commit 5afacfe

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
title: "Register New Document Types with Corresponding Editor"
3+
linktitle: "Introduce New Document Types"
4+
url: /apidocs-mxsdk/apidocs/web-extensibility-api-11/custom-blob-document-api/
5+
---
6+
7+
## Introduction
8+
9+
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.
10+
11+
## Prerequisites
12+
13+
Before starting this how-to, make sure you have completed the following prerequisites:
14+
15+
* [Get Started with the Web Extensibility API](/apidocs-mxsdk/apidocs/web-extensibility-api-11/getting-started/).
16+
17+
## Custom Document Model
18+
19+
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,
20+
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
21+
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
22+
new documents, App explorer and other UI elements that show Studio Pro documents. Custom editors can be registered as either tabs or modal dialogs.
23+
24+
## Registering a New Document Type
25+
26+
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/).
27+
We will explain which files of the generated extension to edit and then explain what the edits achieve.
28+
29+
First, replace the contents of `src/main/index.ts` with:
30+
31+
```typescript {hl_lines=["8-24"]}
32+
import { IComponent, getStudioProApi } from "@mendix/extensions-api";
33+
import { personDarkThemeIcon, personDocumentType, personLightThemeIcon } from "../model/constants";
34+
import { PersonInfo } from "../model/PersonInfo";
35+
36+
export const component: IComponent = {
37+
async loaded(componentContext) {
38+
const studioPro = getStudioProApi(componentContext);
39+
await studioPro.app.model.customBlobDocuments.registerDocumentType<PersonInfo>({
40+
type: personDocumentType,
41+
readableTypeName: 'Person',
42+
defaultContent: {
43+
firstName: '',
44+
lastName: '',
45+
age: 0,
46+
email: ''
47+
}
48+
});
49+
await studioPro.ui.editors.registerEditorForCustomDocument({
50+
documentType: personDocumentType,
51+
editorEntryPoint: 'editor',
52+
editorKind: 'tab',
53+
iconLight: personLightThemeIcon,
54+
iconDark: personDarkThemeIcon
55+
})
56+
57+
}
58+
}
59+
```
60+
61+
Then add a new file `src/model/contants.ts` with contents:
62+
63+
```typescript
64+
export const personDocumentType = 'myextension.Person';
65+
export const personLightThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAABKElEQVRIDd2Vyw3CMBBEAxIUAWVQBxIcKIBiuNAAFVAIV2iAA2cKoAGYF9nIctaxscIBRhrZ2Z3d9T9N8++YaoIb8ShexYcjfWz40FRhraib+MwQDdpijKXci7nEsZ8YYrOoSe6LEdsLpurFtW1yudiskjXPFSaHufGciFxwqZ9cLcJNWXnjAK2Zi7NdOsKcjlwdcIlygaV+crUIl8jbwnauj5F4CY2ujw0fmiTCAndDtXC2g+HzNq8JJVau9m2Jl+CkKAYxEbfi2ZE+Nnxo4jjeqQ5Sx3QnZThTH4gNX5yc7/cx9WLavovGKJfizJG+NXKSJy+afO2raI3oE1vyqaAA+OpjRwHWtqYIMdZekdMEUy15/NBkl8WsICMbz4ng2A3+y1TOH8ALNqHxhf/P+xwAAAAASUVORK5CYII=';
66+
export const personDarkThemeIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAWdJREFUSIm1ljFuwkAQRd/giFTkABS5gMsolBRcIFBwCOTGNUfgDtDRJ9yDioaCKlJ8B0dYmyLjZGLtrh0Jj7SyNPP3f894dtbinHP0aIM+yQHuYkERuQdegDnwBIw1VABH4BV4c86VQRIXMGABXADXsi7AIsjjIR4AG0NwAnIgBUa6UvWdDG4DDLoI1OQlkAFJJMtEMWUtEhXQstTksxCxR2hmRP6UCwMamppnXcnN/sx8k6FPYGlqHixLRCAx32RZ++05mOtz65y7Btsu3I1XYNvgwmZwJty1XbNINYOzL4MxgIg8/Pftjb1bLmgZFSJSiAgiMvHEJhorYhxWoAY+Gt9RnyvP3lUDY/f+ipr67fmuX258U6ACPoEd8Kxrp74KmBp8rhz7H58JetsUWCtRcwZVwLqtTTsdNM3kAHzoOtg3V0z8oCmov1FhwP0NO93U77g2Qje5cETJvHaLKzMqcAvr/a/iC+JcVEP5CMhEAAAAAElFTkSuQmCC';
67+
```
68+
69+
and another file `src/model/PersonInfo.ts` in the same directory with contents
70+
71+
```typescript
72+
export type PersonInfo = {
73+
firstName: string;
74+
lastName: string;
75+
age: number;
76+
email: string;
77+
}
78+
```
79+
80+
Then rename the file `src/ui/index.tsx` to `src/ui/editor.tsx` and paste the following contents into it:
81+
82+
```typescript {hl_lines=["16-22", "24-35", "37-43"]}
83+
import React, { StrictMode, useCallback, useEffect, useState } from "react";
84+
import { createRoot } from "react-dom/client";
85+
import { getStudioProApi, IComponent, StudioProApi } from "@mendix/extensions-api";
86+
import type { PersonInfo } from "../model/PersonInfo";
87+
88+
function PersonEditor(input : { studioPro: StudioProApi, documentId: string }) {
89+
const {studioPro,documentId} = input;
90+
const [person, setPerson] = useState<PersonInfo>({
91+
firstName: "",
92+
lastName: "",
93+
age: 0,
94+
email: "",
95+
});
96+
const [documentVersion, setDocumentVersion] = useState(0); // Used to trigger re-fetching the document
97+
98+
useEffect(() => {
99+
studioPro.app.model.customBlobDocuments.addEventListener("documentsChanged", ({ documents }) => {
100+
if (documents.some(doc => doc.id === documentId)) {
101+
setDocumentVersion(v => v + 1); // Trigger re-fetch of the document
102+
}
103+
});
104+
}, [studioPro]);
105+
106+
useEffect(() => {
107+
studioPro.app.model.customBlobDocuments
108+
.getDocumentById<PersonInfo>(documentId)
109+
.then(documentFromModel => {
110+
if (documentFromModel && !("error" in documentFromModel)) {
111+
setPerson(documentFromModel.document.contents);
112+
}
113+
})
114+
.catch(err => {
115+
studioPro.ui.messageBoxes.show("error", "Error loading document", "Details: " + err?.message || err);
116+
});
117+
}, [studioPro, documentId, documentVersion]);
118+
119+
const savePerson = useCallback(async () => {
120+
try {
121+
await studioPro.app.model.customBlobDocuments.updateDocumentContent<PersonInfo>(documentId, person)
122+
} catch (error) {
123+
studioPro.ui.messageBoxes.show("error", "Error saving document", "Details: " + ((error as {message?: string})?.message || error));
124+
}
125+
}, [studioPro, documentId, person]);
126+
127+
const labelStyle = { display: 'inline-block', width: '300px' };
128+
129+
return (
130+
<div style={{ backgroundColor: 'white', padding: '20px' }}>
131+
<h2>Person Editor</h2>
132+
<div>
133+
<label style={labelStyle}>
134+
First name:
135+
<input
136+
type="text"
137+
value={person.firstName}
138+
onChange={e => setPerson({ ...person, firstName: e.target.value })}
139+
/>
140+
</label>
141+
</div>
142+
<div>
143+
<label style={labelStyle}>
144+
Last name:
145+
<input
146+
type="text"
147+
value={person.lastName}
148+
onChange={e => setPerson({ ...person, lastName: e.target.value })}
149+
/>
150+
</label>
151+
</div>
152+
<div>
153+
<label style={labelStyle}>
154+
Age:
155+
<input
156+
type="number"
157+
value={person.age}
158+
onChange={e => setPerson({ ...person, age: Number(e.target.value) })}
159+
/>
160+
</label>
161+
</div>
162+
<div>
163+
<label style={labelStyle}>
164+
Email:
165+
<input
166+
type="email"
167+
value={person.email}
168+
onChange={e => setPerson({ ...person, email: e.target.value })}
169+
/>
170+
</label>
171+
</div>
172+
<div style={{ marginTop: 8 }}>
173+
<button onClick={savePerson}>Save</button>
174+
</div>
175+
</div>
176+
);
177+
}
178+
179+
export const component: IComponent = {
180+
async loaded(componentContext, args: { documentId: string; }) {
181+
const studioPro = getStudioProApi(componentContext);
182+
createRoot(document.getElementById("root")!).render(
183+
<StrictMode>
184+
<PersonEditor studioPro={studioPro} documentId={args.documentId} />
185+
</StrictMode>
186+
);
187+
}
188+
};
189+
```
190+
191+
Lastly, we need to update the build instructions and manifest. To do so, replace the contents of `build-extension.mjs` with
192+
193+
```javascript {hl_lines=["16-19"]}
194+
import * as esbuild from 'esbuild'
195+
import {copyToAppPlugin, copyManifestPlugin, commonConfig} from "./build.helpers.mjs"
196+
import parseArgs from "minimist"
197+
198+
const outDir = `dist/myextension`
199+
const appDir = "/Users/petar.vukmirovic/Mendix/App-Guide-Test-Blob"
200+
const extensionDirectoryName = "extensions"
201+
202+
const entryPoints = [
203+
{
204+
in: 'src/main/index.ts',
205+
out: 'main'
206+
}
207+
]
208+
209+
entryPoints.push({
210+
in: 'src/ui/editor.tsx',
211+
out: 'editor'
212+
})
213+
214+
const args = parseArgs(process.argv.slice(2))
215+
const buildContext = await esbuild.context({
216+
...commonConfig,
217+
outdir: outDir,
218+
plugins: [copyManifestPlugin(outDir), copyToAppPlugin(appDir, outDir, extensionDirectoryName)],
219+
entryPoints
220+
})
221+
222+
if('watch' in args) {
223+
await buildContext.watch();
224+
}
225+
else {
226+
await buildContext.rebuild();
227+
await buildContext.dispose();
228+
}
229+
```
230+
231+
and the contents of `manifest.json` with
232+
233+
```json {hl_lines=["6"]}
234+
{
235+
"mendixComponent": {
236+
"entryPoints": {
237+
"main": "main.js",
238+
"ui": {
239+
"editor": "editor.js"
240+
}
241+
}
242+
}
243+
}
244+
```
245+
246+
### Walking through the example code
247+
248+
In `src/main/index.ts`, we are first registering a new document type. When a new document type is registered, you can perform all
249+
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
250+
that you can optionally provide `readableTypeName` which will be shown instead of full type name in every user-facing context such
251+
as in logs and in the Studio Pro UI. You can optionally customize the (de)serialization of the document contents to string, which
252+
defaults to `JSON.stringify` and `JSON.parse`. The next call to `studioPro` API registers editor for our document type. It registers
253+
`editor` entry point of our extension as the editor that will be shown when user interacts with the document in StudioPro (for example
254+
through App Explorer or Find Results). This editor will be shown as a tab, but you can also register editors to be shown as
255+
modal dialogs. Lastly, icons for both light and dark theme are registered. Those icons will be shown in all UI elements where
256+
document icon is needed.
257+
258+
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
259+
active document is shown. Note that the document can be changed outside currently open editor by calls to custom blob document API,
260+
as well as by some Studio Pro operations such as undoing changes. In the next highlighted block, we fetch the document contents
261+
whenever new document is open or updated. Lastly, we make sure that the current changes can be saved.
262+
263+
We also highlighted changes that need to be made to `build-extension.mjs` and `manifest.json` to make sure that `editor` entry point
264+
is properly built and loaded.
265+
266+
## Extensibility Feedback
267+
268+
If you would like to provide us with additional feedback, you can complete a small [survey](https://survey.alchemer.eu/s3/90801191/Extensibility-Feedback).
269+
270+
Any feedback is appreciated.

0 commit comments

Comments
 (0)