1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Mobile: Resolves #10592: Make mobile plugin settings screen UI closer to desktop (#10598)

This commit is contained in:
Henry Heino 2024-06-15 02:00:21 -07:00 committed by GitHub
parent a4a4170d49
commit e465b45d6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 248 additions and 186 deletions

View File

@ -619,9 +619,10 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.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/EnablePluginSupportPage.js
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.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/PluginTitle.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.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/PluginInfoModal.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
@ -629,6 +630,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.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/SearchPlugins.js
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.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/buttons/InstallButton.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js

4
.gitignore vendored
View File

@ -598,9 +598,10 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.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/EnablePluginSupportPage.js
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.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/PluginTitle.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.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/PluginInfoModal.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
@ -608,6 +609,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.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/SearchPlugins.js
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.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/buttons/InstallButton.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js

View File

@ -0,0 +1,57 @@
import * as React from 'react';
import { Chip, ChipProps } from 'react-native-paper';
import { useMemo } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../../../../utils/types';
import { themeStyle } from '../../../../global-style';
type Props = {
themeId: number;
color?: string;
faded?: boolean;
onPress?: ()=> void;
icon?: string;
children: React.ReactNode;
};
const fadedStyle = { opacity: 0.87 };
const PluginChip: React.FC<Props> = props => {
const themeOverride = useMemo(() => {
const theme = themeStyle(props.themeId);
const foreground = props.color ?? theme.color;
const background = theme.backgroundColor;
return {
colors: {
secondaryContainer: background,
onSecondaryContainer: foreground,
primary: foreground,
outline: foreground,
onPrimary: foreground,
onSurfaceVariant: foreground,
},
};
}, [props.themeId, props.color]);
const accessibilityProps: Partial<ChipProps> = {};
if (!props.onPress) {
// 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';
}
return <Chip
theme={themeOverride}
style={props.faded ? fadedStyle : null}
mode='outlined'
{...accessibilityProps}
{...props}
/>;
};
export default connect((state: AppState) => {
return {
themeId: state.settings.theme,
};
})(PluginChip);

View File

@ -2,10 +2,10 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
import PluginService from '@joplin/lib/services/plugins/PluginService'; import PluginService from '@joplin/lib/services/plugins/PluginService';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import * as React from 'react'; import * as React from 'react';
import { Alert, Linking, View, ViewStyle } from 'react-native'; import { View, ViewStyle } from 'react-native';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { PluginCallback } from '../utils/usePluginCallbacks'; import { PluginCallback } from '../utils/usePluginCallbacks';
import StyledChip from './StyledChip'; import PluginChip from './PluginChip';
import { themeStyle } from '../../../../global-style'; import { themeStyle } from '../../../../global-style';
interface Props { interface Props {
@ -19,23 +19,6 @@ interface Props {
onShowPluginLog?: PluginCallback; 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 containerStyle: ViewStyle = { const containerStyle: ViewStyle = {
flexDirection: 'row', flexDirection: 'row',
gap: 4, gap: 4,
@ -54,43 +37,28 @@ const PluginChips: React.FC<Props> = props => {
if (!props.hasErrors) return null; if (!props.hasErrors) return null;
return ( return (
<StyledChip <PluginChip
background={theme.backgroundColor2} color={theme.colorError2}
foreground={theme.colorError2}
icon='alert' icon='alert'
mode='flat'
onPress={() => props.onShowPluginLog({ item })} onPress={() => props.onShowPluginLog({ item })}
> >
{_('Error')} {_('Error')}
</StyledChip> </PluginChip>
); );
}; };
const renderRecommendedChip = () => {
if (!props.item.manifest._recommended || !props.isCompatible) {
return null;
}
return <StyledChip
background={theme.searchMarkerBackgroundColor}
foreground={theme.searchMarkerColor}
icon='crown'
onPress={onRecommendedPress}
>{_('Recommended')}</StyledChip>;
};
const renderBuiltInChip = () => { const renderBuiltInChip = () => {
if (!props.item.builtIn) { if (!props.item.builtIn) {
return null; return null;
} }
return <StyledChip icon='code-tags-check'>{_('Built-in')}</StyledChip>; return <PluginChip icon='code-tags-check'>{_('Built-in')}</PluginChip>;
}; };
const renderIncompatibleChip = () => { const renderIncompatibleChip = () => {
if (props.isCompatible) return null; if (props.isCompatible) return null;
return ( return (
<StyledChip <PluginChip
background={theme.backgroundColor3} color={theme.color3}
foreground={theme.color3}
icon='alert' icon='alert'
onPress={() => { onPress={() => {
void shim.showMessageBox( void shim.showMessageBox(
@ -98,7 +66,7 @@ const PluginChips: React.FC<Props> = props => {
{ buttons: [_('OK')] }, { buttons: [_('OK')] },
); );
}} }}
>{_('Incompatible')}</StyledChip> >{_('Incompatible')}</PluginChip>
); );
}; };
@ -106,7 +74,7 @@ const PluginChips: React.FC<Props> = props => {
if (!props.isCompatible || !props.canUpdate) return null; if (!props.isCompatible || !props.canUpdate) return null;
return ( return (
<StyledChip>{_('Update available')}</StyledChip> <PluginChip>{_('Update available')}</PluginChip>
); );
}; };
@ -114,21 +82,20 @@ const PluginChips: React.FC<Props> = props => {
if (props.item.enabled || !props.item.installed) { if (props.item.enabled || !props.item.installed) {
return null; return null;
} }
return <StyledChip>{_('Disabled')}</StyledChip>; return <PluginChip faded={true}>{_('Disabled')}</PluginChip>;
}; };
const renderInstalledChip = () => { const renderInstalledChip = () => {
if (!props.showInstalledChip) { if (!props.showInstalledChip) {
return null; return null;
} }
return <StyledChip>{_('Installed')}</StyledChip>; return <PluginChip faded={true}>{_('Installed')}</PluginChip>;
}; };
return <View style={containerStyle}> return <View style={containerStyle}>
{renderIncompatibleChip()} {renderIncompatibleChip()}
{renderInstalledChip()} {renderInstalledChip()}
{renderErrorsChip()} {renderErrorsChip()}
{renderRecommendedChip()}
{renderBuiltInChip()} {renderBuiltInChip()}
{renderUpdatableChip()} {renderUpdatableChip()}
{renderDisabledChip()} {renderDisabledChip()}

View File

@ -0,0 +1,74 @@
import { _ } from '@joplin/lib/locale';
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
import * as React from 'react';
import IconButton from '../../../../IconButton';
import { Alert, Linking, StyleSheet } from 'react-native';
import { themeStyle } from '../../../../global-style';
import { useMemo } from 'react';
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 },
);
};
interface Props {
themeId: number;
manifest: PluginManifest;
isCompatible: boolean;
}
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
container: {
opacity: 0.8,
},
wrapper: {
borderColor: theme.colorWarn,
borderWidth: 1,
borderRadius: 20,
justifyContent: 'center',
height: 32,
width: 32,
textAlign: 'center',
},
icon: {
fontSize: 14,
color: theme.colorWarn,
marginLeft: 'auto',
marginRight: 'auto',
},
});
}, [themeId]);
};
const RecommendedBadge: React.FC<Props> = props => {
const styles = useStyles(props.themeId);
if (!props.manifest._recommended || !props.isCompatible) return null;
return <IconButton
onPress={onRecommendedPress}
iconName='fas fa-crown'
containerStyle={styles.container}
contentWrapperStyle={styles.wrapper}
iconStyle={styles.icon}
themeId={props.themeId}
description={_('Recommended')}
/>;
};
export default RecommendedBadge;

View File

@ -1,39 +0,0 @@
import * as React from 'react';
import { Chip, ChipProps } from 'react-native-paper';
import { useMemo } from 'react';
type Props = ({
foreground: string;
background: string;
}|{
foreground?: undefined;
background?: undefined;
}) & ChipProps;
const RecommendedChip: React.FC<Props> = props => {
const themeOverride = useMemo(() => {
if (!props.foreground) return {};
return {
colors: {
secondaryContainer: props.background,
onSecondaryContainer: props.foreground,
primary: props.foreground,
},
};
}, [props.foreground, props.background]);
const accessibilityProps: Partial<Props> = {};
if (!props.onPress) {
// 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';
}
return <Chip
theme={themeOverride}
{...accessibilityProps}
{...props}
/>;
};
export default RecommendedChip;

View File

@ -8,9 +8,10 @@ import PluginChips from './PluginChips';
import { UpdateState } from '../utils/useUpdateState'; import { UpdateState } from '../utils/useUpdateState';
import { PluginCallback } from '../utils/usePluginCallbacks'; import { PluginCallback } from '../utils/usePluginCallbacks';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import InstallButton from '../buttons/InstallButton'; import InstallButton from '../buttons/InstallButton';
import PluginTitle from './PluginTitle'; import PluginTitle from './PluginTitle';
import RecommendedBadge from './RecommendedBadge';
export enum InstallState { export enum InstallState {
NotInstalled, NotInstalled,
@ -92,8 +93,13 @@ const PluginBox: React.FC<Props> = props => {
testID='plugin-card' testID='plugin-card'
> >
<Card.Content style={styles.content}> <Card.Content style={styles.content}>
<PluginTitle manifest={item.manifest} /> <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text numberOfLines={2}>{manifest.description}</Text> <View style={{ flexShrink: 1 }}>
<PluginTitle manifest={item.manifest} />
<Text numberOfLines={2}>{manifest.description}</Text>
</View>
<RecommendedBadge manifest={item.manifest} isCompatible={props.isCompatible} themeId={props.themeId} />
</View>
<PluginChips <PluginChips
themeId={props.themeId} themeId={props.themeId}
item={props.item} item={props.item}

View File

@ -47,11 +47,6 @@ const loadMockPlugin = async (id: string, name: string, version: string, pluginS
}); });
}; };
const showInstalledTab = async () => {
const installedTab = await screen.findByText('Installed plugins');
await userEvent.press(installedTab);
};
const abcPluginId = 'org.joplinapp.plugins.AbcSheetMusic'; const abcPluginId = 'org.joplinapp.plugins.AbcSheetMusic';
const backlinksPluginId = 'joplin.plugin.ambrt.backlinksToNote'; const backlinksPluginId = 'joplin.plugin.ambrt.backlinksToNote';
@ -99,7 +94,6 @@ describe('PluginStates.installed', () => {
store={reduxStore} store={reduxStore}
/>, />,
); );
await showInstalledTab();
expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible();
expect(await screen.findByText(/^Backlinks to note/)).toBeVisible(); expect(await screen.findByText(/^Backlinks to note/)).toBeVisible();
@ -129,7 +123,6 @@ describe('PluginStates.installed', () => {
store={reduxStore} store={reduxStore}
/>, />,
); );
await showInstalledTab();
const abcSheetMusicCard = await screen.findByText(/^ABC Sheet Music/); const abcSheetMusicCard = await screen.findByText(/^ABC Sheet Music/);
expect(abcSheetMusicCard).toBeVisible(); expect(abcSheetMusicCard).toBeVisible();
@ -150,7 +143,7 @@ describe('PluginStates.installed', () => {
); );
// Initially, no plugins should be installed // Initially, no plugins should be installed
expect(screen.queryByText(/^You currently have 0 plugins? installed/)).toBeNull(); expect(screen.queryByText('Installed (0):')).toBeNull();
const testPluginId1 = 'org.joplinapp.plugins.AbcSheetMusic'; const testPluginId1 = 'org.joplinapp.plugins.AbcSheetMusic';
const testPluginId2 = 'org.joplinapp.plugins.test.plugin.id'; const testPluginId2 = 'org.joplinapp.plugins.test.plugin.id';
@ -158,8 +151,6 @@ describe('PluginStates.installed', () => {
await act(() => loadMockPlugin(testPluginId2, 'A test plugin', '1.0.0', pluginSettings)); await act(() => loadMockPlugin(testPluginId2, 'A test plugin', '1.0.0', pluginSettings));
expect(PluginService.instance().plugins[testPluginId1]).toBeTruthy(); expect(PluginService.instance().plugins[testPluginId1]).toBeTruthy();
await showInstalledTab();
// Should update the list of installed plugins even though the plugin settings didn't change. // Should update the list of installed plugins even though the plugin settings didn't change.
expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible(); expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible();
expect(await screen.findByText(/^A test plugin/)).toBeVisible(); expect(await screen.findByText(/^A test plugin/)).toBeVisible();
@ -184,7 +175,6 @@ describe('PluginStates.installed', () => {
store={reduxStore} store={reduxStore}
/>, />,
); );
await showInstalledTab();
const card = await screen.findByText('ABC Sheet Music'); const card = await screen.findByText('ABC Sheet Music');
const user = userEvent.setup(); const user = userEvent.setup();
@ -226,7 +216,6 @@ describe('PluginStates.installed', () => {
store={reduxStore} store={reduxStore}
/>, />,
); );
await showInstalledTab();
// Open the plugin dialog // Open the plugin dialog
const card = await screen.findByText('ABC Sheet Music'); const card = await screen.findByText('ABC Sheet Music');
@ -279,7 +268,6 @@ describe('PluginStates.installed', () => {
store={reduxStore} store={reduxStore}
/>, />,
); );
await showInstalledTab();
// Should be shown as installed. // Should be shown as installed.
const card = await screen.findByText('ABC Sheet Music'); const card = await screen.findByText('ABC Sheet Music');

View File

@ -18,14 +18,9 @@ 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. // The search box is initially read-only -- waits for it to be editable.
const getEditableSearchBox = async () => { const getEditableSearchBox = async () => {
const searchBox = await screen.findByPlaceholderText('Search plugins'); const searchBox = await screen.findByPlaceholderText('Search for plugins...');
expect(searchBox).toBeVisible(); expect(searchBox).toBeVisible();
await waitFor(() => { await waitFor(() => {
@ -53,7 +48,6 @@ describe('PluginStates.search', () => {
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>); const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);
const user = userEvent.setup(); const user = userEvent.setup();
await showSearchTab();
const searchBox = await getEditableSearchBox(); const searchBox = await getEditableSearchBox();
await user.type(searchBox, 'backlinks'); await user.type(searchBox, 'backlinks');
@ -81,8 +75,6 @@ describe('PluginStates.search', () => {
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>); const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);
const user = userEvent.setup(); const user = userEvent.setup();
await showSearchTab();
const searchBox = await getEditableSearchBox(); const searchBox = await getEditableSearchBox();
await user.press(searchBox); await user.press(searchBox);
@ -111,8 +103,6 @@ describe('PluginStates.search', () => {
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>); const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);
const user = userEvent.setup(); const user = userEvent.setup();
await showSearchTab();
const searchBox = await getEditableSearchBox(); const searchBox = await getEditableSearchBox();
await user.press(searchBox); await user.press(searchBox);
await user.type(searchBox, 'abc'); await user.type(searchBox, 'abc');

View File

@ -2,8 +2,8 @@ import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ConfigScreenStyles } from '../configScreenStyles'; import { ConfigScreenStyles } from '../configScreenStyles';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import { Banner, Text, Button, ProgressBar, List, Divider } from 'react-native-paper'; import { Banner, Text, Button, ProgressBar, Divider } from 'react-native-paper';
import { _, _n } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginService, { PluginSettings, SerializedPluginSettings } from '@joplin/lib/services/plugins/PluginService';
import InstalledPluginBox from './InstalledPluginBox'; import InstalledPluginBox from './InstalledPluginBox';
import SearchPlugins from './SearchPlugins'; import SearchPlugins from './SearchPlugins';
@ -13,6 +13,7 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import PluginInfoModal from './PluginInfoModal'; import PluginInfoModal from './PluginInfoModal';
import usePluginCallbacks from './utils/usePluginCallbacks'; import usePluginCallbacks from './utils/usePluginCallbacks';
import BetaChip from '../../../BetaChip'; import BetaChip from '../../../BetaChip';
import SectionLabel from './SectionLabel';
interface Props { interface Props {
themeId: number; themeId: number;
@ -59,8 +60,8 @@ const useLoadedPluginIds = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
installedPluginsContainer: { installedPluginsContainer: {
marginLeft: 8, marginLeft: 12,
marginRight: 8, marginRight: 12,
marginBottom: 10, marginBottom: 10,
}, },
}); });
@ -134,11 +135,16 @@ const PluginStates: React.FC<Props> = props => {
const installedPluginCards = []; const installedPluginCards = [];
const pluginService = PluginService.instance(); const pluginService = PluginService.instance();
const [searchQuery, setSearchQuery] = useState('');
const isPluginSearching = !!searchQuery;
const pluginIds = useLoadedPluginIds(); const pluginIds = useLoadedPluginIds();
for (const pluginId of pluginIds) { for (const pluginId of pluginIds) {
const plugin = pluginService.plugins[pluginId]; const plugin = pluginService.plugins[pluginId];
if (!props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(plugin.manifest.name)) { const matchesGlobalSearch = !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(plugin.manifest.name);
const showCard = !isPluginSearching && matchesGlobalSearch;
if (showCard) {
installedPluginCards.push( installedPluginCards.push(
<InstalledPluginBox <InstalledPluginBox
key={`plugin-${pluginId}`} key={`plugin-${pluginId}`}
@ -159,40 +165,25 @@ const PluginStates: React.FC<Props> = props => {
!props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText()) !props.shouldShowBasedOnSearchQuery || props.shouldShowBasedOnSearchQuery(searchInputSearchText())
); );
const [searchQuery, setSearchQuery] = useState(''); const searchSection = (
<SearchPlugins
pluginSettings={pluginSettings}
themeId={props.themeId}
onUpdatePluginStates={props.updatePluginStates}
installingPluginIds={installingPluginIds}
callbacks={pluginCallbacks}
repoApiInitialized={repoApiLoaded}
repoApi={repoApi}
updatingPluginIds={updatingPluginIds}
updatablePluginIds={updatablePluginIds}
onShowPluginInfo={onShowPluginInfo}
const searchAccordion = ( searchQuery={searchQuery}
<List.Accordion setSearchQuery={setSearchQuery}
title={_('Install new plugins')} />
description={_('Browse and install community plugins.')}
id='search'
>
<SearchPlugins
pluginSettings={pluginSettings}
themeId={props.themeId}
onUpdatePluginStates={props.updatePluginStates}
installingPluginIds={installingPluginIds}
callbacks={pluginCallbacks}
repoApiInitialized={repoApiLoaded}
repoApi={repoApi}
updatingPluginIds={updatingPluginIds}
updatablePluginIds={updatablePluginIds}
onShowPluginInfo={onShowPluginInfo}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
</List.Accordion>
); );
const isSearching = !!props.shouldShowBasedOnSearchQuery; const isSearching = !!props.shouldShowBasedOnSearchQuery || isPluginSearching;
// 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;
// Using a different wrapper prevents the installed item group from being openable when
// there are no plugins:
const InstalledItemWrapper = pluginIds.length ? List.Accordion : List.Item;
return ( return (
<View> <View>
@ -202,20 +193,14 @@ const PluginStates: React.FC<Props> = props => {
</Banner> </Banner>
<Divider/> <Divider/>
<List.AccordionGroup> {showSearch ? searchSection : null}
<InstalledItemWrapper <View style={styles.installedPluginsContainer}>
title={_('Installed plugins')} <SectionLabel visible={!isSearching}>
description={installedAccordionDescription} {pluginIds.length ? _('Installed (%d):', pluginIds.length) : _('No plugins are installed.')}
id='installed' </SectionLabel>
> {installedPluginCards}
<View style={styles.installedPluginsContainer}> </View>
{installedPluginCards}
</View>
</InstalledItemWrapper>
<Divider/>
{showSearch ? searchAccordion : null}
<Divider/>
</List.AccordionGroup>
<PluginInfoModal <PluginInfoModal
themeId={props.themeId} themeId={props.themeId}
pluginSettings={pluginSettings} pluginSettings={pluginSettings}
@ -227,6 +212,7 @@ const PluginStates: React.FC<Props> = props => {
onModalDismiss={onPluginDialogClosed} onModalDismiss={onPluginDialogClosed}
pluginCallbacks={pluginCallbacks} pluginCallbacks={pluginCallbacks}
/> />
<Divider/>
</View> </View>
); );
}; };

View File

@ -5,7 +5,7 @@ import { _ } from '@joplin/lib/locale';
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types'; import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native'; import { FlatList, StyleSheet, View } from 'react-native';
import { TextInput, Text } from 'react-native-paper'; import { TextInput } from 'react-native-paper';
import PluginBox, { InstallState } from './PluginBox'; import PluginBox, { InstallState } from './PluginBox';
import PluginService, { PluginSettings } 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 { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
@ -13,6 +13,7 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import openWebsiteForPlugin from './utils/openWebsiteForPlugin'; import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks'; import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks';
import InstalledPluginBox from './InstalledPluginBox'; import InstalledPluginBox from './InstalledPluginBox';
import SectionLabel from './SectionLabel';
interface Props { interface Props {
themeId: number; themeId: number;
@ -42,14 +43,11 @@ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'column', flexDirection: 'column',
margin: 12, margin: 12,
}, marginBottom: 0,
resultsCounter: {
margin: 12,
marginTop: 17,
marginBottom: 4,
}, },
}); });
const PluginSearch: React.FC<Props> = props => { const PluginSearch: React.FC<Props> = props => {
const { searchQuery, setSearchQuery } = props; const { searchQuery, setSearchQuery } = props;
const [searchResultManifests, setSearchResultManifests] = useState<PluginManifest[]>([]); const [searchResultManifests, setSearchResultManifests] = useState<PluginManifest[]>([]);
@ -133,12 +131,16 @@ const PluginSearch: React.FC<Props> = props => {
} }
}, [onInstall, props.themeId, props.pluginSettings, props.updatingPluginIds, props.updatablePluginIds, props.onShowPluginInfo, props.callbacks]); }, [onInstall, props.themeId, props.pluginSettings, props.updatingPluginIds, props.updatablePluginIds, props.onShowPluginInfo, props.callbacks]);
const renderResultsCount = () => { const onClearSearch = useCallback(() => {
if (!searchQuery.length) return null; setSearchQuery('');
}, [setSearchQuery]);
return <Text style={styles.resultsCounter} variant='labelLarge'> const renderSearchButton = () => {
{_('Results (%d):', searchResults.length)} if (searchQuery) {
</Text>; return <TextInput.Icon onPress={onClearSearch} accessibilityLabel={_('Clear search')} icon='close' />;
} else {
return <TextInput.Icon icon='magnify' aria-hidden={true} importantForAccessibility='no-hide-descendants'/>;
}
}; };
return ( return (
@ -146,13 +148,13 @@ const PluginSearch: React.FC<Props> = props => {
<TextInput <TextInput
testID='searchbar' testID='searchbar'
mode='outlined' mode='outlined'
left={<TextInput.Icon icon='magnify' />} right={renderSearchButton()}
placeholder={_('Search plugins')} placeholder={_('Search for plugins...')}
onChangeText={setSearchQuery} onChangeText={setSearchQuery}
value={searchQuery} value={searchQuery}
editable={props.repoApiInitialized} editable={props.repoApiInitialized}
/> />
{renderResultsCount()} <SectionLabel visible={!!searchQuery.length}>{_('Results (%d):', searchResults.length)}</SectionLabel>
<FlatList <FlatList
data={searchResults} data={searchResults}
renderItem={renderResult} renderItem={renderResult}

View File

@ -0,0 +1,24 @@
import * as React from 'react';
import { ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
interface Props {
children: React.ReactNode;
visible: boolean;
}
const style: ViewStyle = {
margin: 12,
marginTop: 17,
marginBottom: 4,
};
const SectionLabel: React.FC<Props> = props => {
if (!props.visible) return null;
return <Text style={style} variant='labelLarge'>
{props.children}
</Text>;
};
export default SectionLabel;

View File

@ -8,6 +8,7 @@ import { PaperProvider } from 'react-native-paper';
import PluginStates from '../PluginStates'; import PluginStates from '../PluginStates';
import { AppState } from '../../../../../utils/types'; import { AppState } from '../../../../../utils/types';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { MenuProvider } from 'react-native-popup-menu';
interface WrapperProps { interface WrapperProps {
initialPluginSettings: PluginSettings; initialPluginSettings: PluginSettings;
@ -29,15 +30,17 @@ const PluginStatesWrapper = (props: WrapperProps) => {
return ( return (
<Provider store={props.store}> <Provider store={props.store}>
<PaperProvider> <MenuProvider>
<PluginStates <PaperProvider>
styles={styles} <PluginStates
themeId={Setting.THEME_LIGHT} styles={styles}
updatePluginStates={updatePluginStates} themeId={Setting.THEME_LIGHT}
pluginSettings={pluginSettings} updatePluginStates={updatePluginStates}
shouldShowBasedOnSearchQuery={shouldShowBasedOnSettingSearchQuery} pluginSettings={pluginSettings}
/> shouldShowBasedOnSearchQuery={shouldShowBasedOnSettingSearchQuery}
</PaperProvider> />
</PaperProvider>
</MenuProvider>
</Provider> </Provider>
); );
}; };