You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Chore: Mobile: Add additional plugin panel integration tests (#13152)
This commit is contained in:
@@ -924,6 +924,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
|||||||
packages/app-mobile/utils/ShareUtils.js
|
packages/app-mobile/utils/ShareUtils.js
|
||||||
packages/app-mobile/utils/TlsUtils.js
|
packages/app-mobile/utils/TlsUtils.js
|
||||||
packages/app-mobile/utils/appDefaultState.js
|
packages/app-mobile/utils/appDefaultState.js
|
||||||
|
packages/app-mobile/utils/appReducer.js
|
||||||
packages/app-mobile/utils/autodetectTheme.js
|
packages/app-mobile/utils/autodetectTheme.js
|
||||||
packages/app-mobile/utils/buildStartupTasks.js
|
packages/app-mobile/utils/buildStartupTasks.js
|
||||||
packages/app-mobile/utils/checkPermissions.js
|
packages/app-mobile/utils/checkPermissions.js
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -897,6 +897,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
|||||||
packages/app-mobile/utils/ShareUtils.js
|
packages/app-mobile/utils/ShareUtils.js
|
||||||
packages/app-mobile/utils/TlsUtils.js
|
packages/app-mobile/utils/TlsUtils.js
|
||||||
packages/app-mobile/utils/appDefaultState.js
|
packages/app-mobile/utils/appDefaultState.js
|
||||||
|
packages/app-mobile/utils/appReducer.js
|
||||||
packages/app-mobile/utils/autodetectTheme.js
|
packages/app-mobile/utils/autodetectTheme.js
|
||||||
packages/app-mobile/utils/buildStartupTasks.js
|
packages/app-mobile/utils/buildStartupTasks.js
|
||||||
packages/app-mobile/utils/checkPermissions.js
|
packages/app-mobile/utils/checkPermissions.js
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
|||||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||||
import PluginRunnerWebView from './PluginRunnerWebView';
|
import PluginRunnerWebView from './PluginRunnerWebView';
|
||||||
import TestProviderStack from '../testing/TestProviderStack';
|
import TestProviderStack from '../testing/TestProviderStack';
|
||||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
import { act, render, screen, waitFor } from '../../utils/testing/testingLibrary';
|
||||||
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
|
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
|
||||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
|
||||||
let store: Store<AppState>;
|
let store: Store<AppState>;
|
||||||
|
|
||||||
@@ -30,6 +31,16 @@ const defaultManifestProperties = {
|
|||||||
name: 'Some plugin name',
|
name: 'Some plugin name',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PluginSlice = { manifest: { id: string } };
|
||||||
|
const waitForPluginToLoad = (plugin: PluginSlice) => {
|
||||||
|
return waitFor(async () => {
|
||||||
|
expect(PluginService.instance().pluginById(plugin.manifest.id)).toBeTruthy();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const webViewId = 'joplin__PluginDialogWebView';
|
||||||
|
const getUserWebViewDom = () => getWebViewDomById(webViewId);
|
||||||
|
|
||||||
describe('PluginRunnerWebView', () => {
|
describe('PluginRunnerWebView', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await setupDatabaseAndSynchronizer(0);
|
await setupDatabaseAndSynchronizer(0);
|
||||||
@@ -56,16 +67,68 @@ describe('PluginRunnerWebView', () => {
|
|||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
render(<WrappedPluginRunnerWebView/>);
|
render(<WrappedPluginRunnerWebView/>);
|
||||||
|
await waitForPluginToLoad(testPlugin);
|
||||||
// Should load the plugin
|
|
||||||
await waitFor(async () => {
|
|
||||||
expect(PluginService.instance().pluginById(testPlugin.manifest.id)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should show the dialog
|
// Should show the dialog
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const dom = await getWebViewDomById('joplin__PluginDialogWebView');
|
const dom = await getUserWebViewDom();
|
||||||
expect(dom.querySelector('h1').textContent).toBe('Test!');
|
expect(dom.querySelector('h1').textContent).toBe('Test!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should load a plugin that adds a panel', async () => {
|
||||||
|
const testPlugin = await createTestPlugin({
|
||||||
|
...defaultManifestProperties,
|
||||||
|
id: 'org.joplinapp.panel-test',
|
||||||
|
}, {
|
||||||
|
onStart: `
|
||||||
|
const panels = joplin.views.panels;
|
||||||
|
const handle = await panels.create('test-panel');
|
||||||
|
await panels.setHtml(
|
||||||
|
handle,
|
||||||
|
'<h1>Panel content</h1><p>Test</p>',
|
||||||
|
);
|
||||||
|
|
||||||
|
const commands = joplin.commands;
|
||||||
|
await commands.register({
|
||||||
|
name: 'hideTestPanel',
|
||||||
|
label: 'Hide the test plugin panel',
|
||||||
|
execute: async () => {
|
||||||
|
await panels.hide(handle);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await commands.register({
|
||||||
|
name: 'showTestPanel',
|
||||||
|
execute: async () => {
|
||||||
|
await panels.show(handle);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
render(<WrappedPluginRunnerWebView/>);
|
||||||
|
await waitForPluginToLoad(testPlugin);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
store.dispatch({ type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE', visible: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectPanelVisible = async () => {
|
||||||
|
const dom = await getUserWebViewDom();
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(dom.querySelector('h1').textContent).toBe('Panel content');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
await expectPanelVisible();
|
||||||
|
|
||||||
|
// Should hide the panel
|
||||||
|
await act(() => CommandService.instance().execute('hideTestPanel'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('webViewId')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show the panel again
|
||||||
|
await act(() => CommandService.instance().execute('showTestPanel'));
|
||||||
|
await expectPanelVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.webViewContainer}>
|
<View style={styles.webViewContainer} testID='plugin-tab-content'>
|
||||||
<PluginUserWebView
|
<PluginUserWebView
|
||||||
key={selectedTabId}
|
key={selectedTabId}
|
||||||
themeId={props.themeId}
|
themeId={props.themeId}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import NoteScreen from './components/screens/Note/Note';
|
|||||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||||
import Setting, { } from '@joplin/lib/models/Setting';
|
import Setting, { } from '@joplin/lib/models/Setting';
|
||||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||||
import reducer, { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
import { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||||
import ShareExtension, { UnsubscribeShareListener } from './utils/ShareExtension';
|
import ShareExtension, { UnsubscribeShareListener } from './utils/ShareExtension';
|
||||||
import handleShared from './utils/shareHandler';
|
import handleShared from './utils/shareHandler';
|
||||||
import { _, setLocale } from '@joplin/lib/locale';
|
import { _, setLocale } from '@joplin/lib/locale';
|
||||||
@@ -28,7 +28,6 @@ import NetInfo, { NetInfoSubscription } from '@react-native-community/netinfo';
|
|||||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||||
import SafeAreaView from './components/SafeAreaView';
|
import SafeAreaView from './components/SafeAreaView';
|
||||||
const { connect, Provider } = require('react-redux');
|
const { connect, Provider } = require('react-redux');
|
||||||
import fastDeepEqual = require('fast-deep-equal');
|
|
||||||
import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
|
import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
|
||||||
import BackButtonService, { BackButtonHandler } from './services/BackButtonService';
|
import BackButtonService, { BackButtonHandler } from './services/BackButtonService';
|
||||||
import NavService from '@joplin/lib/services/NavService';
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
@@ -95,7 +94,6 @@ import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTh
|
|||||||
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
|
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
|
||||||
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
|
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||||
import ShareManager from './components/screens/ShareManager';
|
import ShareManager from './components/screens/ShareManager';
|
||||||
import appDefaultState from './utils/appDefaultState';
|
|
||||||
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
||||||
import DialogManager from './components/DialogManager';
|
import DialogManager from './components/DialogManager';
|
||||||
import { AppState } from './utils/types';
|
import { AppState } from './utils/types';
|
||||||
@@ -108,6 +106,7 @@ import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
|
|||||||
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
|
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
|
||||||
import buildStartupTasks from './utils/buildStartupTasks';
|
import buildStartupTasks from './utils/buildStartupTasks';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
import appReducer from './utils/appReducer';
|
||||||
|
|
||||||
const logger = Logger.create('root');
|
const logger = Logger.create('root');
|
||||||
const perfLogger = PerformanceLogger.create();
|
const perfLogger = PerformanceLogger.create();
|
||||||
@@ -235,204 +234,6 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const navHistory: any[] = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
function historyCanGoBackTo(route: any) {
|
|
||||||
if (route.routeName === 'Folder') return false;
|
|
||||||
|
|
||||||
// This is an intermediate screen that acts more like a modal -- it should be skipped in the
|
|
||||||
// navigation history.
|
|
||||||
if (route.routeName === 'DocumentScanner') return false;
|
|
||||||
|
|
||||||
// There's no point going back to these screens in general and, at least in OneDrive case,
|
|
||||||
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
|
||||||
if (route.routeName === 'OneDriveLogin') return false;
|
|
||||||
if (route.routeName === 'DropboxLogin') return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const appReducer = (state = appDefaultState, action: any) => {
|
|
||||||
let newState = state;
|
|
||||||
let historyGoingBack = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case 'NAV_BACK':
|
|
||||||
case 'NAV_GO':
|
|
||||||
|
|
||||||
if (action.type === 'NAV_BACK') {
|
|
||||||
if (!navHistory.length) break;
|
|
||||||
|
|
||||||
const newAction = navHistory.pop();
|
|
||||||
action = newAction ? newAction : navHistory.pop();
|
|
||||||
|
|
||||||
historyGoingBack = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const currentRoute = state.route;
|
|
||||||
|
|
||||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
|
||||||
const previousRoute = navHistory.length && navHistory[navHistory.length - 1];
|
|
||||||
const isDifferentRoute = !previousRoute || !fastDeepEqual(navHistory[navHistory.length - 1], currentRoute);
|
|
||||||
|
|
||||||
// Avoid multiple consecutive duplicate screens in the navigation history -- these can make
|
|
||||||
// pressing "back" seem to have no effect.
|
|
||||||
if (isDifferentRoute) {
|
|
||||||
navHistory.push(currentRoute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.clearHistory) {
|
|
||||||
navHistory.splice(0, navHistory.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
|
|
||||||
newState.selectedNoteHash = '';
|
|
||||||
|
|
||||||
if (action.routeName === 'Search') {
|
|
||||||
newState.notesParentType = 'Search';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('noteId' in action) {
|
|
||||||
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('folderId' in action) {
|
|
||||||
newState.selectedFolderId = action.folderId;
|
|
||||||
newState.notesParentType = 'Folder';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('tagId' in action) {
|
|
||||||
newState.selectedTagId = action.tagId;
|
|
||||||
newState.notesParentType = 'Tag';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('smartFilterId' in action) {
|
|
||||||
newState.smartFilterId = action.smartFilterId;
|
|
||||||
newState.selectedSmartFilterId = action.smartFilterId;
|
|
||||||
newState.notesParentType = 'SmartFilter';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('itemType' in action) {
|
|
||||||
newState.selectedItemType = action.itemType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('noteHash' in action) {
|
|
||||||
newState.selectedNoteHash = action.noteHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('sharedData' in action) {
|
|
||||||
newState.sharedData = action.sharedData;
|
|
||||||
} else {
|
|
||||||
newState.sharedData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
newState.route = action;
|
|
||||||
newState.historyCanGoBack = !!navHistory.length;
|
|
||||||
|
|
||||||
logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SIDE_MENU_TOGGLE':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.showSideMenu = !newState.showSideMenu;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SIDE_MENU_OPEN':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.showSideMenu = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SIDE_MENU_CLOSE':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.showSideMenu = false;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
|
||||||
newState = { ...state };
|
|
||||||
newState.showPanelsDialog = action.visible;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_SELECTION_TOGGLE':
|
|
||||||
|
|
||||||
{
|
|
||||||
newState = { ...state };
|
|
||||||
|
|
||||||
const noteId = action.id;
|
|
||||||
const newSelectedNoteIds = state.selectedNoteIds.slice();
|
|
||||||
const existingIndex = state.selectedNoteIds.indexOf(noteId);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
newSelectedNoteIds.splice(existingIndex, 1);
|
|
||||||
} else {
|
|
||||||
newSelectedNoteIds.push(noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
newState.selectedNoteIds = newSelectedNoteIds;
|
|
||||||
newState.noteSelectionEnabled = !!newSelectedNoteIds.length;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_SELECTION_START':
|
|
||||||
|
|
||||||
if (!state.noteSelectionEnabled) {
|
|
||||||
newState = { ...state };
|
|
||||||
newState.noteSelectionEnabled = true;
|
|
||||||
newState.selectedNoteIds = [action.id];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_SELECTION_END':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.noteSelectionEnabled = false;
|
|
||||||
newState.selectedNoteIds = [];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_SIDE_MENU_OPTIONS_SET':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.noteSideMenuOptions = action.options;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED':
|
|
||||||
newState = { ...state };
|
|
||||||
newState.disableSideMenuGestures = action.disableSideMenuGestures;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'MOBILE_DATA_WARNING_UPDATE':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.isOnMobileData = action.isOnMobileData;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'KEYBOARD_VISIBLE_CHANGE':
|
|
||||||
newState = { ...state, keyboardVisible: action.visible };
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_EDITOR_VISIBLE_CHANGE':
|
|
||||||
newState = { ...state, noteEditorVisible: action.visible };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reducer(newState, action) as AppState;
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
||||||
storeDispatch = store.dispatch;
|
storeDispatch = store.dispatch;
|
||||||
|
|
||||||
|
|||||||
207
packages/app-mobile/utils/appReducer.ts
Normal file
207
packages/app-mobile/utils/appReducer.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import reducer from '@joplin/lib/reducer';
|
||||||
|
import { AppState } from './types';
|
||||||
|
import appDefaultState from './appDefaultState';
|
||||||
|
import fastDeepEqual = require('fast-deep-equal');
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
|
||||||
|
const logger = Logger.create('appReducer');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
const navHistory: any[] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
function historyCanGoBackTo(route: any) {
|
||||||
|
if (route.routeName === 'Folder') return false;
|
||||||
|
|
||||||
|
// This is an intermediate screen that acts more like a modal -- it should be skipped in the
|
||||||
|
// navigation history.
|
||||||
|
if (route.routeName === 'DocumentScanner') return false;
|
||||||
|
|
||||||
|
// There's no point going back to these screens in general and, at least in OneDrive case,
|
||||||
|
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
||||||
|
if (route.routeName === 'OneDriveLogin') return false;
|
||||||
|
if (route.routeName === 'DropboxLogin') return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
const appReducer = (state = appDefaultState, action: any) => {
|
||||||
|
let newState = state;
|
||||||
|
let historyGoingBack = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case 'NAV_BACK':
|
||||||
|
case 'NAV_GO':
|
||||||
|
|
||||||
|
if (action.type === 'NAV_BACK') {
|
||||||
|
if (!navHistory.length) break;
|
||||||
|
|
||||||
|
const newAction = navHistory.pop();
|
||||||
|
action = newAction ? newAction : navHistory.pop();
|
||||||
|
|
||||||
|
historyGoingBack = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const currentRoute = state.route;
|
||||||
|
|
||||||
|
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
||||||
|
const previousRoute = navHistory.length && navHistory[navHistory.length - 1];
|
||||||
|
const isDifferentRoute = !previousRoute || !fastDeepEqual(navHistory[navHistory.length - 1], currentRoute);
|
||||||
|
|
||||||
|
// Avoid multiple consecutive duplicate screens in the navigation history -- these can make
|
||||||
|
// pressing "back" seem to have no effect.
|
||||||
|
if (isDifferentRoute) {
|
||||||
|
navHistory.push(currentRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.clearHistory) {
|
||||||
|
navHistory.splice(0, navHistory.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
|
||||||
|
newState.selectedNoteHash = '';
|
||||||
|
|
||||||
|
if (action.routeName === 'Search') {
|
||||||
|
newState.notesParentType = 'Search';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('noteId' in action) {
|
||||||
|
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('folderId' in action) {
|
||||||
|
newState.selectedFolderId = action.folderId;
|
||||||
|
newState.notesParentType = 'Folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('tagId' in action) {
|
||||||
|
newState.selectedTagId = action.tagId;
|
||||||
|
newState.notesParentType = 'Tag';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smartFilterId' in action) {
|
||||||
|
newState.smartFilterId = action.smartFilterId;
|
||||||
|
newState.selectedSmartFilterId = action.smartFilterId;
|
||||||
|
newState.notesParentType = 'SmartFilter';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('itemType' in action) {
|
||||||
|
newState.selectedItemType = action.itemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('noteHash' in action) {
|
||||||
|
newState.selectedNoteHash = action.noteHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('sharedData' in action) {
|
||||||
|
newState.sharedData = action.sharedData;
|
||||||
|
} else {
|
||||||
|
newState.sharedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.route = action;
|
||||||
|
newState.historyCanGoBack = !!navHistory.length;
|
||||||
|
|
||||||
|
logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SIDE_MENU_TOGGLE':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.showSideMenu = !newState.showSideMenu;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SIDE_MENU_OPEN':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.showSideMenu = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SIDE_MENU_CLOSE':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.showSideMenu = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
||||||
|
newState = { ...state };
|
||||||
|
newState.showPanelsDialog = action.visible;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTE_SELECTION_TOGGLE':
|
||||||
|
|
||||||
|
{
|
||||||
|
newState = { ...state };
|
||||||
|
|
||||||
|
const noteId = action.id;
|
||||||
|
const newSelectedNoteIds = state.selectedNoteIds.slice();
|
||||||
|
const existingIndex = state.selectedNoteIds.indexOf(noteId);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
newSelectedNoteIds.splice(existingIndex, 1);
|
||||||
|
} else {
|
||||||
|
newSelectedNoteIds.push(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.selectedNoteIds = newSelectedNoteIds;
|
||||||
|
newState.noteSelectionEnabled = !!newSelectedNoteIds.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTE_SELECTION_START':
|
||||||
|
|
||||||
|
if (!state.noteSelectionEnabled) {
|
||||||
|
newState = { ...state };
|
||||||
|
newState.noteSelectionEnabled = true;
|
||||||
|
newState.selectedNoteIds = [action.id];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTE_SELECTION_END':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.noteSelectionEnabled = false;
|
||||||
|
newState.selectedNoteIds = [];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTE_SIDE_MENU_OPTIONS_SET':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.noteSideMenuOptions = action.options;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED':
|
||||||
|
newState = { ...state };
|
||||||
|
newState.disableSideMenuGestures = action.disableSideMenuGestures;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MOBILE_DATA_WARNING_UPDATE':
|
||||||
|
|
||||||
|
newState = { ...state };
|
||||||
|
newState.isOnMobileData = action.isOnMobileData;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'KEYBOARD_VISIBLE_CHANGE':
|
||||||
|
newState = { ...state, keyboardVisible: action.visible };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTE_EDITOR_VISIBLE_CHANGE':
|
||||||
|
newState = { ...state, noteEditorVisible: action.visible };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reducer(newState, action) as AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default appReducer;
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import reducer from '@joplin/lib/reducer';
|
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
import appDefaultState from '../appDefaultState';
|
import appDefaultState from '../appDefaultState';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { AppState } from '../types';
|
import { AppState } from '../types';
|
||||||
|
import appReducer from '../appReducer';
|
||||||
|
|
||||||
const testReducer = (state: AppState|undefined, action: unknown): AppState => {
|
const testReducer = (state: AppState|undefined, action: unknown): AppState => {
|
||||||
state ??= {
|
state ??= {
|
||||||
...appDefaultState,
|
...appDefaultState,
|
||||||
settings: Setting.toPlainObject(),
|
settings: Setting.toPlainObject(),
|
||||||
};
|
};
|
||||||
return { ...state, ...reducer(state, action) };
|
|
||||||
|
return { ...state, ...appReducer(state, action) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMockReduxStore = () => {
|
const createMockReduxStore = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user