From a629ce3a53066399b9e8623b713bd3cb242fd5d5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 Apr 2023 12:55:29 +0100 Subject: [PATCH] Use generics to better type TabbedView (#10726) --- src/TextForEvent.tsx | 4 +- src/components/structures/TabbedView.tsx | 30 +++++++------- .../views/context_menus/RoomContextMenu.tsx | 4 +- src/components/views/dialogs/InviteDialog.tsx | 2 +- .../views/dialogs/RoomSettingsDialog.tsx | 40 ++++++++++--------- .../views/dialogs/SpacePreferencesDialog.tsx | 2 +- .../views/dialogs/SpaceSettingsDialog.tsx | 2 +- .../views/dialogs/UserSettingsDialog.tsx | 6 +-- .../elements/DesktopCapturerSourcePicker.tsx | 6 ++- src/components/views/rooms/NewRoomIntro.tsx | 4 +- .../views/settings/JoinRuleSettings.tsx | 4 +- .../components/structures/TabbedView-test.tsx | 4 +- 12 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 790ce2571b..c8aa99385d 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -32,7 +32,7 @@ import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore"; import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import defaultDispatcher from "./dispatcher/dispatcher"; import { MatrixClientPeg } from "./MatrixClientPeg"; -import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; +import { RoomSettingsTab } from "./components/views/dialogs/RoomSettingsDialog"; import AccessibleButton, { ButtonEvent } from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; @@ -236,7 +236,7 @@ function textForTombstoneEvent(ev: MatrixEvent): (() => string) | null { const onViewJoinRuleSettingsClick = (): void => { defaultDispatcher.dispatch({ action: "open_room_settings", - initial_tab_id: ROOM_SECURITY_TAB, + initial_tab_id: RoomSettingsTab.Security, }); }; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 1efada7143..5e79ffc1f3 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -29,7 +29,7 @@ import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibil /** * Represents a tab for the TabbedView. */ -export class Tab { +export class Tab { /** * Creates a new tab. * @param {string} id The tab's ID. @@ -39,7 +39,7 @@ export class Tab { * @param {string} screenName The screen name to report to Posthog. */ public constructor( - public readonly id: string, + public readonly id: T, public readonly label: string, public readonly icon: string | null, public readonly body: React.ReactNode, @@ -52,20 +52,20 @@ export enum TabLocation { TOP = "top", } -interface IProps { - tabs: NonEmptyArray; - initialTabId?: string; +interface IProps { + tabs: NonEmptyArray>; + initialTabId?: T; tabLocation: TabLocation; - onChange?: (tabId: string) => void; + onChange?: (tabId: T) => void; screenName?: ScreenName; } -interface IState { - activeTabId: string; +interface IState { + activeTabId: T; } -export default class TabbedView extends React.Component { - public constructor(props: IProps) { +export default class TabbedView extends React.Component, IState> { + public constructor(props: IProps) { super(props); const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); @@ -78,7 +78,7 @@ export default class TabbedView extends React.Component { tabLocation: TabLocation.LEFT, }; - private getTabById(id: string): Tab | undefined { + private getTabById(id: T): Tab | undefined { return this.props.tabs.find((tab) => tab.id === id); } @@ -87,7 +87,7 @@ export default class TabbedView extends React.Component { * @param {Tab} tab the tab to show * @private */ - private setActiveTab(tab: Tab): void { + private setActiveTab(tab: Tab): void { // make sure this tab is still in available tabs if (!!this.getTabById(tab.id)) { if (this.props.onChange) this.props.onChange(tab.id); @@ -97,7 +97,7 @@ export default class TabbedView extends React.Component { } } - private renderTabLabel(tab: Tab): JSX.Element { + private renderTabLabel(tab: Tab): JSX.Element { const isActive = this.state.activeTabId === tab.id; const classes = classNames("mx_TabbedView_tabLabel", { mx_TabbedView_tabLabel_active: isActive, @@ -130,11 +130,11 @@ export default class TabbedView extends React.Component { ); } - private getTabId(tab: Tab): string { + private getTabId(tab: Tab): string { return `mx_tabpanel_${tab.id}`; } - private renderTabPanel(tab: Tab): React.ReactNode { + private renderTabPanel(tab: Tab): React.ReactNode { const id = this.getTabId(tab); return (
diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 4a01503496..a3e56c79e5 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -38,7 +38,7 @@ import ExportDialog from "../dialogs/ExportDialog"; import { useFeatureEnabled } from "../../../hooks/useSettings"; import { usePinnedEvents } from "../right_panel/PinnedMessagesCard"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog"; +import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import DMRoomMap from "../../../utils/DMRoomMap"; @@ -199,7 +199,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { dis.dispatch({ action: "open_room_settings", room_id: room.roomId, - initial_tab_id: ROOM_NOTIFICATIONS_TAB, + initial_tab_id: RoomSettingsTab.Notifications, }); onFinished(); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index f71e455e22..cf71fde5e6 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1494,7 +1494,7 @@ export default class InviteDialog extends React.PureComponent = [ + const tabs: NonEmptyArray> = [ new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection), ]; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index c070292cfa..2c4a9745b5 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -40,14 +40,16 @@ import { NonEmptyArray } from "../../../@types/common"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import ErrorBoundary from "../elements/ErrorBoundary"; -export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; -export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB"; -export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; -export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; -export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; -export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; -export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; -export const ROOM_POLL_HISTORY_TAB = "ROOM_POLL_HISTORY_TAB"; +export const enum RoomSettingsTab { + General = "ROOM_GENERAL_TAB", + Voip = "ROOM_VOIP_TAB", + Security = "ROOM_SECURITY_TAB", + Roles = "ROOM_ROLES_TAB", + Notifications = "ROOM_NOTIFICATIONS_TAB", + Bridges = "ROOM_BRIDGES_TAB", + Advanced = "ROOM_ADVANCED_TAB", + PollHistory = "ROOM_POLL_HISTORY_TAB", +} interface IProps { roomId: string; @@ -118,12 +120,12 @@ class RoomSettingsDialog extends React.Component { this.forceUpdate(); }; - private getTabs(): NonEmptyArray { - const tabs: Tab[] = []; + private getTabs(): NonEmptyArray> { + const tabs: Tab[] = []; tabs.push( new Tab( - ROOM_GENERAL_TAB, + RoomSettingsTab.General, _td("General"), "mx_RoomSettingsDialog_settingsIcon", , @@ -133,7 +135,7 @@ class RoomSettingsDialog extends React.Component { if (SettingsStore.getValue("feature_group_calls")) { tabs.push( new Tab( - ROOM_VOIP_TAB, + RoomSettingsTab.Voip, _td("Voice & Video"), "mx_RoomSettingsDialog_voiceIcon", , @@ -142,7 +144,7 @@ class RoomSettingsDialog extends React.Component { } tabs.push( new Tab( - ROOM_SECURITY_TAB, + RoomSettingsTab.Security, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", this.props.onFinished(true)} />, @@ -151,7 +153,7 @@ class RoomSettingsDialog extends React.Component { ); tabs.push( new Tab( - ROOM_ROLES_TAB, + RoomSettingsTab.Roles, _td("Roles & Permissions"), "mx_RoomSettingsDialog_rolesIcon", , @@ -160,7 +162,7 @@ class RoomSettingsDialog extends React.Component { ); tabs.push( new Tab( - ROOM_NOTIFICATIONS_TAB, + RoomSettingsTab.Notifications, _td("Notifications"), "mx_RoomSettingsDialog_notificationsIcon", ( @@ -176,7 +178,7 @@ class RoomSettingsDialog extends React.Component { if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( new Tab( - ROOM_BRIDGES_TAB, + RoomSettingsTab.Bridges, _td("Bridges"), "mx_RoomSettingsDialog_bridgesIcon", , @@ -187,7 +189,7 @@ class RoomSettingsDialog extends React.Component { tabs.push( new Tab( - ROOM_POLL_HISTORY_TAB, + RoomSettingsTab.PollHistory, _td("Poll history"), "mx_RoomSettingsDialog_pollsIcon", this.props.onFinished(true)} />, @@ -197,7 +199,7 @@ class RoomSettingsDialog extends React.Component { if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { tabs.push( new Tab( - ROOM_ADVANCED_TAB, + RoomSettingsTab.Advanced, _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", ( @@ -211,7 +213,7 @@ class RoomSettingsDialog extends React.Component { ); } - return tabs as NonEmptyArray; + return tabs as NonEmptyArray>; } public render(): React.ReactNode { diff --git a/src/components/views/dialogs/SpacePreferencesDialog.tsx b/src/components/views/dialogs/SpacePreferencesDialog.tsx index 3042405cef..0963a53118 100644 --- a/src/components/views/dialogs/SpacePreferencesDialog.tsx +++ b/src/components/views/dialogs/SpacePreferencesDialog.tsx @@ -70,7 +70,7 @@ const SpacePreferencesAppearanceTab: React.FC> = ({ space }; const SpacePreferencesDialog: React.FC = ({ space, initialTabId, onFinished }) => { - const tabs: NonEmptyArray = [ + const tabs: NonEmptyArray> = [ new Tab( SpacePreferenceTab.Appearance, _td("Appearance"), diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index 8cf8e8418b..8683a43f43 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -80,7 +80,7 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin , ) : null, - ].filter(Boolean) as NonEmptyArray; + ].filter(Boolean) as NonEmptyArray>; }, [cli, space, onFinished]); return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index f8cbae2e9f..5f534bba1f 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -81,8 +81,8 @@ export default class UserSettingsDialog extends React.Component this.setState({ newSessionManagerEnabled: newValue }); }; - private getTabs(): NonEmptyArray { - const tabs: Tab[] = []; + private getTabs(): NonEmptyArray> { + const tabs: Tab[] = []; tabs.push( new Tab( @@ -208,7 +208,7 @@ export default class UserSettingsDialog extends React.Component ), ); - return tabs as NonEmptyArray; + return tabs as NonEmptyArray>; } public render(): React.ReactNode { diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index b34f951af3..20bbc729ad 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -85,6 +85,8 @@ export interface PickerIProps { onFinished(sourceId?: string): void; } +type TabId = "screen" | "window"; + export default class DesktopCapturerSourcePicker extends React.Component { public interval: number; @@ -134,7 +136,7 @@ export default class DesktopCapturerSourcePicker extends React.Component { const sources = this.state.sources .filter((source) => source.id.startsWith(type)) .map((source) => { @@ -152,7 +154,7 @@ export default class DesktopCapturerSourcePicker extends React.Component = [ + const tabs: NonEmptyArray> = [ this.getTab("screen", _t("Share entire screen")), this.getTab("window", _t("Application window")), ]; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 8e51784293..b01a703683 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -33,7 +33,7 @@ import { Action } from "../../../dispatcher/actions"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import { showSpaceInvite } from "../../../utils/space"; import EventTileBubble from "../messages/EventTileBubble"; -import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; +import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; @@ -268,7 +268,7 @@ const NewRoomIntro: React.FC = () => { event.preventDefault(); defaultDispatcher.dispatch({ action: "open_room_settings", - initial_tab_id: ROOM_SECURITY_TAB, + initial_tab_id: RoomSettingsTab.Security, }); } diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index 5706aa4dfa..d478639dc0 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -31,7 +31,7 @@ import { upgradeRoom } from "../../../utils/RoomUpgrade"; import { arrayHasDiff } from "../../../utils/arrays"; import { useLocalEcho } from "../../../hooks/useLocalEcho"; import dis from "../../../dispatcher/dispatcher"; -import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; +import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions"; @@ -320,7 +320,7 @@ const JoinRuleSettings: React.FC = ({ // open new settings on this tab dis.dispatch({ action: "open_room_settings", - initial_tab_id: ROOM_SECURITY_TAB, + initial_tab_id: RoomSettingsTab.Security, }); }, }); diff --git a/test/components/structures/TabbedView-test.tsx b/test/components/structures/TabbedView-test.tsx index 922c7ccc3f..70e1cf6c9a 100644 --- a/test/components/structures/TabbedView-test.tsx +++ b/test/components/structures/TabbedView-test.tsx @@ -26,11 +26,11 @@ describe("", () => { const securityTab = new Tab("SECURITY", "Security", "security",
security
); const defaultProps = { tabLocation: TabLocation.LEFT, - tabs: [generalTab, labsTab, securityTab] as NonEmptyArray, + tabs: [generalTab, labsTab, securityTab] as NonEmptyArray>, }; const getComponent = (props = {}): React.ReactElement => ; - const getTabTestId = (tab: Tab): string => `settings-tab-${tab.id}`; + const getTabTestId = (tab: Tab): string => `settings-tab-${tab.id}`; const getActiveTab = (container: HTMLElement): Element | undefined => container.getElementsByClassName("mx_TabbedView_tabLabel_active")[0]; const getActiveTabBody = (container: HTMLElement): Element | undefined =>