Migrate remaining javascript files in webapp/channels/src/plugins to typescript (#29386)

* feat: Migrate actions.js to TypeScript with type annotations

* feat: Migrate actions.js to TypeScript with correct action types

* feat: Add actions plugin for webapp channels

* feat: Migrate actions.js to TypeScript with type annotations

* feat: Add interactive dialog plugin for webapp channels

* feat: Migrate interactive_dialog.js to TypeScript with type definitions

* feat: Add export plugin module for webapp channels

* feat: Migrate export.js to TypeScript with type declarations

* feat: Add initial plugin index file for webapp channels

* feat: migrate plugins/index.js to TypeScript with type definitions

* test: Add export plugin test file

* feat: Migrate export.test.js to TypeScript with type assertions

* feat: Add emoji actions file to webapp channels

* feat: Migrate emoji_actions.js to TypeScript with full type support

* Fixing some issues

* Revert "feat: Migrate emoji_actions.js to TypeScript with full type support"

This reverts commit e64aabe9fc6d36938cbaa40b7acb3356729a6686.

* fixing linter errors

* Fixing CI

* Addressing pr review comments

* Apply suggestions from code review

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* Fixing linter errors

* Fixing CI

---------

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Этот коммит содержится в:
Jesús Espino
2024-12-12 10:03:13 +01:00
коммит произвёл GitHub
родитель f3309633b2
Коммит 4e1e7334d8
8 изменённых файлов: 130 добавлений и 37 удалений

Просмотреть файл

@@ -136,6 +136,7 @@
"@types/react-window": "1.8.5",
"@types/react-window-infinite-loader": "1.0.6",
"@types/redux-mock-store": "1.0.3",
"@types/regenerator-runtime": "0.13.8",
"@types/semver": "7.5.8",
"@types/shallow-equals": "1.0.3",
"@types/styled-components": "5.1.32",

Просмотреть файл

@@ -47,7 +47,7 @@ import RootRedirect from './root_redirect';
import type {PropsFromRedux} from './index';
import 'plugins/export.js';
import 'plugins/export';
const MobileViewWatcher = makeAsyncComponent('MobileViewWatcher', lazy(() => import('components/mobile_view_watcher')));
const WindowSizeObserver = makeAsyncComponent('WindowSizeObserver', lazy(() => import('components/window_size_observer/WindowSizeObserver')));

Просмотреть файл

@@ -1,20 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PluginManifest} from '@mattermost/types/plugins';
import {hideRHSPlugin as hideRHSPluginAction} from 'actions/views/rhs';
import {getPluggableId} from 'selectors/rhs';
import {ActionTypes} from 'utils/constants';
export const removeWebappPlugin = (manifest) => {
import type {GlobalState, ActionFunc} from 'types/store';
export const removeWebappPlugin = (manifest: PluginManifest): ActionFunc<boolean, GlobalState> => {
return (dispatch) => {
dispatch(hideRHSPlugin(manifest.id));
dispatch({type: ActionTypes.REMOVED_WEBAPP_PLUGIN, data: manifest});
return {data: true};
};
};
// hideRHSPlugin closes the RHS if currently showing this plugin.
const hideRHSPlugin = (manifestId) => {
const hideRHSPlugin = (manifestId: string): ActionFunc<boolean, GlobalState> => {
return (dispatch, getState) => {
const state = getState();
const rhsPlugins = state.plugins.components.RightHandSidebarComponent || [];
@@ -25,5 +30,6 @@ const hideRHSPlugin = (manifestId) => {
if (pluginComponent) {
dispatch(hideRHSPluginAction(pluggableId));
}
return {data: true};
};
};

Просмотреть файл

@@ -13,25 +13,25 @@ describe('messageHtmlToComponent wrapper', () => {
const isRHS = false;
test('should call messageHtmlToComponent properly with only message', () => {
window.PostUtils.messageHtmlToComponent(message);
(window as any).PostUtils.messageHtmlToComponent(message);
expect(messageHtmlToComponent).toHaveBeenCalledWith(message, undefined);
});
test('should call messageHtmlToComponent properly with message and options', () => {
window.PostUtils.messageHtmlToComponent(message, options);
(window as any).PostUtils.messageHtmlToComponent(message, options);
expect(messageHtmlToComponent).toHaveBeenCalledWith(message, options);
});
test('should call messageHtmlToComponent properly with only message when deprecated isRHS parameter is passed', () => {
window.PostUtils.messageHtmlToComponent(message, isRHS);
(window as any).PostUtils.messageHtmlToComponent(message, isRHS);
expect(messageHtmlToComponent).toHaveBeenCalledWith(message, undefined);
});
test('should call messageHtmlToComponent properly with message and options when deprecated isRHS parameter is passed', () => {
window.PostUtils.messageHtmlToComponent(message, isRHS, options);
(window as any).PostUtils.messageHtmlToComponent(message, isRHS, options);
expect(messageHtmlToComponent).toHaveBeenCalledWith(message, options);
});

Просмотреть файл

@@ -31,6 +31,63 @@ import {imageURLForUser} from 'utils/utils';
import {openInteractiveDialog} from './interactive_dialog'; // This import has intentional side effects. Do not remove without research.
import Textbox from './textbox';
interface WindowWithLibraries {
React: typeof import('react');
ReactDOM: typeof import('react-dom');
ReactIntl: typeof import('react-intl');
Redux: typeof import('redux');
ReactRedux: typeof import('react-redux');
ReactBootstrap: typeof import('react-bootstrap');
ReactRouterDom: typeof import('react-router-dom');
PropTypes: typeof import('prop-types');
Luxon: typeof import('luxon');
StyledComponents: typeof import('styled-components');
PostUtils: {
formatText: typeof formatText;
messageHtmlToComponent: (html: string, ...args: any[]) => JSX.Element;
};
openInteractiveDialog: typeof openInteractiveDialog;
useNotifyAdmin: typeof useNotifyAdmin;
WebappUtils: {
modals: {
openModal: typeof openModal;
ModalIdentifiers: typeof ModalIdentifiers;
};
notificationSounds: {
ring: typeof NotificationSounds.ring;
stopRing: typeof NotificationSounds.stopRing;
};
sendDesktopNotificationToMe: typeof notifyMe;
openUserSettings: (dialogProps: any) => void;
browserHistory: ReturnType<typeof getHistory>;
};
openPricingModal: () => typeof openPricingModal;
Components: {
Textbox: typeof Textbox;
Timestamp: typeof Timestamp;
ChannelInviteModal: typeof ChannelInviteModal;
ChannelMembersModal: typeof ChannelMembersModal;
Avatar: typeof Avatar;
imageURLForUser: typeof imageURLForUser;
BotBadge: typeof BotTag;
StartTrialFormModal: typeof StartTrialFormModal;
ThreadViewer: typeof ThreadViewer;
PostMessagePreview: typeof PostMessagePreview;
AdvancedTextEditor: typeof AdvancedTextEditor;
};
ProductApi: {
useWebSocket: typeof useWebSocket;
useWebSocketClient: typeof useWebSocketClient;
WebSocketProvider: typeof WebSocketContext;
closeRhs: typeof closeRightHandSide;
selectRhsPost: typeof selectPostById;
getRhsSelectedPostId: typeof getSelectedPostId;
getIsRhsOpen: typeof getIsRhsOpen;
};
DesktopApp: typeof DesktopApp;
}
declare let window: WindowWithLibraries;
// Common libraries exposed on window for plugins to use as Webpack externals.
window.React = require('react');
window.ReactDOM = require('react-dom');
@@ -46,7 +103,7 @@ window.StyledComponents = require('styled-components');
// Functions exposed on window for plugins to use.
window.PostUtils = {
formatText,
messageHtmlToComponent: (html, ...otherArgs) => {
messageHtmlToComponent: (html: string, ...otherArgs: any[]) => {
// Previously, this function took an extra isRHS argument as the second parameter. For backwards compatibility,
// support calling this as either messageHtmlToComponent(html, options) or messageHtmlToComponent(html, isRhs, options)
@@ -63,6 +120,9 @@ window.PostUtils = {
window.openInteractiveDialog = openInteractiveDialog;
window.useNotifyAdmin = useNotifyAdmin;
window.WebappUtils = {
get browserHistory() {
return getHistory();
},
modals: {openModal, ModalIdentifiers},
notificationSounds: {ring: NotificationSounds.ring, stopRing: NotificationSounds.stopRing},
sendDesktopNotificationToMe: notifyMe,
@@ -72,9 +132,6 @@ window.WebappUtils = {
dialogProps,
}),
};
Object.defineProperty(window.WebappUtils, 'browserHistory', {
get: () => getHistory(),
});
// This need to be a function because `openPricingModal`
// is initialized when `UpgradeCloudButton` is loaded.

Просмотреть файл

@@ -3,6 +3,8 @@
import regeneratorRuntime from 'regenerator-runtime';
import type {PluginManifest} from '@mattermost/types/plugins';
import {Client4} from 'mattermost-redux/client';
import {Preferences} from 'mattermost-redux/constants';
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
@@ -18,6 +20,8 @@ import PluginRegistry from 'plugins/registry';
import {ActionTypes} from 'utils/constants';
import {getSiteURL} from 'utils/url';
import type {GlobalState, ActionFuncAsync} from 'types/store';
import {removeWebappPlugin} from './actions';
// Including the fullscreen modal css to make it available to the plugins
@@ -25,6 +29,26 @@ import {removeWebappPlugin} from './actions';
// have all plugins migrated to common components that can be reused there.
import 'components/widgets/modals/full_screen_modal.scss';
interface Plugin {
initialize?: (registry: PluginRegistry, store: any) => void;
uninitialize?: () => void;
/**
* @deprecated Define an uninitialize method instead.
*/
deinitialize?: () => void;
}
interface WindowWithPlugins extends Window {
plugins: {
[key: string]: Plugin;
};
registerPlugin: (id: string, plugin: Plugin) => void;
regeneratorRuntime: typeof regeneratorRuntime;
}
declare let window: WindowWithPlugins;
// Plugins may have been compiled with the regenerator runtime. Ensure this remains available
// as a global export even though the webapp does not depend on same.
window.regeneratorRuntime = regeneratorRuntime;
@@ -37,7 +61,7 @@ window.plugins = {};
//
// During the beta, plugins manipulated the global window.plugins data structure directly. This
// remains possible, but is officially deprecated and may be removed in a future release.
function registerPlugin(id, plugin) {
function registerPlugin(id: string, plugin: Plugin): void {
const oldPlugin = window.plugins[id];
if (oldPlugin && oldPlugin.uninitialize) {
oldPlugin.uninitialize();
@@ -47,7 +71,7 @@ function registerPlugin(id, plugin) {
}
window.registerPlugin = registerPlugin;
function arePluginsEnabled(state) {
function arePluginsEnabled(state: GlobalState): boolean {
if (getConfig(state).PluginsEnabled !== 'true') {
return false;
}
@@ -63,7 +87,7 @@ function arePluginsEnabled(state) {
}
// initializePlugins queries the server for all enabled plugins and loads each in turn.
export async function initializePlugins() {
export async function initializePlugins(): Promise<void> {
if (!arePluginsEnabled(store.getState())) {
return;
}
@@ -78,8 +102,8 @@ export async function initializePlugins() {
return;
}
await Promise.all(data.map((m) => {
return loadPlugin(m).catch((loadErr) => {
await Promise.all(data.map((m: PluginManifest) => {
return loadPlugin(m).catch((loadErr: Error) => {
console.error(loadErr.message); //eslint-disable-line no-console
});
}));
@@ -88,7 +112,7 @@ export async function initializePlugins() {
}
// getPlugins queries the server for all enabled plugins
export function getPlugins() {
export function getPlugins(): ActionFuncAsync {
return async (dispatch) => {
let plugins;
try {
@@ -104,16 +128,16 @@ export function getPlugins() {
}
// loadedPlugins tracks which plugins have been added as script tags to the page
const loadedPlugins = {};
const loadedPlugins: { [key: string]: PluginManifest } = {};
// describePlugin takes a manifest and spits out a string suitable for console.log messages.
const describePlugin = (manifest) => (
const describePlugin = (manifest: PluginManifest): string => (
'plugin ' + manifest.id + ', version ' + manifest.version
);
// loadPlugin fetches the web app bundle described by the given manifest, waits for the bundle to
// load, and then ensures the plugin has been initialized.
export function loadPlugin(manifest) {
export function loadPlugin(manifest: PluginManifest): Promise<void> {
return new Promise((resolve, reject) => {
if (!arePluginsEnabled(store.getState())) {
return;
@@ -121,7 +145,7 @@ export function loadPlugin(manifest) {
// Don't load it again if previously loaded
const oldManifest = loadedPlugins[manifest.id];
if (oldManifest && oldManifest.webapp.bundle_path === manifest.webapp.bundle_path) {
if (oldManifest && oldManifest.webapp?.bundle_path === manifest.webapp?.bundle_path) {
resolve();
return;
}
@@ -142,8 +166,8 @@ export function loadPlugin(manifest) {
}
// Backwards compatibility for old plugins
let bundlePath = manifest.webapp.bundle_path;
if (bundlePath.includes('/static/') && !bundlePath.includes('/static/plugins/')) {
let bundlePath = manifest.webapp?.bundle_path;
if (bundlePath && bundlePath.includes('/static/') && !bundlePath.includes('/static/plugins/')) {
bundlePath = bundlePath.replace('/static/', '/static/plugins/');
}
@@ -164,7 +188,7 @@ export function loadPlugin(manifest) {
// initializePlugin creates a registry specific to the plugin and invokes any initialize function
// on the registered plugin class.
function initializePlugin(manifest) {
function initializePlugin(manifest: PluginManifest): void {
// Initialize the plugin
const plugin = window.plugins[manifest.id];
const registry = new PluginRegistry(manifest.id);
@@ -176,7 +200,7 @@ function initializePlugin(manifest) {
// removePlugin triggers any uninitialize callback on the registered plugin, unregisters any
// event handlers, and removes the plugin script from the DOM entirely. The plugin is responsible
// for removing any of its registered components.
export function removePlugin(manifest) {
export function removePlugin(manifest: PluginManifest): void {
if (!loadedPlugins[manifest.id]) {
return;
}
@@ -190,7 +214,7 @@ export function removePlugin(manifest) {
if (plugin && plugin.uninitialize) {
plugin.uninitialize();
// Support the deprecated deinitialize callback from the plugins beta.
// Support the deprecated deinitialize callback from the plugins beta.
} else if (plugin && plugin.deinitialize) {
plugin.deinitialize();
}
@@ -202,18 +226,18 @@ export function removePlugin(manifest) {
if (!script) {
return;
}
script.parentNode.removeChild(script);
script.parentNode?.removeChild(script);
console.log('Removed ' + describePlugin(manifest)); //eslint-disable-line no-console
}
// loadPluginsIfNecessary synchronizes the current state of loaded plugins with that of the server,
// loading any newly added plugins and unloading any removed ones.
export async function loadPluginsIfNecessary() {
export async function loadPluginsIfNecessary(): Promise<void> {
if (!arePluginsEnabled(store.getState())) {
return;
}
const oldManifests = store.getState().plugins.plugins;
const oldManifests = store.getState().plugins.plugins as { [key: string]: PluginManifest };
const {error} = await store.dispatch(getPlugins());
if (error) {
@@ -221,20 +245,20 @@ export async function loadPluginsIfNecessary() {
return;
}
const newManifests = store.getState().plugins.plugins;
const newManifests = store.getState().plugins.plugins as { [key: string]: PluginManifest };
// Get new plugins and update existing plugins if version changed
Object.values(newManifests).forEach((newManifest) => {
Object.values(newManifests).forEach((newManifest: PluginManifest) => {
const oldManifest = oldManifests[newManifest.id];
if (!oldManifest || oldManifest.version !== newManifest.version) {
loadPlugin(newManifest).catch((loadErr) => {
loadPlugin(newManifest).catch((loadErr: Error) => {
console.error(loadErr.message); //eslint-disable-line no-console
});
}
});
// Remove old plugins
Object.keys(oldManifests).forEach((id) => {
Object.keys(oldManifests).forEach((id: string) => {
if (!Object.hasOwn(newManifests, id)) {
const oldManifest = oldManifests[id];
removePlugin(oldManifest);

Просмотреть файл

@@ -1,9 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
IntegrationTypes,
} from 'mattermost-redux/action_types';
import {IntegrationTypes} from 'mattermost-redux/action_types';
import {openModal} from 'actions/views/modals';
import store from 'stores/redux_store';
@@ -12,7 +10,7 @@ import InteractiveDialog from 'components/interactive_dialog';
import {ModalIdentifiers} from 'utils/constants';
export function openInteractiveDialog(dialog) {
export function openInteractiveDialog(dialog: any): void {
store.dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: dialog});
store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: InteractiveDialog}));

7
webapp/package-lock.json сгенерированный
Просмотреть файл

@@ -187,6 +187,7 @@
"@types/react-window": "1.8.5",
"@types/react-window-infinite-loader": "1.0.6",
"@types/redux-mock-store": "1.0.3",
"@types/regenerator-runtime": "0.13.8",
"@types/semver": "7.5.8",
"@types/shallow-equals": "1.0.3",
"@types/styled-components": "5.1.32",
@@ -7147,6 +7148,12 @@
"redux": "^4.0.5"
}
},
"node_modules/@types/regenerator-runtime": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/@types/regenerator-runtime/-/regenerator-runtime-0.13.8.tgz",
"integrity": "sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==",
"dev": true
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",