﻿# Custom file viewing and editing module

> [HTML Version](external_viewer_intergation.html)

You can use BRIX extension modules to view and edit files in formats that aren’t supported in the system by default. 

For example, you may want to make an order page that includes not only text descriptions but also equipment installation drawings in the **.djvu** format. By default, you cannot view files in this format in the system, but you can create a [custom module](extentions.md) that allows you to do it.

````
начало внимание

````
Only users included in the [Administrators](360006871932.md#administrator) group can create and configure modules.

````
конец внимание

## ````
Architecture

The module consists of the following essential parts: configuration properties, the [Preview page widget](extension-zones.md#preview-page), and [API methods](extention-api.md).

Configuration properties determine a module’s overall behavior. These can be, for example, the supported file formats, the viewer server address, the interface language, etc.

The viewing and editing logic is determined in the **Preview page** type widget using client and server [scripts](client_server_scripts.md).

If an integration with an external application is set up within the module, API methods enable data exchange.

## Create and configure the module

To create a new module, go to **Administration > Modules** and click **+Module**. In the window that opens, select **Create**. Enter information about the module and click the **Create** button. Read more in [Create a custom module](create-extention.md).

### Add configuration parameters

On the **Settings** tab, you can create, edit, and delete the [general parameters](extention-settings.md) of the module which are filled in when it is enabled.

These parameters are global: they apply to all files in the system that are viewed or edited using the module.

To create a parameter, go to the **Settings** tab and click the **+Add** button.

**(external_viewer_intergation-1.png)**

In the opened window, fill in the fields and click the **Create** button.

For a file viewing and editing module, a mandatory parameter is \[OBJECT\]. It stores the supported file formats. Use the following settings for the property:

**(external_viewer_intergation-SupportedFileTypes.png)**

- **Display Name\***. Specify the name of the field that will appear on the module connection page. It can be any name, for example, **Supported formats**.

- **Property Name\***. Specify the value \[OBJECT\]. This property name is mandatory. It is used to search for modules that can open files with a specific extension.

- **Type\***. Select [Category](360009707032.md#category). To enable the module to support multiple file extensions, choose the **Multiple** type.

- **Values**. Add file formats that will be opened using the module, for example, **.doc**, **.docx**, and **.odt**.

You can also add fields for switching the file opening mode, specifying the address of an external server, etc.

All added properties are displayed on the module connection page in the **Administration** workspace.

**(external_viewer_intergation-2.png)**

If needed, you can change the standard form used to present the module settings, hide some fields. To do that, on the **Settings** tab, click the **Change Form** button. The interface designer will open. There you can set up the module connection page. Read more in [Module settings](extention-settings.md).

### API methods

On the **API Methods** tab, add [methods](extention-api.md) for data exchange with the external server. If data exchange is not required for the module you create, you don’t need to add methods.

For example, you can create a \[OBJECT\] POST request that will obtain the current status of the document from the server and add a new version to BRIX whenever it’s edited. The source code of the method can be found in the [Example of an integration with an external file viewing and editing service](#api) section. 

### Viewing and editing widget

The **Preview page** type widget is the module’s main component. It provides data exchange between BRIX and the external application used for viewing documents. To create the widget, do the following:

1. In the module settings, go to the **Widgets** tab.

2. Click the **+Widget** button.

3. In the opened window, specify the widget parameters:

	- **Name\***. Enter the widget’s name.

	- **Widget code\***. Set the widget’s ID.

	- **Extension**. Select **File preview >** **Preview page**.

**(external_viewer_intergation-3.png)**

4. Click **Save**.

The interface designer will open. Here you can set up the widget content.

By default, the widget context includes the following properties:

- **File**. A [Files](360009707032.md#file_type) type property. A document opened by the viewing and editing module.

- **ForFile**. A [Files](360009707032.md#file_type) type property. It is used when saving the result of comparing a document from the **File** field with another version of it or a different file. It is applied, for example, if the document is not yet saved in the system or a version needs to be added to another file. The property contains the document for which a version with the comparison result is created.

- **Action**. A [String](360009707032.md#string) type property. The mode in which the document opens: for viewing only or for view and editing. The possible values are **view** and **edit**.

- **Extension**. A [String](360009707032.md#string) type property. The extension of the document in the **File** property.

You can find these properties on the right-side panel of the interface designer, on the **Properties** tab.

**(external_viewer_intergation-4.png)**

Let’s consider an example of how a widget context can be used. You can create a method that checks whether the file preview module supports documents of a certain type. First, let’s get the file’s extension from the **Extension** context variable of the widget:

````
function getFileExtension() \{  
    if (Context.data.extension)\{  
        return (Context.data.extension\[0\]);  
    \}  
    else \{  
        return "";  
    \}  
\}

````
If the format is supported, the method will return \[OBJECT\], otherwise \[OBJECT\]. In the following example, only **.xlsx** files are supported:

````
async function canRender() \{  
    let fileType:string = getFileExtension();     
    if (fileType == "xlsx")  
    \{  
        return  true;  
    \}  
    else  
    \{  
        return  false;  
    \}  
\}

````
**Note**:** **To obtain document data, use the server scripts of the viewing and editing widget. The use of client scripts is not recommended, as the user may not have sufficient permissions to work with the app item or file.

## Example of a viewing module without integration with an external service

Let’s consider an example of creating a module for playing **.mp3** files in BRIX. We’ll use standard HTML elements without integration with an external service.

To create the module:

1. Go to **Administration > Modules**. In the upper right corner of the page, click **+Module** and select **Create**. Specify the module’s name and description.

2. Go to the **Settings** tab and add the mandatory property for specifying the file format. Specify an arbitrary display name. Use the \[OBJECT\] as the property name, select the **Category > Single** type, and add the **mp3 **value.

3. Create a widget that will play **.mp3** files. To do that, on the **Widgets** tab, click **+Widget**. Enter any name and code for the widget, for example, \[OBJECT\]. In the **Extension** field, select **File preview > Preview page**. Click **Save**.

4. In the [interface designer](interface_designer.md), go to the **Context** tab.

5. Create a variable that will store a link to the file that needs to be opened. To do that, click **+Add** and set the following parameters:

- **Display Name\***. Enter **File link**.

- **Property Name\***. Enter \[OBJECT\].

- **Type\***. Select **String**.

6. Go to the **Template** tab. Add the [Code](code_widget.md) widget to the modeling canvas. In the window that opens, paste the following:

````
<% if (Context.data.link) \{ %>  
    <audio controls>  
        <source src="<%= Context.data.link%>" type="audio/mp3">  
        your browser does not support the audio element. Try downloading the file  
        <a href="<%= Context.data.link%>" download>here</a>.  
    </audio>  
<% \} %>

````
In this code, standard HTML elements are used to create a player for the audio file. The file source is the link specified in step 5.

When you save the script, the player will appear on the modeling canvas:

**(external_viewer_intergation-6.png)**

7. Go to the **Scripts** tab. In the upper right corner, click **Client**. Add a script that will initialize the widget:

````
/\*\* Event that occurs upon widget initialization \*/  
async function onInit() \{  
    // Get the file download URL from the \`file\` context variable  
    const file\_link = await Context.data.file?.getDownloadUrl();  
    // If no file is found, quit  
    if (\!file\_link) \{  
        return;  
    \}  
    // Assign the \`link\` context variable to the file download URL  
    Context.data.link = file\_link;  
\}

8. ````
 In the upper left corner, click **Save**. Then click **Check **to make sure the widget doesn’t have errors. Then click **Publish**.

When this module is enabled, you can play **.mp3** files in the BRIX interface when you add them to [Files](360009707032.md) type fields on app item pages.

## Example of an integration with an external file viewing and editing service

Let’s see how to integrate a viewing and editing module with an external application using OnlyOffice as an example. In this integration, we are going to use JavaScript to send documents and parameters to an external service, receive the result, and display it in BRIX.

To create the module, do the following:

1. Go to **Administration > Modules**. In the upper right corner, click **+Module**, then click **Create**. Specify the module’s name and brief description.

2. Go to the **Settings** tab and add a mandatory property to specify the file format. Specify an arbitrary display name. Use the \[OBJECT\] value as the property name, select the **Category >** **Multiple** type, and in the **Values** field add the formats supported by the module: **.doc**, **.docx**, **.odt**, **.txt**, **.xls**, **.xlsx**, **.ods**, **.csv**, **.ppt**, **.pptx**, **.odp**.

3. In the same way, create two properties that will be used to connect to the external file viewer server. Set the following parameters:

	- **Display Name\***. Enter the property names: **BRIX server address** and **OnlyOffice server address**.

	- **Property Name\***. Enter the property unique code names: \[OBJECT\] and \[OBJECT\].

	- **Type\***. Select **String** for both properties.

4. Create a widget that will open files of the specified formats. To do that, on the **Widgets** tab, click **+Widget**. Enter any **Name**\* and **Widget code**\*, for example, **doc preview**. In the **Extension** field, select **File preview > Preview page**.

5. In the [interface designer](interface_designer.md), go to the **Context** tab.

6. Create a context variable that will be used to pass parameters from the server to the client side. To do that, click **+Add** and set the following values:

	- **Display Name\***. Enter **Additional parameters**.

	- **Property Name\***. Enter \[OBJECT\].

	- **Type\***. Select **String**.

7. Go to the **Template** tab. Add the [Code](code_widget.md) widget to the modeling canvas. In the window that opens, enter the following:

Script

````
<div style="height: 100%;width: 100%;display: flex;">  
    <script type="text/javascript" src="<%= getServerAddress() %>"></script>  
     
    <div id="<%= getrandname() %>" style="height: 100%">      
    </div>  
    <script type="text/javascript">  
        // Function creating an OnlyOffice editing window  
        (function() \{ function createEditor(data) \{   
            // Initialization of the editing window                 
            var docEditor = new DocsAPI.DocEditor("<%= getrandname() %>",  
                \{  
                    "document": \{  
                            "fileType": data.fileExtension,  
                            "key": data.fileKey,  
                            "title": data.fileName,  
                            "url": data.fileUrl,  
                            "permissions": \{  
                                "download": false,  
                                "edit": true,  
                                "review": true,  
                                "reviewGroups": \[""\]  
                            \}  
                        \},  
                    "documentType": data.fileType,                  
                    "height": "100%",  
                    "width": "100%",  
                    "editorConfig": \{  
                        "customization": \{  
                            "chat": data.chat,  
                            "comments": data.comments,  
                            "macros": data.macros,  
                            "macrosMode": data.macros ? "warn" : "disable",  
                            "plugins": data.plugins  
                        \},  
                        "callbackUrl": data.callbackUrl,  
                        "mode": data.mode,  
                        "lang": data.language,  
                        "user": \{  
                                "id": data.userId,  
                                "name": data.userName  
                            \}                                                                          
                        \}  
                    \});  
                \}  
  
          
        // Retrieve the file and create the view/editing session   
        let connections\_ch = 3;  
        function waitForDocsApi() \{  
            if (window.DocsAPI) \{  
                let data = \{  
                    serverAddress: '<%= getServerAddress() %>',  
                    callbackUrl: '<%= getCallbackUrl() %>',  
                    userId: '<%= getUserId() %>',  
                    userName: '<%= getUserName() %>',  
                    fileId: '<%= getFileId() %>',  
                    fileKey: '<%= getFileKey() %>',  
                    fileUrl: '<%= getFileUrl() %>',  
                    fileName: '<%= getFileName() %>',  
                    fileExtension: '<%= getFileExtension() %>',  
                    fileType: '<%= getFileType() %>',  
                    mode: '<%= getMode() %>',  
                    language: '<%= getLanguage() %>',  
                    chat: <%= isChat() %>,  
                    comments: <%= isComments() %>,  
                    macros: <%= isMacros() %>,  
                    plugins: <%= isPlugins() %>,  
                    type: '<%= getViewType() %>',  
                    reviewDisplay: '<%= getReviewDisplay() %>',   
                    trackChanges: <%= isTrackChanges() %>,                  
                \};                    
                                      
                // Check whether document properties saved in the variable are correct  
                if (data.fileUrl == "") \{  
                    thrownew Error('File path not found');  
                \} elseif (data.fileType == "") \{  
                    thrownew Error('This file format is not supported by OnlyOffice');  
                \} elseif (data.mode == "") \{  
                    thrownew Error('View/Editing mode parameter not found')  
                \}  
                else \{                          
                    let mode = checkDocAppEdit();  
                    if (data.mode\!="edit" \&\& mode \!== "") \{  
                        data.mode = mode;  
                    \}  
                    // Create the view/editing window  
                    createEditor(data);  
                \}  
            \} elseif(connections\_ch > 0) \{  
                connections\_ch --;  
                setTimeout(waitForDocsApi,500);  
                  
            \}  
            else \{  
                thrownew Error('OnlyOffice API server response error')  
            \}  
        \}  
          
        function checkDocAppEdit() \{  
            var url = window.location.href;  
            var isEdit = url.includes('/edit)');  
            if ('<%= isLegacyEnabled() %>' \!== 'false') \{  
                if (isEdit) \{  
                    return 'edit';  
                \} else \{  
                    return 'view';  
                \}  
            \} else \{  
                return "";  
            \}  
        \}  
  
        setTimeout(waitForDocsApi,1000);  \})();  
  
          
    </script>  
</div>
````

8. ````
The code above contains functions that need to be described on the **Scripts** tab in the interface designer. Switch to this tab. In the upper right corner, click **Client**. Add the following:

Script

````
/\* window webAPI \*/  
declare const window: any;  
  
/\* Client scripts module \*/  
let fileUrl: string = "";  
let file : FileItem;  
let req: string;  
let user: UserItem;  
/\* Extension settings \*/  
let onlyOfficeServerAddress: any;  
let brixServerAddress: any;  
let enabledFileTypes: any;  
let isLegacyMode: any;  
let isChatEnabled: boolean;  
let isCommentsEnabled: boolean;  
let isMacrosEnabled: boolean;  
let isPluginsEnabled: boolean;  
let isTrackChangesEnabled: boolean;  
let language: string;  
let reviewDisplay: string;  
let randname:string;  
// Flag showing that the file is temporary, displayed on the creation form of a Document type app item  
let isNewFilePreview: boolean = false;  
  
function getrandname() \{  
   if(randname)  
   \{  
       return randname;  
   \}  
   else  
   \{  
       let id = getFileId().replace('-','');  
       let key = new Date().valueOf().toString();  
       randname = "placeholder\_"+id+key;  
       return randname;  
   \}  
\}  
  
async function onInit() \{  
    if (Context) \{  
        fileUrl = await getFileUrlInternal() as string;  
        file = await getFileInternal() as FileItem;      
        user = await getUserInternal();  
  
        // Check whether the file's extenstion is correct  
        const fileExtension = getFileExtension().toLowerCase();  
        const availableFileExtensions: Array<string> = \["doc","docx","odt","txt", "rtf", "xls","xlsx","ods","csv", "ppt","pptx","odp"\];  
        if (availableFileExtensions.indexOf(fileExtension) <= -1) \{  
            thrownew Error('This file format is not supported by OnlyOffice');  
        \}  
          
        await Server.rpc.writeParametersToContext();  
        readParametersFromContext();  
    \}  
\}  
  
function readParametersFromContext() \{  
    if (Context.data.ExtensionParameters) \{  
        let parameters = JSON.parse(Context.data.ExtensionParameters);  
  
        isLegacyMode = parameters.isLegacy;  
        isChatEnabled = parameters.isChatEnabled ?? false;  
        isCommentsEnabled = parameters.isCommentsEnabled ?? false;  
        isMacrosEnabled = parameters.isMacrosEnabled;  
        isPluginsEnabled = parameters.isPluginsEnabled;  
        isTrackChangesEnabled = parameters.isTrackChangesEnabled;  
        enabledFileTypes = parameters.enabledTypes;  
        brixServerAddress = parameters.brixAddress;  
        onlyOfficeServerAddress = parameters.onlyOfficeServerAddress;  
        language = parameters.language as string;  
        reviewDisplay = parameters.reviewDisplay as string;  
    \}  
\}  
  
async function canRender() \{  
    const fileType = getFileType();  
      
    if (fileType \!== "") \{  
        returntrue;  
    \}  
    else \{  
        returnfalse;  
    \}  
\}  
  
/\* Get server address from the extension's settings \*/  
  
function getServerAddress() \{  
    const apiPath = "/web-apps/apps/api/documents/api.js";  
      
    if (onlyOfficeServerAddress \!== "") \{  
        if (onlyOfficeServerAddress.includes(apiPath)) \{  
            return onlyOfficeServerAddress;  
        \}  
        else \{  
            return onlyOfficeServerAddress + apiPath;  
        \}  
    \}  
    else \{  
          
    \}  
\}  
  
function getCallbackUrl() \{  
    const callbackUrl = "/api/extensions/00e82811-87d6-4d46-a5d4-e444ed8a857a/script/callback";  
      
    return brixServerAddress+callbackUrl;  
\}  
  
/\* Get links to download files from CDN \*/  
  
function getFileUrl(): any \{  
    return fileUrl;  
\}  
  
async function getFileUrlInternal () \{  
     if (Context.data.file) \{           
         const url = await Context.data.file.getDownloadUrl();  
           
         return url;      
     \}  
\}  
  
/\* Get the file, its extension, and type \*/  
  
async function getFileInternal() \{      
    if (Context.data.file) \{  
        try \{  
            const file = await Context.data.file.fetch();  
            return file;  
        \} catch (ex) \{  
            isNewFilePreview = true;  
            return undefined;  
        \}  
    \}  
\}  
  
function getFileName() \{  
    if (file \&\& \!isNewFilePreview) \{  
        return file.data.\_\_name;  
    \}  
    else \{  
        return " "  
    \}  
\}  
  
function getFileExtension() \{  
    if (file \&\& \!isNewFilePreview) \{  
        const fileExtension = file.data.\_\_name.substr(file.data.\_\_name.lastIndexOf('.') + 1);  
        return fileExtension;  
    \}  
    elseif (Context.data.extension) \{  
        return Context.data.extension\[0\];  
    \}  
    else \{  
        return "";  
    \}  
\}  
  
function getEnabled() \{  
    return enabledFileTypes;  
\}  
  
function getLanguage() \{  
    if (language \&\& language \!= "") \{  
        return language;  
    \} else \{  
        return "en";  
    \}  
\}  
  
function getFileType() \{  
        let type: string = "";  
  
        const fileExtension = getFileExtension().toLowerCase();  
        const documents: Array<string> = \["doc","docx","odt","txt", "rtf"\];  
        const spreadsheets: Array<string> = \["xls","xlsx","ods","csv"\];  
        const presentations: Array<string> = \["ppt","pptx","odp"\]  
  
        if(typeof enabledFileTypes === "undefined") \{  
            return type;  
        \}  
  
        if (enabledFileTypes.findIndex((i: \{ code: string; \}) => i.code == fileExtension) \!== -1) \{  
            if (documents.indexOf(fileExtension) > -1) \{  
                type = "word";  
            \} elseif (spreadsheets.indexOf(fileExtension) > -1) \{  
                type = "cell"  
            \} elseif (presentations.indexOf(fileExtension) > -1) \{  
                type = "slide"  
            \}  
        \}  
  
        return type;  
    \}  
  
function getFileId(): any \{  
    if (Context.data.file \!== undefined) \{  
        if (file \!== undefined) \{  
            return file.data.\_\_id;  
        \}  
        else \{  
            return Context.data.file.id;  
        \}  
    \}  
\}  
  
function getFileKey(): any \{  
    let key: string = "";  
    if (Context.data.file \!== undefined) \{  
        if (file \!== undefined) \{  
            let lastUpdatedUnix = file.data.\_\_updatedAt.format("X");  
            key = getFileId() + "." + lastUpdatedUnix;  
        \}  
        else \{  
            key = new Date().valueOf().toString();  
        \}  
    \}  
    return key;  
\}  
  
function getMode(): any \{  
    if (Context.data.action == "edit" || Context.data.action == "editnew") \{  
        return "edit";  
    \} else \{  
        return "view";  
    \}  
\}  
  
async function getUserInternal() \{  
    const user = await System.users.getCurrentUser();  
  
    return user;  
\}  
  
function getUserName(): string \{  
    let userName = user.data.\_\_name.replace('\\'','');  
      
    return userName;  
\}  
  
function getUserId(): string \{  
    return user.data.\_\_id;  
\}  
  
function getViewType(): string \{  
    if (window.innerWidth <= 991) \{  
        return "mobile";  
    \}  
    return "desktop";  
\}  
  
function isLegacyEnabled(): boolean \{  
    return isLegacyMode;  
\}  
  
function isChat(): boolean \{  
    return isChatEnabled;  
\}  
  
function isComments(): boolean \{  
    return isCommentsEnabled;  
\}  
  
function isMacros(): boolean \{  
    return isMacrosEnabled;  
\}  
  
function isPlugins(): boolean \{  
    return isPluginsEnabled;  
\}  
  
function getReviewDisplay(): string \{  
    return reviewDisplay  
\}  
  
function isTrackChanges(): boolean \{  
    return isTrackChangesEnabled;  
\}
````

9. ````
On the **Scripts** tab, click **Server** in the upper right corner. Enter the following:

Script

````
/\* Server scripts module \*/  
async function writeParametersToContext(): Promise<void> \{  
    let brixAddress = Namespace.params.data.brixServerAddress;  
    let onlyOfficeAddress = Namespace.params.data.onlyOfficeServerAddress;  
    let enabledTypes = Namespace.params.data.SupportedFileTypes;  
  
    let isLegacy = Namespace.params.data.LegacyModeDetectionEnabled;  
    let isChatEnabled = Namespace.params.data.ChatEnabled;  
    let isCommentsEnabled = Namespace.params.data.CommentsEnabled;  
    let isMacrosEnabled = Namespace.params.data.MacrosEnabled;  
    let isPluginsEnabled = Namespace.params.data.PluginsEnabled;  
    let isTrackChangesEnabled = Namespace.params.data.TrackChangesEnabled;  
  
    let interfaceLanguage = Namespace.params.data.InterfaceLanguage;  
    let language = interfaceLanguage.code;  
    let reviewDisplayCategory = Namespace.params.data.ReviewDisplay;  
    let reviewDisplay = reviewDisplayCategory?.code || "original";  
  
    Context.data.ExtensionParameters = JSON.stringify(\{isLegacy, brixAddress, onlyOfficeAddress, enabledTypes, language, isChatEnabled, isCommentsEnabled, isMacrosEnabled, isPluginsEnabled, isTrackChangesEnabled, reviewDisplay\});  
\}
````

10. ````
In the upper left corner, in the toolbar, click **Save**. Then click **Check** to make sure there are no errors in the widget. Finally, click **Publish**.

11. Close the interface designer. In module settings, switch to the **API Methods** tab. Create a new [method](extention-api.md). Add the following script to it:

Script

````
async function callback(req: FetchRequest): Promise<HttpResponse | void> \{  
    let responseCode = 0;  
    if (\!req.body) \{  
        thrownew Error('Empty request body');  
    \}      
  
    // Get the request body from OnlyOffice  
    let requestText = String(req.body);  
  
    if (\!requestText) \{  
        thrownew Error('Wrong request body');  
    \}  
    // Transforming into an object  
    let parsedData = JSON.parse(requestText);  
    // Status 2 means that the document has been changed and closed, so it needs to be saved again as a new BRIX version  
    if (parsedData.status == 2) \{  
        let fileId = parsedData.key.split('.').shift();  
        let url = parsedData.url;  
        let file = await System.files.search().where(f => f.\_\_id.eq(fileId)).first();  
  
        if (file \&\& url) \{  
            const fetchedFile = await fetch(url);  
            if (\!fetchedFile) \{  
                responseCode = 1;  
            \} else \{  
                const buf = await fetchedFile.arrayBuffer();  
                file.addVersion(file.data.\_\_name, buf);  
            \}  
        \}  
    \}  
  
    const resp = new HttpResponse();  
    resp.json(\{"error":responseCode\})  
  
    return resp;  
\}
````

````
After that, when the module is enabled, you will be able to view files with formats specified in its settings directly in the BRIX interface.