Pisanie aplikacji (wtyczek)

Aplikacje dodają funkcje do Antimatter od wewnątrz: część serwerowa w Go, która podpinana jest do serwera, oraz część aplikacji webowej w React, która dodaje menu, panele, typy wiadomości i strony. Ta strona pokazuje, jak wygląda struktura aplikacji, używając jako przykładu dołączonej aplikacji Ankiety, oraz jakie zmiany wprowadza interfejs Fusion w Antimatter.

Jak działa aplikacja

  • Część serwerowa to program w Go. Serwer uruchamia go jako osobny proces i komunikuje się z nim przez RPC: wywołuje haki aplikacji (np. wiadomość ma zostać opublikowana, wykonano polecenie…), a aplikacja wywołuje API wtyczki serwera (np. tworzenie posta, odczyt użytkownika, przechowywanie danych…). Może również obsługiwać trasy HTTP pod /plugins/<app id>/.
  • Część aplikacji webowej to pakiet JavaScript ładowany do przeglądarki wraz z aplikacją webową. Rejestruje komponenty React i wywołania zwrotne w rejestrze: przycisk w menu, panel, sposób rysowania typu wiadomości.
  • Każda z części jest opcjonalna. Aplikacja jest instalowana jako pakiet .tar.gz, a administratorzy systemu mogą ją włączać, wyłączać i konfigurować.
Część serwerowa działa z pełnym dostępem do danych serwera. Traktuj aplikację jak kod wdrażany na serwer.

Antimatter uruchamia aplikacje napisane dla Mattermost i odwrotnie, o ile aplikacja używa wyłącznie haków wspólnych dla obu systemów. Zobacz Haki dostępne tylko w Antimatter.

Struktura pakietu

Nie ma osobnej szablony startowej: zacznij od małej dołączonej aplikacji, takiej jak antimatter-plugin-polls lub antimatter-plugin-todo, i zmień jej identyfikator oraz kod. Aplikacja Ankiety ma następującą strukturę:

antimatter-plugin-polls/
├── plugin.json            the manifest
├── Makefile               build, test, bundle, deploy
├── build/                 the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg        files shipped with the bundle
├── go.mod
├── server/
│   ├── main.go            plugin.ClientMain(&Plugin{})
│   ├── plugin.go          the Plugin type, OnActivate, ServeHTTP
│   ├── manifest.go        generated from plugin.json
│   ├── api.go, command.go, hooks.go, store.go …
│   └── dist/              compiled binaries, one per platform
└── webapp/
    ├── package.json, webpack.config.js, tsconfig.json
    ├── i18n/              en.json and one file per language
    ├── src/index.tsx      registers the app with the web app
    ├── src/manifest.ts    generated from plugin.json
    ├── src/translations.ts
    └── dist/main.js       the bundle

Opcjonalnie: folder public/, serwowany bez zmian pod /plugins/<app id>/public/.

plugin.json

{
  "id": "com.example.weather",
  "name": "Weather",
  "description": "Forecasts in the chat.",
  "homepage_url": "https://github.com/example/weather",
  "support_url": "https://github.com/example/weather/issues",
  "icon_path": "assets/icon.svg",
  "version": "1.0.0",
  "min_server_version": "12.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "linux-arm64": "server/dist/plugin-linux-arm64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "darwin-arm64": "server/dist/plugin-darwin-arm64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    }
  },
  "webapp": {"bundle_path": "webapp/dist/main.js"},
  "settings_schema": {"settings": []}
}
  • id: unikalny i stały; zwykle ma postać odwróconej nazwy domeny. Określa nazwy tras, przechowywanych ustawień oraz zdarzeń WebSocket.
  • version: wersja semantyczna. Dołączone aplikacje pomijają ją w pliku plugin.json; ich narzędzie budowania pobiera ją z najnowszej etykiety git v*.
  • min_server_version: Antimatter zgłasza 12.0.0.
  • settings_schema: strona ustawień aplikacji w Konsoli systemu (patrz Ustawienia).

make apply kopiuje manifest do server/manifest.go i webapp/src/manifest.ts; make manifest-check go waliduje.

Część serwerowa

Część serwerowa importuje ten sam moduł Go co aplikacje Mattermost, github.com/mattermost/mattermost/server/public, z proxy modułów Go. Jej main tylko uruchamia aplikację:

package main

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
	plugin.ClientMain(&Plugin{})
}

Typ Plugin osadza plugin.MattermostPlugin, co daje mu p.API, i implementuje potrzebne mu haki:

type Plugin struct {
	plugin.MattermostPlugin
	router *mux.Router
}

func (p *Plugin) OnActivate() error {
	p.router = p.newRouter()
	return p.API.RegisterCommand(&model.Command{
		Trigger:          "weather",
		AutoComplete:     true,
		AutoCompleteDesc: "Show the forecast",
		AutoCompleteHint: "[city]",
	})
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
	return &model.CommandResponse{
		ResponseType: model.CommandResponseTypeEphemeral,
		Text:         "Forecast for " + city + ": sunny",
	}, nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	p.router.ServeHTTP(w, r)
}

Wspólne haki

HakKiedy się uruchamia
OnActivate, OnDeactivateGdy aplikacja jest włączana lub wyłączana, a także gdy serwer się uruchamia i zatrzymuje. Rejestruj polecenia i boty w OnActivate.
OnConfigurationChangeGdy zmieniają się jej ustawienia; wczytaj je za pomocą p.API.LoadPluginConfiguration(&cfg).
ServeHTTPŻądania do /plugins/<app id>/…. Serwer ustawia nagłówek Mattermost-User-Id, gdy żądanie pochodzi od zalogowanego użytkownika; odrzucaj żądania bez niego, chyba że mają być publiczne.
ExecuteCommandGdy wykonano polecenie zarejestrowane przez aplikację.
MessageWillBePosted, MessageWillBeUpdatedZanim wiadomość zostanie zapisana: zwróć zmienioną wiadomość lub powód jej odrzucenia.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedPo fakcie, w celu wykonania działań ubocznych.
ReactionHasBeenAdded, ReactionHasBeenRemovedReakcje.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedZmiany członkostwa i kanałów. Zespół to społeczność.
FileWillBeUploadedZanim plik zostanie zapisany: zmień go lub go odrzuć.
OnPluginClusterEventWiadomość z tej samej aplikacji z innego serwera klastra.

Pełna lista, z dokładnymi sygnaturami, znajduje się w server/public/plugin/hooks.go; API jest w server/public/plugin/api.go. Pakiet pluginapi (server/public/pluginapi) opakowuje API w wygodniejsze pomocnicze funkcje.

Przydatne wywołania API

  • CreatePost, UpdatePost, SendEphemeralPost: publikuj jako bot aplikacji lub jako użytkownik.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): utwórz bota aplikacji raz i uzyskaj jego identyfikator.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): wyślij zdarzenie do aplikacji webowej. Dociera jako custom_<app id>_poll_updated.
  • OpenInteractiveDialog: otwiera okno dialogowe.
  • LogInfo, LogError i podobne: zapisuj do dziennika serwera, z oznaczeniem identyfikatorem aplikacji.
  • GetUser, GetChannel, HasPermissionToChannel: sprawdzaj, kto może co zrobić, zanim wykonasz akcję w imieniu użytkownika.

Aplikacja działa na każdym serwerze klastra. Przechowuj stan w magazynie kluczo-wartości lub w bazie danych, a nie tylko w pamięci.

Usługa AI

Serwer Antimatter wywołuje modele AI dla aplikacji za pośrednictwem połączeń z modelami skonfigurowanych przez administratora (patrz AI w przewodniku dla administratorów). Aplikacja nigdy nie przechowuje klucza dostawcy: podaje osobę, w imieniu której działa, oraz miejsce, a serwer sprawdza, czy ta osoba może użyć połączenia w tym miejscu, stosuje jego limity i budżety, rozlicza tokeny na osobę i aplikację oraz audytuje wywołanie bez jego zawartości.

MetodaOpis
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)Ukończenie czatu: wiadomości, narzędzia, strukturalna odpowiedź (JSONSchema).
AIChatStream(…)To samo, ale w strumieniu: odczytuj Events strumienia, aż kanał się zamknie, albo zamknij go za pomocą Close, aby zatrzymać wywołanie.
AIEmbed(…, *ai.EmbedRequest)Wektoryzacja (embeddings) tekstów.
AITranscribe(…, *ai.TranscribeRequest)Rozpoznawanie mowy (speech to text).
AIDecide(…, *ai.DecideRequest)Zadawanie typowanych pytań do modelu decyzyjnego (tak lub nie, wybór, ocena) dotyczących tekstu.
AIConnectionsForUser(userID, channelID, teamID)Połączenia, których dana osoba może tam użyć, wraz z ich modelami i możliwościami, nigdy z kluczami.

ConnectionID w każdym żądaniu wybiera połączenie, a Model jeden z jego modeli (puste dla modelu domyślnego połączenia). Operation to krótka etykieta dla rekordów użycia i audytu, nigdy zawartość. Minimalna wersja serwera to 11.11.

Pobieraj actingUserID wyłącznie ze źródła zweryfikowanego przez serwer: nagłówka Mattermost-User-Id żądania do Twojego ServeHTTP lub użytkownika z kontekstu hooka (autora wiadomości, użytkownika, który wykonał polecenie). Nigdy nie pobieraj go z wiadomości, obciążnika webhooka, adresu URL ani żądania innej aplikacji: Twoja aplikacja zużyłaby dostęp kogoś innego.

Te metody i pakiet ai (server/public/ai) istnieją wyłącznie w kopii modułu Antimatter, a nie w wersji opublikowanej przez Mattermost. Aby ich użyć, kompiluj w oparciu o server/public Antimatter, dodając linię replace w swoim pliku go.mod, na przykład replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Taka aplikacja działa wyłącznie na Antimatter.

Przechowywanie danych

Każda aplikacja ma własny magazyn kluczo-wartości w bazie danych serwera:

  • KVSet(key, value), KVGet(key), KVDelete(key): wartości to bajty, zwykle JSON. Klucze mają maksymalnie 150 znaków.
  • KVSetWithExpiry(key, value, seconds): wartość znika po upływie tego czasu.
  • KVCompareAndSet(key, old, new) oraz KVSetWithOptions z opcją Atomic: bezpieczne aktualizacje, gdy wiele żądań lub serwerów zapisuje ten sam klucz. Ankieta zapisuje w ten sposób każdy głos i ponawia próbę w przypadku konfliktu.
  • KVList(page, perPage): klucze, strona po stronie.

Część aplikacji webowej

Pakiet rejestruje klasę za pomocą window.registerPlugin. Jej metoda initialize otrzymuje rejestr oraz magazyn Redux:

import manifest from './manifest';

class Plugin {
    initialize(registry, store) {
        registry.registerTranslations((locale) => translationsFor(locale));
        registry.registerPostTypeComponent('custom_poll', PollPost);
        registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
            store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
        });
        registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
    }
}

window.registerPlugin(manifest.id, new Plugin());
  • Biblioteki współdzielone. Aplikacja webowa udostępnia React, ReactDOM, Redux, React Redux i React Intl: wymień je jako externals w webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl), zamiast pakować własne kopie. Zobacz webpack.config.js w Ankiecie.
  • Wywoływanie części serwerowej. Używaj ${window.basename}/plugins/<app id>/… z opcjami z Client4.getOptions() (z mattermost-redux/client): zawierają one nagłówki, których serwer potrzebuje, aby zaakceptować żądanie z przeglądarki.
  • Własny stan. registerReducer dodaje reducer, przechowywany pod state['plugins-<app id>'].
  • Typy. Dodatkowe metody rejestru Antimatter nie są zawarte w opublikowanych typach Mattermost: Ankieta deklaruje używany rejestr w webapp/src/types/host.ts.

Metody rejestru i ich parametry są udokumentowane w kodzie, w webapp/fusion/src/plugins/registry.ts (Fusion) oraz webapp/channels/src/plugins/registry.ts (klasyczny).

Obsługa interfejsu Fusion

Antimatter ma dwa interfejsy webowe: Fusion (domyślny) i klasyczny. Oba ładują te same pakiety aplikacji i mają ten sam rejestr, ale Fusion rysuje wiele elementów samodzielnie i umieszcza komponenty pluginów na własny sposób. Przetestuj aplikację w obu interfejsach.

Wykrywanie interfejsu

Zanim załaduje się jakakolwiek aplikacja, oba interfejsy ustawiają window.antimatterWebUI na 'fusion' lub 'classic', a tę samą wartość na elemencie głównym strony jako data-am-web-ui. W Mattermost żadna z nich nie jest ustawiona.

export function isFusionUI(): boolean {
    return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}

Zainstalowane aplikacje używają tego mechanizmu, aby renderować interfejs w inny sposób. Ankieta rysuje swoje karty ankiety oraz okno dialogowe tworzenia w stylu Fusion i otwiera własne okno dialogowe dla polecenia /poll w Fusion. Rozmowy i kanały głosowe pomijają pasek boczny oraz komponenty toast, które Fusion zastępuje własnym interfejsem rozmów. Aplikacja Agent umieszcza swoje akcje AI w menu wiadomości, jako grupę pod Więcej Akcji, zamiast na pasku narzędzi pojawiającym się po najechaniu. Własne klasy CSS Fusion (z prefiksem am-) oraz jej warstwa okien dialogowych (element am-layer) to elementy, których te aplikacje używają, aby dopasować się do interfejsu; nie stanowią one udokumentowanego, stabilnego API.

Gdzie Fusion wyświetla komponenty wtyczek

Metoda rejestruGdzie pojawia się w Fusion
registerAppBarComponent, registerChannelHeaderButtonActionPasek aplikacji, kolumna ikon na prawej krawędzi, między Twoim awatarem a przyciskiem O Antimatter. Przyciski nagłówka kanału również umieszczane są w tym miejscu, a nie w nagłówku kanału. Ukryty, gdy administrator systemu włączy Wyłącz pasek aplikacji (ExperimentalSettings.DisableAppBar, domyślnie wyłączony).
registerRightHandSidebarComponentPrawy panel. Użytkownicy mogą zmieniać jego rozmiar, przeciągając jego krawędź, oraz otworzyć go w nowym oknie. Jeśli aplikacja jest również produktem, panel oferuje opcję Otwórz na całej stronie. Zobacz Panel w nowym oknie.
registerChannelHeaderIconIkony w nagłówku kanału, po przyciskach rozmów; na telefonach w menu Więcej nagłówka.
registerCallButtonActionPrzycisk rozmowy w nagłówku kanału; kilku dostawców udostępnia menu Inne opcje rozmowy.
registerPostDropdownMenuActionMenu Wiadomości akcji (⋯) wiadomości, w podmenu Więcej Akcji; również w menu Akcje wątku panelu wątku, pod Więcej Akcji. Fusion rysuje ikonę aplikacji przed każdą akcją, w kolorze szarym: własną ikonę dla zainstalowanych aplikacji, w przeciwnym razie ikonę, którą aplikacja wyświetla na pasku aplikacji, a jeśli nie ma takiej, to ikonę neutralną. Element bez tekstu na początku opisu akcji (ikona rysowana samodzielnie przez aplikację) jest ukrywany.
registerPostDropdownSubMenuActionGrupa z nagłówkiem w tym samym podmenu Więcej Akcji, po pojedynczych akcjach. Żadna aplikacja nie otrzymuje własnego wpisu w menu wiadomości.
registerPostActionComponentPasek narzędzi pojawiający się po najechaniu na wiadomość, przed przyciskiem ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentSama wiadomość: jej treść, pod jej tekstem, po czasie, oraz w miejscu podglądów linków.
registerChannelHeaderMenuActionMenu Opcje kanału (⋯ na kanale w pasku bocznym lub kliknięcie prawym przyciskiem myszy), jako elementy z ikoną wtyczki.
registerMainMenuActionMenu społeczności (nazwa społeczności u góry listy kanałów), pod Więcej akcji, pogrupowane według aplikacji.
registerSidebarBrowseOrAddChannelMenuActionMenu społeczności, po Utwórz kanał i Utwórz kategorię.
registerFileUploadMethodMenu + kompozytora (Dodaj do wiadomości), po Prześlij plik.
registerPostEditorActionComponentPasek formatowania kompozytora, gdy formatowanie jest wyświetlane.
registerAIActionMenuItemComponentMenu AI kompozytora.
registerFileDropdownMenuAction, registerFilePreviewComponentMenu opcji pliku oraz podgląd pliku.
registerUserSettingsProfil i ustawienia, jedna zakładka na aplikację pod nagłówkiem Wtyczki.
registerChannelSettingsTabOkno dialogowe ustawień kanału, pod nagłówkiem Aplikacje.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentKarta profilu wyświetlana po kliknięciu nazwy lub awatara.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentGórna część listy kanałów przestrzeni roboczej, obok nazwy kanału, ikony kanału oraz dolna część paska bocznego po lewej stronie.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderPoczątek historii kanału, górna część listy wiadomości, linia nieprzeczytanych, nad polem tworzenia wiadomości oraz placeholder w tym polu.
registerChannelTypeOptionTypy kanałów w oknie dialogowym Utwórz kanał.
registerSearchComponentsPole wyszukiwania w nagłówku kanału: zakresy aplikacji pojawiają się, gdy jest puste.
registerUserGuideDropdownMenuActionO Antimatter (na dole paska aplikacji), pod Pomoc.
registerProductPełne strony pod adresem URL produktu, wymienione pod Produkty w sekcji O Antimatter. Strony produktów zachowują klasyczny szablon strony, którego menu produktów pojawia się tylko wtedy, gdy aplikacja zarejestruje produkt lub doda do niego wpis. Zainstalowane aplikacje Kanban i Playbooks nie rejestrują już produktów: ich strony otwierają się w panelach bocznych.
registerRootComponent, registerGlobalComponent, haki takie jak registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsDziałają tak jak w klasycznym interfejsie.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingKonsola systemu, która jest klasyczna w obu interfejsach.

Czego Fusion nie wyświetla ani nie uruchamia

  • registerPostDropdownMenuComponent (komponent w menu wiadomości), registerPostCardTypeComponent (karta wiadomości w panelu po prawej) oraz registerProductSwitcherMenuItem nie są wyświetlane w głównych widokach Fusion. Zamiast pierwszego użyj registerPostDropdownMenuAction.
  • registerMessageWillBeUpdatedHook nie uruchamia się, gdy wiadomość jest edytowana w Fusion. Sprawdź edycje na serwerze za pomocą haka MessageWillBeUpdated.
  • registerActionAfterChannelCreation uruchamia się tylko z klasycznego okna dialogowego tworzenia kanału, które Fusion otwiera z opcji Więcej opcji… w swoim oknie dialogowym Utwórz kanał.
  • Komponenty paska aplikacji są wyświetlane niezależnie od wartości supportedProductIds.
  • Trasy z registerNeedsTeamRoute i registerCustomRoute zachowują klasyczny układ strony, podobnie jak produkty.

Haki dostępne tylko w Antimatter

Te metody rejestru nie istnieją w Mattermost. Sprawdź, czy metoda istnieje, przed jej wywołaniem, aby aplikacja działała również tam:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
MetodaCo dodaje
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})Pozycja w menu + w polu wiadomości, w obu interfejsach. action otrzymuje {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})Zakładka w oknie wyboru emoji w polu wiadomości, na przykład GIF-y lub naklejki. W Fusion zakładki z wartością order poniżej 10 pojawiają się przed zakładką Emoji. Komponent otrzymuje channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion oraz, w Fusion, pickerKit (patrz niżej).
registerChannelViewPanel({matcher, component})Panel umieszczony między nagłówkiem kanału a wiadomościami, dla kanałów wybranych przez matcher(state, channel). Może ukryć wiadomości i pole kompozycji (setMessagesVisible), tak jak to robią kanały głosowe.
registerSidebarChannelFooterComponent(component)Wiersz pod kanałem w pasku bocznym, np. osoby uczestniczące w jego rozmowie. Otrzymuje channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Tylko Fusion: wpis na stronach Integracje, obok niestandardowych emoji. Aplikacja GIF dodaje tam swoje niestandardowe GIF-y i naklejki.

Dodawanie i zarządzanie widokami w oknie wyboru emoji

W Fusion tab wyboru emoji otrzymuje prop pickerKit: komponenty, których tab Emoji używa do własnych widoków dodawania i zarządzania, dzięki czemu widoki danej karty wyglądają tak samo i pozostają wewnątrz okna wyboru. Aplikacje GIF i Memes używają go do dodawania i zarządzania GIF-ami, naklejkami i szablonami memów. Zawiera version (1), haki usePickerView i useOnEscape oraz komponenty PickerViewButtons (przyciski + i zarządzania obok pola wyszukiwania), PickerViewFrame (widok ze strzałką wsteczną), PickerNotice, PickerField, PickerSearch, PickerDropZone i PickerManageList. Klawisz Escape cofa z widoku do siatki karty. Sprawdź, czy prop jest obecny: inne hosty go pomijają, a wtedy karta używa własnych okien dialogowych. Typy znajdują się w webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps), a komponenty w webapp/fusion/src/fusion/popovers/picker_views.tsx.

Panel w nowym oknie

Fusion przekazuje komponentowi z registerRightHandSidebarComponent dwa dodatkowe propy, dzięki czemu opcja Otwórz w nowym oknie otwiera panel w miejscu, w którym się znajdował, a nie na stronie głównej aplikacji:

  • setPanelState(state): wywołaj tę funkcję, gdy panel zmienia miejsce, przekazując krótki ciąg znaków zdefiniowany przez aplikację, np. 'note/<id>' lub 'board/<id>/card/<id>', albo null w przypadku strony głównej. Stan trafia do adresu okna, więc nigdy nie umieszczaj w nim danych poufnych. Stany dłuższe niż 512 znaków lub zawierające znaki sterujące są odrzucane.
  • initialPanelState: w nowym oknie jest to stan, który miał panel, lub undefined. Traktuj go jako niezaufane dane wejściowe: pobierz element ponownie jako zalogowana osoba i wyświetl stronę główną aplikacji lub widok „nie znaleziono”, jeśli nie ma do niego dostępu.

Stan jest zapomniany, gdy panel się zamyka lub panel innej aplikacji zajmuje jego miejsce. Aby samodzielnie otworzyć okno panelu, użyj popoutRhsPlugin (dostępnego również w window.WebappUtils.popouts), przekazując stan jako opcjonalny piąty argument. Z tej funkcji korzystają Notatki, Biała tablica, Do zrobienia, Kanban i Playbooki.

fusionIcon określa nazwę ikony z zestawu ikon Fusion (np. poll, sticker, calendar lub image; patrz webapp/fusion/src/fusion/components/icon). Fusion wyświetla ją zamiast icon, aby element był spójny z pozostałymi.

Ustawienia

settings_schema w plugin.json opisuje stronę aplikacji w Konsola systemu › Wtyczki:

"settings_schema": {
  "header": "Forecasts come from the weather service below.",
  "settings": [
    {"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
    {"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
     "options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
    {"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
     "help_text": "Post alerts in the channels that follow a city."}
  ]
}

Typy ustawień: bool, text, longtext, number, dropdown, radio, generated (losowa wartość tajna), username oraz custom (rysowane przez komponent zarejestrowany przez część aplikacji webowej za pomocą registerAdminConsoleCustomSetting). Ustawienia można grupować w sections.

Część serwerowa odczytuje je do struktury, której pola odpowiadają kluczom:

type configuration struct {
	ServiceURL   string
	Units        string
	EnableAlerts bool
}

func (p *Plugin) OnConfigurationChange() error {
	var cfg configuration
	if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
		return err
	}
	p.setConfiguration(&cfg)
	return nil
}

Administratorzy mogą je również ustawić w konfiguracji, pod PluginSettings.Plugins.<app id>, z kluczami zapisanymi małymi literami (patrz przewodnik administratora).

Tłumaczenia

Antimatter jest tłumaczony na 37 języków, a także na języki używane przez dołączone aplikacje. Część aplikacji webowej robi to w następujący sposób:

  1. Zapisz każdy ciąg za pomocą React Intl, z identyfikatorem i jego tekstem po angielsku: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> lub defineMessages. Prefiksuj identyfikatory nazwą swojej aplikacji.
  2. Wyodrębnij angielskie ciągi do webapp/i18n/en.json (dołączone aplikacje używają formatjs extract).
  3. Dodaj jeden plik dla każdego języka obok niego, z tymi samymi identyfikatorami: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Przekaż je aplikacji webowej, która zapyta o język użytkownika:
    import fr from '../i18n/fr.json';
    import ptBR from '../i18n/pt-BR.json';
    
    const catalogs = {fr, 'pt-BR': ptBR};
    
    export function translationsFor(locale) {
        return catalogs[locale] || catalogs[locale.split('-')[0]] || {};
    }
    
    registry.registerTranslations(translationsFor);

Brakujący ciąg jest zwracany jako jego angielski defaultMessage. W przypadku ciągów wysyłanych przez część serwerową (odpowiedzi na polecenia, wiadomości botów), pakiet pluginapi/i18n ładuje pliki tłumaczeń dołączone do pakietu (na przykład assets/i18n) i udostępnia lokalizator w języku każdego użytkownika.

Kompilacja, instalacja i debugowanie

Wymagane są Go i Node.js (wersja z pliku .nvmrc aplikacji). W folderze aplikacji:

PolecenieOpis
make distKompiluje część serwerową dla każdej platformy oraz część aplikacji webowej i zapisuje pakiet do dist/<app id>-<version>.tar.gz.
make testUruchamia testy Go i JavaScript.
make check-styleSprawdza kod (golangci-lint, ESLint) i typy TypeScript.
make deployKompiluje, a następnie instaluje i włącza pakiet na serwerze.
make watch, make deploy-from-watchPierwsze polecenie kompiluje część aplikacji webowej za każdym razem, gdy zapiszesz zmiany; drugie tworzy pakiet z wygenerowanych plików i instaluje go na serwerze.
make enable, make disable, make resetWłącza, wyłącza lub ponownie uruchamia aplikację na serwerze.
make logs, make logs-watchWyświetla wpisy aplikacji w dzienniku serwera.
make attachDołącza debugger Delve do działającej części serwerowej (najpierw skompiluj z AM_DEBUG=1).

make deploy i inne polecenia serwerowe używają lokalnego gniazda trybu lokalnego, jeśli je znajdą (/var/tmp/antimatter_local.socket lub AM_LOCALSOCKETPATH): uruchom je na maszynie serwera z włączonym trybem lokalnym. W przeciwnym razie logują się na serwerze określonym przez MM_SERVICESETTINGS_SITEURL za pomocą AM_ADMIN_TOKEN lub AM_ADMIN_USERNAME i AM_ADMIN_PASSWORD. Narzędzia do kompilacji nie odczytują jeszcze nazwy AM_ dla adresu URL witryny.

Ręczna instalacja pakietu

  • Administrator systemu umożliwia przesyłanie plików za pomocą AM_PLUGINSETTINGS_ENABLEUPLOADS=true (domyślnie wyłączona; nie ma przełącznika w Konsoli systemu).
  • Następnie Konsola systemu › Wtyczki › Zarządzanie wtyczkami › Wyślij Wtyczkę, albo amctl plugin add weather.tar.gz, a następnie amctl plugin enable com.example.weather.
  • Jeśli na serwerze włączona jest opcja Wymagaj sygnatury wtyczki, akceptowane są tylko pakiety podpisane kluczem, któremu serwer ufa; zob. Instalowanie innych aplikacji.
Do szybkiej iteracji na serwerze deweloperskim ustaw AM_SERVICESETTINGS_ENABLEDEVELOPER=true na serwerze: błędy będą wtedy zawierać szczegóły. Makefile'y dołączonych aplikacji kompilują część serwerową tylko dla Twojej platformy, gdy w powłoce ustawione jest MM_SERVICESETTINGS_ENABLEDEVELOPER.