Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile: Resolves #10883: Remove slider component module and replace integer settings with validated text inputs and ensure all validation error flows work correctly #11069

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e55db5a
Create and utilise new component for note history and trashed item tt…
mrjo118 Sep 16, 2024
454da7c
Revert "Create and utilise new component for note history and trashed…
mrjo118 Oct 2, 2024
3d35235
Merge remote-tracking branch 'origin/dev' into note-history-and-trash…
mrjo118 Oct 2, 2024
d001a1e
Allow slider component values to be incremented by tapping the value …
mrjo118 Oct 2, 2024
a0f95a9
Merge branch 'dev' into note-history-and-trash-ttl-text-input
personalizedrefrigerator Oct 5, 2024
dc84c0c
Remove slider component mode and replace integer settings with valida…
mrjo118 Nov 12, 2024
70dbbb3
Make switching to the plugins screen on desktop trigger the switching…
mrjo118 Nov 13, 2024
11b05af
Merge branch 'note-history-and-trash-ttl-text-input2' into note-histo…
mrjo118 Nov 13, 2024
d563598
Remove unused import
mrjo118 Nov 13, 2024
10cb151
Remove license changes with incorrect formatting
mrjo118 Nov 13, 2024
3d7e0c9
Add license change back in
mrjo118 Nov 13, 2024
4a116c3
Revert "Add license change back in"
mrjo118 Nov 13, 2024
e754232
Add license change back in
mrjo118 Nov 13, 2024
47c2fc4
Avoid validating enum values as integers, as they are already restricted
mrjo118 Nov 13, 2024
1aa395e
Fix validation issue
mrjo118 Nov 13, 2024
1e4b87e
Add automated tests
mrjo118 Nov 19, 2024
ad8a277
Merge remote-tracking branch 'origin/dev' into note-history-and-trash…
mrjo118 Nov 19, 2024
57304d2
Merge branch 'dev' into note-history-and-trash-ttl-text-input
mrjo118 Nov 19, 2024
1aa63f0
Resolved compile error due to merging in updates
mrjo118 Nov 19, 2024
960af00
Merge branch 'note-history-and-trash-ttl-text-input' of https://githu…
mrjo118 Nov 19, 2024
bf039c3
Merge branch 'dev' into note-history-and-trash-ttl-text-input
mrjo118 Nov 28, 2024
5428a45
Merge branch 'dev' into note-history-and-trash-ttl-text-input
mrjo118 Dec 9, 2024
2595f61
Change formatting of values back to always using integer type and add…
mrjo118 Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@
"[email protected]": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch",
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",
"chokidar@^2.0.0": "3.5.3",
"[email protected]": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
Expand Down
14 changes: 10 additions & 4 deletions packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,24 @@ class ConfigScreenComponent extends React.Component<any, any> {
public async switchSection(name: string) {
const section = this.sectionByName(name);
let screenName = '';
if (section.isScreen) {
screenName = section.name;
if (section.isScreen || name === 'plugins') {
if (section.isScreen) {
screenName = section.name;
}

if (this.hasChanges()) {
const ok = confirm(_('This will open a new screen. Save your current changes?'));
const ok = bridge().showConfirmMessageBox(_('This will open a new screen. Save your current changes?'));
if (ok) {
await shared.saveSettings(this);
const result = await shared.saveSettings(this);
if (result === false) return false;
} else {
return false;
}
}
}

this.setState({ selectedSectionName: section.name, screenName: screenName });
return true;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Setting, { AppType, SettingItemSubType } from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { useCallback, useId } from 'react';
import { useCallback, useId, useState } from 'react';
import control_PluginsStates from './plugins/PluginsStates';
import bridge from '../../../services/bridge';
import { _ } from '@joplin/lib/locale';
Expand Down Expand Up @@ -70,6 +70,7 @@ const SettingComponent: React.FC<Props> = props => {
const inputId = useId();
const descriptionId = useId();
const descriptionComp = <SettingDescription id={descriptionId} text={descriptionText}/>;
const [valueState, setValueState] = useState(props.value?.toString());

if (key in settingKeyToControl) {
const CustomSettingComponent = settingKeyToControl[key];
Expand Down Expand Up @@ -321,10 +322,9 @@ const SettingComponent: React.FC<Props> = props => {
);
}
} else if (md.type === Setting.TYPE_INT) {
const value = props.value as number;

const onNumChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
updateSettingValue(key, event.target.value);
setValueState(event.target.value);
updateSettingValue(key, Number.isInteger(Number(event.target.value)) ? event.target.value : ''); // Prevent invalid values being mapped to 0
};

const label = [md.label()];
Expand All @@ -336,7 +336,7 @@ const SettingComponent: React.FC<Props> = props => {
<input
type="number"
style={textInputBaseStyle}
value={value}
value={valueState}
onChange={onNumChange}
min={md.minimum}
max={md.maximum}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
const shouldSetIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors');

const done = await shared.saveSettings(this);
if (!done) return;
if (!done) return false;

if (shouldSetIgnoreTlsErrors) {
await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors'));
}

return true;
};

private syncStatusButtonPress_ = () => {
Expand Down Expand Up @@ -262,22 +264,21 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return this.state.changedSettingKeys.length > 0;
}

private async promptSaveChanges(): Promise<void> {
private async promptSaveChanges(): Promise<boolean> {
if (this.hasUnsavedChanges()) {
const response = await shim.showMessageBox(_('There are unsaved changes.'), {
buttons: [_('Save changes'), _('Discard changes')],
});
if (response === 0) {
await this.saveButton_press();
return await this.saveButton_press();
}
}

return true;
}

private handleNavigateToNewScreen = async (): Promise<boolean> => {
await this.promptSaveChanges();

// Continue navigation
return false;
return !(await this.promptSaveChanges());
};

private handleBackButtonPress = (): boolean => {
Expand All @@ -300,8 +301,10 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi

if (this.hasUnsavedChanges()) {
void (async () => {
await this.promptSaveChanges();
await goBack();
const result = await this.promptSaveChanges();
if (result) {
await goBack();
}
})();
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { View, Text, TextInput } from 'react-native';
import Setting, { AppType } from '@joplin/lib/models/Setting';
import Dropdown from '../../Dropdown';
import { ConfigScreenStyles } from './configScreenStyles';
import Slider from '@react-native-community/slider';
import SettingsToggle from './SettingsToggle';
import FileSystemPathSelector from './FileSystemPathSelector';
import shim from '@joplin/lib/shim';
import { themeStyle } from '../../global-style';
import { useId } from 'react';
import { useId, useState } from 'react';

interface Props {
settingId: string;
Expand Down Expand Up @@ -41,6 +40,7 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
const containerStyle = props.styles.getContainerStyle(!!settingDescription);

const labelId = useId();
const [valueState, setValueState] = useState(props.value?.toString());

if (md.isEnum) {
const value = props.value?.toString();
Expand Down Expand Up @@ -92,33 +92,33 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
/>
);
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(props.value) : props.value;
const minimum = 'minimum' in md ? md.minimum : 0;
const maximum = 'maximum' in md ? md.maximum : 10;
const label = md.label();
const label = md.unitLabel?.toString() !== undefined ? `${md.label()} (${md.unitLabel(md.value)})` : `${md.label()}`;

// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
// https://github.com/laurent22/joplin/issues/2733
// https://github.com/react-native-community/react-native-slider/issues/161
return (
<View key={props.settingId} style={styleSheet.settingContainer}>
<Text key="label" style={styleSheet.settingText} nativeID={labelId}>
{label}
</Text>
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<Text style={styleSheet.sliderUnits}>{unitLabel}</Text>
<Slider
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={props.settingId} style={containerStyle}>
<Text key="label" style={styleSheet.settingText}>
{label}
</Text>
<TextInput
keyboardType="numeric"
autoCorrect={false}
autoComplete="off"
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
autoCapitalize="none"
key="control"
style={{ flex: 1 }}
step={md.step}
minimumValue={minimum}
maximumValue={maximum}
value={props.value}
onValueChange={newValue => void props.updateSettingValue(props.settingId, newValue)}
accessibilityHint={label}
style={styleSheet.settingControl}
value={valueState}
onChangeText={newValue => {
setValueState(newValue);
void props.updateSettingValue(props.settingId, Number.isInteger(Number(newValue)) ? newValue : ''); // Prevent invalid values being mapped to 0
}}
maxLength={15}
secureTextEntry={!!md.secure}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
Expand Down
3 changes: 0 additions & 3 deletions packages/app-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,6 @@ DEPENDENCIES:
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- react-native-version-info (from `../node_modules/react-native-version-info`)
- react-native-webview (from `../node_modules/react-native-webview`)
Expand Down Expand Up @@ -1594,8 +1593,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/@joplin/react-native-saf-x"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-sqlite-storage:
:path: "../node_modules/react-native-sqlite-storage"
react-native-version-info:
Expand Down
1 change: 0 additions & 1 deletion packages/app-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.3.2",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-community/slider": "4.5.2",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
Expand Down
8 changes: 6 additions & 2 deletions packages/lib/components/shared/config/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const ObjectUtils = require('../../../ObjectUtils');
const { _ } = require('../../../locale');
import { createSelector } from 'reselect';
import Logger from '@joplin/utils/Logger';

import shim, { MessageBoxType } from '../../../shim';
import { type ReactNode } from 'react';
import { type Registry } from '../../../registry';
import settingValidations from '../../../models/settings/settingValidations';
Expand Down Expand Up @@ -155,7 +155,11 @@ export const saveSettings = async (comp: ConfigScreenComponent) => {

const validationMessage = await settingValidations(savedSettingKeys, comp.state.settings);
if (validationMessage) {
alert(validationMessage);
await shim.showMessageBox(validationMessage, {
type: MessageBoxType.Error,
buttons: [_('OK')],
});

return false;
}

Expand Down
15 changes: 15 additions & 0 deletions packages/lib/models/Setting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,19 @@ describe('models/Setting', () => {
await Setting.saveAll();
}
});

test.each([
['1', 1],
['-1', -1],
['+1', 1],
['', null],
[null, 0],
[undefined, 0],
['1e3', 1000],
['1e20', 1e20],
['99999999999999999999', 1e20], // Value exceeds integer limit, output exhibits a rounding issue but still retains integer type
])('should format integer type setting as an integer or null', (async (input, expectedOutput) => {
const value = Setting.formatValue('revisionService.ttlDays', input);
expect(value).toBe(expectedOutput);
}));
});
9 changes: 8 additions & 1 deletion packages/lib/models/Setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,14 @@ class Setting extends BaseModel {
public static formatValue(key: string | SettingItemType, value: any) {
const type = typeof key === 'string' ? this.settingMetadata(key).type : key;

if (type === SettingItemType.Int) return !value ? 0 : Math.floor(Number(value));
if (type === SettingItemType.Int) {
if (value === '') {
// Set empty string as null instead of zero, so that validations will fail
return null;
} else {
return !value ? 0 : Math.floor(Number(value));
}
}

if (type === SettingItemType.Bool) {
if (typeof value === 'string') {
Expand Down
38 changes: 38 additions & 0 deletions packages/lib/models/settings/settingValidations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,42 @@ describe('settingValidations', () => {
}
expect(await settingValidations(['sync.target'], { 'sync.target': newSyncTargetId })).toBe('');
});

test('should return error message for null value for setting without range', async () => {
const value = await settingValidations(['style.editor.contentMaxWidth'], { 'style.editor.contentMaxWidth': null });
expect(value).toBe('Editor maximum width must be a valid whole number');
});

test('should return error message for null value for setting with range', async () => {
const value = await settingValidations(['revisionService.ttlDays'], { 'revisionService.ttlDays': null });
expect(value).toBe('Keep note history for must be a valid whole number');
});

test.each(
[0, -1],
)('should return error message for too low integer values', async (input) => {
const value = await settingValidations(['revisionService.ttlDays'], { 'revisionService.ttlDays': input });
expect(value).toBe('Keep note history for cannot be less than 1');
});

test.each(
[731, 1e20],
)('should return error message for too high integer values', async (input) => {
const value = await settingValidations(['revisionService.ttlDays'], { 'revisionService.ttlDays': input });
expect(value).toBe('Keep note history for cannot be greater than 730');
});

test.each(
[-999999999999999, 0, 999999999999999],
)('should return empty string for valid integer values for setting without range', async (input) => {
const value = await settingValidations(['style.editor.contentMaxWidth'], { 'style.editor.contentMaxWidth': input });
expect(value).toBe('');
});

test.each(
[1, 300, 730],
)('should return empty string for valid integer values for setting with range', async (input) => {
const value = await settingValidations(['revisionService.ttlDays'], { 'revisionService.ttlDays': input });
expect(value).toBe('');
});
});
Loading
Loading