Skip to main content

Using the Vault Messages Package

⚠️ Note

Sample codes are for demonstration purposes only. Before using in production, add proper error handling such as try-catch blocks, null checks, and input validation.

Overview

This sample demonstrates how to use the @m-filescorporation/uix-vault-messages NPM package to work with M-Files vault gRPC messages using type-safe constructors and helpers instead of raw JSON.

The application creates a custom command that searches the vault for documents matching a keyword, then displays the results in a message box.

This sample does not show how to create a local development folder or to deploy the code to the M-Files server. It is assumed that a local development folder already exists, and that is the location in which the development is occurring.

Prerequisites

Since this sample uses an NPM package, a bundler is needed to bundle the package into a single file that M-Files can load. This sample uses Vite.

Initialize your project, install the dependencies, and add a build script to your package.json:

npm init -y
npm install @m-filescorporation/uix-vault-messages
npm install --save-dev vite
npm pkg set scripts.build="vite build"

Create a vite.config.js to bundle the module as an IIFE (immediately-invoked function expression) that M-Files can load directly:

vite.config.js
import { defineConfig } from 'vite';
import { copyFileSync } from 'fs';
import { execSync } from 'child_process';

export default defineConfig({
build: {
lib: {
entry: 'main.js',
formats: ['iife'],
name: 'window',
fileName: () => 'main.js',
},
outDir: 'dist',
minify: false,
rollupOptions: {
output: {
// Extend window instead of creating a new variable.
// This places exported functions (e.g. OnNewShellUI) directly
// on the global scope so M-Files can call them.
extend: true,
},
},
},
plugins: [{
name: 'package-app',
closeBundle() {
copyFileSync('appdef.xml', 'dist/appdef.xml');
// Create a deployable zip from the dist folder.
execSync(
'powershell -Command "Compress-Archive -Path dist/appdef.xml,dist/main.js -DestinationPath dist/app.zip -Force"',
);
},
}],
});

After writing the application code, run npm run build to produce the bundled output in the dist folder. The build copies appdef.xml alongside the bundled main.js and creates a ready-to-deploy app.zip.

See the Application Structure guide for general details on UIX project structure.

Creating the application structure

Creating the application definition file

Into this folder we will create an application definition file. This file must be named appdef.xml. The application will use version 5 of the client schema (as we are only targeting newer M-Files versions). The application will declare a single Shell UI module (with its code in main.js), and no dashboards.

appdef.xml
<?xml version="1.0"?>
<application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.m-files.com/schemas/appdef-client-v5.xsd">
<guid>A7E3B2D1-9F4C-4E8A-B5D6-1C2E3F4A5B6C</guid>
<name>Vault Messages Package Sample</name>
<version>0.1</version>
<description>Demonstrates usage of @m-filescorporation/uix-vault-messages for type-safe vault operations.</description>
<publisher>M-Files Corporation</publisher>
<enabled-by-default>true</enabled-by-default>
<modules>
<module environment="shellui">
<file>main.js</file>
</module>
</modules>
</application>
Ensure that your application has a unique GUID by using a GUID generator.

Creating the module

Next we will create a module file to contain our actual application logic. Initially:

  • We will import MFGrpc and the enums we need from the package.
  • We will declare and export the entry point for the ShellUI module. The export keyword ensures the bundler places OnNewShellUI on the global scope (via the Vite config above).
  • We will react to the NewShellFrame event and obtain a reference to the shell frame.
  • We will react to the shell frame's Started event (as using the shell frame before this point will result in an exception).
  • Create a command button, place it into the menu area, and set it as active.
  • React to the shell frame's CustomCommand event and add placeholder code to execute when the command is clicked.
main.js
import { MFGrpc, Datatype, ConditionType, StatusType } from '@m-filescorporation/uix-vault-messages';

export function OnNewShellUI(shellUI) {
/// <summary>The entry point of ShellUI module.</summary>
/// <param name="shellUI" type="MFiles.ShellUI">The new shell UI object.</param>

// Register to be notified when a new shell frame is created.
shellUI.Events.Register(MFiles.Event.NewShellFrame, handleNewShellFrame);
}

function handleNewShellFrame(shellFrame) {
/// <summary>Handles the OnNewShellFrame event for an IShellUI.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The shell frame object which was created.</param>

// Register to listen to the started event.
shellFrame.Events.Register(
MFiles.Event.Started,
getShellFrameStartedHandler(shellFrame),
);
}

function getShellFrameStartedHandler(shellFrame) {
/// <summary>Gets a function to handle the Started event for shell frame.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The current shell frame object.</param>
/// <returns type="MFiles.Events.OnStarted">The event handler.</returns>

// Return the handler function for ShellFrame's Started event.
return async () => {
// Create a command button in the top menu.
const searchCommandId =
await shellFrame.Commands.CreateCustomCommand('Search Documents');

// Add the command to the main menu.
await shellFrame.Commands.AddCustomCommandToMenu(
searchCommandId,
MFiles.MenuLocation.MenuLocation_TopPaneMenu,
1,
);

// Set the command as active (visible and enabled).
await shellFrame.Commands.SetCommandState(
searchCommandId,
MFiles.CommandLocation.All,
MFiles.CommandState.CommandState_Active,
);

// Register to respond to commands being clicked.
shellFrame.Commands.Events.Register(
MFiles.Event.CustomCommand,
async (command) => {
// We only care about our command.
// If the command being clicked is something else then return.
if (command !== searchCommandId) {
return;
}

// TODO: Build search conditions and execute the search.
},
);
};
}

Building search conditions with helpers

Next we will add a function that builds search conditions using the package's SearchConditionArray helpers. We will:

  • Create a new SearchConditionArray using MFGrpc.SearchConditionArray.Create().
  • Use Status() with STATUS_TYPE_OBJECT_TYPE to filter by object type (0 = Document).
  • Use Status() with STATUS_TYPE_IS_DELETED to exclude deleted objects.
  • Use the Property() method to add a condition that searches the "Name or Title" property (ID 0) for the keyword.
  • All three methods return the array, so calls can be chained.
main.js
import { MFGrpc, Datatype, ConditionType, StatusType } from '@m-filescorporation/uix-vault-messages';

export function OnNewShellUI(shellUI) {
/// <summary>The entry point of ShellUI module.</summary>
/// <param name="shellUI" type="MFiles.ShellUI">The new shell UI object.</param>

// Register to be notified when a new shell frame is created.
shellUI.Events.Register(MFiles.Event.NewShellFrame, handleNewShellFrame);
}

function handleNewShellFrame(shellFrame) {
/// <summary>Handles the OnNewShellFrame event for an IShellUI.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The shell frame object which was created.</param>

// Register to listen to the started event.
shellFrame.Events.Register(
MFiles.Event.Started,
getShellFrameStartedHandler(shellFrame),
);
}

function getShellFrameStartedHandler(shellFrame) {
/// <summary>Gets a function to handle the Started event for shell frame.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The current shell frame object.</param>
/// <returns type="MFiles.Events.OnStarted">The event handler.</returns>

// Return the handler function for ShellFrame's Started event.
return async () => {
// Create a command button in the top menu.
const searchCommandId =
await shellFrame.Commands.CreateCustomCommand('Search Documents');

// Add the command to the main menu.
await shellFrame.Commands.AddCustomCommandToMenu(
searchCommandId,
MFiles.MenuLocation.MenuLocation_TopPaneMenu,
1,
);

// Set the command as active (visible and enabled).
await shellFrame.Commands.SetCommandState(
searchCommandId,
MFiles.CommandLocation.All,
MFiles.CommandState.CommandState_Active,
);

// Register to respond to commands being clicked.
shellFrame.Commands.Events.Register(
MFiles.Event.CustomCommand,
async (command) => {
// We only care about our command.
// If the command being clicked is something else then return.
if (command !== searchCommandId) {
return;
}

// TODO: Execute the search and display results.
const conditions = buildDocumentSearchConditions('Report');
},
);
};
}

function buildDocumentSearchConditions(keyword) {
/// <summary>Builds search conditions for finding documents by keyword.</summary>
/// <param name="keyword" type="string">The keyword to search for in document titles.</param>
/// <returns type="SearchConditionArray">The search conditions.</returns>

// Create a new SearchConditionArray and build conditions using chaining.
// Status() and Property() return the array, so calls can be chained.
const conditions = MFGrpc.SearchConditionArray.Create()

// Filter by object type: Documents (type 0).
.Status(
StatusType.STATUS_TYPE_OBJECT_TYPE,
ConditionType.CONDITION_TYPE_EQUAL,
Datatype.DATATYPE_LOOKUP,
MFGrpc.Lookup.Create(0),
)

// Exclude deleted objects.
.Status(
StatusType.STATUS_TYPE_IS_DELETED,
ConditionType.CONDITION_TYPE_EQUAL,
Datatype.DATATYPE_BOOLEAN,
false,
)

// Search "Name or Title" property (ID 0) for the keyword.
.Property(
0,
ConditionType.CONDITION_TYPE_CONTAINS,
Datatype.DATATYPE_TEXT,
keyword,
);

return conditions;
}

Executing the search and reading results

Finally, we will add a function that executes the search and displays the results. We will:

  • Call SearchOperations.SearchObjects on the vault, passing our search conditions. Use toJSON() to convert proxy objects to plain data before sending over PostMessage.
  • Wrap each result in MFGrpc.ObjectVersionEx to access helper properties like Title, ID, and Version.
  • Display the results in a message box.
main.js
import { MFGrpc, Datatype, ConditionType, StatusType } from '@m-filescorporation/uix-vault-messages';

export function OnNewShellUI(shellUI) {
/// <summary>The entry point of ShellUI module.</summary>
/// <param name="shellUI" type="MFiles.ShellUI">The new shell UI object.</param>

// Register to be notified when a new shell frame is created.
shellUI.Events.Register(MFiles.Event.NewShellFrame, handleNewShellFrame);
}

function handleNewShellFrame(shellFrame) {
/// <summary>Handles the OnNewShellFrame event for an IShellUI.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The shell frame object which was created.</param>

// Register to listen to the started event.
shellFrame.Events.Register(
MFiles.Event.Started,
getShellFrameStartedHandler(shellFrame),
);
}

function getShellFrameStartedHandler(shellFrame) {
/// <summary>Gets a function to handle the Started event for shell frame.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The current shell frame object.</param>
/// <returns type="MFiles.Events.OnStarted">The event handler.</returns>

// Return the handler function for ShellFrame's Started event.
return async () => {
// Create a command button in the top menu.
const searchCommandId =
await shellFrame.Commands.CreateCustomCommand('Search Documents');

// Add the command to the main menu.
await shellFrame.Commands.AddCustomCommandToMenu(
searchCommandId,
MFiles.MenuLocation.MenuLocation_TopPaneMenu,
1,
);

// Set the command as active (visible and enabled).
await shellFrame.Commands.SetCommandState(
searchCommandId,
MFiles.CommandLocation.All,
MFiles.CommandState.CommandState_Active,
);

// Register to respond to commands being clicked.
shellFrame.Commands.Events.Register(
MFiles.Event.CustomCommand,
async (command) => {
// We only care about our command.
// If the command being clicked is something else then return.
if (command !== searchCommandId) {
return;
}

// Search for documents and display results.
await searchAndDisplayResults(shellFrame);
},
);
};
}

function buildDocumentSearchConditions(keyword) {
/// <summary>Builds search conditions for finding documents by keyword.</summary>
/// <param name="keyword" type="string">The keyword to search for in document titles.</param>
/// <returns type="SearchConditionArray">The search conditions.</returns>

// Create a new SearchConditionArray and build conditions using chaining.
// Status() and Property() return the array, so calls can be chained.
const conditions = MFGrpc.SearchConditionArray.Create()

// Filter by object type: Documents (type 0).
.Status(
StatusType.STATUS_TYPE_OBJECT_TYPE,
ConditionType.CONDITION_TYPE_EQUAL,
Datatype.DATATYPE_LOOKUP,
MFGrpc.Lookup.Create(0),
)

// Exclude deleted objects.
.Status(
StatusType.STATUS_TYPE_IS_DELETED,
ConditionType.CONDITION_TYPE_EQUAL,
Datatype.DATATYPE_BOOLEAN,
false,
)

// Search "Name or Title" property (ID 0) for the keyword.
.Property(
0,
ConditionType.CONDITION_TYPE_CONTAINS,
Datatype.DATATYPE_TEXT,
keyword,
);

return conditions;
}

async function searchAndDisplayResults(shellFrame) {
/// <summary>Searches for documents and displays results in a message box.</summary>
/// <param name="shellFrame" type="MFiles.ShellFrame">The current shell frame object.</param>

try {
// Get the vault reference.
const iVault = shellFrame.ShellUI.Vault;

// Build search conditions using package helpers.
const conditions = buildDocumentSearchConditions('Report');

// Execute the search.
// Use toJSON() to convert proxy objects to plain data — proxy
// objects cannot be sent over PostMessage to the vault.
const searchResponse = await iVault.SearchOperations.SearchObjects({
call_importance: 1, // CallImportance.CALL_IMPORTANCE_NORMAL
target: {
mode: 0, // SearchMode.SEARCH_MODE_DEFAULT
},
conditions: [conditions.toJSON()],
options: {},
limit: 10,
timeout_in_seconds: 30,
});

// Process the results.
const results = searchResponse.results || [];

if (results.length === 0) {
await shellFrame.ShowMessage('No documents found matching "Report".');
return;
}

// Read object metadata from each search result.
const summaryLines = results.map((result) => {
// Wrap the server response to get access to helper properties
// like Title and ID, instead of navigating deeply nested JSON.
const objVerEx = new MFGrpc.ObjectVersionEx(result.object);

const title = objVerEx.Title;
const id = objVerEx.ID;
const version = objVerEx.Version;

return `- ${title} (ID: ${id}, Version: ${version})`;
});

const message = `Found ${results.length} document(s):\n\n${summaryLines.join('\n')}`;
await shellFrame.ShowMessage(message);

} catch (exception) {
console.error('Search failed:', exception);
MFiles.ReportException(exception);
}
}

Building and testing the application

  1. Run npm run build to bundle the application. The output is in the dist folder.
  2. Deploy dist/app.zip to your M-Files vault.
  3. Open M-Files web and navigate to the vault.
  4. Click the Search Documents button in the top menu.
  5. A message box will appear listing documents whose title contains "Report".
For the full reference of all available helper methods, see the Vault API Helpers overview page.

Download

Download the complete sample project — contains appdef.xml, main.js, vite.config.js, and package.json. After extracting, run npm install and npm run build to produce the deployable dist/app.zip.