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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
f3309633b2
Коммит
4e1e7334d8
@@ -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
сгенерированный
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",
|
||||
|
||||
Ссылка в новой задаче
Block a user