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 #10245: Allow marking items as "ignored" in sync status #10261

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
Expand Down Expand Up @@ -858,6 +859,7 @@ packages/lib/models/Tag.test.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/settings/settingValidations.test.js
packages/lib/models/settings/settingValidations.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
Expand Down Expand Up @@ -897,6 +899,7 @@ packages/lib/services/KvStore.js
packages/lib/services/MigrationService.js
packages/lib/services/NavService.js
packages/lib/services/PostMessageService.js
packages/lib/services/ReportService.test.js
packages/lib/services/ReportService.js
packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/reducer.js
Expand Down Expand Up @@ -924,6 +927,7 @@ packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/migrations/45.js
packages/lib/services/database/migrations/46.js
packages/lib/services/database/migrations/47.js
packages/lib/services/database/migrations/index.js
packages/lib/services/database/sqlStringToLines.js
packages/lib/services/database/types.js
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
Expand Down Expand Up @@ -838,6 +839,7 @@ packages/lib/models/Tag.test.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/settings/settingValidations.test.js
packages/lib/models/settings/settingValidations.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
Expand Down Expand Up @@ -877,6 +879,7 @@ packages/lib/services/KvStore.js
packages/lib/services/MigrationService.js
packages/lib/services/NavService.js
packages/lib/services/PostMessageService.js
packages/lib/services/ReportService.test.js
packages/lib/services/ReportService.js
packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/reducer.js
Expand Down Expand Up @@ -904,6 +907,7 @@ packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/migrations/45.js
packages/lib/services/database/migrations/46.js
packages/lib/services/database/migrations/47.js
packages/lib/services/database/migrations/index.js
packages/lib/services/database/sqlStringToLines.js
packages/lib/services/database/types.js
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,64 @@
const React = require('react');

const { View, Text, Button, FlatList } = require('react-native');
const Setting = require('@joplin/lib/models/Setting').default;
const { connect } = require('react-redux');
const { ScreenHeader } = require('../ScreenHeader');
const ReportService = require('@joplin/lib/services/ReportService').default;
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen');
const { themeStyle } = require('../global-style');

class StatusScreenComponent extends BaseScreenComponent {
static navigationOptions() {
return { header: null };
}
import * as React from 'react';

import { View, Text, Button, FlatList, TextStyle, StyleSheet } from 'react-native';
import Setting from '@joplin/lib/models/Setting';
import { connect } from 'react-redux';
import { ScreenHeader } from '../ScreenHeader';
import ReportService, { ReportSection } from '@joplin/lib/services/ReportService';
import { _ } from '@joplin/lib/locale';
import { BaseScreenComponent } from '../base-screen';
import { themeStyle } from '../global-style';
import { AppState } from '../../utils/types';
import checkDisabledSyncItemsNotification from '@joplin/lib/services/synchronizer/utils/checkDisabledSyncItemsNotification';
import { Dispatch } from 'redux';

interface Props {
themeId: number;
dispatch: Dispatch;
}

interface State {
report: ReportSection[];
}

constructor() {
super();
class StatusScreenComponent extends BaseScreenComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
report: [],
};
}

UNSAFE_componentWillMount() {
this.resfreshScreen();
public override componentDidMount() {
void this.refreshScreen();
laurent22 marked this conversation as resolved.
Show resolved Hide resolved
}

async resfreshScreen() {
private async refreshScreen() {
const service = new ReportService();
const report = await service.status(Setting.value('sync.target'));
this.setState({ report: report });
}

styles() {
private styles() {
const theme = themeStyle(this.props.themeId);
return {
return StyleSheet.create({
body: {
flex: 1,
margin: theme.margin,
},
};
actionButton: {
flex: 0,
marginLeft: 2,
marginRight: 2,
},
});
}

render() {
public override render() {
const theme = themeStyle(this.props.themeId);
const styles = this.styles();

const renderBody = report => {
const renderBody = (report: ReportSection[]) => {
const baseStyle = {
paddingLeft: 6,
paddingRight: 6,
Expand All @@ -60,7 +74,7 @@ class StatusScreenComponent extends BaseScreenComponent {
for (let i = 0; i < report.length; i++) {
const section = report[i];

let style = { ...baseStyle };
let style: TextStyle = { ...baseStyle };
style.fontWeight = 'bold';
if (i > 0) style.paddingTop = 20;
lines.push({ key: `section_${i}`, isSection: true, text: section.title });
Expand All @@ -76,19 +90,27 @@ class StatusScreenComponent extends BaseScreenComponent {
let text = '';

let retryHandler = null;
let ignoreHandler = null;
if (typeof item === 'object') {
if (item.canRetry) {
retryHandler = async () => {
await item.retryHandler();
this.resfreshScreen();
await this.refreshScreen();
};
}
if (item.canIgnore) {
ignoreHandler = async () => {
await item.ignoreHandler();
await this.refreshScreen();
await checkDisabledSyncItemsNotification((action) => this.props.dispatch(action));
};
}
text = item.text;
} else {
text = item;
}

lines.push({ key: `item_${i}_${n}`, text: text, retryHandler: retryHandler });
lines.push({ key: `item_${i}_${n}`, text: text, retryHandler, ignoreHandler });
}

lines.push({ key: `divider2_${i}`, isDivider: true });
Expand All @@ -98,7 +120,7 @@ class StatusScreenComponent extends BaseScreenComponent {
<FlatList
data={lines}
renderItem={({ item }) => {
const style = { ...baseStyle };
const style: TextStyle = { ...baseStyle };

if (item.isSection === true) {
style.fontWeight = 'bold';
Expand All @@ -114,17 +136,24 @@ class StatusScreenComponent extends BaseScreenComponent {
) : null;

const retryButton = item.retryHandler ? (
<View style={{ flex: 0 }}>
<View style={styles.actionButton}>
<Button title={_('Retry')} onPress={item.retryHandler} />
</View>
) : null;

const ignoreButton = item.ignoreHandler ? (
<View style={styles.actionButton}>
<Button title={_('Ignore')} onPress={item.ignoreHandler} />
</View>
) : null;

if (item.isDivider) {
return <View style={{ borderBottomWidth: 1, borderBottomColor: theme.dividerColor, marginTop: 20, marginBottom: 20 }} />;
} else {
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<Text style={style}>{item.text}</Text>
{ignoreButton}
{retryAllButton}
{retryButton}
</View>
Expand All @@ -140,17 +169,15 @@ class StatusScreenComponent extends BaseScreenComponent {
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Status')} />
<View style={this.styles().body}>{body}</View>
<Button title={_('Refresh')} onPress={() => this.resfreshScreen()} />
<View style={styles.body}>{body}</View>
<Button title={_('Refresh')} onPress={() => this.refreshScreen()} />
</View>
);
}
}

const StatusScreen = connect(state => {
export default connect((state: AppState) => {
return {
themeId: state.settings.theme,
};
})(StatusScreenComponent);

module.exports = { StatusScreen };
2 changes: 1 addition & 1 deletion packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const { TagsScreen } = require('./components/screens/tags.js');
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
const { FolderScreen } = require('./components/screens/folder.js');
import LogScreen from './components/screens/LogScreen';
const { StatusScreen } = require('./components/screens/status.js');
import StatusScreen from './components/screens/status';
const { SearchScreen } = require('./components/screens/search.js');
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
import EncryptionConfigScreen from './components/screens/encryption-config';
Expand Down
24 changes: 22 additions & 2 deletions packages/lib/models/BaseItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import JoplinError from '../JoplinError';
import { LoadOptions, SaveOptions } from './utils/types';
import { State as ShareState } from '../services/share/reducer';
import { checkIfItemCanBeAddedToFolder, checkIfItemCanBeChanged, checkIfItemsCanBeChanged, needsShareReadOnlyChecks } from './utils/readOnly';
import { checkObjectHasProperties } from '@joplin/utils/object';

const { sprintf } = require('sprintf-js');
const moment = require('moment');
Expand Down Expand Up @@ -820,16 +821,26 @@ export default class BaseItem extends BaseModel {
syncInfo: row,
location: row.item_location,
item: item,
warning_ignored: row.sync_warning_ignored,
});
}
return output;
}

public static async syncDisabledItemsCount(syncTargetId: number) {
const r = await this.db().selectOne('SELECT count(*) as total FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
public static async syncDisabledItemsCount(syncTargetId: number, includeIgnored = false) {
const whereQueries = ['sync_disabled = 1', 'sync_target = ?'];
const whereArgs = [syncTargetId];
if (!includeIgnored) {
whereQueries.push('sync_warning_ignored = 0');
}
const r = await this.db().selectOne(`SELECT count(*) as total FROM sync_items WHERE ${whereQueries.join(' AND ')}`, whereArgs);
return r ? r.total : 0;
}

public static async syncDisabledItemsCountIncludingIgnored(syncTargetId: number) {
return this.syncDisabledItemsCount(syncTargetId, true);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public static updateSyncTimeQueries(syncTarget: number, item: any, syncTime: number, syncDisabled = false, syncDisabledReason = '', itemLocation: number = null) {
const itemType = item.type_;
Expand Down Expand Up @@ -867,6 +878,15 @@ export default class BaseItem extends BaseModel {
await this.db().exec('DELETE FROM sync_items WHERE item_type = ? AND item_id = ?', [itemType, itemId]);
}

public static async ignoreItemSyncWarning(syncTarget: number, item: { type_?: number; id?: string }) {
checkObjectHasProperties(item, ['type_', 'id']);
const itemType = item.type_;
const itemId = item.id;
const sql = 'UPDATE sync_items SET sync_warning_ignored = ? WHERE item_id = ? AND item_type = ? AND sync_target = ?';
const params = [1, itemId, itemType, syncTarget];
await this.db().exec(sql, params);
}

// When an item is deleted, its associated sync_items data is not immediately deleted for
// performance reason. So this function is used to look for these remaining sync_items and
// delete them.
Expand Down
35 changes: 35 additions & 0 deletions packages/lib/models/settings/settingValidations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SyncTargetRegistry from '../../SyncTargetRegistry';
import { createNTestNotes, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import BaseItem from '../BaseItem';
import Folder from '../Folder';
import Setting from '../Setting';
import settingValidations from './settingValidations';

describe('settingValidations', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
});

test('sync disabled items should prevent switching sync targets unless ignored', async () => {
const folder = await Folder.save({ title: 'Test' });
const noteCount = 5;
const testNotes = await createNTestNotes(noteCount, folder);
const syncTargetId = SyncTargetRegistry.nameToId('memory');
Setting.setValue('sync.target', syncTargetId);

for (const testNote of testNotes) {
await BaseItem.saveSyncDisabled(syncTargetId, testNote, 'Disabled reason');
}

const newSyncTargetId = SyncTargetRegistry.nameToId('dropbox');
// Validation should fail with some error message.
expect(await settingValidations(['sync.target'], { 'sync.target': newSyncTargetId })).not.toBe('');

// Should pass after dismissing all warnings
for (const testNote of testNotes) {
await BaseItem.ignoreItemSyncWarning(syncTargetId, testNote);
}
expect(await settingValidations(['sync.target'], { 'sync.target': newSyncTargetId })).toBe('');
});
});
Loading
Loading