From 7d043b350434795b3869f9433dc203e4247445ec Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 14 May 2024 15:15:54 -0700 Subject: [PATCH 01/46] Refactor: Rename CustomButton -> IconButton --- .eslintignore | 2 +- .gitignore | 2 +- packages/app-mobile/components/Icon.tsx | 8 +- .../{CustomButton.tsx => IconButton.tsx} | 140 +++++++++--------- .../MarkdownToolbar/ToggleOverflowButton.tsx | 2 +- .../MarkdownToolbar/ToggleSpaceButton.tsx | 17 +-- .../MarkdownToolbar/ToolbarButton.tsx | 15 +- .../buttons/useActionButtons.ts | 4 +- .../components/NoteEditor/SearchPanel.tsx | 26 ++-- .../components/ScreenHeader/index.tsx | 80 +++++----- packages/tools/cspell/dictionary4.txt | 1 + 11 files changed, 147 insertions(+), 150 deletions(-) rename packages/app-mobile/components/{CustomButton.tsx => IconButton.tsx} (56%) diff --git a/.eslintignore b/.eslintignore index 3f9c753e354..3bfe95b0b72 100644 --- a/.eslintignore +++ b/.eslintignore @@ -510,13 +510,13 @@ packages/app-mobile/commands/util/goToNote.js packages/app-mobile/components/ActionButton.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/CameraView.js -packages/app-mobile/components/CustomButton.js packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js packages/app-mobile/components/FolderPicker.js packages/app-mobile/components/Icon.js +packages/app-mobile/components/IconButton.js packages/app-mobile/components/Modal.js packages/app-mobile/components/ModalDialog.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js diff --git a/.gitignore b/.gitignore index a173867befd..187a395b4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -489,13 +489,13 @@ packages/app-mobile/commands/util/goToNote.js packages/app-mobile/components/ActionButton.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/CameraView.js -packages/app-mobile/components/CustomButton.js packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js packages/app-mobile/components/FolderPicker.js packages/app-mobile/components/Icon.js +packages/app-mobile/components/IconButton.js packages/app-mobile/components/Modal.js packages/app-mobile/components/ModalDialog.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js diff --git a/packages/app-mobile/components/Icon.tsx b/packages/app-mobile/components/Icon.tsx index a074623e454..1f9ca927b7d 100644 --- a/packages/app-mobile/components/Icon.tsx +++ b/packages/app-mobile/components/Icon.tsx @@ -4,8 +4,8 @@ import { TextStyle, Text } from 'react-native'; const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default; const AntIcon = require('react-native-vector-icons/AntDesign').default; -const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default; - +const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default; +const Ionicon = require('react-native-vector-icons/Ionicons').default; interface Props { name: string; @@ -53,7 +53,9 @@ const Icon: React.FC = props => { } else if (namePrefix === 'ant') { return ; } else if (namePrefix === 'material') { - return ; + return ; + } else if (namePrefix === 'ionicon') { + return ; } else if (namePrefix === 'text') { return ( void; interface ButtonProps { onPress: ButtonClickListener; // Accessibility label and text shown in a tooltip - description?: string; + description: string; - children: ReactNode; + iconName: string; + iconStyle: TextStyle; themeId: number; - style?: ViewStyle; - pressedStyle?: ViewStyle; - contentStyle?: ViewStyle; + containerStyle?: ViewStyle; + contentWrapperStyle?: ViewStyle; // Additional accessibility information. See View.accessibilityHint accessibilityHint?: string; @@ -35,7 +35,7 @@ interface ButtonProps { disabled?: boolean; } -const CustomButton = (props: ButtonProps) => { +const IconButton = (props: ButtonProps) => { const [tooltipVisible, setTooltipVisible] = useState(false); const [buttonLayout, setButtonLayout] = useState(null); const tooltipStyles = useTooltipStyles(props.themeId); @@ -67,19 +67,6 @@ const CustomButton = (props: ButtonProps) => { setTooltipVisible(true); }, []); - // Select different user-specified styles if selected/unselected. - const onStyleChange = useCallback((state: PressableStateCallbackType): StyleProp => { - let result = { ...props.style }; - - if (state.pressed) { - result = { - ...result, - ...props.pressedStyle, - }; - } - return result; - }, [props.pressedStyle, props.style]); - const onButtonLayout = useCallback((event: LayoutChangeEvent) => { const layoutEvt = event.nativeEvent.layout; @@ -87,7 +74,6 @@ const CustomButton = (props: ButtonProps) => { setButtonLayout({ ...layoutEvt }); }, []); - const button = ( { onPressIn={onPressIn} onPressOut={onPressOut} - style={ onStyleChange } + style={ props.containerStyle } disabled={ props.disabled ?? false } onLayout={ onButtonLayout } @@ -107,62 +93,70 @@ const CustomButton = (props: ButtonProps) => { > - { props.children } + ); - const tooltip = ( - - - - - - {props.description} - - - - - ); + const renderTooltip = () => { + if (!props.description) return null; + + return ( + + + + + + {props.description} + + + + + ); + }; return ( <> - {props.description ? tooltip : null} + {renderTooltip()} {button} ); @@ -187,4 +181,4 @@ const useTooltipStyles = (themeId: number) => { }, [themeId]); }; -export default CustomButton; +export default IconButton; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx index b528dbb342b..4f7b6afba1e 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx @@ -14,7 +14,7 @@ interface ToggleOverflowButtonProps { // Button that shows/hides the overflow menu. const ToggleOverflowButton: React.FC = (props: ToggleOverflowButtonProps) => { const spec: ButtonSpec = { - icon: 'material more-horiz', + icon: 'material dots-horizontal', description: props.overflowVisible ? _('Hide more actions') : _('Show more actions'), active: props.overflowVisible, diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.tsx index e1f316fd42f..7a486a241cd 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.tsx @@ -14,9 +14,7 @@ import { Theme } from '@joplin/lib/themes/type'; import * as React from 'react'; import { ReactNode, useCallback, useState, useEffect } from 'react'; import { View, ViewStyle } from 'react-native'; -import CustomButton from '../../CustomButton'; - -const AntIcon = require('react-native-vector-icons/AntDesign').default; +import IconButton from '../../IconButton'; interface Props { children: ReactNode; @@ -54,10 +52,10 @@ const ToggleSpaceButton = (props: Props) => { height: additionalPositiveSpace, zIndex: -2, }} /> - { alignItems: 'center', }} onPress={onDecreaseSpace} - > - - + + iconName='material chevron-down' + iconStyle={{ color: theme.color }} + /> ); diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx index 17272890b9c..3ec396d25f4 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { useCallback, useMemo } from 'react'; import { TextStyle, StyleSheet } from 'react-native'; import { ButtonSpec, StyleSheetData } from './types'; -import CustomButton from '../../CustomButton'; -import Icon from '../../Icon'; +import IconButton from '../../IconButton'; export const buttonSize = 54; @@ -58,16 +57,16 @@ const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarBut }, [disabled, sourceOnPress, onActionComplete]); return ( - - - + + iconName={spec.icon} + iconStyle={styles.iconStyle} + /> ); }; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts index 47d7afaa568..330c62434e6 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts @@ -51,7 +51,7 @@ const useActionButtons = (props: ActionButtonRowProps) => { }); actionButtons.push({ - icon: 'material search', + icon: 'material magnify', description: ( props.searchState.dialogVisible ? _('Close') : _('Find and replace') ), @@ -63,7 +63,7 @@ const useActionButtons = (props: ActionButtonRowProps) => { }); actionButtons.push({ - icon: 'material keyboard-hide', + icon: 'material keyboard-close', description: _('Hide keyboard'), disabled: !props.keyboardVisible, visible: props.hasSoftwareKeyboard && Platform.OS === 'ios', diff --git a/packages/app-mobile/components/NoteEditor/SearchPanel.tsx b/packages/app-mobile/components/NoteEditor/SearchPanel.tsx index ddbb9bf21b6..e6b4a22b9ec 100644 --- a/packages/app-mobile/components/NoteEditor/SearchPanel.tsx +++ b/packages/app-mobile/components/NoteEditor/SearchPanel.tsx @@ -2,13 +2,12 @@ const React = require('react'); const { useMemo, useState, useEffect } = require('react'); -const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default; import { EditorSettings } from './types'; import { _ } from '@joplin/lib/locale'; import { BackHandler, TextInput, View, Text, StyleSheet, ViewStyle } from 'react-native'; import { Theme } from '@joplin/lib/themes/type'; -import CustomButton from '../CustomButton'; +import IconButton from '../IconButton'; import { SearchState } from '@joplin/editor/types'; import { SearchControl } from './types'; @@ -43,14 +42,14 @@ interface ActionButtonProps { const ActionButton = (props: ActionButtonProps) => { return ( - - - + iconName={`material ${props.iconName}`} + iconStyle={props.styles.buttonText} + /> ); }; @@ -68,9 +67,9 @@ const ToggleButton = (props: ToggleButtonProps) => { const active = props.active; return ( - { }} description={props.title} accessibilityRole='switch' - > - - + } + /> ); }; diff --git a/packages/app-mobile/components/ScreenHeader/index.tsx b/packages/app-mobile/components/ScreenHeader/index.tsx index b6132d0015a..58a6820fe66 100644 --- a/packages/app-mobile/components/ScreenHeader/index.tsx +++ b/packages/app-mobile/components/ScreenHeader/index.tsx @@ -15,7 +15,7 @@ const { dialogs } = require('../../utils/dialogs.js'); const DialogBox = require('react-native-dialogbox').default; import { FolderEntity } from '@joplin/lib/services/database/types'; import { State } from '@joplin/lib/reducer'; -import CustomButton from '../CustomButton'; +import IconButton from '../IconButton'; import FolderPicker from '../FolderPicker'; import { itemIsInTrash } from '@joplin/lib/services/trash'; import restoreItems from '@joplin/lib/services/trash/restoreItems'; @@ -363,20 +363,19 @@ class ScreenHeaderComponent extends PureComponent { if (!options.visible) return null; - const icon = ; const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton; return ( - - {icon} - + iconName={options.iconName} + iconStyle={this.styles().topIcon} + /> ); }; @@ -402,30 +401,32 @@ class ScreenHeaderComponent extends PureComponent - - + contentWrapperStyle={styles.iconButton} + + iconName="ionicon checkmark-circle-outline" + iconStyle={styles.topIcon} + /> ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function searchButton(styles: any, onPress: OnPressCallback) { return ( - - - + contentWrapperStyle={styles.iconButton} + + iconName='ionicon search' + iconStyle={styles.topIcon} + /> ); } @@ -438,21 +439,22 @@ class ScreenHeaderComponent extends PureComponent - - + contentWrapperStyle={styles.iconButton} + + iconName="extension-puzzle" + iconStyle={styles.topIcon} + /> ); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function deleteButton(styles: any, onPress: OnPressCallback, disabled: boolean) { return ( - - - + contentWrapperStyle={disabled ? styles.iconButtonDisabled : styles.iconButton} + + iconName='ionicon trash' + iconStyle={styles.topIcon} + /> ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function restoreButton(styles: any, onPress: OnPressCallback, disabled: boolean) { return ( - - - + contentWrapperStyle={disabled ? styles.iconButtonDisabled : styles.iconButton} + + iconName='ionicon reload-circle' + iconStyle={styles.topIcon} + /> ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function duplicateButton(styles: any, onPress: OnPressCallback, disabled: boolean) { return ( - - - + contentWrapperStyle={disabled ? styles.iconButtonDisabled : styles.iconButton} + iconName='ionicon copy' + iconStyle={styles.topIcon} + /> ); } diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 33ee97b0699..933b559e51f 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -108,3 +108,4 @@ libasound libatk ENOTFOUND Scaleway +Ionicon From 6c9f8cb0a6b30e43d60dfba6cc92f2afe4b0e149 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Wed, 15 May 2024 22:44:55 -0700 Subject: [PATCH 02/46] Make card and button styles closer to the original design --- .eslintignore | 2 + .gitignore | 2 + .../components/ScreenHeader/index.tsx | 2 +- .../components/buttons/TextButton.tsx | 81 +++++++++++++++++++ .../app-mobile/components/buttons/index.tsx | 14 ++++ .../plugins/EnablePluginSupportPage.tsx | 20 +---- .../plugins/PluginBox/ActionButton.tsx | 8 +- .../ConfigScreen/plugins/PluginBox/index.tsx | 8 +- packages/app-mobile/root.tsx | 2 + 9 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 packages/app-mobile/components/buttons/TextButton.tsx create mode 100644 packages/app-mobile/components/buttons/index.tsx diff --git a/.eslintignore b/.eslintignore index 3bfe95b0b72..00ebe26e0e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -584,6 +584,8 @@ packages/app-mobile/components/base-screen.js packages/app-mobile/components/biometrics/BiometricPopup.js packages/app-mobile/components/biometrics/biometricAuthenticate.js packages/app-mobile/components/biometrics/sensorInfo.js +packages/app-mobile/components/buttons/TextButton.js +packages/app-mobile/components/buttons/index.js packages/app-mobile/components/getResponsiveValue.test.js packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/global-style.js diff --git a/.gitignore b/.gitignore index 187a395b4e3..7d30ba20c51 100644 --- a/.gitignore +++ b/.gitignore @@ -563,6 +563,8 @@ packages/app-mobile/components/base-screen.js packages/app-mobile/components/biometrics/BiometricPopup.js packages/app-mobile/components/biometrics/biometricAuthenticate.js packages/app-mobile/components/biometrics/sensorInfo.js +packages/app-mobile/components/buttons/TextButton.js +packages/app-mobile/components/buttons/index.js packages/app-mobile/components/getResponsiveValue.test.js packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/global-style.js diff --git a/packages/app-mobile/components/ScreenHeader/index.tsx b/packages/app-mobile/components/ScreenHeader/index.tsx index 58a6820fe66..a768408fcef 100644 --- a/packages/app-mobile/components/ScreenHeader/index.tsx +++ b/packages/app-mobile/components/ScreenHeader/index.tsx @@ -445,7 +445,7 @@ class ScreenHeaderComponent extends PureComponent ); diff --git a/packages/app-mobile/components/buttons/TextButton.tsx b/packages/app-mobile/components/buttons/TextButton.tsx new file mode 100644 index 00000000000..0475f4c636f --- /dev/null +++ b/packages/app-mobile/components/buttons/TextButton.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { ReactNode, useMemo } from 'react'; +import { StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { themeStyle } from '../global-style'; +import { Button, ButtonProps } from 'react-native-paper'; +import { connect } from 'react-redux'; +import { AppState } from '../../utils/types'; + +export enum ButtonType { + Primary, + Secondary, + Link, +} + +export interface Props extends Omit { + themeId: number; + type: ButtonType; + onPress: ()=> void; + children: ReactNode; + loading?: boolean; +} + +const useStyles = (themeId: number, styleOverride: StyleProp) => { + return useMemo(() => { + const theme = themeStyle(themeId); + + const styles = StyleSheet.create({ + button: { + borderRadius: 10, + ...StyleSheet.flatten(styleOverride), + }, + }); + + const themeOverride = { + secondaryButton: { + colors: { + primary: theme.color4, + outline: theme.color4, + }, + }, + primaryButton: { + colors: { + primary: theme.color4, + onPrimary: theme.backgroundColor4, + }, + }, + }; + + return { styles, themeOverride }; + }, [themeId, styleOverride]); +}; + +const TextButton: React.FC = props => { + const { styles, themeOverride } = useStyles(props.themeId, props.style); + + let mode: ButtonProps['mode']; + let theme: ButtonProps['theme']; + + if (props.type === ButtonType.Primary) { + theme = themeOverride.primaryButton; + mode = 'contained'; + } else if (props.type === ButtonType.Secondary) { + theme = themeOverride.secondaryButton; + mode = 'outlined'; + } else if (props.type === ButtonType.Link) { + theme = themeOverride.secondaryButton; + mode = 'text'; + } + + return ; +}; + +export default connect((state: AppState) => { + return { themeId: state.settings.theme }; +})(TextButton); diff --git a/packages/app-mobile/components/buttons/index.tsx b/packages/app-mobile/components/buttons/index.tsx new file mode 100644 index 00000000000..640ee8187fb --- /dev/null +++ b/packages/app-mobile/components/buttons/index.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import TextButton, { ButtonType, Props as TextButtonProps } from './TextButton'; + +type Props = Omit; + +const makeTextButtonComponent = (type: ButtonType) => { + return (props: Props) => { + return ; + }; +}; + +export const PrimaryButton = makeTextButtonComponent(ButtonType.Primary); +export const SecondaryButton = makeTextButtonComponent(ButtonType.Secondary); +export const LinkButton = makeTextButtonComponent(ButtonType.Link); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.tsx index 45c1116f2a2..511616ce2fd 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.tsx @@ -3,7 +3,8 @@ import { themeStyle } from '../../../global-style'; import * as React from 'react'; import { useMemo } from 'react'; import { Linking, View, StyleSheet, ViewStyle, TextStyle } from 'react-native'; -import { Button, Card, Divider, Icon, List, Text } from 'react-native-paper'; +import { Card, Divider, Icon, List, Text } from 'react-native-paper'; +import { LinkButton, PrimaryButton } from '../../../buttons'; interface Props { themeId: number; @@ -50,7 +51,6 @@ const useStyles = (themeId: number) => { marginBottom: 0, }, actionButton: { - borderRadius: 10, marginLeft: theme.marginLeft * 2, marginRight: theme.marginRight * 2, marginBottom: theme.margin, @@ -58,18 +58,6 @@ const useStyles = (themeId: number) => { }); const themeOverride = { - secondaryButton: { - colors: { - primary: theme.color4, - outline: theme.color4, - }, - }, - primaryButton: { - colors: { - primary: theme.color4, - onPrimary: theme.backgroundColor4, - }, - }, card: { colors: { outline: theme.codeBorderColor, @@ -127,8 +115,8 @@ const EnablePluginSupportPage: React.FC = props => { {renderCard('source-branch-check', _('Open Source'), _('Most plugins have source code available for review on the plugin website.'))} {renderCard('flag-remove', _('Report system'), _('We have a system for reporting and removing problematic plugins.'))} - - + {_('Learn more')} + {_('Enable plugin support')} ); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.tsx index d7e1f53c2ce..15d99f083e4 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { useCallback } from 'react'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; -import { Button, ButtonProps } from 'react-native-paper'; +import { ButtonProps } from 'react-native-paper'; +import TextButton, { ButtonType } from '../../../../buttons/TextButton'; export type PluginCallback = (event: ItemEvent)=> void; @@ -24,11 +25,12 @@ const ActionButton: React.FC = props => { // marked as translatable. const accessibilityLabel = `${props.title} ${props.item.manifest.name}`; return ( - + >{props.title} ); }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index c6b9b82d346..489e22f890c 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Icon, Card, Chip, Text } from 'react-native-paper'; +import { Card, Chip, Text } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import { Alert, Linking, StyleSheet, View } from 'react-native'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; @@ -55,9 +55,6 @@ const onRecommendedPress = () => { ); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -const PluginIcon = (props: any) => ; - const styles = StyleSheet.create({ versionText: { opacity: 0.8, @@ -180,12 +177,11 @@ const PluginBox: React.FC = props => { {manifest.name} v{manifest.version} ; return ( - + diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 5b42946c35a..f74cd78554c 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -1187,6 +1187,8 @@ class AppComponent extends React.Component { onPrimaryContainer: theme.color5, primaryContainer: theme.backgroundColor5, + outline: theme.codeBorderColor, + primary: theme.color, onPrimary: theme.backgroundColor, From 5e946c3ce6dc5345f405aca783bcd8b3132a3291 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 17 May 2024 13:25:46 -0700 Subject: [PATCH 03/46] Make plugin screen styles (but not yet layout) closer to the Figma design --- .eslintignore | 1 + .gitignore | 1 + .../components/buttons/TextButton.tsx | 52 ++++++++++++++--- .../app-mobile/components/buttons/index.tsx | 4 +- .../plugins/PluginBox/ActionButton.tsx | 9 +-- .../plugins/PluginBox/RecommendedChip.tsx | 56 +++++++++++++++++++ .../ConfigScreen/plugins/PluginBox/index.tsx | 34 +++-------- .../plugins/SearchPlugins.test.tsx | 6 +- .../ConfigScreen/plugins/SearchPlugins.tsx | 3 +- .../lib/services/style/themeToCss.test.ts | 1 + packages/lib/themes/dark.ts | 1 + packages/lib/themes/light.ts | 1 + packages/lib/themes/type.ts | 1 + 13 files changed, 126 insertions(+), 44 deletions(-) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.tsx diff --git a/.eslintignore b/.eslintignore index 00ebe26e0e4..6b19ea317e3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -612,6 +612,7 @@ packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js diff --git a/.gitignore b/.gitignore index 7d30ba20c51..ee860dcd54a 100644 --- a/.gitignore +++ b/.gitignore @@ -591,6 +591,7 @@ packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js diff --git a/packages/app-mobile/components/buttons/TextButton.tsx b/packages/app-mobile/components/buttons/TextButton.tsx index 0475f4c636f..7291a2a2dc4 100644 --- a/packages/app-mobile/components/buttons/TextButton.tsx +++ b/packages/app-mobile/components/buttons/TextButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ReactNode, useMemo } from 'react'; -import { StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; import { themeStyle } from '../global-style'; import { Button, ButtonProps } from 'react-native-paper'; import { connect } from 'react-redux'; @@ -9,25 +9,50 @@ import { AppState } from '../../utils/types'; export enum ButtonType { Primary, Secondary, + Delete, Link, } -export interface Props extends Omit { +interface Props extends Omit { themeId: number; type: ButtonType; onPress: ()=> void; children: ReactNode; - loading?: boolean; + inline?: boolean; } +export type TextButtonProps = Omit; + const useStyles = (themeId: number, styleOverride: StyleProp) => { return useMemo(() => { const theme = themeStyle(themeId); + const overrides = StyleSheet.flatten(styleOverride); + + const button: TextStyle = { + borderRadius: 10, + fontWeight: '600', + fontSize: theme.fontSize, + }; + const styles = StyleSheet.create({ - button: { - borderRadius: 10, - ...StyleSheet.flatten(styleOverride), + blockButton: { + ...button, + ...overrides, + }, + inlineButton: { + ...button, + borderRadius: 5, + marginRight: theme.marginRight / 2, + marginLeft: theme.marginLeft / 2, + padding: 0, + ...overrides, + }, + buttonLabel: { + fontWeight: '600', + fontSize: theme.fontSize * 0.95, + marginLeft: 14, + marginRight: 14, }, }); @@ -38,6 +63,12 @@ const useStyles = (themeId: number, styleOverride: StyleProp) => { outline: theme.color4, }, }, + deleteButton: { + colors: { + primary: theme.deleteColor, + outline: theme.deleteColor, + }, + }, primaryButton: { colors: { primary: theme.color4, @@ -62,14 +93,21 @@ const TextButton: React.FC = props => { } else if (props.type === ButtonType.Secondary) { theme = themeOverride.secondaryButton; mode = 'outlined'; + } else if (props.type === ButtonType.Delete) { + theme = themeOverride.deleteButton; + mode = 'outlined'; } else if (props.type === ButtonType.Link) { theme = themeOverride.secondaryButton; mode = 'text'; + } else { + const exhaustivenessCheck: never = props.type; + return exhaustivenessCheck; } return - - - ); -}; - const PluginInfoButton: React.FC = props => { const [showInfoModal, setShowInfoModal] = useState(false); const onInfoButtonPress = useCallback(() => { @@ -102,7 +24,7 @@ const PluginInfoButton: React.FC = props => { return ( <> - {showInfoModal ? : null} + {showInfoModal ? : null} = props => { + return + { + props.manifest.name + } v{ + props.manifest.version + } + ; +}; + +export default PluginTitle; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 8f4417fa438..8c6cab53970 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -1,16 +1,12 @@ import * as React from 'react'; -import { Card, Text } from 'react-native-paper'; +import { Card } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; -import { StyleSheet, View } from 'react-native'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; -import shim from '@joplin/lib/shim'; -import PluginService from '@joplin/lib/services/plugins/PluginService'; import ActionButton, { PluginCallback } from './ActionButton'; import PluginInfoButton from './PluginInfoButton'; import { ButtonType } from '../../../../buttons/TextButton'; -import RecommendedChip from './RecommendedChip'; -import SmallChip from './SmallChip'; -import { themeStyle } from '@joplin/lib/theme'; +import PluginChips from './PluginChips'; +import PluginTitle from './PluginTitle'; export enum InstallState { NotInstalled, @@ -29,6 +25,7 @@ interface Props { themeId: number; item: PluginItem; isCompatible: boolean; + showInfoButton: boolean; hasErrors?: boolean; installState?: InstallState; @@ -42,16 +39,6 @@ interface Props { onShowPluginLog?: PluginCallback; } -const styles = StyleSheet.create({ - versionText: { - opacity: 0.8, - }, - title: { - // Prevents the title text from being clipped on Android - verticalAlign: 'middle', - }, -}); - const PluginBox: React.FC = props => { const manifest = props.item.manifest; const item = props.item; @@ -102,79 +89,27 @@ const PluginBox: React.FC = props => { const enableButton = ; const aboutButton = ; - const theme = themeStyle(props.themeId); - - const renderErrorsChip = () => { - if (!props.hasErrors) return null; - - return ( - props.onShowPluginLog({ item })} - > - {_('Error')} - - ); - }; - - const renderRecommendedChip = () => { - if (!props.item.manifest._recommended || !props.isCompatible) { - return null; - } - return ; - }; - - const renderBuiltInChip = () => { - if (!props.item.builtIn) { - return null; - } - return {_('Built-in')}; - }; - - const renderIncompatibleChip = () => { - if (props.isCompatible) return null; - return ( - { - void shim.showMessageBox( - PluginService.instance().describeIncompatibility(props.item.manifest), - { buttons: [_('OK')] }, - ); - }} - >{_('Incompatible')} - ); - }; - const renderRightEdgeButton = (buttonProps: { size: number }) => { - // If .onAboutPress is given (e.g. when searching), there's another way to get information - // about the plugin. In this case, we don't show the right-side information link. - if (props.onAboutPress) return null; + if (!props.showInfoButton) return null; return ; }; const updateStateIsIdle = props.updateState !== UpdateState.Idle; - const titleComponent = <> - {manifest.name} v{manifest.version} - ; return ( } subtitle={manifest.description} right={renderRightEdgeButton} /> - - {renderIncompatibleChip()} - {renderErrorsChip()} - {renderRecommendedChip()} - {renderBuiltInChip()} - + {props.onAboutPress ? aboutButton : null} diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx new file mode 100644 index 00000000000..786b6b6117b --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -0,0 +1,121 @@ +import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; +import * as React from 'react'; +import { _ } from '@joplin/lib/locale'; +import { useCallback, useMemo } from 'react'; +import { Button, Card, List, Portal, Text } from 'react-native-paper'; +import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl'; +import { Linking, ScrollView, StyleSheet, View } from 'react-native'; +import DismissibleDialog from '../../../DismissibleDialog'; +import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; +import PluginService from '@joplin/lib/services/plugins/PluginService'; +import PluginChips from './PluginBox/PluginChips'; +import PluginTitle from './PluginBox/PluginTitle'; + +interface Props { + themeId: number; + size: number; + item: PluginItem|null; + visible: boolean; + onModalDismiss: ()=> void; +} + +const styles = StyleSheet.create({ + aboutPluginContainer: { + paddingLeft: 10, + paddingRight: 10, + paddingBottom: 10, + }, + descriptionText: { + marginTop: 5, + marginBottom: 5, + }, + fraudulentPluginButton: { + opacity: 0.6, + }, +}); + +const PluginInfoModalContent: React.FC = props => { + const manifest = props.item.manifest; + const isCompatible = useMemo(() => { + return PluginService.instance().isCompatible(manifest); + }, [manifest]); + + const plugin = useMemo(() => { + return PluginService.instance().pluginById(manifest.id); + }, [manifest]); + + const aboutPlugin = ( + + + + {_('by %s', manifest.author)} + + + {manifest.description} + + + + ); + + const onAboutPress = useCallback(() => { + void openWebsiteForPlugin({ item: props.item }); + }, [props.item]); + + const reportIssueUrl = useMemo(() => { + return getPluginIssueReportUrl(props.item.manifest); + }, [props.item]); + + const onReportIssuePress = useCallback(() => { + void Linking.openURL(reportIssueUrl); + }, [reportIssueUrl]); + + const reportIssueButton = ( + } + title={_('Report an issue')} + onPress={onReportIssuePress} + /> + ); + + const onReportFraudulentPress = useCallback(() => { + void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new'); + }, []); + + return <> + + {aboutPlugin} + } + title={_('About')} + onPress={onAboutPress} + /> + { reportIssueUrl ? reportIssueButton : null } + + + ; +}; + +const PluginInfoModal: React.FC = props => { + return ( + + + { props.item ? : null } + + + ); +}; + +export default PluginInfoModal; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx index a1b95bc57f4..31f3c08f64d 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx @@ -94,6 +94,7 @@ const PluginToggle: React.FC = props => { = props => { item={item.item} installState={item.installState} isCompatible={PluginService.instance().isCompatible(manifest)} + showInfoButton={false} onInstall={installPlugin} onAboutPress={openWebsiteForPlugin} /> From e8afcc4dfba17291fd53b7b3e2af6b4c7c57f5d6 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 17 May 2024 17:21:53 -0700 Subject: [PATCH 08/46] Restyle and add enable button to info modal --- .../plugins/PluginBox/PluginInfoButton.tsx | 11 ++- .../ConfigScreen/plugins/PluginBox/index.tsx | 11 ++- .../ConfigScreen/plugins/PluginInfoModal.tsx | 67 ++++++++++++++----- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx index aae0c48fbda..1975c97a136 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx @@ -3,12 +3,17 @@ import * as React from 'react'; import { useCallback, useState } from 'react'; import { IconButton } from 'react-native-paper'; import PluginInfoModal from '../PluginInfoModal'; +import { PluginCallback } from './ActionButton'; interface Props { themeId: number; size: number; item: PluginItem; onModalDismiss?: ()=> void; + + onUpdate: PluginCallback; + onDelete: PluginCallback; + onToggle: PluginCallback; } const PluginInfoButton: React.FC = props => { @@ -24,7 +29,11 @@ const PluginInfoButton: React.FC = props => { return ( <> - {showInfoModal ? : null} + = props => { const renderRightEdgeButton = (buttonProps: { size: number }) => { if (!props.showInfoButton) return null; - return ; + return ( + + ); }; const updateStateIsIdle = props.updateState !== UpdateState.Idle; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index 786b6b6117b..be86035b091 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -2,7 +2,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import * as React from 'react'; import { _ } from '@joplin/lib/locale'; import { useCallback, useMemo } from 'react'; -import { Button, Card, List, Portal, Text } from 'react-native-paper'; +import { Button, Card, Divider, Portal, Switch, Text } from 'react-native-paper'; import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl'; import { Linking, ScrollView, StyleSheet, View } from 'react-native'; import DismissibleDialog from '../../../DismissibleDialog'; @@ -10,6 +10,8 @@ import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import PluginChips from './PluginBox/PluginChips'; import PluginTitle from './PluginBox/PluginTitle'; +import { PluginCallback } from './PluginBox/ActionButton'; +import TextButton, { ButtonType } from '../../../buttons/TextButton'; interface Props { themeId: number; @@ -17,23 +19,54 @@ interface Props { item: PluginItem|null; visible: boolean; onModalDismiss: ()=> void; + + onUpdate: PluginCallback; + onDelete: PluginCallback; + onToggle: PluginCallback; } const styles = StyleSheet.create({ - aboutPluginContainer: { - paddingLeft: 10, - paddingRight: 10, - paddingBottom: 10, - }, descriptionText: { marginTop: 5, marginBottom: 5, }, + buttonContainer: { + display: 'flex', + flexDirection: 'column', + gap: 20, + marginLeft: 10, + marginRight: 10, + marginTop: 30, + }, fraudulentPluginButton: { opacity: 0.6, }, + enabledSwitchContainer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 10, + marginTop: 12, + marginBottom: 14, + }, }); +interface EnabledSwitchProps { + item: PluginItem; + onToggle: PluginCallback; +} + +const EnabledSwitch: React.FC = props => { + const onChange = useCallback(() => { + props.onToggle({ item: props.item }); + }, [props.item, props.onToggle]); + return + {_('Enabled')} + + ; +}; + const PluginInfoModalContent: React.FC = props => { const manifest = props.item.manifest; const isCompatible = useMemo(() => { @@ -75,11 +108,10 @@ const PluginInfoModalContent: React.FC = props => { }, [reportIssueUrl]); const reportIssueButton = ( - } - title={_('Report an issue')} + + >{_('Report an issue')} ); const onReportFraudulentPress = useCallback(() => { @@ -89,12 +121,15 @@ const PluginInfoModalContent: React.FC = props => { return <> {aboutPlugin} - } - title={_('About')} - onPress={onAboutPress} - /> - { reportIssueUrl ? reportIssueButton : null } + + + + {_('About')} + { reportIssueUrl ? reportIssueButton : null } + ; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 07e8acb8eca..6cccce9a320 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; -import { View } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import { Banner, Button, Text } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; @@ -55,6 +55,13 @@ const useLoadedPluginIds = () => { return loadedPluginIds; }; +const styles = StyleSheet.create({ + installedPluginsContainer: { + marginLeft: 8, + marginRight: 8, + }, +}); + const PluginStates: React.FC = props => { const [repoApiError, setRepoApiError] = useState(null); const [repoApiLoaded, setRepoApiLoaded] = useState(false); @@ -170,7 +177,9 @@ const PluginStates: React.FC = props => { return ( {renderRepoApiStatus()} - {installedPluginCards} + + {installedPluginCards} + {showSearch ? searchComponent : null} = props => { }, [installPlugin, props.themeId]); return ( - - + } + label={_('Search plugins')} onChangeText={setSearchQuery} - mode='view' value={searchQuery} editable={props.repoApiInitialized} /> From 00d285f7846d9d85db666f3124d4f0293fdbf6d0 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 11:54:44 -0700 Subject: [PATCH 15/46] Restyle "install from file --- .../screens/ConfigScreen/ConfigScreen.tsx | 1 + .../plugins/PluginUploadButton.tsx | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index c9a9de8b651..6499b0c9bdb 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -504,6 +504,7 @@ class ConfigScreenComponent extends BaseScreenComponent, pluginUploadButtonSearchText(), { advanced: true }, diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx index 988dc7492f1..63c80ec9c58 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx @@ -3,18 +3,20 @@ import { _ } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService'; import * as React from 'react'; import { useCallback, useState } from 'react'; -import { Button } from 'react-native-paper'; import pickDocument from '../../../../utils/pickDocument'; import shim from '@joplin/lib/shim'; import Logger from '@joplin/utils/Logger'; -import { Platform } from 'react-native'; +import { Platform, View, ViewStyle } from 'react-native'; import { join, extname } from 'path'; import uuid from '@joplin/lib/uuid'; import Setting from '@joplin/lib/models/Setting'; +import TextButton, { ButtonType } from '../../../buttons/TextButton'; +import { ConfigScreenStyles } from '../configScreenStyles'; interface Props { updatePluginStates: (settingValue: PluginSettings)=> void; pluginSettings: SerializedPluginSettings; + styles: ConfigScreenStyles; } const logger = Logger.create('PluginUploadButton'); @@ -26,6 +28,8 @@ export const canInstallPluginsFromFile = () => { return shim.mobilePlatform() !== 'ios' || Setting.value('env') === 'dev'; }; +const buttonStyle: ViewStyle = { flexGrow: 1 }; + const PluginUploadButton: React.FC = props => { const [showLoadingAnimation, setShowLoadingAnimation] = useState(false); @@ -85,13 +89,17 @@ const PluginUploadButton: React.FC = props => { }, [props.pluginSettings, props.updatePluginStates]); return ( - + + + {buttonLabel()} + + ); }; From c77be22ccdb810ed7c936d88e89fead9c26b609e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 12:06:09 -0700 Subject: [PATCH 16/46] Slightly decrease size of plugin chips --- .../screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index 4d26ea9fdad..7f5c918da41 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -63,7 +63,7 @@ const PluginChips: React.FC = props => { ); }; - return + return {renderIncompatibleChip()} {renderErrorsChip()} {renderRecommendedChip()} From 4f3565738e5550beecbd6a39a590682d897bd292 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 13:35:57 -0700 Subject: [PATCH 17/46] Fix error chip, adjust global styles to simplify changes --- .../screens/ConfigScreen/ConfigScreen.tsx | 20 ++++------ .../plugins/PluginBox/PluginChips.tsx | 10 +++++ .../ConfigScreen/plugins/PluginBox/index.tsx | 2 + .../ConfigScreen/plugins/PluginInfoModal.tsx | 20 +++++----- .../ConfigScreen/plugins/PluginStates.tsx | 7 ---- .../ConfigScreen/plugins/PluginToggle.tsx | 3 +- .../plugins/utils/usePluginCallbacks.js | 40 ------------------- .../plugins/utils/usePluginCallbacks.ts | 24 +++++++---- packages/app-mobile/root.tsx | 4 +- 9 files changed, 50 insertions(+), 80 deletions(-) delete mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index 6499b0c9bdb..940ef826cad 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -26,7 +26,7 @@ import ExportProfileButton, { exportProfileButtonTitle } from './NoteExportSecti import SettingComponent from './SettingComponent'; import ExportDebugReportButton, { exportDebugReportTitle } from './NoteExportSection/ExportDebugReportButton'; import SectionSelector from './SectionSelector'; -import { Button, TextInput } from 'react-native-paper'; +import { TextInput, List } from 'react-native-paper'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginStates, { getSearchText as getPluginStatesSearchText } from './plugins/PluginStates'; import PluginUploadButton, { canInstallPluginsFromFile, buttonLabel as pluginUploadButtonSearchText } from './plugins/PluginUploadButton'; @@ -674,19 +674,15 @@ class ConfigScreenComponent extends BaseScreenComponent { if (!advancedSettingComps.length) return null; - const toggleAdvancedLabel = this.state.showAdvancedSettings ? _('Hide Advanced Settings') : _('Show Advanced Settings'); + const toggleAdvancedLabel = _('Advanced settings'); return ( - <> - - + this.setState({ showAdvancedSettings: !this.state.showAdvancedSettings })} + > {this.state.showAdvancedSettings ? advancedSettingComps : null} - + ); }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index 7f5c918da41..2353f35affa 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -13,6 +13,7 @@ interface Props { item: PluginItem; hasErrors: boolean; isCompatible: boolean; + canUpdate: boolean; onShowPluginLog?: PluginCallback; } @@ -63,11 +64,20 @@ const PluginChips: React.FC = props => { ); }; + const renderUpdatableChip = () => { + if (!props.isCompatible || !props.canUpdate) return null; + + return ( + {_('Update available')} + ); + }; + return {renderIncompatibleChip()} {renderErrorsChip()} {renderRecommendedChip()} {renderBuiltInChip()} + {renderUpdatableChip()} ; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index e70a51e5a65..9530c39bfe9 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -92,6 +92,8 @@ const PluginBox: React.FC = props => { themeId={props.themeId} item={props.item} hasErrors={props.hasErrors} + canUpdate={props.updateState === UpdateState.CanUpdate} + onShowPluginLog={props.onShowPluginLog} isCompatible={props.isCompatible} /> diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index 07d49e34be0..21777221840 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -8,7 +8,6 @@ import { Linking, ScrollView, StyleSheet, View, ViewStyle } from 'react-native'; import DismissibleDialog from '../../../DismissibleDialog'; import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; -import PluginChips from './PluginBox/PluginChips'; import PluginTitle from './PluginBox/PluginTitle'; import ActionButton from './buttons/ActionButton'; import TextButton, { ButtonType } from '../../../buttons/TextButton'; @@ -17,6 +16,7 @@ import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks'; import usePluginItem from './utils/usePluginItem'; import InstallButton from './buttons/InstallButton'; import { InstallState } from './PluginBox'; +import PluginChips from './PluginBox/PluginChips'; interface Props { themeId: number; @@ -105,6 +105,13 @@ const PluginInfoModalContent: React.FC = props => { return service.pluginById(pluginId); }, [pluginId]); + const updateState = useUpdateState({ + pluginId: plugin?.id, + pluginSettings: props.pluginSettings, + updatablePluginIds: props.updatablePluginIds, + updatingPluginIds: props.updatingPluginIds, + }); + const aboutPlugin = ( @@ -114,7 +121,9 @@ const PluginInfoModalContent: React.FC = props => { {manifest.description} @@ -146,13 +155,6 @@ const PluginInfoModalContent: React.FC = props => { void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new'); }, []); - const updateState = useUpdateState({ - pluginId: plugin?.id, - pluginSettings: props.pluginSettings, - updatablePluginIds: props.updatablePluginIds, - updatingPluginIds: props.updatingPluginIds, - }); - const getUpdateButtonTitle = () => { if (updateState === UpdateState.Updating) return _('Updating...'); if (updateState === UpdateState.HasBeenUpdated) return _('Updated'); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 6cccce9a320..57fa4316601 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -8,7 +8,6 @@ import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin import PluginToggle from './PluginToggle'; import SearchPlugins from './SearchPlugins'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; -import NavService from '@joplin/lib/services/NavService'; import useRepoApi from './utils/useRepoApi'; import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; @@ -128,11 +127,6 @@ const PluginStates: React.FC = props => { pluginSettings, updatePluginStates: props.updatePluginStates, repoApi, }); - const onShowPluginLog = useCallback((event: ItemEvent) => { - const pluginId = event.item.manifest.id; - void NavService.go('Log', { defaultFilter: pluginId }); - }, []); - const installedPluginCards = []; const pluginService = PluginService.instance(); @@ -152,7 +146,6 @@ const PluginStates: React.FC = props => { updatingPluginIds={updatingPluginIds} updatePluginStates={props.updatePluginStates} onShowPluginInfo={onShowPluginInfo} - onShowPluginLog={onShowPluginLog} callbacks={pluginCallbacks} repoApi={repoApi} />, diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx index 703fca2a9ec..70fa937fc64 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx @@ -20,7 +20,6 @@ interface Props { callbacks: PluginCallbacks; onShowPluginInfo: PluginCallback; - onShowPluginLog: PluginCallback; updatePluginStates: (settingValue: PluginSettings)=> void; } @@ -45,7 +44,7 @@ const PluginToggle: React.FC = props => { item={pluginItem} isCompatible={isCompatible} hasErrors={plugin.hasErrors} - onShowPluginLog={props.onShowPluginLog} + onShowPluginLog={props.callbacks.onShowPluginLog} onShowPluginInfo={props.onShowPluginInfo} updateState={updateState} /> diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js deleted file mode 100644 index a1413f1de80..00000000000 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const useOnDeleteHandler_1 = require("@joplin/lib/components/shared/config/plugins/useOnDeleteHandler"); -const useOnInstallHandler_1 = require("@joplin/lib/components/shared/config/plugins/useOnInstallHandler"); -const react_1 = require("react"); -const usePluginCallbacks = (props) => { - const onPluginSettingsChange = (0, react_1.useCallback)((event) => { - props.updatePluginStates(event.value); - }, [props.updatePluginStates]); - const updatePluginEnabled = (0, react_1.useCallback)((pluginId, enabled) => { - const newSettings = Object.assign({}, props.pluginSettings); - newSettings[pluginId].enabled = enabled; - props.updatePluginStates(newSettings); - }, [props.pluginSettings, props.updatePluginStates]); - const onToggle = (0, react_1.useCallback)((event) => { - const pluginId = event.item.manifest.id; - const settings = props.pluginSettings[pluginId]; - updatePluginEnabled(pluginId, !settings.enabled); - }, [props.pluginSettings, updatePluginEnabled]); - const onDelete = (0, useOnDeleteHandler_1.default)(props.pluginSettings, onPluginSettingsChange, true); - const [updatingPluginIds, setUpdatingPluginIds] = (0, react_1.useState)({}); - const onUpdate = (0, useOnInstallHandler_1.default)(setUpdatingPluginIds, props.pluginSettings, props.repoApi, onPluginSettingsChange, true); - const [installingPluginIds, setInstallingPluginIds] = (0, react_1.useState)({}); - const onInstall = (0, useOnInstallHandler_1.default)(setInstallingPluginIds, props.pluginSettings, props.repoApi, onPluginSettingsChange, false); - const callbacks = (0, react_1.useMemo)(() => { - return { - onToggle, - onDelete, - onUpdate, - onInstall, - }; - }, [onToggle, onDelete, onUpdate]); - return { - callbacks, - updatingPluginIds, - installingPluginIds, - }; -}; -exports.default = usePluginCallbacks; -//# sourceMappingURL=usePluginCallbacks.js.map \ No newline at end of file diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.ts index e87e2b0a6a9..bb3b000e2c0 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.ts @@ -1,9 +1,10 @@ -import { ItemEvent, OnPluginSettingChangeEvent } from "@joplin/lib/components/shared/config/plugins/types"; -import useOnDeleteHandler from "@joplin/lib/components/shared/config/plugins/useOnDeleteHandler"; -import useOnInstallHandler from "@joplin/lib/components/shared/config/plugins/useOnInstallHandler"; -import { PluginSettings } from "@joplin/lib/services/plugins/PluginService"; -import RepositoryApi from "@joplin/lib/services/plugins/RepositoryApi"; -import { useCallback, useMemo, useState } from "react"; +import { ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types'; +import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/useOnDeleteHandler'; +import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler'; +import NavService from '@joplin/lib/services/NavService'; +import { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; +import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; +import { useCallback, useMemo, useState } from 'react'; interface Props { updatePluginStates: (settingValue: PluginSettings)=> void; @@ -18,6 +19,7 @@ export interface PluginCallbacks { onUpdate: PluginCallback; onInstall: PluginCallback; onDelete: PluginCallback; + onShowPluginLog: PluginCallback; } const usePluginCallbacks = (props: Props) => { @@ -48,14 +50,20 @@ const usePluginCallbacks = (props: Props) => { setInstallingPluginIds, props.pluginSettings, props.repoApi, onPluginSettingsChange, false, ); + const onShowPluginLog = useCallback((event: ItemEvent) => { + const pluginId = event.item.manifest.id; + void NavService.go('Log', { defaultFilter: pluginId }); + }, []); + const callbacks = useMemo((): PluginCallbacks => { return { onToggle, onDelete, onUpdate, onInstall, + onShowPluginLog, }; - }, [onToggle, onDelete, onUpdate]); + }, [onToggle, onDelete, onUpdate, onInstall, onShowPluginLog]); return { callbacks, @@ -64,4 +72,4 @@ const usePluginCallbacks = (props: Props) => { }; }; -export default usePluginCallbacks; \ No newline at end of file +export default usePluginCallbacks; diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index f74cd78554c..4e0f178aae1 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -1189,8 +1189,8 @@ class AppComponent extends React.Component { outline: theme.codeBorderColor, - primary: theme.color, - onPrimary: theme.backgroundColor, + primary: theme.color4, + onPrimary: theme.backgroundColor4, background: theme.backgroundColor, From cca5ebcfca2c14fe172b9d66a46780f67e959e59 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 15:06:12 -0700 Subject: [PATCH 18/46] Adjust chip and card title styles --- .eslintignore | 2 +- .gitignore | 2 +- .../plugins/PluginBox/PluginChips.tsx | 45 +++++++++++++--- .../plugins/PluginBox/RecommendedChip.tsx | 53 ------------------- .../plugins/PluginBox/StyledChip.tsx | 29 ++++++++++ .../ConfigScreen/plugins/PluginBox/index.tsx | 19 +++---- .../ConfigScreen/plugins/PluginInfoModal.tsx | 6 ++- 7 files changed, 80 insertions(+), 76 deletions(-) delete mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx diff --git a/.eslintignore b/.eslintignore index 8ab16a3ffe0..94526807a5f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -612,7 +612,7 @@ packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js diff --git a/.gitignore b/.gitignore index b70e99038ff..7a39ecce33b 100644 --- a/.gitignore +++ b/.gitignore @@ -591,7 +591,7 @@ packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index 2353f35affa..128fb829884 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -2,11 +2,12 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import shim from '@joplin/lib/shim'; import * as React from 'react'; -import { View } from 'react-native'; +import { Alert, Linking, View } from 'react-native'; import { _ } from '@joplin/lib/locale'; -import RecommendedChip from './RecommendedChip'; import { Chip } from 'react-native-paper'; import { PluginCallback } from '../utils/usePluginCallbacks'; +import StyledChip from './StyledChip'; +import { themeStyle } from '../../../../global-style'; interface Props { themeId: number; @@ -18,20 +19,41 @@ interface Props { onShowPluginLog?: PluginCallback; } +const onRecommendedPress = () => { + Alert.alert( + '', + _('The Joplin team has vetted this plugin and it meets our standards for security and performance.'), + [ + { + text: _('Learn more'), + onPress: () => Linking.openURL('https://github.com/joplin/plugins/blob/master/readme/recommended.md'), + }, + { + text: _('OK'), + }, + ], + { cancelable: true }, + ); +}; + const PluginChips: React.FC = props => { const item = props.item; + const theme = themeStyle(props.themeId); + const renderErrorsChip = () => { if (!props.hasErrors) return null; return ( - props.onShowPluginLog({ item })} > {_('Error')} - + ); }; @@ -39,7 +61,12 @@ const PluginChips: React.FC = props => { if (!props.item.manifest._recommended || !props.isCompatible) { return null; } - return ; + return {_('Recommended')}; }; const renderBuiltInChip = () => { @@ -52,7 +79,9 @@ const PluginChips: React.FC = props => { const renderIncompatibleChip = () => { if (props.isCompatible) return null; return ( - { void shim.showMessageBox( @@ -60,7 +89,7 @@ const PluginChips: React.FC = props => { { buttons: [_('OK')] }, ); }} - >{_('Incompatible')} + >{_('Incompatible')} ); }; @@ -72,7 +101,7 @@ const PluginChips: React.FC = props => { ); }; - return + return {renderIncompatibleChip()} {renderErrorsChip()} {renderRecommendedChip()} diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.tsx deleted file mode 100644 index 1bfaf44dfe7..00000000000 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedChip.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { _ } from '@joplin/lib/locale'; -import * as React from 'react'; -import { themeStyle } from '../../../../global-style'; -import { Alert, Linking } from 'react-native'; -import { Chip } from 'react-native-paper'; -import { useMemo } from 'react'; - - -interface Props { - themeId: number; -} - -const onRecommendedPress = () => { - Alert.alert( - '', - _('The Joplin team has vetted this plugin and it meets our standards for security and performance.'), - [ - { - text: _('Learn more'), - onPress: () => Linking.openURL('https://github.com/joplin/plugins/blob/master/readme/recommended.md'), - }, - { - text: _('OK'), - }, - ], - { cancelable: true }, - ); -}; - -const RecommendedChip: React.FC = props => { - const themeOverride = useMemo(() => { - const theme = themeStyle(props.themeId); - - return { - colors: { - secondaryContainer: theme.searchMarkerBackgroundColor, - onSecondaryContainer: theme.searchMarkerColor, - primary: theme.searchMarkerColor, - }, - }; - }, [props.themeId]); - - return - {_('Recommended')} - ; -}; - -export default RecommendedChip; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx new file mode 100644 index 00000000000..a530ab89dac --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Chip, ChipProps } from 'react-native-paper'; +import { useMemo } from 'react'; + + +interface Props extends ChipProps { + foreground: string; + background: string; +} + +const RecommendedChip: React.FC = props => { + const themeOverride = useMemo(() => { + return { + colors: { + secondaryContainer: props.background, + onSecondaryContainer: props.foreground, + primary: props.foreground, + }, + }; + }, [props.foreground, props.background]); + + return ; +}; + +export default RecommendedChip; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 9530c39bfe9..683bb5ce0e0 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; -import { Card } from 'react-native-paper'; +import { Card, Text } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import ActionButton from '../buttons/ActionButton'; import { ButtonType } from '../../../../buttons/TextButton'; import PluginChips from './PluginChips'; -import PluginTitle from './PluginTitle'; import { UpdateState } from '../utils/useUpdateState'; import { PluginCallback } from '../utils/usePluginCallbacks'; import { useCallback, useMemo } from 'react'; import { StyleSheet, ViewStyle } from 'react-native'; import InstallButton from '../buttons/InstallButton'; +import PluginTitle from './PluginTitle'; export enum InstallState { NotInstalled, @@ -46,9 +46,8 @@ const useStyles = (compatible: boolean) => { ...baseCardStyle, opacity: 0.7, }, - title: { - paddingTop: 10, - paddingBottom: 0, + content: { + gap: 5, }, }); }, [compatible]); @@ -81,13 +80,9 @@ const PluginBox: React.FC = props => { onPress={props.onShowPluginInfo ? onPress : null} testID='plugin-card' > - } - subtitle={manifest.description} - subtitleNumberOfLines={2} - /> - + + + {manifest.description} { marginTop: 12, marginBottom: 14, }, + pluginDescriptionContainer: { + marginTop: 8, + gap: 8, + }, }); })(); @@ -117,7 +121,7 @@ const PluginInfoModalContent: React.FC = props => { {_('by %s', manifest.author)} - + Date: Thu, 23 May 2024 15:08:46 -0700 Subject: [PATCH 19/46] Ignore auto-generated file --- .../plugins/utils/useUpdateState.js | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js deleted file mode 100644 index 65a40a24718..00000000000 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.UpdateState = void 0; -const react_1 = require("react"); -var UpdateState; -(function (UpdateState) { - UpdateState[UpdateState["Idle"] = 1] = "Idle"; - UpdateState[UpdateState["CanUpdate"] = 2] = "CanUpdate"; - UpdateState[UpdateState["Updating"] = 3] = "Updating"; - UpdateState[UpdateState["HasBeenUpdated"] = 4] = "HasBeenUpdated"; -})(UpdateState || (exports.UpdateState = UpdateState = {})); -const useUpdateState = ({ pluginId, pluginSettings, updatablePluginIds, updatingPluginIds }) => { - return (0, react_1.useMemo)(() => { - const settings = pluginSettings[pluginId]; - // Uninstalled - if (!settings) - return UpdateState.Idle; - if (settings.hasBeenUpdated) { - return UpdateState.HasBeenUpdated; - } - if (updatingPluginIds[pluginId]) { - return UpdateState.Updating; - } - if (updatablePluginIds[pluginId]) { - return UpdateState.CanUpdate; - } - return UpdateState.Idle; - }, [pluginSettings, updatingPluginIds, pluginId, updatablePluginIds]); -}; -exports.default = useUpdateState; -//# sourceMappingURL=useUpdateState.js.map \ No newline at end of file From ac5b9c621852302bcf0490c3ff559f2d93ed7c11 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 15:17:45 -0700 Subject: [PATCH 20/46] Add a disabled chip --- .../plugins/PluginBox/PluginChips.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index 128fb829884..a5a98eb6b6c 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -2,7 +2,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import shim from '@joplin/lib/shim'; import * as React from 'react'; -import { Alert, Linking, View } from 'react-native'; +import { Alert, Linking, View, ViewStyle } from 'react-native'; import { _ } from '@joplin/lib/locale'; import { Chip } from 'react-native-paper'; import { PluginCallback } from '../utils/usePluginCallbacks'; @@ -36,6 +36,15 @@ const onRecommendedPress = () => { ); }; +const containerStyle: ViewStyle = { + flexDirection: 'row', + gap: 4, + + // Smaller than default chip size + transform: [{ scale: 0.84 }], + transformOrigin: 'left', +}; + const PluginChips: React.FC = props => { const item = props.item; @@ -101,12 +110,20 @@ const PluginChips: React.FC = props => { ); }; - return + const renderDisabledChip = () => { + if (props.item.enabled || !props.item.installed) { + return null; + } + return {_('Disabled')}; + }; + + return {renderIncompatibleChip()} {renderErrorsChip()} {renderRecommendedChip()} {renderBuiltInChip()} {renderUpdatableChip()} + {renderDisabledChip()} ; }; From 360a3b8b0912486ac71ea22490fcdf9835b982d7 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 15:22:56 -0700 Subject: [PATCH 21/46] Refactor plugin search to re-use the install handler created in PluginStates --- .../ConfigScreen/plugins/PluginStates.tsx | 2 ++ .../ConfigScreen/plugins/SearchPlugins.tsx | 25 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 57fa4316601..b9ff8f10beb 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -162,6 +162,8 @@ const PluginStates: React.FC = props => { pluginSettings={props.pluginSettings} themeId={props.themeId} onUpdatePluginStates={props.updatePluginStates} + installingPluginIds={installingPluginIds} + callbacks={pluginCallbacks} repoApiInitialized={repoApiLoaded} repoApi={repoApi} /> diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx index 5c5e439710f..652d2d29c7b 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx @@ -8,10 +8,10 @@ import { FlatList, View } from 'react-native'; import { TextInput } from 'react-native-paper'; import PluginBox, { InstallState } from './PluginBox'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; -import useInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler'; -import { OnPluginSettingChangeEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; +import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; +import { PluginCallbacks } from './utils/usePluginCallbacks'; interface Props { themeId: number; @@ -19,6 +19,9 @@ interface Props { repoApiInitialized: boolean; onUpdatePluginStates: (states: PluginSettings)=> void; repoApi: RepositoryApi; + + installingPluginIds: Record; + callbacks: PluginCallbacks; } interface SearchResultRecord { @@ -42,8 +45,6 @@ const PluginSearch: React.FC = props => { } }, [searchQuery, props.repoApi, setSearchResultManifests, props.repoApiInitialized]); - const [installingPluginsIds, setInstallingPluginIds] = useState>({}); - const pluginSettings = useMemo(() => { return { ...PluginService.instance().unserializePluginSettings(props.pluginSettings) }; }, [props.pluginSettings]); @@ -56,7 +57,7 @@ const PluginSearch: React.FC = props => { if (settings && !settings.deleted) { installState = InstallState.Installed; } - if (installingPluginsIds[manifest.id]) { + if (props.installingPluginIds[manifest.id]) { installState = InstallState.Installing; } @@ -76,16 +77,10 @@ const PluginSearch: React.FC = props => { installState, }; }); - }, [searchResultManifests, installingPluginsIds, pluginSettings]); + }, [searchResultManifests, props.installingPluginIds, pluginSettings]); - const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => { - props.onUpdatePluginStates(event.value); - }, [props.onUpdatePluginStates]); - - const installPlugin = useInstallHandler( - setInstallingPluginIds, pluginSettings, props.repoApi, onPluginSettingsChange, false, - ); + const onInstall = props.callbacks.onInstall; const renderResult = useCallback(({ item }: { item: SearchResultRecord }) => { const manifest = item.item.manifest; @@ -96,11 +91,11 @@ const PluginSearch: React.FC = props => { item={item.item} installState={item.installState} isCompatible={PluginService.instance().isCompatible(manifest)} - onInstall={installPlugin} + onInstall={onInstall} onAboutPress={openWebsiteForPlugin} /> ); - }, [installPlugin, props.themeId]); + }, [onInstall, props.themeId]); return ( From 77a678490d8c2d8785c24cccda9a3732ba1f84b0 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 16:08:00 -0700 Subject: [PATCH 22/46] Add search results text --- .../ConfigScreen/plugins/SearchPlugins.tsx | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx index 652d2d29c7b..f063fe0b9c1 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx @@ -4,8 +4,8 @@ import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import { _ } from '@joplin/lib/locale'; import { PluginManifest } from '@joplin/lib/services/plugins/utils/types'; import { useCallback, useMemo, useState } from 'react'; -import { FlatList, View } from 'react-native'; -import { TextInput } from 'react-native-paper'; +import { FlatList, StyleSheet, View } from 'react-native'; +import { TextInput, Text } from 'react-native-paper'; import PluginBox, { InstallState } from './PluginBox'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; @@ -30,6 +30,18 @@ interface SearchResultRecord { installState: InstallState; } +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + margin: 12, + }, + resultsCounter: { + margin: 12, + marginTop: 17, + marginBottom: 4, + }, +}); + const PluginSearch: React.FC = props => { const [searchQuery, setSearchQuery] = useState(''); const [searchResultManifests, setSearchResultManifests] = useState([]); @@ -97,8 +109,16 @@ const PluginSearch: React.FC = props => { ); }, [onInstall, props.themeId]); + const renderResultsCount = () => { + if (!searchQuery.length) return null; + + return + {_('Results (%d):', searchResults.length)} + ; + }; + return ( - + = props => { value={searchQuery} editable={props.repoApiInitialized} /> + {renderResultsCount()} Date: Thu, 23 May 2024 17:05:34 -0700 Subject: [PATCH 23/46] Fix test compilation (running still failing) --- .eslintignore | 6 +- .gitignore | 6 +- .../plugins/PluginBox/PluginChips.tsx | 7 +- .../plugins/PluginBox/StyledChip.tsx | 18 +++-- .../ConfigScreen/plugins/PluginBox/index.tsx | 68 +++++++++--------- ...st.tsx => PluginStates.installed.test.tsx} | 69 +++++-------------- ....test.tsx => PluginStates.search.test.tsx} | 50 ++++---------- .../plugins/testUtils/WrappedPluginStates.tsx | 44 ++++++++++++ .../testUtils/mockRepositoryApiConstructor.ts | 17 +++++ 9 files changed, 151 insertions(+), 134 deletions(-) rename packages/app-mobile/components/screens/ConfigScreen/plugins/{PluginStates.test.tsx => PluginStates.installed.test.tsx} (72%) rename packages/app-mobile/components/screens/ConfigScreen/plugins/{SearchPlugins.test.tsx => PluginStates.search.test.tsx} (68%) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.ts diff --git a/.eslintignore b/.eslintignore index 94526807a5f..baa8ba4735c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -615,14 +615,16 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitl packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js -packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js +packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js diff --git a/.gitignore b/.gitignore index 7a39ecce33b..b00472641f0 100644 --- a/.gitignore +++ b/.gitignore @@ -594,14 +594,16 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitl packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js -packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js +packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index a5a98eb6b6c..b18f87af4ef 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -4,7 +4,6 @@ import shim from '@joplin/lib/shim'; import * as React from 'react'; import { Alert, Linking, View, ViewStyle } from 'react-native'; import { _ } from '@joplin/lib/locale'; -import { Chip } from 'react-native-paper'; import { PluginCallback } from '../utils/usePluginCallbacks'; import StyledChip from './StyledChip'; import { themeStyle } from '../../../../global-style'; @@ -82,7 +81,7 @@ const PluginChips: React.FC = props => { if (!props.item.builtIn) { return null; } - return {_('Built-in')}; + return {_('Built-in')}; }; const renderIncompatibleChip = () => { @@ -106,7 +105,7 @@ const PluginChips: React.FC = props => { if (!props.isCompatible || !props.canUpdate) return null; return ( - {_('Update available')} + {_('Update available')} ); }; @@ -114,7 +113,7 @@ const PluginChips: React.FC = props => { if (props.item.enabled || !props.item.installed) { return null; } - return {_('Disabled')}; + return {_('Disabled')}; }; return diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx index a530ab89dac..fb04bd7af19 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx @@ -2,14 +2,17 @@ import * as React from 'react'; import { Chip, ChipProps } from 'react-native-paper'; import { useMemo } from 'react'; - -interface Props extends ChipProps { +type Props = ({ foreground: string; background: string; -} +}|{ + foreground?: undefined; + background?: undefined; +}) & ChipProps; const RecommendedChip: React.FC = props => { const themeOverride = useMemo(() => { + if (!props.foreground) return {}; return { colors: { secondaryContainer: props.background, @@ -19,9 +22,16 @@ const RecommendedChip: React.FC = props => { }; }, [props.foreground, props.background]); + const accessibilityProps: Partial = {}; + if (!props.onPress) { + // TODO: May have no effect until a future version of RN Paper. + // See https://github.com/callstack/react-native-paper/pull/4327 + accessibilityProps.accessibilityRole = 'text'; + } + return ; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 683bb5ce0e0..24aa799e7ca 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Card, Text } from 'react-native-paper'; +import { Card, Text, TouchableRipple } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import ActionButton from '../buttons/ActionButton'; @@ -8,7 +8,7 @@ import PluginChips from './PluginChips'; import { UpdateState } from '../utils/useUpdateState'; import { PluginCallback } from '../utils/usePluginCallbacks'; import { useCallback, useMemo } from 'react'; -import { StyleSheet, ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native'; import InstallButton from '../buttons/InstallButton'; import PluginTitle from './PluginTitle'; @@ -35,17 +35,16 @@ interface Props { const useStyles = (compatible: boolean) => { return useMemo(() => { - const baseCardStyle: ViewStyle = { - margin: 0, - marginTop: 8, - padding: 0, - }; - return StyleSheet.create({ - card: compatible ? baseCardStyle : { - ...baseCardStyle, - opacity: 0.7, + cardContainer: { + margin: 0, + marginTop: 8, + padding: 0, + borderRadius: 14, }, + card: !compatible ? { + opacity: 0.7, + } : { }, content: { gap: 5, }, @@ -74,29 +73,34 @@ const PluginBox: React.FC = props => { const styles = useStyles(props.isCompatible); return ( - - - - {manifest.description} - - - - {props.onAboutPress ? aboutButton : null} - {props.onInstall ? installButton : null} - - + + + + {manifest.description} + + + + {props.onAboutPress ? aboutButton : null} + {props.onInstall ? installButton : null} + + + ); }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx similarity index 72% rename from packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.tsx rename to packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 2192efd2a8f..6ae794a065b 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -1,65 +1,24 @@ import * as React from 'react'; -import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; -import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils'; +import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; import { act, render, screen } from '@testing-library/react-native'; import '@testing-library/react-native/extend-expect'; -import Setting from '@joplin/lib/models/Setting'; import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService'; -import { useCallback, useState } from 'react'; import pluginServiceSetup from './testUtils/pluginServiceSetup'; -import PluginStates from './PluginStates'; -import configScreenStyles from '../configScreenStyles'; -import { remove, writeFile } from 'fs-extra'; +import { writeFile } from 'fs-extra'; import { join } from 'path'; import shim from '@joplin/lib/shim'; import { resetRepoApi } from './utils/useRepoApi'; import { Store } from 'redux'; import { AppState } from '../../../../utils/types'; import createMockReduxStore from '../../../../utils/testing/createMockReduxStore'; +import WrappedPluginStates from './testUtils/WrappedPluginStates'; +import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor'; -interface WrapperProps { - initialPluginSettings: PluginSettings; -} let reduxStore: Store = null; -const shouldShowBasedOnSettingSearchQuery = ()=>true; -const PluginStatesWrapper = (props: WrapperProps) => { - const styles = configScreenStyles(Setting.THEME_LIGHT); - - const [pluginSettings, setPluginSettings] = useState(() => { - return props.initialPluginSettings ?? {}; - }); - - const updatePluginStates = useCallback((newStates: PluginSettings) => { - setPluginSettings(newStates); - }, []); - - return ( - - ); -}; - -let repoTempDir: string|null = null; -const mockRepositoryApiConstructor = async () => { - if (repoTempDir) { - await remove(repoTempDir); - } - repoTempDir = await createTempDir(); - - RepositoryApi.ofDefaultJoplinRepo = jest.fn((_tempDirPath: string, appType, installMode) => { - return new RepositoryApi(`${supportDir}/pluginRepo`, repoTempDir, appType, installMode); - }); -}; - const loadMockPlugin = async (id: string, name: string, version: string, pluginSettings: PluginSettings) => { const service = PluginService.instance(); const pluginSource = ` @@ -87,7 +46,7 @@ const loadMockPlugin = async (id: string, name: string, version: string, pluginS }); }; -describe('PluginStates', () => { +describe('PluginStates/installed', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); @@ -129,17 +88,18 @@ describe('PluginStates', () => { expect(PluginService.instance().plugins[backlinksPluginId]).toBeTruthy(); render( - , ); expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); expect(await screen.findByText(/^Backlinks to note/)).toBeVisible(); - expect(await screen.findByRole('button', { name: 'Update ABC Sheet Music', disabled: false })).toBeVisible(); + expect(await screen.findByText(/^ABC Sheet Music.*Update available/)).toBeVisible(); // Backlinks to note should not be updatable on iOS (it's not _recommended). - const backlinksToNoteQuery = { name: 'Update Backlinks to note', disabled: false }; + const backlinksToNoteQuery = { name: /^Backlinks to note.*Update available/ }; if (platform === 'android') { expect(await screen.findByRole('button', backlinksToNoteQuery)).toBeVisible(); } else { @@ -156,12 +116,14 @@ describe('PluginStates', () => { expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy(); render( - , ); - expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); - expect(await screen.findByRole('button', { name: 'Update ABC Sheet Music', disabled: false })).toBeVisible(); + const abcSheetMusicCard = await screen.findByText(/^ABC Sheet Music/); + expect(abcSheetMusicCard).toBeVisible(); + expect(await screen.findByText('Update available')).toBeVisible(); expect(await screen.findByText(`v${outdatedVersion}`)).toBeVisible(); }); @@ -169,8 +131,9 @@ describe('PluginStates', () => { const pluginSettings: PluginSettings = { }; render( - , ); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx similarity index 68% rename from packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.tsx rename to packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx index 4cbfd73233c..ae0bcf041e2 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx @@ -1,37 +1,14 @@ import * as React from 'react'; -import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi'; import { mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; import { render, screen, userEvent, waitFor } from '@testing-library/react-native'; import '@testing-library/react-native/extend-expect'; -import SearchPlugins from './SearchPlugins'; -import Setting from '@joplin/lib/models/Setting'; -import { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import pluginServiceSetup from './testUtils/pluginServiceSetup'; -import newRepoApi from './testUtils/newRepoApi'; import createMockReduxStore from '../../../../utils/testing/createMockReduxStore'; - -interface WrapperProps { - repoApi: RepositoryApi; - repoApiInitialized?: boolean; - pluginSettings?: PluginSettings; - onUpdatePluginStates?: (states: PluginSettings)=> void; -} - -const noOpFunction = ()=>{}; - -const SearchWrapper = (props: WrapperProps) => { - return ( - - ); -}; +import WrappedPluginStates from './testUtils/WrappedPluginStates'; +import { AppState } from '../../../../utils/types'; +import { Store } from 'redux'; const expectSearchResultCountToBe = async (count: number) => { await waitFor(() => { @@ -39,16 +16,19 @@ const expectSearchResultCountToBe = async (count: number) => { }); }; -describe('SearchPlugins', () => { +let reduxStore: Store; + +describe('PluginStates/search', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); - pluginServiceSetup(createMockReduxStore()); + reduxStore = createMockReduxStore(); + pluginServiceSetup(reduxStore); + mockMobilePlatform('android'); }); it('should find results', async () => { - const repoApi = await newRepoApi(InstallMode.Default); - render(); + render(); const searchBox = screen.queryByPlaceholderText('Search plugins'); expect(searchBox).toBeVisible(); @@ -75,8 +55,8 @@ describe('SearchPlugins', () => { it('should only show recommended plugin search results on iOS-like environments', async () => { // iOS uses restricted install mode - const repoApi = await newRepoApi(InstallMode.Restricted); - render(); + mockMobilePlatform('ios'); + render(); const searchBox = screen.queryByPlaceholderText('Search plugins'); expect(searchBox).toBeVisible(); @@ -100,9 +80,7 @@ describe('SearchPlugins', () => { }); it('should mark incompatible plugins as incompatible', async () => { - const mock = mockMobilePlatform('android'); - const repoApi = await newRepoApi(InstallMode.Default); - render(); + render(); const searchBox = screen.queryByPlaceholderText('Search plugins'); const user = userEvent.setup(); @@ -116,7 +94,5 @@ describe('SearchPlugins', () => { await expectSearchResultCountToBe(1); expect(await screen.findByText(/Note list and side bar/i)).toBeVisible(); expect(await screen.findByText('Incompatible')).toBeVisible(); - - mock.reset(); }); }); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx new file mode 100644 index 00000000000..aba08002722 --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; +import configScreenStyles from '../../configScreenStyles'; +import Setting from '@joplin/lib/models/Setting'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { PaperProvider } from 'react-native-paper'; +import PluginStates from '../PluginStates'; +import { AppState } from '../../../../../utils/types'; +import { useCallback, useState } from 'react'; + +interface WrapperProps { + initialPluginSettings: PluginSettings; + store: Store; +} +const shouldShowBasedOnSettingSearchQuery = ()=>true; + +const PluginStatesWrapper = (props: WrapperProps) => { + const styles = configScreenStyles(Setting.THEME_LIGHT); + + const [pluginSettings, setPluginSettings] = useState(() => { + return props.initialPluginSettings ?? {}; + }); + + const updatePluginStates = useCallback((newStates: PluginSettings) => { + setPluginSettings(newStates); + }, []); + + return ( + + + + + + ); +}; + +export default PluginStatesWrapper; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.ts new file mode 100644 index 00000000000..22cd96389ed --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.ts @@ -0,0 +1,17 @@ +import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; +import { createTempDir, supportDir } from '@joplin/lib/testing/test-utils'; +import { remove } from 'fs-extra'; + +let repoTempDir: string|null = null; +const mockRepositoryApiConstructor = async () => { + if (repoTempDir) { + await remove(repoTempDir); + } + repoTempDir = await createTempDir(); + + RepositoryApi.ofDefaultJoplinRepo = jest.fn((_tempDirPath: string, appType, installMode) => { + return new RepositoryApi(`${supportDir}/pluginRepo`, repoTempDir, appType, installMode); + }); +}; + +export default mockRepositoryApiConstructor; From 81fb68428b551b4fe908e86b41229530b6ddf685 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 17:21:25 -0700 Subject: [PATCH 24/46] Fix several tests (many still broken) --- .../screens/ConfigScreen/plugins/PluginBox/index.tsx | 1 + .../ConfigScreen/plugins/PluginStates.installed.test.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 24aa799e7ca..d0b9725c055 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -75,6 +75,7 @@ const PluginBox: React.FC = props => { return ( diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 6ae794a065b..e24e8bdcb71 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -96,14 +96,14 @@ describe('PluginStates/installed', () => { expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); expect(await screen.findByText(/^Backlinks to note/)).toBeVisible(); - expect(await screen.findByText(/^ABC Sheet Music.*Update available/)).toBeVisible(); + const updateMarkers = await screen.findAllByText('Update available'); // Backlinks to note should not be updatable on iOS (it's not _recommended). - const backlinksToNoteQuery = { name: /^Backlinks to note.*Update available/ }; + // ABC Sheet Music should always be updatable if (platform === 'android') { - expect(await screen.findByRole('button', backlinksToNoteQuery)).toBeVisible(); + expect(updateMarkers).toHaveLength(2); } else { - expect(await screen.queryByRole('button', backlinksToNoteQuery)).toBeNull(); + expect(updateMarkers).toHaveLength(1); } }); From e7bfe064470705360a57fe545e6d878b249970a3 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 17:22:58 -0700 Subject: [PATCH 25/46] Try to reduce number of warnings during testing --- .../plugins/PluginStates.installed.test.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index e24e8bdcb71..13fcd642053 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -87,7 +87,7 @@ describe('PluginStates/installed', () => { await loadMockPlugin(backlinksPluginId, 'Backlinks to note', '0.0.1', defaultPluginSettings); expect(PluginService.instance().plugins[backlinksPluginId]).toBeTruthy(); - render( + const wrapper = render( { } else { expect(updateMarkers).toHaveLength(1); } + + wrapper.unmount(); }); it('should show the current plugin version on updatable plugins', async () => { @@ -115,7 +117,7 @@ describe('PluginStates/installed', () => { await loadMockPlugin(abcPluginId, 'ABC Sheet Music', outdatedVersion, defaultPluginSettings); expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy(); - render( + const wrapper = render( { expect(abcSheetMusicCard).toBeVisible(); expect(await screen.findByText('Update available')).toBeVisible(); expect(await screen.findByText(`v${outdatedVersion}`)).toBeVisible(); + + wrapper.unmount(); }); it('should update the list of installed plugins when a plugin is installed and uninstalled', async () => { const pluginSettings: PluginSettings = { }; - render( + const wrapper = render( { await act(() => PluginService.instance().uninstallPlugin(testPluginId1)); expect(await screen.findByText(/^A test plugin/)).toBeVisible(); expect(screen.queryByText(/^ABC Sheet Music/)).toBeNull(); + + wrapper.unmount(); }); }); From 4639da89bcdfcbfc3761b91cc04dadd5ffc39b03 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 19:59:40 -0700 Subject: [PATCH 26/46] Fix tests --- .../plugins/PluginStates.installed.test.tsx | 2 +- .../plugins/PluginStates.search.test.tsx | 50 ++++++++++++++----- .../ConfigScreen/plugins/SearchPlugins.tsx | 2 +- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 13fcd642053..8d120a9f992 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -46,7 +46,7 @@ const loadMockPlugin = async (id: string, name: string, version: string, pluginS }); }; -describe('PluginStates/installed', () => { +describe('PluginStates.installed', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx index ae0bcf041e2..22a6920edda 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx @@ -9,6 +9,8 @@ import createMockReduxStore from '../../../../utils/testing/createMockReduxStore import WrappedPluginStates from './testUtils/WrappedPluginStates'; import { AppState } from '../../../../utils/types'; import { Store } from 'redux'; +import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor'; +import { resetRepoApi } from './utils/useRepoApi'; const expectSearchResultCountToBe = async (count: number) => { await waitFor(() => { @@ -16,26 +18,36 @@ const expectSearchResultCountToBe = async (count: number) => { }); }; +// The search box is initially read-only -- waits for it to be editable. +const getEditableSearchBox = async () => { + const searchBox = await screen.findByPlaceholderText('Search plugins'); + expect(searchBox).toBeVisible(); + + await waitFor(() => { + expect(searchBox.props.editable).toBe(true); + }); + + return searchBox; +}; + let reduxStore: Store; -describe('PluginStates/search', () => { +describe('PluginStates.search', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); reduxStore = createMockReduxStore(); pluginServiceSetup(reduxStore); mockMobilePlatform('android'); + resetRepoApi(); + + await mockRepositoryApiConstructor(); }); it('should find results', async () => { - render(); - - const searchBox = screen.queryByPlaceholderText('Search plugins'); - expect(searchBox).toBeVisible(); - - // No plugin cards should be visible by default - expect(screen.queryAllByTestId('plugin-card')).toHaveLength(0); + const wrapper = render(); + const searchBox = await getEditableSearchBox(); const user = userEvent.setup(); await user.type(searchBox, 'backlinks'); @@ -51,19 +63,25 @@ describe('PluginStates/search', () => { await waitFor(() => { expect(screen.queryAllByTestId('plugin-card').length).toBeGreaterThan(2); }); + + wrapper.unmount(); }); it('should only show recommended plugin search results on iOS-like environments', async () => { // iOS uses restricted install mode mockMobilePlatform('ios'); - render(); + await mockRepositoryApiConstructor(); + + const wrapper = render(); - const searchBox = screen.queryByPlaceholderText('Search plugins'); - expect(searchBox).toBeVisible(); + const searchBox = await getEditableSearchBox(); const user = userEvent.setup(); + await user.press(searchBox); await user.type(searchBox, 'abc'); + expect(searchBox.props.value).toBe('abc'); + // Should find recommended plugins await expectSearchResultCountToBe(1); @@ -77,14 +95,18 @@ describe('PluginStates/search', () => { await expectSearchResultCountToBe(1); expect(screen.getByText(/ABC Sheet Music/i)).toBeTruthy(); expect(screen.queryByText(/backlink/i)).toBeNull(); + + wrapper.unmount(); }); it('should mark incompatible plugins as incompatible', async () => { - render(); + const wrapper = render(); - const searchBox = screen.queryByPlaceholderText('Search plugins'); + const searchBox = await getEditableSearchBox(); const user = userEvent.setup(); + await user.press(searchBox); await user.type(searchBox, 'abc'); + expect(searchBox.props.value).toBe('abc'); await expectSearchResultCountToBe(1); expect(screen.queryByText('Incompatible')).toBeNull(); @@ -94,5 +116,7 @@ describe('PluginStates/search', () => { await expectSearchResultCountToBe(1); expect(await screen.findByText(/Note list and side bar/i)).toBeVisible(); expect(await screen.findByText('Incompatible')).toBeVisible(); + + wrapper.unmount(); }); }); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx index f063fe0b9c1..cd81c189903 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx @@ -123,7 +123,7 @@ const PluginSearch: React.FC = props => { testID='searchbar' mode='outlined' left={} - label={_('Search plugins')} + placeholder={_('Search plugins')} onChangeText={setSearchQuery} value={searchQuery} editable={props.repoApiInitialized} From 75a3e936353bf1549172e54d69ab50d58d89b882 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 20:17:06 -0700 Subject: [PATCH 27/46] Adjust loading indicator --- .../components/screens/ConfigScreen/plugins/PluginStates.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index b9ff8f10beb..ca2a6d62e6e 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; import { View, StyleSheet } from 'react-native'; -import { Banner, Button, Text } from 'react-native-paper'; +import { Banner, Text, Button, ProgressBar } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginToggle from './PluginToggle'; @@ -107,7 +107,7 @@ const PluginStates: React.FC = props => { ; } else { - return {_('Loading plugin repository...')}; + return ; } }; From 8d957872e99f8f25d6dbb8a6c588f2e8634294b3 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 20:37:04 -0700 Subject: [PATCH 28/46] Wrap installed and search sections in different accordion groups --- .../plugins/PluginStates.installed.test.tsx | 11 ++++- .../plugins/PluginStates.search.test.tsx | 16 +++++-- .../ConfigScreen/plugins/PluginStates.tsx | 47 +++++++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 8d120a9f992..d2415e268b1 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; -import { act, render, screen } from '@testing-library/react-native'; +import { act, render, screen, userEvent } from '@testing-library/react-native'; import '@testing-library/react-native/extend-expect'; import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService'; @@ -46,6 +46,11 @@ const loadMockPlugin = async (id: string, name: string, version: string, pluginS }); }; +const showInstalledTab = async () => { + const installedTab = await screen.findByText('Installed plugins'); + await userEvent.press(installedTab); +}; + describe('PluginStates.installed', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); @@ -93,6 +98,7 @@ describe('PluginStates.installed', () => { store={reduxStore} />, ); + await showInstalledTab(); expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); expect(await screen.findByText(/^Backlinks to note/)).toBeVisible(); @@ -123,6 +129,8 @@ describe('PluginStates.installed', () => { store={reduxStore} />, ); + await showInstalledTab(); + const abcSheetMusicCard = await screen.findByText(/^ABC Sheet Music/); expect(abcSheetMusicCard).toBeVisible(); expect(await screen.findByText('Update available')).toBeVisible(); @@ -140,6 +148,7 @@ describe('PluginStates.installed', () => { store={reduxStore} />, ); + await showInstalledTab(); // Initially, no plugins should be visible. expect(screen.queryByText(/^ABC Sheet Music/)).toBeNull(); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx index 22a6920edda..b5954708bdd 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.tsx @@ -18,6 +18,11 @@ const expectSearchResultCountToBe = async (count: number) => { }); }; +const showSearchTab = async () => { + const searchAccordion = await screen.findByText('Install new plugins'); + await userEvent.press(searchAccordion); +}; + // The search box is initially read-only -- waits for it to be editable. const getEditableSearchBox = async () => { const searchBox = await screen.findByPlaceholderText('Search plugins'); @@ -47,8 +52,9 @@ describe('PluginStates.search', () => { it('should find results', async () => { const wrapper = render(); - const searchBox = await getEditableSearchBox(); const user = userEvent.setup(); + await showSearchTab(); + const searchBox = await getEditableSearchBox(); await user.type(searchBox, 'backlinks'); // Should find one result @@ -74,9 +80,11 @@ describe('PluginStates.search', () => { const wrapper = render(); + const user = userEvent.setup(); + await showSearchTab(); + const searchBox = await getEditableSearchBox(); - const user = userEvent.setup(); await user.press(searchBox); await user.type(searchBox, 'abc'); @@ -102,8 +110,10 @@ describe('PluginStates.search', () => { it('should mark incompatible plugins as incompatible', async () => { const wrapper = render(); - const searchBox = await getEditableSearchBox(); const user = userEvent.setup(); + await showSearchTab(); + + const searchBox = await getEditableSearchBox(); await user.press(searchBox); await user.type(searchBox, 'abc'); expect(searchBox.props.value).toBe('abc'); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index ca2a6d62e6e..115ce1fbb40 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; import { View, StyleSheet } from 'react-native'; -import { Banner, Text, Button, ProgressBar } from 'react-native-paper'; +import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; import { _ } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginToggle from './PluginToggle'; @@ -58,6 +58,7 @@ const styles = StyleSheet.create({ installedPluginsContainer: { marginLeft: 8, marginRight: 8, + marginBottom: 10, }, }); @@ -157,25 +158,41 @@ const PluginStates: React.FC = props => { !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText()) ); - const searchComponent = ( - + const searchAccordion = ( + + + ); return ( {renderRepoApiStatus()} - - {installedPluginCards} - - {showSearch ? searchComponent : null} + + + + {installedPluginCards} + + + + {showSearch ? searchAccordion : null} + + Date: Thu, 23 May 2024 21:15:33 -0700 Subject: [PATCH 29/46] Use correct singular/plural description for installed plugins accordion group --- .../screens/ConfigScreen/plugins/PluginStates.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 115ce1fbb40..b6863b8d9ce 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; import { View, StyleSheet } from 'react-native'; import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; -import { _ } from '@joplin/lib/locale'; +import { _, _n } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginToggle from './PluginToggle'; import SearchPlugins from './SearchPlugins'; @@ -162,7 +162,7 @@ const PluginStates: React.FC = props => { = props => { {installedPluginCards} From dda04eddb1302355337615bf8d143ec09d1fd54a Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 21:38:06 -0700 Subject: [PATCH 30/46] Search: Use data from installed plugins, when available --- .../plugins/PluginBox/PluginChips.tsx | 9 +++ .../ConfigScreen/plugins/PluginBox/index.tsx | 2 + .../ConfigScreen/plugins/PluginInfoModal.tsx | 1 + .../ConfigScreen/plugins/PluginStates.tsx | 14 +++-- .../ConfigScreen/plugins/PluginToggle.tsx | 7 +-- .../ConfigScreen/plugins/SearchPlugins.tsx | 56 +++++++++++++------ .../plugins/utils/usePluginItem.ts | 1 + 7 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index b18f87af4ef..cffaccd361d 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -14,6 +14,7 @@ interface Props { hasErrors: boolean; isCompatible: boolean; canUpdate: boolean; + showInstalledChip: boolean; onShowPluginLog?: PluginCallback; } @@ -116,8 +117,16 @@ const PluginChips: React.FC = props => { return {_('Disabled')}; }; + const renderInstalledChip = () => { + if (!props.showInstalledChip) { + return null; + } + return {_('Installed')}; + }; + return {renderIncompatibleChip()} + {renderInstalledChip()} {renderErrorsChip()} {renderRecommendedChip()} {renderBuiltInChip()} diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index d0b9725c055..2d589b1eb61 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -22,6 +22,7 @@ interface Props { themeId: number; item: PluginItem; isCompatible: boolean; + showInstalledChip: boolean; hasErrors?: boolean; installState?: InstallState; @@ -90,6 +91,7 @@ const PluginBox: React.FC = props => { = props => { = props => { key={`plugin-${pluginId}`} themeId={props.themeId} pluginId={pluginId} - styles={props.styles} pluginSettings={pluginSettings} updatablePluginIds={updatablePluginIds} updatingPluginIds={updatingPluginIds} - updatePluginStates={props.updatePluginStates} + showInstalledChip={false} onShowPluginInfo={onShowPluginInfo} callbacks={pluginCallbacks} - repoApi={repoApi} />, ); } @@ -158,6 +156,8 @@ const PluginStates: React.FC = props => { !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText()) ); + const [searchQuery, setSearchQuery] = useState(''); + const searchAccordion = ( = props => { id='search' > ); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx index 70fa937fc64..59de7ec5895 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; -import { ConfigScreenStyles } from '../configScreenStyles'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { useMemo } from 'react'; import PluginBox from './PluginBox'; -import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; import useUpdateState from './utils/useUpdateState'; import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks'; import usePluginItem from './utils/usePluginItem'; @@ -12,15 +10,13 @@ import usePluginItem from './utils/usePluginItem'; interface Props { pluginId: string; themeId: number; - styles: ConfigScreenStyles; pluginSettings: PluginSettings; updatablePluginIds: Record; updatingPluginIds: Record; - repoApi: RepositoryApi; + showInstalledChip: boolean; callbacks: PluginCallbacks; onShowPluginInfo: PluginCallback; - updatePluginStates: (settingValue: PluginSettings)=> void; } const PluginToggle: React.FC = props => { @@ -44,6 +40,7 @@ const PluginToggle: React.FC = props => { item={pluginItem} isCompatible={isCompatible} hasErrors={plugin.hasErrors} + showInstalledChip={props.showInstalledChip} onShowPluginLog={props.callbacks.onShowPluginLog} onShowPluginInfo={props.onShowPluginInfo} updateState={updateState} diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx index cd81c189903..da23cfd69c0 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.tsx @@ -7,21 +7,29 @@ import { useCallback, useMemo, useState } from 'react'; import { FlatList, StyleSheet, View } from 'react-native'; import { TextInput, Text } from 'react-native-paper'; import PluginBox, { InstallState } from './PluginBox'; -import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; +import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; -import { PluginCallbacks } from './utils/usePluginCallbacks'; +import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks'; +import PluginToggle from './PluginToggle'; interface Props { themeId: number; - pluginSettings: SerializedPluginSettings; + pluginSettings: PluginSettings; repoApiInitialized: boolean; onUpdatePluginStates: (states: PluginSettings)=> void; repoApi: RepositoryApi; installingPluginIds: Record; + updatingPluginIds: Record; + updatablePluginIds: Record; + callbacks: PluginCallbacks; + onShowPluginInfo: PluginCallback; + + searchQuery: string; + setSearchQuery: (newQuery: string)=> void; } interface SearchResultRecord { @@ -43,7 +51,7 @@ const styles = StyleSheet.create({ }); const PluginSearch: React.FC = props => { - const [searchQuery, setSearchQuery] = useState(''); + const { searchQuery, setSearchQuery } = props; const [searchResultManifests, setSearchResultManifests] = useState([]); useAsyncEffect(async event => { @@ -96,18 +104,34 @@ const PluginSearch: React.FC = props => { const renderResult = useCallback(({ item }: { item: SearchResultRecord }) => { const manifest = item.item.manifest; - return ( - - ); - }, [onInstall, props.themeId]); + if (item.installState === InstallState.Installed && PluginService.instance().isPluginLoaded(manifest.id)) { + return ( + + ); + } else { + return ( + + ); + } + }, [onInstall, props.themeId, props.pluginSettings, props.updatingPluginIds, props.updatablePluginIds, props.onShowPluginInfo, props.callbacks]); const renderResultsCount = () => { if (!searchQuery.length) return null; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts index d73a8397bb0..6cb2a7b5198 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts @@ -13,6 +13,7 @@ const usePluginItem = (id: string, pluginSettings: PluginSettings): PluginItem = }, [id]); return useMemo(() => { + if (!plugin) return null; const settings = pluginSettings[id]; const manifest: PluginManifest|null = plugin?.manifest ?? null; From 91cbaa3faec976046a3fd053624a3efaca5af06f Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 23 May 2024 22:12:52 -0700 Subject: [PATCH 31/46] Fix desktop build --- .../app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx | 1 + .../gui/ConfigScreen/controls/plugins/PluginsStates.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx index 7fe971e03bd..356cf87148c 100644 --- a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx +++ b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx @@ -38,6 +38,7 @@ interface Props { function manifestToItem(manifest: PluginManifest): PluginItem { return { manifest: manifest, + installed: true, enabled: true, deleted: false, devMode: false, diff --git a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx index 425818807bd..651a61047ab 100644 --- a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx +++ b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx @@ -83,6 +83,7 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[ output.push({ manifest: plugin.manifest, + installed: true, enabled: setting.enabled, deleted: setting.deleted, devMode: plugin.devMode, From 4fdb5f79e8b60dca1e7e2ac04a449d8ed2960368 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 24 May 2024 14:20:30 -0700 Subject: [PATCH 32/46] Adjust modal styles --- packages/app-mobile/components/DismissibleDialog.tsx | 4 ++++ packages/app-mobile/components/Modal.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/app-mobile/components/DismissibleDialog.tsx b/packages/app-mobile/components/DismissibleDialog.tsx index 3670560dffc..82c7fac7cda 100644 --- a/packages/app-mobile/components/DismissibleDialog.tsx +++ b/packages/app-mobile/components/DismissibleDialog.tsx @@ -51,6 +51,10 @@ const useStyles = (themeId: number, containerStyle: ViewStyle) => { dialogContainer: { display: 'flex', flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + flexGrow: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', }, }); }, [themeId, windowSize.width, windowSize.height, containerStyle]); diff --git a/packages/app-mobile/components/Modal.tsx b/packages/app-mobile/components/Modal.tsx index 0f8f5da18b8..d596b4a032d 100644 --- a/packages/app-mobile/components/Modal.tsx +++ b/packages/app-mobile/components/Modal.tsx @@ -15,13 +15,13 @@ const useStyles = () => { return useMemo(() => { return StyleSheet.create({ modalContainer: isLandscape ? { - marginRight: hasNotch() ? 60 : 0, - marginLeft: hasNotch() ? 60 : 0, - marginTop: 15, - marginBottom: 15, + paddingRight: hasNotch() ? 60 : 0, + paddingLeft: hasNotch() ? 60 : 0, + paddingTop: 15, + paddingBottom: 15, } : { - marginTop: hasNotch() ? 65 : 15, - marginBottom: hasNotch() ? 35 : 15, + paddingTop: hasNotch() ? 65 : 15, + paddingBottom: hasNotch() ? 35 : 15, }, }); }, [isLandscape]); From 106d453e69ae5bce07f9b553cc1fca6de17d588d Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 24 May 2024 14:22:35 -0700 Subject: [PATCH 33/46] Simplify primary button default colors --- packages/app-mobile/components/buttons/TextButton.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/app-mobile/components/buttons/TextButton.tsx b/packages/app-mobile/components/buttons/TextButton.tsx index d9264a39790..acc93b574e2 100644 --- a/packages/app-mobile/components/buttons/TextButton.tsx +++ b/packages/app-mobile/components/buttons/TextButton.tsx @@ -38,12 +38,7 @@ const useStyles = ({ themeId }: Props) => { outline: theme.destructiveColor, }, }, - primaryButton: { - colors: { - primary: theme.color4, - onPrimary: theme.backgroundColor4, - }, - }, + primaryButton: { }, }; return { themeOverride }; From 56ffd134d41f052ff9aa0b695f43aedb3da16f2b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 24 May 2024 14:25:23 -0700 Subject: [PATCH 34/46] Fix failing test --- packages/lib/services/style/themeToCss.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/services/style/themeToCss.test.ts b/packages/lib/services/style/themeToCss.test.ts index d1abbb949d1..8ff2f2c39be 100644 --- a/packages/lib/services/style/themeToCss.test.ts +++ b/packages/lib/services/style/themeToCss.test.ts @@ -90,6 +90,7 @@ const expected = ` --joplin-color-warn2: #ffcb81; --joplin-color-warn3: #ff7626; --joplin-color-warn-url: #155BDA; + --joplin-destructive-color: #F00000; --joplin-divider-color: #dddddd; --joplin-header-background-color: #ffffff; --joplin-odd-background-color: #eeeeee; From a9622c4b69c38a36bc1384785cb25ec998824e43 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 24 May 2024 15:48:48 -0700 Subject: [PATCH 35/46] Fix linter errors --- .../screens/ConfigScreen/plugins/utils/useUpdateState.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.ts index b8b3bf30ab9..2b02a622e27 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.ts @@ -1,5 +1,5 @@ -import { PluginSettings } from "@joplin/lib/services/plugins/PluginService"; -import { useMemo } from "react"; +import { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; +import { useMemo } from 'react'; export enum UpdateState { Idle = 1, @@ -36,4 +36,4 @@ const useUpdateState = ({ pluginId, pluginSettings, updatablePluginIds, updating }, [pluginSettings, updatingPluginIds, pluginId, updatablePluginIds]); }; -export default useUpdateState; \ No newline at end of file +export default useUpdateState; From 9cbaa77bfc0316ceaef8e8184719547093735724 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 27 May 2024 05:30:00 -0700 Subject: [PATCH 36/46] Remove extra styles from Modal.tsx These styles were introduced by an incorrect merge. --- packages/app-mobile/components/Modal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-mobile/components/Modal.tsx b/packages/app-mobile/components/Modal.tsx index fbf73aa98a1..58fff399f5e 100644 --- a/packages/app-mobile/components/Modal.tsx +++ b/packages/app-mobile/components/Modal.tsx @@ -49,7 +49,6 @@ const ModalElement: React.FC = ({ {content} From 735c5e58073e238dd91001a00d819e999dc1c4b3 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 27 May 2024 05:52:30 -0700 Subject: [PATCH 37/46] Better handle showing an info modal for an uninstalled plugin --- .../ConfigScreen/plugins/PluginInfoModal.tsx | 2 +- .../ConfigScreen/plugins/PluginToggle.tsx | 2 +- .../plugins/utils/usePluginItem.ts | 29 ++++++++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index ab54f65575e..a7ed13dcb46 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -94,7 +94,7 @@ const EnabledSwitch: React.FC = props => { const PluginInfoModalContent: React.FC = props => { const pluginId = props.initialItem.manifest.id; - const item = usePluginItem(pluginId, props.pluginSettings); + const item = usePluginItem(pluginId, props.pluginSettings, props.initialItem); const manifest = item.manifest; const isCompatible = useMemo(() => { diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx index 59de7ec5895..25a7607ac2c 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx @@ -27,7 +27,7 @@ const PluginToggle: React.FC = props => { updatingPluginIds: props.updatingPluginIds, pluginSettings: props.pluginSettings, }); - const pluginItem = usePluginItem(pluginId, props.pluginSettings); + const pluginItem = usePluginItem(pluginId, props.pluginSettings, null); const plugin = useMemo(() => PluginService.instance().pluginById(pluginId), [pluginId]); const isCompatible = useMemo(() => { diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts index 6cb2a7b5198..d2ddcd859c1 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts @@ -1,9 +1,11 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { PluginManifest } from '@joplin/lib/services/plugins/utils/types'; -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; -const usePluginItem = (id: string, pluginSettings: PluginSettings): PluginItem => { +// initialItem is used when the plugin is not installed. For example, if the plugin item is being +// created from search results. +const usePluginItem = (id: string, pluginSettings: PluginSettings, initialItem: PluginItem|null): PluginItem => { const plugin = useMemo(() => { if (!PluginService.instance().pluginIds.includes(id)) { return null; @@ -12,23 +14,30 @@ const usePluginItem = (id: string, pluginSettings: PluginSettings): PluginItem = return PluginService.instance().pluginById(id); }, [id]); + const lastManifest = useRef(); + if (plugin) { + lastManifest.current = plugin.manifest; + } else if (!lastManifest.current) { + lastManifest.current = initialItem?.manifest; + } + const manifest = lastManifest.current; + return useMemo(() => { - if (!plugin) return null; + if (!manifest) return null; const settings = pluginSettings[id]; - const manifest: PluginManifest|null = plugin?.manifest ?? null; return { id, manifest, installed: !!settings, - enabled: settings?.enabled, - deleted: settings?.deleted, - devMode: plugin.devMode, - builtIn: plugin.builtIn, - hasBeenUpdated: settings?.hasBeenUpdated, + enabled: settings?.enabled ?? false, + deleted: settings?.deleted ?? false, + hasBeenUpdated: settings?.hasBeenUpdated ?? false, + devMode: plugin?.devMode ?? false, + builtIn: plugin?.builtIn ?? false, }; - }, [plugin, id, pluginSettings]); + }, [plugin, id, pluginSettings, manifest]); }; export default usePluginItem; From 04dcb5a48c922561bc9a005eec772d83ea993adb Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 27 May 2024 06:01:03 -0700 Subject: [PATCH 38/46] Fix TouchableRipple animation on Android --- .../screens/ConfigScreen/plugins/PluginBox/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 2d589b1eb61..1af8947ae5d 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -36,6 +36,8 @@ interface Props { const useStyles = (compatible: boolean) => { return useMemo(() => { + // For the TouchableRipple to work on Android, the card needs a transparent background. + const baseCard = { backgroundColor: 'transparent' }; return StyleSheet.create({ cardContainer: { margin: 0, @@ -44,8 +46,9 @@ const useStyles = (compatible: boolean) => { borderRadius: 14, }, card: !compatible ? { + ...baseCard, opacity: 0.7, - } : { }, + } : baseCard, content: { gap: 5, }, From e424dafcab7a39cc251534efe4443ea931c1e524 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 27 May 2024 06:09:11 -0700 Subject: [PATCH 39/46] Improve commenting, file names --- .eslintignore | 2 +- .gitignore | 2 +- .../plugins/{PluginToggle.tsx => InstalledPluginBox.tsx} | 4 ++-- .../screens/ConfigScreen/plugins/PluginBox/index.tsx | 3 +++ .../components/screens/ConfigScreen/plugins/PluginStates.tsx | 4 ++-- .../components/screens/ConfigScreen/plugins/SearchPlugins.tsx | 4 ++-- 6 files changed, 11 insertions(+), 8 deletions(-) rename packages/app-mobile/components/screens/ConfigScreen/plugins/{PluginToggle.tsx => InstalledPluginBox.tsx} (94%) diff --git a/.eslintignore b/.eslintignore index baa8ba4735c..d3a84defd3a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -610,6 +610,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js +packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js @@ -618,7 +619,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js diff --git a/.gitignore b/.gitignore index b00472641f0..084fd7ff966 100644 --- a/.gitignore +++ b/.gitignore @@ -589,6 +589,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js +packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js @@ -597,7 +598,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js -packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx similarity index 94% rename from packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx rename to packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx index 25a7607ac2c..7433e4b0d9a 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx @@ -19,7 +19,7 @@ interface Props { onShowPluginInfo: PluginCallback; } -const PluginToggle: React.FC = props => { +const InstalledPluginBox: React.FC = props => { const pluginId = props.pluginId; const updateState = useUpdateState({ pluginId, @@ -48,4 +48,4 @@ const PluginToggle: React.FC = props => { ); }; -export default PluginToggle; +export default InstalledPluginBox; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 1af8947ae5d..34c369ac9c4 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -22,6 +22,9 @@ interface Props { themeId: number; item: PluginItem; isCompatible: boolean; + + // In some cases, showing an "installed" chip is redundant (e.g. in the "installed plugins" + // tab). In other places (e.g. search), an "installed" chip is important. showInstalledChip: boolean; hasErrors?: boolean; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 2454be2b2fd..5be01928d0c 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -5,7 +5,7 @@ import { View, StyleSheet } from 'react-native'; import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; import { _, _n } from '@joplin/lib/locale'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; -import PluginToggle from './PluginToggle'; +import InstalledPluginBox from './InstalledPluginBox'; import SearchPlugins from './SearchPlugins'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import useRepoApi from './utils/useRepoApi'; @@ -137,7 +137,7 @@ const PluginStates: React.FC = props => { if (!props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(plugin.manifest.name)) { installedPluginCards.push( - = props => { if (item.installState === InstallState.Installed && PluginService.instance().isPluginLoaded(manifest.id)) { return ( - Date: Mon, 27 May 2024 07:58:06 -0700 Subject: [PATCH 40/46] Limit the maximum dialog size --- .../components/DismissibleDialog.tsx | 22 ++++++++++++++----- .../ConfigScreen/plugins/PluginInfoModal.tsx | 3 ++- .../dialogs/PluginPanelViewer.tsx | 3 ++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/app-mobile/components/DismissibleDialog.tsx b/packages/app-mobile/components/DismissibleDialog.tsx index 67c61068748..ab07f5fd9c2 100644 --- a/packages/app-mobile/components/DismissibleDialog.tsx +++ b/packages/app-mobile/components/DismissibleDialog.tsx @@ -6,20 +6,32 @@ import { themeStyle } from './global-style'; import Modal from './Modal'; import { _ } from '@joplin/lib/locale'; +export enum DialogSize { + Small = 'small', + + // Ideal for panels and dialogs that should be fullscreen even on large devices + Large = 'large', +} + interface Props { themeId: number; visible: boolean; onDismiss: ()=> void; containerStyle?: ViewStyle; children: React.ReactNode; + + size: DialogSize; } -const useStyles = (themeId: number, containerStyle: ViewStyle) => { +const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize) => { const windowSize = useWindowDimensions(); return useMemo(() => { const theme = themeStyle(themeId); + const maxWidth = size === DialogSize.Large ? Infinity : 500; + const maxHeight = size === DialogSize.Large ? Infinity : 700; + return StyleSheet.create({ webView: { backgroundColor: 'transparent', @@ -38,8 +50,8 @@ const useStyles = (themeId: number, containerStyle: ViewStyle) => { borderRadius: 12, padding: 10, - height: windowSize.height * 0.9, - width: windowSize.width * 0.97, + height: Math.min(maxHeight, windowSize.height * 0.9), + width: Math.min(maxWidth, windowSize.width * 0.97), flexShrink: 1, // Center @@ -56,11 +68,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle) => { flexGrow: 1, }, }); - }, [themeId, windowSize.width, windowSize.height, containerStyle]); + }, [themeId, windowSize.width, windowSize.height, containerStyle, size]); }; const DismissibleDialog: React.FC = props => { - const styles = useStyles(props.themeId, props.containerStyle); + const styles = useStyles(props.themeId, props.containerStyle, props.size); const closeButton = ( diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index a7ed13dcb46..cec4e9a7e3e 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'react'; import { Card, Divider, List, Portal, Switch, Text } from 'react-native-paper'; import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl'; import { Linking, ScrollView, StyleSheet, View, ViewStyle } from 'react-native'; -import DismissibleDialog from '../../../DismissibleDialog'; +import DismissibleDialog, { DialogSize } from '../../../DismissibleDialog'; import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginTitle from './PluginBox/PluginTitle'; @@ -247,6 +247,7 @@ const PluginInfoModal: React.FC = props => { { props.initialItem ? : null } diff --git a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx index b27e03d533d..df8c890c72f 100644 --- a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx +++ b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx @@ -13,7 +13,7 @@ import { View, StyleSheet, AccessibilityInfo } from 'react-native'; import { _ } from '@joplin/lib/locale'; import Setting from '@joplin/lib/models/Setting'; import { Dispatch } from 'redux'; -import DismissibleDialog from '../../../components/DismissibleDialog'; +import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog'; interface Props { themeId: number; @@ -168,6 +168,7 @@ const PluginPanelViewer: React.FC = props => { {renderTabContent()} From 836a9e6f4cf5ed90923a5655f7c279caf6669f1e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 27 May 2024 08:52:12 -0700 Subject: [PATCH 41/46] Hide accordion description when searching through settings --- .../screens/ConfigScreen/plugins/PluginStates.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 5be01928d0c..a845e629a8c 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -182,13 +182,18 @@ const PluginStates: React.FC = props => { ); + const isSearching = !!props.shouldShowBasedOnSearchQuery; + // Don't include the number of installed plugins when searching -- only a few of the total + // may be shown by the search. + const installedAccordionDescription = !isSearching ? _n('You currently have %d plugin installed.', 'You currently have %d plugins installed.', pluginIds.length, pluginIds.length) : null; + return ( {renderRepoApiStatus()} From ab2cc8b3f8774acdb69417d8e32191c0f1e7369e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 28 May 2024 22:56:40 -0700 Subject: [PATCH 42/46] Refactoring: Props/naming --- .../plugins/InstalledPluginBox.tsx | 8 ++++--- .../ConfigScreen/plugins/PluginInfoModal.tsx | 22 +++++++++++-------- .../ConfigScreen/plugins/PluginStates.tsx | 2 +- .../components/screens/ConfigScreen/types.ts | 4 ++++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx index 7433e4b0d9a..0ffce331d70 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx @@ -6,13 +6,15 @@ import PluginBox from './PluginBox'; import useUpdateState from './utils/useUpdateState'; import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks'; import usePluginItem from './utils/usePluginItem'; +import { PluginStatusRecord } from '../types'; interface Props { - pluginId: string; themeId: number; + + pluginId: string; pluginSettings: PluginSettings; - updatablePluginIds: Record; - updatingPluginIds: Record; + updatablePluginIds: PluginStatusRecord; + updatingPluginIds: PluginStatusRecord; showInstalledChip: boolean; callbacks: PluginCallbacks; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index cec4e9a7e3e..00e3c3ff82d 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -17,18 +17,21 @@ import usePluginItem from './utils/usePluginItem'; import InstallButton from './buttons/InstallButton'; import { InstallState } from './PluginBox'; import PluginChips from './PluginBox/PluginChips'; +import { PluginStatusRecord } from '../types'; interface Props { themeId: number; - initialItem: PluginItem|null; - updatablePluginIds: Record; - updatingPluginIds: Record; - installingPluginIds: Record; + visible: boolean; - pluginSettings: PluginSettings; - onModalDismiss: ()=> void; + item: PluginItem|null; + + updatablePluginIds: PluginStatusRecord; + updatingPluginIds: PluginStatusRecord; + installingPluginIds: PluginStatusRecord; pluginCallbacks: PluginCallbacks; + pluginSettings: PluginSettings; + onModalDismiss: ()=> void; } const styles = (() => { @@ -93,8 +96,9 @@ const EnabledSwitch: React.FC = props => { }; const PluginInfoModalContent: React.FC = props => { - const pluginId = props.initialItem.manifest.id; - const item = usePluginItem(pluginId, props.pluginSettings, props.initialItem); + const initialItem = props.item; + const pluginId = initialItem.manifest.id; + const item = usePluginItem(pluginId, props.pluginSettings, initialItem); const manifest = item.manifest; const isCompatible = useMemo(() => { @@ -250,7 +254,7 @@ const PluginInfoModal: React.FC = props => { size={DialogSize.Small} onDismiss={props.onModalDismiss} > - { props.initialItem ? : null } + { props.item ? : null } ); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index a845e629a8c..6bb63df6b3f 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -210,7 +210,7 @@ const PluginStates: React.FC = props => { updatablePluginIds={updatablePluginIds} updatingPluginIds={updatingPluginIds} installingPluginIds={installingPluginIds} - initialItem={shownInDialogItem} + item={shownInDialogItem} visible={!!shownInDialogItem} onModalDismiss={onPluginDialogClosed} pluginCallbacks={pluginCallbacks} diff --git a/packages/app-mobile/components/screens/ConfigScreen/types.ts b/packages/app-mobile/components/screens/ConfigScreen/types.ts index 0252f72bceb..5e9301473cc 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/types.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/types.ts @@ -9,3 +9,7 @@ export interface CustomSettingSection { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export type UpdateSettingValueCallback = (key: string, value: any)=> Promise; + +export interface PluginTaskStatus { + [pluginId: string]: boolean; +} From cd875129673a484676425e13e4ae3639691a5d2f Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 28 May 2024 23:01:12 -0700 Subject: [PATCH 43/46] Fix type name --- packages/app-mobile/components/screens/ConfigScreen/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/types.ts b/packages/app-mobile/components/screens/ConfigScreen/types.ts index 5e9301473cc..c66eb19f8db 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/types.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/types.ts @@ -10,6 +10,6 @@ export interface CustomSettingSection { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export type UpdateSettingValueCallback = (key: string, value: any)=> Promise; -export interface PluginTaskStatus { +export interface PluginStatusRecord { [pluginId: string]: boolean; } From e992cced25a1cb7ead8a92371976926c2566e839 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Wed, 29 May 2024 00:22:36 -0700 Subject: [PATCH 44/46] Add test for the "enabled" toggle --- .../ConfigScreen/plugins/PluginInfoModal.tsx | 10 +++-- .../plugins/PluginStates.installed.test.tsx | 41 ++++++++++++++++++- .../plugins/testUtils/WrappedPluginStates.tsx | 1 + 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index 00e3c3ff82d..ae6869e7353 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -81,8 +81,10 @@ interface EnabledSwitchProps { } const EnabledSwitch: React.FC = props => { - const onChange = useCallback(() => { - props.onToggle({ item: props.item }); + const onChange = useCallback((value: boolean) => { + if (value !== props.item.enabled) { + props.onToggle({ item: props.item }); + } }, [props.item, props.onToggle]); if (!props.item?.installed || props.item.deleted) { @@ -90,8 +92,8 @@ const EnabledSwitch: React.FC = props => { } return - {_('Enabled')} - + {_('Enabled')} + ; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index d2415e268b1..1bd1fd37f23 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; -import { act, render, screen, userEvent } from '@testing-library/react-native'; +import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native'; import '@testing-library/react-native/extend-expect'; import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService'; @@ -15,6 +15,7 @@ import { AppState } from '../../../../utils/types'; import createMockReduxStore from '../../../../utils/testing/createMockReduxStore'; import WrappedPluginStates from './testUtils/WrappedPluginStates'; import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor'; +import Setting from '@joplin/lib/models/Setting'; let reduxStore: Store = null; @@ -170,4 +171,42 @@ describe('PluginStates.installed', () => { wrapper.unmount(); }); + + it('should support disabling plugins from the info modal', async () => { + const abcPluginId = 'org.joplinapp.plugins.AbcSheetMusic'; + const defaultPluginSettings: PluginSettings = { [abcPluginId]: defaultPluginSetting() }; + + await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '1.2.3', defaultPluginSettings); + expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy(); + + const wrapper = render( + , + ); + await showInstalledTab(); + + const card = await screen.findByText('ABC Sheet Music'); + const user = userEvent.setup(); + + // Open the plugin dialog + await user.press(card); + + const enabledSwitch = await screen.findByLabelText('Enabled'); + expect(enabledSwitch).toBeVisible(); + + // Use fireEvent instead of userEvent.press -- .press doesn't seem to work + // for Switches. Similar issue: https://github.com/callstack/react-native-testing-library/issues/518. + fireEvent(enabledSwitch, 'valueChange', false); + + // The plugin should now be disabled + await waitFor(() => { + expect(Setting.value('plugins.states')).toMatchObject({ + [abcPluginId]: { enabled: false }, + }); + }); + + wrapper.unmount(); + }); }); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx index aba08002722..c4d2abe5439 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.tsx @@ -24,6 +24,7 @@ const PluginStatesWrapper = (props: WrapperProps) => { const updatePluginStates = useCallback((newStates: PluginSettings) => { setPluginSettings(newStates); + Setting.setValue('plugins.states', newStates); }, []); return ( From eb981acb108cd222c185b89d20fec666bb313b1b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 3 Jun 2024 11:38:32 -0700 Subject: [PATCH 45/46] Fix version number doesn't update in modal --- .../components/DismissibleDialog.tsx | 2 + .../plugins/InstalledPluginBox.tsx | 1 - .../plugins/PluginBox/StyledChip.tsx | 2 +- .../plugins/PluginStates.installed.test.tsx | 58 +++++++++++++++++++ .../ConfigScreen/plugins/PluginStates.tsx | 16 ++--- .../plugins/utils/usePluginItem.ts | 11 +--- .../app-mobile/plugins/hooks/usePlugin.ts | 38 +++++++++++- .../lib/services/plugins/PluginService.ts | 13 +++-- 8 files changed, 116 insertions(+), 25 deletions(-) diff --git a/packages/app-mobile/components/DismissibleDialog.tsx b/packages/app-mobile/components/DismissibleDialog.tsx index ab07f5fd9c2..6300e8f3679 100644 --- a/packages/app-mobile/components/DismissibleDialog.tsx +++ b/packages/app-mobile/components/DismissibleDialog.tsx @@ -50,6 +50,8 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize) borderRadius: 12, padding: 10, + // Use Math.min with width and height -- the maxWidth and maxHeight style + // properties don't seem to limit the size for this. height: Math.min(maxHeight, windowSize.height * 0.9), width: Math.min(maxWidth, windowSize.width * 0.97), flexShrink: 1, diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx index 0ffce331d70..fc83b1ecf8e 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.tsx @@ -1,4 +1,3 @@ - import * as React from 'react'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { useMemo } from 'react'; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx index fb04bd7af19..feb33ede6bf 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.tsx @@ -24,7 +24,7 @@ const RecommendedChip: React.FC = props => { const accessibilityProps: Partial = {}; if (!props.onPress) { - // TODO: May have no effect until a future version of RN Paper. + // Note: May have no effect until a future version of RN Paper. // See https://github.com/callstack/react-native-paper/pull/4327 accessibilityProps.accessibilityRole = 'text'; } diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 1bd1fd37f23..6ee6920d8da 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -209,4 +209,62 @@ describe('PluginStates.installed', () => { wrapper.unmount(); }); + + it('should support updating plugins from the info modal', async () => { + await mockRepositoryApiConstructor(); + + const abcPluginId = 'org.joplinapp.plugins.AbcSheetMusic'; + + const defaultPluginSettings: PluginSettings = { + [abcPluginId]: defaultPluginSetting(), + }; + + // Load an outdated recommended plugin + await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '0.0.1', defaultPluginSettings); + expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy(); + + const wrapper = render( + , + ); + await showInstalledTab(); + + // Open the plugin dialog + const card = await screen.findByText('ABC Sheet Music'); + const user = userEvent.setup(); + await user.press(card); + + const updateButton = await screen.findByRole('button', { name: 'Update' }); + expect(updateButton).toBeVisible(); + await user.press(updateButton); + + // After updating, the update button should read "updated" + const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true }); + expect(updatedButton).toBeVisible(); + + // Should be marked as updated. + await waitFor(() => { + expect(Setting.value('plugins.states')).toMatchObject({ + [abcPluginId]: { enabled: true, hasBeenUpdated: true }, + }); + }); + + // Simulate the behavior of the plugin loader -- unloading and reloading plugins is generally + // handled elsewhere. This does, however, help verify that the verison number changes correctly + // in the UI. + await act(async () => { + await PluginService.instance().unloadPlugin(abcPluginId); + await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '0.0.2', defaultPluginSettings); + }); + + // Version should change in two places -- the plugin list and the modal. + await waitFor(() => { + const versionText = screen.getAllByText('v0.0.2'); + expect(versionText).toHaveLength(2); + }); + + wrapper.unmount(); + }); }); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 6bb63df6b3f..7241211d863 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { ConfigScreenStyles } from '../configScreenStyles'; import { View, StyleSheet } from 'react-native'; import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; @@ -10,7 +10,6 @@ import SearchPlugins from './SearchPlugins'; import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import useRepoApi from './utils/useRepoApi'; import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi'; -import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import PluginInfoModal from './PluginInfoModal'; import usePluginCallbacks from './utils/usePluginCallbacks'; @@ -44,12 +43,15 @@ const useLoadedPluginIds = () => { }, []); const [loadedPluginIds, setLoadedPluginIds] = useState(getLoadedPlugins); - useAsyncEffect(async event => { - while (!event.cancelled) { - await PluginService.instance().waitForLoadedPluginsChange(); + useEffect(() => { + const { remove } = PluginService.instance().addLoadedPluginsChangeListener(() => { setLoadedPluginIds(getLoadedPlugins()); - } - }, []); + }); + + return () => { + remove(); + }; + }, [getLoadedPlugins]); return loadedPluginIds; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts index d2ddcd859c1..b3085b976aa 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.ts @@ -1,18 +1,13 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; -import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; +import { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import { PluginManifest } from '@joplin/lib/services/plugins/utils/types'; import { useMemo, useRef } from 'react'; +import usePlugin from '../../../../../plugins/hooks/usePlugin'; // initialItem is used when the plugin is not installed. For example, if the plugin item is being // created from search results. const usePluginItem = (id: string, pluginSettings: PluginSettings, initialItem: PluginItem|null): PluginItem => { - const plugin = useMemo(() => { - if (!PluginService.instance().pluginIds.includes(id)) { - return null; - } - - return PluginService.instance().pluginById(id); - }, [id]); + const plugin = usePlugin(id); const lastManifest = useRef(); if (plugin) { diff --git a/packages/app-mobile/plugins/hooks/usePlugin.ts b/packages/app-mobile/plugins/hooks/usePlugin.ts index 663463cb9ab..4593d685a73 100644 --- a/packages/app-mobile/plugins/hooks/usePlugin.ts +++ b/packages/app-mobile/plugins/hooks/usePlugin.ts @@ -1,10 +1,42 @@ import PluginService from '@joplin/lib/services/plugins/PluginService'; -import { useMemo } from 'react'; +import Logger from '@joplin/utils/Logger'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +const logger = Logger.create('usePlugin'); const usePlugin = (pluginId: string) => { - return useMemo(() => { + const [pluginReloadCounter, setPluginReloadCounter] = useState(0); + + const plugin = useMemo(() => { + if (!PluginService.instance().pluginIds.includes(pluginId)) { + return null; + } + + if (pluginReloadCounter > 0) { + logger.debug('Reloading plugin', pluginId, 'because the set of loaded plugins changed.'); + } + return PluginService.instance().pluginById(pluginId); - }, [pluginId]); + // The dependency on pluginReloadCounter is important -- it ensures that the plugin + // matches the one loaded in the PluginService. + }, [pluginId, pluginReloadCounter]); + + const reloadCounterRef = useRef(0); + reloadCounterRef.current = pluginReloadCounter; + + // The plugin may need to be re-fetched from the PluginService. When a plugin is reloaded, + // its Plugin object is replaced with a new one. + useEffect(() => { + const { remove } = PluginService.instance().addLoadedPluginsChangeListener(() => { + setPluginReloadCounter(reloadCounterRef.current + 1); + }); + + return () => { + remove(); + }; + }, []); + + return plugin; }; export default usePlugin; diff --git a/packages/lib/services/plugins/PluginService.ts b/packages/lib/services/plugins/PluginService.ts index 636c3560772..8c70487f2be 100644 --- a/packages/lib/services/plugins/PluginService.ts +++ b/packages/lib/services/plugins/PluginService.ts @@ -142,17 +142,20 @@ export default class PluginService extends BaseService { this.isSafeMode_ = v; } - public waitForLoadedPluginsChange() { - return new Promise(resolve => { - this.pluginsChangeListeners_.push(() => resolve()); - }); + public addLoadedPluginsChangeListener(listener: ()=> void) { + this.pluginsChangeListeners_.push(listener); + + return { + remove: () => { + this.pluginsChangeListeners_ = this.pluginsChangeListeners_.filter(l => (l !== listener)); + }, + }; } private dispatchPluginsChangeListeners() { for (const listener of this.pluginsChangeListeners_) { listener(); } - this.pluginsChangeListeners_ = []; } private setPluginAt(pluginId: string, plugin: Plugin) { From d0804b56d3e7a55b38ccdece9c2c846836a4c73c Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 3 Jun 2024 11:58:21 -0700 Subject: [PATCH 46/46] Increase findByRole timeout for CI --- .../ConfigScreen/plugins/PluginStates.installed.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx index 6ee6920d8da..676230dbd02 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.tsx @@ -241,7 +241,7 @@ describe('PluginStates.installed', () => { await user.press(updateButton); // After updating, the update button should read "updated" - const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true }); + const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true, timeout: 8000 }); expect(updatedButton).toBeVisible(); // Should be marked as updated.