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 title value 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:

  1. Upload the modified file to Tracker using trackerApi.
  2. 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

  1. Entry point (for example, main.tsx): renders into the DOM root and wraps the app in TrackerPluginProvider.
  2. Root component (for example, App.tsx):
    • Uses useTrackerPluginContext and gets theme, slotContext.
    • Reads attachmentBlob and meta from slotContext.
    • If necessary, uses useAttachmentSave to upload the file and close the plugin via hostApi.close.
    • Renders the UI (editor, viewer, etc.).

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

How to debug.

8. Pre-publication checklist

  • The attachment.viewer.action slot is specified in the manifest with a clear title.
  • slotContext is read in the root component — attachmentBlob and meta are accessible.
  • If the plugin returns a modified file — trackerApi is used for uploading and hostApi.close is used to pass the result to the host.