Applications can add custom menu items to Space context menus. When a user clicks on a custom item, Space sends a message to the application. The message contains a payload of the MenuActionPayload type.
Where you can add custom menu items
Currently, the context menus of the following Space entities can be extended with custom items:
Issues
Chat messages
Code reviews
Documents
Document folders
Meetings
Add custom menu items
To add a custom menu item, the application must make the setUiExtensions API call. For example, this is how you can add an item to the issue context menu:
// using Kotlin SDK
spaceClient.applications.setUiExtensions(
// add issue menu item globally, for all projects
contextIdentifier = GlobalPermissionContextIdentifier,
// list of extensions: just a single menu item
extensions = listOf(
IssueMenuItemUiExtensionIn(
// item name shown in the context menu
displayName = "Remove assignee",
// menu item description for the app home page
description = "Remove assignee for current issue",
// identify the menu item when it's clicked
menuItemUniqueCode = "remove-assignee",
// show the item only to users who can edit the issue
visibilityFilters = listOf(IssueEditableByMe)
)
)
)
For the list of possible extensions, see the Set UI Extensions method in the API Playground. For the details on how to call the method, refer to the sample application.
The added menu item will be shown on the application's page in Space, namely, on the Authorization tab:
Enable custom menu items for Space users
Right after the application is installed, menu items are enabled only for the application owner (the user who installed the application). Every Space user can enable/disable menu items for themselves on the application's Authorization tab. When creating an application, make sure to provide your users with the instructions on how they can enable the menu items in the required context (we will improve the discoverability of this setting in the future).
Administrators with the System admin role can enable the menu items for all users in their Space organization. To do this, turn on Enabled by default for everybody.
A project or a channel administrator can enable the items for the corresponding project or channel:
In In-Context Authorization, use the Authorize in project or Authorize in channel button to authorize the application in the requried context.
Open project or channel authorization settings and enable the extension feature.
Handle menu item clicks
1. Handle MenuActionPayload
When a user clicks on the custom menu item, Space sends the MenuActionPayload payload to the application. The application must respond with AppUserActionExecutionResult:
AppUserActionExecutionResult.Success – the action was successfully executed.
AppUserActionExecutionResult.Failure – the action failed to execute.
AppUserActionExecutionResult.AuthCodeFlowRequired– the action requires authorization on behalf of the user and the application doesn't yet have an authorization token. See the next step for details.
For example:
fun Routing.routes() {
post("api/myapp") {
// `processPayload` is a helper function that processes payload from Space
// `KtorRequestAdapter` is an instance of `RequestAdapter` that is
// used to read HTTP body and headers from the incoming requests.
//
// `ktorClient` is an HTTP client that will be used by the application
// to make all requests to Space.
//
// `AppInstanceStorage` is an instance of `SpaceAppInstanceStorage` that stores
// data on application's client ID, secret, and URL of the Space instance.
Space.processPayload(KtorRequestAdapter(call), ktorClient, AppInstanceStorage) { payload ->
when (payload) {
// ...
is MenuActionPayload -> {
// If the app performs the action on behalf of itself
// (the action doesn't require user permissions),
// respond with `AppUserActionExecutionResult.Success` or
// `AppUserActionExecutionResult.Failure`. For example:
//
// doSomething()
// SpaceHttpResponse.RespondWithResult(AppUserActionExecutionResult.Success)
// If the app requires user permissions to perform the action
// and doesn't have an authorization token yet,
// respond with `AppUserActionExecutionResult.AuthCodeFlowRequired`
// and require the permissions in this response.
// `result` must return some `AppUserActionExecutionResult`.
// For example, to perform the action on behalf of the user, it must
// return `AppUserActionExecutionResult.AuthCodeFlowRequired`
// with required permissions.
// If the application already has an authorization token, `result` must
// perform the action and return `AppUserActionExecutionResult.Success`.
val result = reactToMenuItem(payload)
SpaceHttpResponse.RespondWithResult(result)
}
// ...
}
}
}
}
If the action is successful, respond with the following JSON payload:
{
"className": "AppUserActionExecutionResult.Success",
"message": "message to the user"
}
In case of failure:
{
"className": "AppUserActionExecutionResult.Failure",
"message": "message to the user"
}
If user permissions are required, the application must respond with AppUserActionExecutionResult.AuthCodeFlowRequired and require the permissions in this response. See the next step for details.
2. Get user consent
At this point, the application can perform an action in Space on behalf of itself or on behalf of a user. To perform an action on behalf of a user, the application needs to get the user's consent first (learn more about permissions). To get the consent, the application must respond to the MenuActionPayload with AppUserActionExecutionResult.AuthCodeFlowRequired:
// this code extends the example from the previous step
suspend fun ProcessingScope.reactToMenuItem(payload: MenuActionPayload): AppUserActionExecutionResult {
// Check if we already have a token for this particular user for this app instance.
// If not, make `permissionRequest` to Space.
//
// The implementation of `findRefreshTokenData` may vary depending on how you want to store token data.
val refreshTokenAndScope = findRefreshTokenData(payload.clientId, payload.userId)
?: return permissionsRequest
// If we have a valid token, make requests to Space.
// If the token is invalid, make `permissionRequest` to Space.
val client = clientWithRefreshToken(refreshTokenAndScope.refreshToken, refreshTokenAndScope.scope)
return try {
doSomethingInSpace(client, payload)
AppUserActionExecutionResult.Success(null)
} catch (e: PermissionDeniedException) {
permissionsRequest
} catch (e: RefreshTokenRevokedException) {
permissionsRequest
}
}
// The `AppUserActionExecutionResult` class lists possible application responses.
// `AuthCodeFlowRequired` is an app response that requests required permissions.
private val permissionsRequest = AppUserActionExecutionResult.AuthCodeFlowRequired(
permissionsToRequest = listOf(
AuthCodeFlowPermissionsRequest(
// define permission scope: operations you need to perform on behalf of the user
scope = PermissionScope.build(
// e.g., the app requires a global permission to view project details
PermissionScopeElement(
GlobalPermissionContextIdentifier,
PermissionIdentifier.ViewProjectDetails
),
// a project permission to view issues (in the MY-PRJ project)
PermissionScopeElement(
ProjectPermissionContextIdentifier(ProjectIdentifier.Key("MY-PRJ")),
PermissionIdentifier.ViewIssues
),
// and a project permission to update issues (in the MY-PRJ project)
PermissionScopeElement(
ProjectPermissionContextIdentifier(ProjectIdentifier.Key("MY-PRJ")),
PermissionIdentifier.UpdateIssues
)
),
purpose = "Don't ask me why, just give me the permissions"
)
)
)
AuthCodeFlowRequired is an application response that requests required permissions. The example below shows the JSON payload in the response that requests the global permission to view project details and project permissions to view and update issues (for the MY-PRJ project):
{
"className": "AppUserActionExecutionResult.AuthCodeFlowRequired",
"permissionsToRequest": [
{
"purpose": "Don't ask me why, just give me the permissions",
"scope": "global:Project.View project:key:MY-PRJ:Project.Issues.View project:key:MY-PRJ:Project.Issues.Update"
}
]
}
After Space receives permission request, it shows the user a consent dialog.
If they agree, Space sends a RefreshTokenPayload to the application. The refresh token allows the application to access Space on behalf of the user with no time limit (or until the user revokes the authorization). To save the refresh token:
// this code extends the Kotlin SDK example from the previous steps
// ...
when (payload) {
// ...
is RefreshTokenPayload -> {
// Save refresh token in a persistent storage.
// You should implement this method by yourself.
saveRefreshTokenData(payload)
// respond with HTTP 200 OK
SpaceHttpResponse.RespondWithOk
}
}
4. Get action context and perform the action
After the application successfully obtains the refresh token (Space receives HTTP 200 OK), Space sends the MenuActionPayload one more time. This is done to free the user from the need to click the menu item for the second time. This time, the application can use the refresh token to perform the required action.
The MenuActionPayload contains the context: an object on which the menu item was invoked. For example, we know that our application extends the issue context menu, so the context must be an IssueMenuActionContext and contain data about the corresponding issue. You can use the context to perform the action, for example, update an issue:
// this code extends the Kotlin SDK example from the previous steps
suspend fun doSomethingInSpace(client: SpaceClient, payload: MenuActionPayload) {
// get the context - issue details
val context = payload.context as IssueMenuActionContext
// update the issue to remove assignee
client.projects.planning.issues.updateIssue(
context.projectIdentifier,
context.issueIdentifier,
assignee = Option.None
)
}