From b82d856d1cbe91d74c7c8fe26e3ca5869c5e6e3a Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Fri, 13 Mar 2026 10:50:03 -0400 Subject: [PATCH 1/4] Add channel settings tab smoke test and fix GetTeams resilience Register a ChannelSettingsTab pluggable for smoke-testing the new channel settings tab API. The component renders channel info, a dirty-state input, and a tab-switch error banner. Make GetTeams() calls non-fatal in ensureDemoUser, ensureDemoChannels, OnActivate, and OnDeactivate so the plugin can start even when the Teams table has NULL string columns (core server bug). Co-Authored-By: Claude Opus 4.6 (1M context) --- server/activate_hooks.go | 52 +++++++++---------- server/configuration.go | 17 +++--- .../channel_settings_smoke_test.jsx | 52 +++++++++++++++++++ webapp/src/plugin.jsx | 7 +++ 4 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 webapp/src/components/channel_settings_smoke_test.jsx diff --git a/server/activate_hooks.go b/server/activate_hooks.go index 35f3ec6..6fabe6e 100644 --- a/server/activate_hooks.go +++ b/server/activate_hooks.go @@ -38,19 +38,19 @@ func (p *Plugin) OnActivate() error { teams, err := p.API.GetTeams() if err != nil { - return errors.Wrap(err, "failed to query teams OnActivate") - } - - for _, team := range teams { - _, ok := configuration.demoChannelIDs[team.Id] - if !ok { - p.API.LogWarn("No demo channel id for team", "team", team.Id) - continue - } - - msg := fmt.Sprintf("OnActivate: %s", manifest.Id) - if err := p.postPluginMessage(team.Id, msg); err != nil { - return errors.Wrap(err, "failed to post OnActivate message") + p.API.LogWarn("Failed to query teams OnActivate, skipping activation messages", "error", err.Error()) + } else { + for _, team := range teams { + _, ok := configuration.demoChannelIDs[team.Id] + if !ok { + p.API.LogWarn("No demo channel id for team", "team", team.Id) + continue + } + + msg := fmt.Sprintf("OnActivate: %s", manifest.Id) + if err := p.postPluginMessage(team.Id, msg); err != nil { + p.API.LogWarn("Failed to post OnActivate message", "error", err.Error()) + } } } @@ -83,19 +83,19 @@ func (p *Plugin) OnDeactivate() error { teams, err := p.API.GetTeams() if err != nil { - return errors.Wrap(err, "failed to query teams OnDeactivate") - } - - for _, team := range teams { - _, ok := configuration.demoChannelIDs[team.Id] - if !ok { - p.API.LogWarn("No demo channel id for team", "team", team.Id) - continue - } - - msg := fmt.Sprintf("OnDeactivate: %s", manifest.Id) - if err := p.postPluginMessage(team.Id, msg); err != nil { - return errors.Wrap(err, "failed to post OnDeactivate message") + p.API.LogWarn("Failed to query teams OnDeactivate, skipping deactivation messages", "error", err.Error()) + } else { + for _, team := range teams { + _, ok := configuration.demoChannelIDs[team.Id] + if !ok { + p.API.LogWarn("No demo channel id for team", "team", team.Id) + continue + } + + msg := fmt.Sprintf("OnDeactivate: %s", manifest.Id) + if err := p.postPluginMessage(team.Id, msg); err != nil { + p.API.LogWarn("Failed to post OnDeactivate message", "error", err.Error()) + } } } diff --git a/server/configuration.go b/server/configuration.go index e0dac42..60a059f 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -401,13 +401,13 @@ func (p *Plugin) ensureDemoUser(configuration *configuration) (string, error) { teams, err := p.API.GetTeams() if err != nil { - return "", err - } - - for _, team := range teams { - _, err := p.API.CreateTeamMember(team.Id, user.Id) - if err != nil { - p.API.LogError("Failed add demo user to team", "teamID", team.Id, "error", err.Error()) + p.API.LogWarn("Failed to get teams for demo user setup, skipping team membership", "error", err.Error()) + } else { + for _, team := range teams { + _, err := p.API.CreateTeamMember(team.Id, user.Id) + if err != nil { + p.API.LogError("Failed add demo user to team", "teamID", team.Id, "error", err.Error()) + } } } @@ -417,7 +417,8 @@ func (p *Plugin) ensureDemoUser(configuration *configuration) (string, error) { func (p *Plugin) ensureDemoChannels(configuration *configuration) (map[string]string, error) { teams, err := p.API.GetTeams() if err != nil { - return nil, err + p.API.LogWarn("Failed to get teams for demo channel setup, skipping channel creation", "error", err.Error()) + return make(map[string]string), nil } demoChannelIDs := make(map[string]string) diff --git a/webapp/src/components/channel_settings_smoke_test.jsx b/webapp/src/components/channel_settings_smoke_test.jsx new file mode 100644 index 0000000..c23fe6c --- /dev/null +++ b/webapp/src/components/channel_settings_smoke_test.jsx @@ -0,0 +1,52 @@ +import React, {useCallback, useState} from 'react'; +import PropTypes from 'prop-types'; + +export default function ChannelSettingsSmokeTest({channel, setAreThereUnsavedChanges, showTabSwitchError}) { + const [value, setValue] = useState(''); + + const handleChange = useCallback((e) => { + const newValue = e.target.value; + setValue(newValue); + setAreThereUnsavedChanges?.(newValue.length > 0); + }, [setAreThereUnsavedChanges]); + + return ( +
+

{'Channel Settings Smoke Test'}

+
+ {'Display Name: '}{channel.display_name} +
+
+ {'Channel Name: '}{channel.name} +
+
+ {'Channel ID: '}{channel.id} +
+
+ +
+ +
+ {showTabSwitchError && ( +
+ {'You have unsaved changes. Please save or discard before switching tabs.'} +
+ )} +
+ ); +} + +ChannelSettingsSmokeTest.propTypes = { + channel: PropTypes.object.isRequired, + setAreThereUnsavedChanges: PropTypes.func, + showTabSwitchError: PropTypes.bool, +}; diff --git a/webapp/src/plugin.jsx b/webapp/src/plugin.jsx index 5be61d2..a4c812a 100644 --- a/webapp/src/plugin.jsx +++ b/webapp/src/plugin.jsx @@ -18,6 +18,7 @@ import RHSView from './components/right_hand_sidebar'; import SecretMessageSetting from './components/admin_settings/secret_message_setting'; import CustomSetting from './components/admin_settings/custom_setting'; import FilePreviewOverride from './components/file_preview_override'; +import ChannelSettingsSmokeTest from './components/channel_settings_smoke_test'; import RouterShowcase from './components/router_showcase/router_showcase'; import PostType from './components/post_type'; import EphemeralPostType from './components/ephemeral_post_type'; @@ -50,6 +51,12 @@ function getTranslations(locale) { export default class DemoPlugin { initialize(registry, store) { + registry.registerChannelSettingsTab?.({ + uiName: 'Smoke Test', + component: ChannelSettingsSmokeTest, + shouldRender: () => true, + }); + registry.registerRootComponent(Root); registry.registerPopoverUserAttributesComponent(UserAttributes); registry.registerPopoverUserActionsComponent(UserActions); From 47d11b0cbc91045f43f6ef7d2731524ac2e39902 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Fri, 13 Mar 2026 10:54:52 -0400 Subject: [PATCH 2/4] Rename channel settings tab from 'Smoke Test' to 'Demo Plugin' Co-Authored-By: Claude Opus 4.6 (1M context) --- webapp/src/plugin.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/plugin.jsx b/webapp/src/plugin.jsx index a4c812a..d0a65fc 100644 --- a/webapp/src/plugin.jsx +++ b/webapp/src/plugin.jsx @@ -52,7 +52,7 @@ function getTranslations(locale) { export default class DemoPlugin { initialize(registry, store) { registry.registerChannelSettingsTab?.({ - uiName: 'Smoke Test', + uiName: 'Demo Plugin', component: ChannelSettingsSmokeTest, shouldRender: () => true, }); From 2b819ee11488ce3e045f13d1ad7d32ca85d90b16 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 25 Mar 2026 14:22:40 -0400 Subject: [PATCH 3/4] Channel settings tab: host save bar API and fa-plug sidebar icon - Register save/reset via registerSaveBarHandlers; remove inline tab-switch banner - Set icon to fa fa-plug to match other demo plugin surfaces (MainMenu, channel header, etc.) Made-with: Cursor --- .../channel_settings_smoke_test.jsx | 34 ++++++++++++++----- webapp/src/plugin.jsx | 1 + 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/channel_settings_smoke_test.jsx b/webapp/src/components/channel_settings_smoke_test.jsx index c23fe6c..e317c2f 100644 --- a/webapp/src/components/channel_settings_smoke_test.jsx +++ b/webapp/src/components/channel_settings_smoke_test.jsx @@ -1,9 +1,32 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import PropTypes from 'prop-types'; -export default function ChannelSettingsSmokeTest({channel, setAreThereUnsavedChanges, showTabSwitchError}) { +export default function ChannelSettingsSmokeTest({channel, setAreThereUnsavedChanges, registerSaveBarHandlers}) { const [value, setValue] = useState(''); + const handleSave = useCallback(async () => { + // Smoke test: no server persistence; clearing dirty state matches a successful save. + setAreThereUnsavedChanges?.(false); + }, [setAreThereUnsavedChanges]); + + const handleReset = useCallback(() => { + setValue(''); + setAreThereUnsavedChanges?.(false); + }, [setAreThereUnsavedChanges]); + + /* eslint-disable consistent-return -- useEffect may return cleanup or nothing */ + useEffect(() => { + if (!registerSaveBarHandlers) { + return; + } + registerSaveBarHandlers({ + save: handleSave, + reset: handleReset, + }); + return () => registerSaveBarHandlers(null); + }, [registerSaveBarHandlers, handleSave, handleReset]); + /* eslint-enable consistent-return */ + const handleChange = useCallback((e) => { const newValue = e.target.value; setValue(newValue); @@ -36,11 +59,6 @@ export default function ChannelSettingsSmokeTest({channel, setAreThereUnsavedCha style={{marginTop: '4px', padding: '6px', width: '300px'}} /> - {showTabSwitchError && ( -
- {'You have unsaved changes. Please save or discard before switching tabs.'} -
- )} ); } @@ -48,5 +66,5 @@ export default function ChannelSettingsSmokeTest({channel, setAreThereUnsavedCha ChannelSettingsSmokeTest.propTypes = { channel: PropTypes.object.isRequired, setAreThereUnsavedChanges: PropTypes.func, - showTabSwitchError: PropTypes.bool, + registerSaveBarHandlers: PropTypes.func, }; diff --git a/webapp/src/plugin.jsx b/webapp/src/plugin.jsx index d0a65fc..878e68b 100644 --- a/webapp/src/plugin.jsx +++ b/webapp/src/plugin.jsx @@ -54,6 +54,7 @@ export default class DemoPlugin { registry.registerChannelSettingsTab?.({ uiName: 'Demo Plugin', component: ChannelSettingsSmokeTest, + icon: 'fa fa-plug', shouldRender: () => true, }); From 072824553a073a647e12e1b8855fb5d738b7bb6c Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 25 Mar 2026 14:24:47 -0400 Subject: [PATCH 4/4] Use plugin public icon for channel settings tab (match user settings) Made-with: Cursor --- webapp/src/plugin.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/plugin.jsx b/webapp/src/plugin.jsx index 878e68b..f4c36aa 100644 --- a/webapp/src/plugin.jsx +++ b/webapp/src/plugin.jsx @@ -54,7 +54,7 @@ export default class DemoPlugin { registry.registerChannelSettingsTab?.({ uiName: 'Demo Plugin', component: ChannelSettingsSmokeTest, - icon: 'fa fa-plug', + icon: `/plugins/${manifest.id}/public/icon.png`, shouldRender: () => true, });