1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-02-04 19:16:07 +02:00

Mobile: Resolves #8193: Implement parenting of notebooks (#7980)

This commit is contained in:
jcgurango 2023-05-29 18:31:21 +08:00 committed by GitHub
parent 12bba9da29
commit 230e7f6914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 57 deletions

View File

@ -363,6 +363,7 @@ packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js

1
.gitignore vendored
View File

@ -349,6 +349,7 @@ packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js

View File

@ -0,0 +1,90 @@
const React = require('react');
import { FunctionComponent } from 'react';
import { _ } from '@joplin/lib/locale';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
const { themeStyle } = require('./global-style.js');
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
import { FolderEntity } from '@joplin/lib/services/database/types';
interface FolderPickerProps {
disabled?: boolean;
selectedFolderId?: string;
onValueChange?: OnValueChangedListener;
mustSelect?: boolean;
folders: FolderEntity[];
placeholder?: string;
darkText?: boolean;
themeId?: string;
}
const FolderPicker: FunctionComponent<FolderPickerProps> = ({
disabled,
selectedFolderId,
onValueChange,
mustSelect,
folders,
placeholder,
darkText,
themeId,
}) => {
const theme = themeStyle(themeId);
const addFolderChildren = (
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
) => {
folders.sort((a, b) => {
const aTitle = a && a.title ? a.title : '';
const bTitle = b && b.title ? b.title : '';
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
});
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
const icon = Folder.unserializeIcon(f.icon);
const iconString = icon ? `${icon.emoji} ` : '';
pickerItems.push({ label: `${' '.repeat(indent)} ${iconString + Folder.displayTitle(f)}`, value: f.id });
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
}
return pickerItems;
};
const titlePickerItems = (mustSelect: boolean) => {
const folderList = folders.filter(f => f.id !== Folder.conflictFolderId());
let output = [];
if (mustSelect) output.push({ label: placeholder || _('Move to notebook...'), value: '' });
const folderTree = Folder.buildTree(folderList);
output = addFolderChildren(folderTree, output, 0);
return output;
};
return (
<Dropdown
items={titlePickerItems(!!mustSelect)}
disabled={disabled}
labelTransform="trim"
selectedValue={selectedFolderId || ''}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
color: darkText ? theme.colorFaded : theme.colorBright2,
fontSize: theme.fontSize,
opacity: disabled ? theme.disabledOpacity : 1,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(folderId) => {
if (onValueChange) {
onValueChange(folderId);
}
}}
/>
);
};
export default FolderPicker;

View File

@ -10,9 +10,9 @@ import { Menu, MenuOptions, MenuOption, MenuTrigger } from 'react-native-popup-m
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
import Folder from '@joplin/lib/models/Folder';
const { themeStyle } = require('./global-style.js');
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
import { OnValueChangedListener } from './Dropdown';
const { dialogs } = require('../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
@ -20,6 +20,7 @@ import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
import { FolderEntity } from '@joplin/lib/services/database/types';
import { State } from '@joplin/lib/reducer';
import CustomButton from './CustomButton';
import FolderPicker from './FolderPicker';
// We need this to suppress the useless warning
// https://github.com/oblador/react-native-vector-icons/issues/1465
@ -494,58 +495,14 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
}
const createTitleComponent = (disabled: boolean) => {
const themeId = Setting.value('theme');
const theme = themeStyle(themeId);
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
const addFolderChildren = (
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
) => {
folders.sort((a, b) => {
const aTitle = a && a.title ? a.title : '';
const bTitle = b && b.title ? b.title : '';
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
});
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
const icon = Folder.unserializeIcon(f.icon);
const iconString = icon ? `${icon.emoji} ` : '';
pickerItems.push({ label: `${' '.repeat(indent)} ${iconString + Folder.displayTitle(f)}`, value: f.id });
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
}
return pickerItems;
};
const titlePickerItems = (mustSelect: boolean) => {
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
let output = [];
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
const folderTree = Folder.buildTree(folders);
output = addFolderChildren(folderTree, output, 0);
return output;
};
return (
<Dropdown
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
<FolderPicker
themeId={themeId}
disabled={disabled}
labelTransform="trim"
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
color: theme.colorBright2,
fontSize: theme.fontSize,
opacity: disabled ? theme.disabledOpacity : 1,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
selectedFolderId={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
onValueChange={async (folderId) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the
@ -570,6 +527,8 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
await Note.moveToFolder(noteIds[i], folderId);
}
}}
mustSelect={!!folderPickerOptions.mustSelect}
folders={this.props.folders}
/>
);
} else {

View File

@ -1,6 +1,6 @@
const React = require('react');
const { View } = require('react-native');
const { View, StyleSheet } = require('react-native');
const { connect } = require('react-redux');
const Folder = require('@joplin/lib/models/Folder').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
@ -8,6 +8,7 @@ const { ScreenHeader } = require('../ScreenHeader');
const { BaseScreenComponent } = require('../base-screen.js');
const { dialogs } = require('../../utils/dialogs.js');
const { _ } = require('@joplin/lib/locale');
const { default: FolderPicker } = require('../FolderPicker');
const TextInput = require('../TextInput').default;
class FolderScreenComponent extends BaseScreenComponent {
@ -60,10 +61,16 @@ class FolderScreenComponent extends BaseScreenComponent {
this.folderComponent_change('title', text);
}
parent_changeValue(parent) {
this.folderComponent_change('parent_id', parent);
}
async saveFolderButton_press() {
let folder = Object.assign({}, this.state.folder);
try {
if (folder.id && !(await Folder.canNestUnder(folder.id, folder.parent_id))) throw new Error(_('Cannot move notebook to this location'));
folder = await Folder.save(folder, { userSideValidation: true });
} catch (error) {
dialogs.error(this, _('The notebook could not be saved: %s', error.message));
@ -83,7 +90,7 @@ class FolderScreenComponent extends BaseScreenComponent {
}
render() {
const saveButtonDisabled = !this.isModified();
const saveButtonDisabled = !this.isModified() || !this.state.folder.title;
return (
<View style={this.rootStyle(this.props.themeId).root}>
@ -94,7 +101,20 @@ class FolderScreenComponent extends BaseScreenComponent {
autoFocus={true}
value={this.state.folder.title}
onChangeText={text => this.title_changeText(text)}
disabled={this.state.folder.encryption_applied}
/>
<View style={styles.folderPickerContainer}>
<FolderPicker
themeId={this.props.themeId}
placeholder={_('Select parent notebook')}
folders={this.props.folders}
selectedFolderId={this.state.folder.parent_id}
onValueChange={newValue => this.parent_changeValue(newValue)}
mustSelect
darkText
/>
</View>
<View style={{ flex: 1 }} />
<dialogs.DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;
@ -109,7 +129,18 @@ const FolderScreen = connect(state => {
return {
folderId: state.selectedFolderId,
themeId: state.settings.theme,
folders: state.folders.filter((folder) => folder.id !== state.selectedFolderId),
};
})(FolderScreenComponent);
const styles = StyleSheet.create({
folderPickerContainer: {
height: 46,
paddingLeft: 14,
paddingRight: 14,
paddingTop: 12,
paddingBottom: 12,
},
});
module.exports = { FolderScreen };

View File

@ -140,13 +140,8 @@ const SideMenuContentComponent = (props: Props) => {
_('Notebook: %s', folder.title),
[
{
text: _('Rename'),
text: _('Edit'),
onPress: () => {
if (folder.encryption_applied) {
alert(_('Encrypted notebooks cannot be renamed'));
return;
}
props.dispatch({ type: 'SIDE_MENU_CLOSE' });
props.dispatch({