feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import NoteLockMigrationDialog from './Dialog';
|
||||
import { confirmAndSkipNoteLockKeyMigration, startNoteLockKeyMigration } from '@joplin/lib/services/noteLock/NoteLockKeyMigration';
|
||||
|
||||
jest.mock('@joplin/lib/services/noteLock/NoteLockKeyMigration', () => ({
|
||||
startNoteLockKeyMigration: jest.fn(),
|
||||
confirmAndSkipNoteLockKeyMigration: jest.fn(),
|
||||
}));
|
||||
|
||||
// The dialog renders through a portal, jsdom has no modal dialog support, and DialogTitle relies on
|
||||
// the React global the app bundle provides.
|
||||
shim.setReactDom(ReactDom);
|
||||
(globalThis as unknown as { React: typeof React }).React = React;
|
||||
HTMLDialogElement.prototype.showModal = function() { this.open = true; };
|
||||
HTMLDialogElement.prototype.close = function() { this.open = false; };
|
||||
|
||||
const startMock = startNoteLockKeyMigration as jest.Mock;
|
||||
const skipMock = confirmAndSkipNoteLockKeyMigration as jest.Mock;
|
||||
|
||||
const renderDialog = () => {
|
||||
const dispatch = jest.fn();
|
||||
render(<NoteLockMigrationDialog themeId={1} dispatch={dispatch}/>);
|
||||
fireEvent.change(screen.getByLabelText('Note lock password on this device'), { target: { value: 'local' } });
|
||||
fireEvent.change(screen.getByLabelText('Note lock password on the sync target'), { target: { value: 'target' } });
|
||||
return dispatch;
|
||||
};
|
||||
|
||||
describe('NoteLockMigrationDialog/Dialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should close once both passwords are accepted and leave the run to the banner', async () => {
|
||||
startMock.mockImplementation(async (_local: string, _target: string, _dispatch: unknown, onStarted: ()=> void) => onStarted());
|
||||
const dispatch = renderDialog();
|
||||
|
||||
fireEvent.click(screen.getByText('Migrate'));
|
||||
|
||||
await waitFor(() => expect(dispatch).toHaveBeenCalledWith({ type: 'DIALOG_CLOSE', name: 'noteLockMigration' }));
|
||||
expect(startMock).toHaveBeenCalledWith('local', 'target', dispatch, expect.any(Function));
|
||||
expect(skipMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report a wrong password and stay open', async () => {
|
||||
startMock.mockRejectedValue(Object.assign(new Error('bad'), { name: 'OperationError' }));
|
||||
const dispatch = renderDialog();
|
||||
|
||||
fireEvent.click(screen.getByText('Migrate'));
|
||||
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('Invalid password');
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([true, false])('should close only once the skip is confirmed (%s)', async (confirmed) => {
|
||||
skipMock.mockResolvedValue(confirmed);
|
||||
const dispatch = renderDialog();
|
||||
|
||||
fireEvent.click(screen.getByText('Skip'));
|
||||
|
||||
await waitFor(() => expect(skipMock).toHaveBeenCalledWith(dispatch));
|
||||
if (confirmed) await waitFor(() => expect(dispatch).toHaveBeenCalledWith({ type: 'DIALOG_CLOSE', name: 'noteLockMigration' }));
|
||||
else expect(dispatch).not.toHaveBeenCalled();
|
||||
expect(startMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
|
||||
import { confirmAndSkipNoteLockKeyMigration, startNoteLockKeyMigration } from '@joplin/lib/services/noteLock/NoteLockKeyMigration';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [localPassword, setLocalPassword] = useState('');
|
||||
const [targetPassword, setTargetPassword] = useState('');
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'noteLockMigration',
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onLocalPasswordChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLocalPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onTargetPasswordChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
|
||||
if (event.buttonName === 'cancel') {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.buttonName === 'skip') {
|
||||
if (await confirmAndSkipNoteLockKeyMigration(props.dispatch)) onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.buttonName === 'ok') {
|
||||
setMigrating(true);
|
||||
setErrorMessage('');
|
||||
try {
|
||||
await startNoteLockKeyMigration(localPassword, targetPassword, props.dispatch, onClose);
|
||||
} catch (error) {
|
||||
setErrorMessage(error.name === 'OperationError' ? _('Invalid password') : error.message);
|
||||
setMigrating(false);
|
||||
}
|
||||
}
|
||||
}, [localPassword, targetPassword, onClose, props.dispatch]);
|
||||
|
||||
return (
|
||||
<Dialog onCancel={migrating ? undefined : onClose} className="note-lock-migration-dialog">
|
||||
<div className="dialog-root">
|
||||
<DialogTitle title={_('Migrate locked notes')}/>
|
||||
<div className="dialog-content">
|
||||
<p>{_('The sync target uses a different note lock key to the one on your device. Your locked notes must be re-encrypted with the synced key before synchronisation can continue. Locked notes cannot be read while they are being migrated.')}</p>
|
||||
<div className="form">
|
||||
<LabelledPasswordInput
|
||||
labelText={_('Note lock password on this device')}
|
||||
value={localPassword}
|
||||
onChange={onLocalPasswordChange}
|
||||
/>
|
||||
<LabelledPasswordInput
|
||||
labelText={_('Note lock password on the sync target')}
|
||||
value={targetPassword}
|
||||
onChange={onTargetPasswordChange}
|
||||
/>
|
||||
</div>
|
||||
{!!errorMessage && <p className="error-message" role="alert">{errorMessage}</p>}
|
||||
</div>
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={onButtonRowClick}
|
||||
okButtonLabel={_('Migrate')}
|
||||
okButtonDisabled={!localPassword || !targetPassword || migrating}
|
||||
cancelButtonDisabled={migrating}
|
||||
customButtons={[{ name: 'skip', label: _('Skip'), disabled: migrating }]}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户