How to create an "Action in gallery" plugin
The attachment.viewer.action slot allows you to embed a plugin into the attachment viewer gallery. A plugin in this slot receives the binary data of the attachment and its metadata, displays its own UI (for example, an image editor), and provides the ability to return files to the host.
1. Creating a project via the CLI
To create a new plugin for this slot, use the application creation command and select the required template in the dialog:
weavix create
When prompted with "Select a template:", choose attachment.viewer.action.
You will most likely need the tracker:attachments:write permission.
Fill in the remaining steps as requested. As a result, a project will be generated with a manifest already configured for the attachment.viewer.action slot and a code stub.
2. What is the attachment.viewer.action slot
The attachment.viewer.action slot is an integration point in the attachment viewer gallery in Tracker. A plugin in this slot:
- Adds a new action (button) to the gallery alongside all other available plugins.
- When the plugin is selected, it opens in a modal window.
- Receives the file contents as a blob and the attachment metadata from the host via
slotContext.
When the plugin is closed, you can send a payload with an array of attachments. The host will process it and add each file to the current entity — the corresponding function will be called for each attachment in the array.
For example, if the plugin is opened from an issue description and the close method is called internally with a payload, the host will add the provided files to that issue.
3. Plugin manifest
In the manifest, you need to declare the attachment.viewer.action slot.
Example manifest.json structure:
{
"$schema": "./manifest.schema.json",
"slug": "attachment-viewer-action",
"version": "0.1.0",
"permissions": {
"data": ["tracker:attachments:read", "tracker:attachments:write"]
},
"slots": {
"tracker": {
"attachment.viewer.action": [
{
"entrypoint": "index.html",
"title": {
"ru": "Название плагина",
"en": "Plugin name"
},
"description": {
"ru": "Описание плагина",
"en": "Description of the plugin"
}
}
]
}
}
}
Important
- The
titlevalue is displayed as the button label in the gallery — choose a short and clear name. - entrypoint is the plugin's entry point, usually
index.html.
4. Slot context
The plugin receives attachment data via slotContext. The context type is:
export type AttachmentViewerActionSlotContext = {
attachmentBlob: Blob;
meta: {
id: string;
url: string;
date: string;
size: number;
mimetype: string;
};
};
Fields:
| Field | Type | Description |
|---|---|---|
attachmentBlob |
Blob |
Binary file content |
meta.id |
string |
Attachment identifier |
meta.url |
string |
File URL |
meta.date |
string |
Upload date |
meta.size |
number |
File size in bytes |
meta.mimetype |
string |
File MIME type, for example image/png |
Get the context in the code:
const { slotContext } = useTrackerPluginContext();
const { attachmentBlob, meta } = slotContext;
5. Returning data to the host
To return the result of the plugin's work, you need to:
- Upload the modified file to Tracker using
trackerApi. - Close the plugin using
hostApi.close, passing the created attachments.
The host will process the payload and add each file from the attachments array to the current entity. For example, if the plugin is opened from an issue description, the files will be attached to that issue.
Replacing the original attachment
If you pass the replace: true flag in the payload, the original attachment will be replaced by the new one instead of adding an additional file.
Example:
hostApi.close({ attachments: [response.data], replace: true });
5.1. The useAttachmentSave hook
It's convenient to extract the saving logic into a separate hook:
import { hostApi, trackerApi } from "@weavix/sdk-react";
import { useState, useCallback } from "react";
type UseAttachmentSaveReturn = {
save: (blob: Blob, filename: string) => Promise<void>;
loading: boolean;
error: Error | null;
success: boolean;
reset: () => void;
};
export function useAttachmentSave(): UseAttachmentSaveReturn {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [success, setSuccess] = useState(false);
const save = useCallback(async (blob: Blob, filename: string) => {
try {
setLoading(true);
setError(null);
setSuccess(false);
const file = new File([blob], filename, {
type: blob.type || "image/png",
});
const response = await trackerApi.v3.post["/attachments"]({
bodyParams: { filename },
file,
});
hostApi.close({ attachments: [response.data], replace: true });
setSuccess(true);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, []);
const reset = useCallback(() => {
setLoading(false);
setError(null);
setSuccess(false);
}, []);
return { save, loading, error, success, reset };
}
Here:
trackerApi.v3.post['/attachments']uploads the file to Tracker and returns the data of the created attachment.hostApi.close({ attachments: [...] })closes the plugin's modal window and passes an array of attachments to the host for attaching to the entity.
6. Minimum application structure
- Entry point (for example,
main.tsx): renders into the DOM root and wraps the app in TrackerPluginProvider. - Root component (for example,
App.tsx):- Uses useTrackerPluginContext and gets
theme,slotContext. - Reads
attachmentBlobandmetafromslotContext. - If necessary, uses
useAttachmentSaveto upload the file and close the plugin viahostApi.close. - Renders the UI (editor, viewer, etc.).
- Uses useTrackerPluginContext and gets
Wrapping with the provider is mandatory; otherwise, the context and host won't be able to communicate with the plugin:
import { TrackerPluginProvider } from "@weavix/sdk-react";
root.render(
<TrackerPluginProvider>
<App />
</TrackerPluginProvider>,
);
7. Don't forget about debugging
8. Pre-publication checklist
- The attachment.viewer.action slot is specified in the manifest with a clear
title. slotContextis read in the root component —attachmentBlobandmetaare accessible.- If the plugin returns a modified file —
trackerApiis used for uploading andhostApi.closeis used to pass the result to the host.