Extension points
Extension points are the places in groupforce's own views where an app can add a component of its own. This page lists them, shows how to declare one, and explains what contract versions mean.
What an extension point is
An extension point is a named place in one of groupforce's own views where an installed, enabled app may put a component of its own. The place stays in control of the view: the app adds a section, and cannot take the view over. The list of places is fixed. An app cannot invent one, and a manifest that names a place that does not exist is refused when it is built or uploaded.
The places you can add to
| Point | Where it appears | The component receives |
|---|---|---|
contact.detail.section |
On a contact's card in Contacts. | { contact } |
work_item.sidebar.panel |
In the sidebar of a work item, drawn as a section like Details. | { work_item } |
work_item.composer.responses |
In the comment box's "Canned responses" launcher of an open work item. A component there offers texts and puts one in the box with the composer.insert action. |
{ work_item } |
work_item.form.section |
In the New item dialog while a work item is being made. A component there sets custom fields for it with the work_item_form.set action. |
{ draft: { type } } |
inbox.record.detail |
Where an inbox notification about a record of your app's own model is read. You must name the model with model. If your app adds something here, it replaces the generic field list groupforce would otherwise show for that record. |
{ record, model } |
inbox.work_item.detail |
Below the built-in view of a work item in the inbox. | { work_item } |
inbox.event.detail |
Below the built-in view of a calendar event. | { event } |
inbox.contact.detail |
Below the built-in view of a contact. | { contact } |
inbox.project.detail |
Below the built-in view of a project. | { project } |
inbox.channel.detail |
Below the built-in view of a chat channel. | { channel } |
inbox.message.detail |
Below the built-in view of a saved mail message. | { message } |
inbox.chat_message.detail |
Below the built-in view of a single chat message. Chat notifications currently refer to the conversation, so use inbox.channel.detail for chat. |
{ message } |
The context is plain data in the shape the API returns the record. The seven built-in inbox.*.detail points are additive: every matching app adds its part below the built-in content, and an app cannot replace a built-in viewer. The only place where an app's view takes over is inbox.record.detail, and only for records of that app's own models.
How to declare one
An extension is two entries in app.json: a component, and an extension that puts the component at a point. From the Hello example that ships with groupforce:
{
"id": "contact-deals",
"title": "Deals of a contact",
"source": "components/contact-deals.tsx",
"props": [
{
"key": "context",
"label": "Context",
"type": "record"
}
]
}
],
"models": [
"extensions": [
{
"point": "contact.detail.section",
"component": "contact-deals",
"title": "Deals",
"records": [
"app:deal"
]
}
]
}
| Field | Rule |
|---|---|
point |
One of the points above. |
component |
The id of a component declared in the same manifest. |
title |
The heading shown above your component. |
model |
Required for inbox.record.detail and one of the app's own model ids. Not allowed for any other point. |
records, actions |
What the component may read and may ask to run, at most 20 of each. They must lie inside the app's permissions, or the package is refused when it is installed. |
The component is written like a view. It is a class that extends Component, built from the widgets in @groupforce/widgets, with no raw page elements, style or className, and it reads its place's record from this.props.context. The head of Hello's contact-deals.tsx:
import { Component } from 'react'
import { Stack, Text, Pill, Row } from '@groupforce/widgets'
import { data } from '@groupforce/data'
// Added to the contact card (extension point contact.detail.section, see app.json): the deals of the contact being looked at. It is
// handed that contact as `context.contact`, reads this app's own Deal model, and shows the deals whose customer is the contact.
export default class ContactDeals extends Component<{ context: any }, { deals: any[]; status: string }> {
state = { deals: [] as any[], status: 'pending' }
…
render() {
const contact = this.props.context?.contact
const mine = this.state.deals.filter((d: any) => d.data?.customer === contact?.id)
Added for the Queues app
The Queues example app uses two points, two actions a component may ask to run, and one widget. A component asks for an action by listing it in its extension's actions.
- work_item.composer.responses and the action
composer.insert(text into the open comment box of an item). The Queues extension and the call:
{
"point": "work_item.composer.responses",
"component": "queue-responses",
"title": "Queue responses",
"records": [
"work_item",
"app:queue",
"app:canned-response",
"person"
],
"actions": [
"composer.insert"
]
},
private put = async (wi: any, text: string) => {
try {
await actions.run('composer.insert', { itemId: wi.id, text })
} catch (e) {
this.setState({ error: (e as Error).message })
}
}
- work_item.form.section and the action
work_item_form.set: custom fields for the item being made, and also aproject_id, apriority(used only if the person left the dialog's own) andlock_type. Queues' own call, fromqueue-form.tsx:actions.run('work_item_form.set', { custom_fields: { [QUEUE]: id || null }, priority: q?.data.default_priority ?? null, project_id: q?.data.project ?? null, lock_type: !!id }). - WorkItemPane, a widget of
@groupforce/widgetsthat a view can place: it draws one work item with its conversation (itemIdis its prop);plainleaves out the sidebar panels apps add.
The actions an app may ask for are fixed by the SDK list: entity.create, entity.update, entity.delete, entity.transition, window.open, composer.insert and work_item_form.set. Views and extensions may name only those, and only if permissions.actions includes them.
What people see
Each extension appears under the title you gave it, followed by the small caption "Added by" and your app's name, so nobody mistakes it for part of the built-in view. It runs in the same browser sandbox as the app's own views, inside your app's stylesheet scope, as the person who is looking, and only with the records and actions you declared. It is started again when the record changes.
If several apps add to the same place, their sections stack. If an app is switched off or uninstalled, its extensions disappear, and a place where nothing was added takes no room. Apps cannot add to the mail composer, the calendar editor or any other place that is not on the list above.
App Studio
An extension component is a normal component of the app, declared under components in the manifest and drawn from a .tsx file. The extension entries themselves (extensions) are written in the manifest. The Studio's parts (see Data, views and the visual editor) edit data, views, processes, automations and server code; none of them is an editor for extensions, so an app that adds to a built-in view declares that in its manifest, as on Your first app in code.
Contract versions
An app declares the lowest extension contract it needs in requires.groupforce. This groupforce offers contract 8. A manifest that needs a higher one is refused, and so is a manifest that uses something newer than the contract it declares. There is no negotiation, so an older groupforce rejects a newer package outright. Declare the lowest number that covers everything the manifest uses.
| Contract | Added |
|---|---|
1 |
Views, components, models, workflows, server code, extensions, styles and permissions. |
2 |
Extra work item and project statuses. |
3 |
The rows field type, layout hints for forms and lists, a view's default window size, and workflow steps. |
4 |
Settings each workspace decides for itself, and the setting: selector in workflows. |
5 |
Office editors offered to Documents. |
6 |
ODT print templates for an app's records. |
7 |
The model-bound blocks a view is built from, and rules on a workflow's steps. |
8 |
subscriptions (events of work items, projects and comments), server.timers, custom_fields (names for the keys an app writes on work items) and editable_by on a model. |
Extension points do not need a contract above 1: the validator only gates the later features in the table. If you are unsure whether your manifest declares enough, run gfpkg build, which reports every manifest problem at once.
Examples to read
groupforce ships small example apps built around extension points. Hello, deals adds a section to the contact card. CRM Companion adds linked deals to inbox views of work items and contacts. Feedback gives its own record type a custom view in the inbox. The Bestellschein (purchase requests) example does the same for its purchase requests and adds an action button for workflow steps. Queues (examples/apps/queues) is the larger one: a work item sidebar section, a canned-responses launcher and a section in the New item dialog, with server code behind them (Work items, events and timers).