mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-17 18:44:45 +02:00
Chore: Mobile: Migrate the tags dialog to TypeScript (#10185)
This commit is contained in:
parent
0331d2a8db
commit
b5a16f756a
@ -496,6 +496,7 @@ packages/app-mobile/components/ExtendedWebView.js
|
|||||||
packages/app-mobile/components/FolderPicker.js
|
packages/app-mobile/components/FolderPicker.js
|
||||||
packages/app-mobile/components/Icon.js
|
packages/app-mobile/components/Icon.js
|
||||||
packages/app-mobile/components/Modal.js
|
packages/app-mobile/components/Modal.js
|
||||||
|
packages/app-mobile/components/ModalDialog.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||||
@ -592,6 +593,7 @@ packages/app-mobile/components/screens/ConfigScreen/types.js
|
|||||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||||
packages/app-mobile/components/screens/LogScreen.js
|
packages/app-mobile/components/screens/LogScreen.js
|
||||||
packages/app-mobile/components/screens/Note.js
|
packages/app-mobile/components/screens/Note.js
|
||||||
|
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes.js
|
||||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||||
packages/app-mobile/components/screens/encryption-config.js
|
packages/app-mobile/components/screens/encryption-config.js
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -476,6 +476,7 @@ packages/app-mobile/components/ExtendedWebView.js
|
|||||||
packages/app-mobile/components/FolderPicker.js
|
packages/app-mobile/components/FolderPicker.js
|
||||||
packages/app-mobile/components/Icon.js
|
packages/app-mobile/components/Icon.js
|
||||||
packages/app-mobile/components/Modal.js
|
packages/app-mobile/components/Modal.js
|
||||||
|
packages/app-mobile/components/ModalDialog.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||||
@ -572,6 +573,7 @@ packages/app-mobile/components/screens/ConfigScreen/types.js
|
|||||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||||
packages/app-mobile/components/screens/LogScreen.js
|
packages/app-mobile/components/screens/LogScreen.js
|
||||||
packages/app-mobile/components/screens/Note.js
|
packages/app-mobile/components/screens/Note.js
|
||||||
|
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes.js
|
||||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||||
packages/app-mobile/components/screens/encryption-config.js
|
packages/app-mobile/components/screens/encryption-config.js
|
||||||
|
@ -1,24 +1,41 @@
|
|||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
const { Text, View, StyleSheet, Button } = require('react-native');
|
import { ReactNode } from 'react';
|
||||||
const { themeStyle } = require('./global-style');
|
import { Text, View, StyleSheet, Button, TextStyle, ViewStyle } from 'react-native';
|
||||||
const { _ } = require('@joplin/lib/locale');
|
import { themeStyle } from './global-style';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
class ModalDialog extends React.Component {
|
interface Props {
|
||||||
constructor() {
|
themeId: number;
|
||||||
super();
|
ContentComponent: ReactNode;
|
||||||
|
|
||||||
|
buttonBarEnabled: boolean;
|
||||||
|
title: string;
|
||||||
|
onOkPress: ()=> void;
|
||||||
|
onCancelPress: ()=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModalDialog extends React.Component<Props, State> {
|
||||||
|
private styles_: any;
|
||||||
|
|
||||||
|
public constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
styles() {
|
private styles() {
|
||||||
const themeId = this.props.themeId;
|
const themeId = this.props.themeId;
|
||||||
const theme = themeStyle(themeId);
|
const theme = themeStyle(themeId);
|
||||||
|
|
||||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
|
|
||||||
const styles = {
|
const styles: Record<string, ViewStyle|TextStyle> = {
|
||||||
modalWrapper: {
|
modalWrapper: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -53,7 +70,7 @@ class ModalDialog extends React.Component {
|
|||||||
return this.styles_[themeId];
|
return this.styles_[themeId];
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public override render() {
|
||||||
const ContentComponent = this.props.ContentComponent;
|
const ContentComponent = this.props.ContentComponent;
|
||||||
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
|
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
|
||||||
|
|
||||||
@ -76,4 +93,4 @@ class ModalDialog extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ModalDialog;
|
export default ModalDialog;
|
@ -26,7 +26,7 @@ import ActionButton from '../ActionButton';
|
|||||||
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
|
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
|
||||||
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
|
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
|
||||||
import ScreenHeader, { MenuOptionType } from '../ScreenHeader';
|
import ScreenHeader, { MenuOptionType } from '../ScreenHeader';
|
||||||
const NoteTagsDialog = require('./NoteTagsDialog');
|
import NoteTagsDialog from './NoteTagsDialog';
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
const { Checkbox } = require('../checkbox.js');
|
const { Checkbox } = require('../checkbox.js');
|
||||||
import { _, currentLocale } from '@joplin/lib/locale';
|
import { _, currentLocale } from '@joplin/lib/locale';
|
||||||
|
@ -1,213 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
|
|
||||||
const { StyleSheet, View, Text, FlatList, TouchableOpacity, TextInput } = require('react-native');
|
|
||||||
const { connect } = require('react-redux');
|
|
||||||
const Tag = require('@joplin/lib/models/Tag').default;
|
|
||||||
const { _ } = require('@joplin/lib/locale');
|
|
||||||
const { themeStyle } = require('../global-style');
|
|
||||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
|
||||||
const ModalDialog = require('../ModalDialog');
|
|
||||||
const naturalCompare = require('string-natural-compare');
|
|
||||||
|
|
||||||
// We need this to suppress the useless warning
|
|
||||||
// https://github.com/oblador/react-native-vector-icons/issues/1465
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
Icon.loadFont().catch((error) => { console.info(error); });
|
|
||||||
|
|
||||||
class NoteTagsDialogComponent extends React.Component {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.styles_ = {};
|
|
||||||
this.state = {
|
|
||||||
noteTagIds: [],
|
|
||||||
noteId: null,
|
|
||||||
tagListData: [],
|
|
||||||
newTags: '',
|
|
||||||
savingTags: false,
|
|
||||||
tagFilter: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const noteHasTag = tagId => {
|
|
||||||
for (let i = 0; i < this.state.tagListData.length; i++) {
|
|
||||||
if (this.state.tagListData[i].id === tagId) return this.state.tagListData[i].selected;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newTagTitles = () => {
|
|
||||||
return this.state.newTags
|
|
||||||
.split(',')
|
|
||||||
.map(t => t.trim().toLowerCase())
|
|
||||||
.filter(t => !!t);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tag_press = tagId => {
|
|
||||||
const newData = this.state.tagListData.slice();
|
|
||||||
for (let i = 0; i < newData.length; i++) {
|
|
||||||
const t = newData[i];
|
|
||||||
if (t.id === tagId) {
|
|
||||||
const newTag = { ...t };
|
|
||||||
newTag.selected = !newTag.selected;
|
|
||||||
newData[i] = newTag;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ tagListData: newData });
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderTag = data => {
|
|
||||||
const tag = data.item;
|
|
||||||
const iconName = noteHasTag(tag.id) ? 'checkbox-outline' : 'square-outline';
|
|
||||||
return (
|
|
||||||
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
|
|
||||||
<View style={this.styles().tagIconText}>
|
|
||||||
<Icon name={iconName} style={this.styles().tagCheckbox} />
|
|
||||||
<Text style={this.styles().tagText}>{tag.title}</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tagKeyExtractor = (tag) => tag.id;
|
|
||||||
|
|
||||||
this.okButton_press = async () => {
|
|
||||||
this.setState({ savingTags: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tagIds = this.state.tagListData.filter(t => t.selected).map(t => t.id);
|
|
||||||
await Tag.setNoteTagsByIds(this.state.noteId, tagIds);
|
|
||||||
|
|
||||||
const extraTitles = newTagTitles();
|
|
||||||
for (let i = 0; i < extraTitles.length; i++) {
|
|
||||||
await Tag.addNoteTagByTitle(this.state.noteId, extraTitles[i]);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.setState({ savingTags: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.cancelButton_press = () => {
|
|
||||||
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.filterTags = (allTags) => {
|
|
||||||
return allTags.filter((tag) => tag.title.toLowerCase().includes(this.state.tagFilter.toLowerCase()), allTags);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount() {
|
|
||||||
const noteId = this.props.noteId;
|
|
||||||
this.setState({ noteId: noteId });
|
|
||||||
this.loadNoteTags(noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNoteTags(noteId) {
|
|
||||||
const tags = await Tag.tagsByNoteId(noteId);
|
|
||||||
const tagIds = tags.map(t => t.id);
|
|
||||||
|
|
||||||
const tagListData = this.props.tags.map(tag => {
|
|
||||||
return {
|
|
||||||
id: tag.id,
|
|
||||||
title: tag.title,
|
|
||||||
selected: tagIds.indexOf(tag.id) >= 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
tagListData.sort((a, b) => {
|
|
||||||
if (a.selected === b.selected) return naturalCompare(a.title, b.title, { caseInsensitive: true });
|
|
||||||
else if (b.selected === true) return 1;
|
|
||||||
else return -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ tagListData: tagListData });
|
|
||||||
}
|
|
||||||
|
|
||||||
styles() {
|
|
||||||
const themeId = this.props.themeId;
|
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
|
|
||||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
|
||||||
this.styles_ = {};
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
tag: {
|
|
||||||
padding: 10,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: theme.dividerColor,
|
|
||||||
},
|
|
||||||
tagIconText: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
tagText: { ...theme.normalText },
|
|
||||||
tagCheckbox: {
|
|
||||||
marginRight: 8,
|
|
||||||
fontSize: 20,
|
|
||||||
color: theme.color,
|
|
||||||
},
|
|
||||||
tagBox: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: 10,
|
|
||||||
paddingRight: 10,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: theme.dividerColor,
|
|
||||||
},
|
|
||||||
newTagBoxLabel: { ...theme.normalText, marginRight: 8 },
|
|
||||||
tagBoxInput: { ...theme.lineInput, flex: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
this.styles_[themeId] = StyleSheet.create(styles);
|
|
||||||
return this.styles_[themeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const dialogContent = (
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<View style={this.styles().tagBox}>
|
|
||||||
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
|
|
||||||
<TextInput
|
|
||||||
selectionColor={theme.textSelectionColor}
|
|
||||||
keyboardAppearance={theme.keyboardAppearance}
|
|
||||||
value={this.state.newTags}
|
|
||||||
onChangeText={value => {
|
|
||||||
this.setState({ newTags: value });
|
|
||||||
}}
|
|
||||||
style={this.styles().tagBoxInput}
|
|
||||||
placeholder={_('tag1, tag2, ...')}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={this.styles().tagBox}>
|
|
||||||
<TextInput
|
|
||||||
selectionColor={theme.textSelectionColor}
|
|
||||||
keyboardAppearance={theme.keyboardAppearance}
|
|
||||||
value={this.state.tagFilter}
|
|
||||||
onChangeText={value => {
|
|
||||||
this.setState({ tagFilter: value });
|
|
||||||
}}
|
|
||||||
placeholder={_('Filter tags')}
|
|
||||||
style={this.styles().tagBoxInput}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<FlatList data={this.filterTags(this.state.tagListData)} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <ModalDialog themeId={this.props.themeId} ContentComponent={dialogContent} title={_('Type new tags or select from list')} onOkPress={this.okButton_press} onCancelPress={this.cancelButton_press} buttonBarEnabled={!this.state.savingTags} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NoteTagsDialog = connect(state => {
|
|
||||||
return {
|
|
||||||
themeId: state.settings.theme,
|
|
||||||
tags: state.tags,
|
|
||||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
|
||||||
};
|
|
||||||
})(NoteTagsDialogComponent);
|
|
||||||
|
|
||||||
module.exports = NoteTagsDialog;
|
|
239
packages/app-mobile/components/screens/NoteTagsDialog.tsx
Normal file
239
packages/app-mobile/components/screens/NoteTagsDialog.tsx
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { StyleSheet, View, Text, FlatList, TouchableOpacity, TextInput } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Tag from '@joplin/lib/models/Tag';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { themeStyle } from '../global-style';
|
||||||
|
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||||
|
import ModalDialog from '../ModalDialog';
|
||||||
|
import { AppState } from '../../utils/types';
|
||||||
|
import { TagEntity } from '@joplin/lib/services/database/types';
|
||||||
|
const naturalCompare = require('string-natural-compare');
|
||||||
|
|
||||||
|
// We need this to suppress the useless warning
|
||||||
|
// https://github.com/oblador/react-native-vector-icons/issues/1465
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
Icon.loadFont().catch((error: any) => { console.info(error); });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
noteId: string|null;
|
||||||
|
onCloseRequested?: ()=> void;
|
||||||
|
tags: TagEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagListRecord {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
noteTagIds: string[];
|
||||||
|
tagListData: TagListRecord[];
|
||||||
|
noteId: string|null;
|
||||||
|
newTags: string;
|
||||||
|
savingTags: boolean;
|
||||||
|
tagFilter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteTagsDialogComponent extends React.Component<Props, State> {
|
||||||
|
private styles_: any;
|
||||||
|
|
||||||
|
public constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.styles_ = {};
|
||||||
|
this.state = {
|
||||||
|
noteTagIds: [],
|
||||||
|
noteId: null,
|
||||||
|
tagListData: [],
|
||||||
|
newTags: '',
|
||||||
|
savingTags: false,
|
||||||
|
tagFilter: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private noteHasTag(tagId: string) {
|
||||||
|
for (let i = 0; i < this.state.tagListData.length; i++) {
|
||||||
|
if (this.state.tagListData[i].id === tagId) return this.state.tagListData[i].selected;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private newTagTitles() {
|
||||||
|
return this.state.newTags
|
||||||
|
.split(',')
|
||||||
|
.map(t => t.trim().toLowerCase())
|
||||||
|
.filter(t => !!t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tag_press = (tagId: string) => {
|
||||||
|
const newData = this.state.tagListData.slice();
|
||||||
|
for (let i = 0; i < newData.length; i++) {
|
||||||
|
const t = newData[i];
|
||||||
|
if (t.id === tagId) {
|
||||||
|
const newTag = { ...t };
|
||||||
|
newTag.selected = !newTag.selected;
|
||||||
|
newData[i] = newTag;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ tagListData: newData });
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderTag = (data: { item: TagListRecord }) => {
|
||||||
|
const tag = data.item;
|
||||||
|
const iconName = this.noteHasTag(tag.id) ? 'checkbox-outline' : 'square-outline';
|
||||||
|
return (
|
||||||
|
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
|
||||||
|
<View style={this.styles().tagIconText}>
|
||||||
|
<Icon name={iconName} style={this.styles().tagCheckbox} />
|
||||||
|
<Text style={this.styles().tagText}>{tag.title}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private tagKeyExtractor = (tag: TagListRecord) => tag.id;
|
||||||
|
|
||||||
|
private okButton_press = async () => {
|
||||||
|
this.setState({ savingTags: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tagIds = this.state.tagListData.filter(t => t.selected).map(t => t.id);
|
||||||
|
await Tag.setNoteTagsByIds(this.state.noteId, tagIds);
|
||||||
|
|
||||||
|
const extraTitles = this.newTagTitles();
|
||||||
|
for (let i = 0; i < extraTitles.length; i++) {
|
||||||
|
await Tag.addNoteTagByTitle(this.state.noteId, extraTitles[i]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.setState({ savingTags: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
||||||
|
};
|
||||||
|
|
||||||
|
private cancelButton_press = () => {
|
||||||
|
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
||||||
|
};
|
||||||
|
|
||||||
|
private filterTags(allTags: TagListRecord[]) {
|
||||||
|
return allTags.filter((tag) => tag.title.toLowerCase().includes(this.state.tagFilter.toLowerCase()), allTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UNSAFE_componentWillMount() {
|
||||||
|
const noteId = this.props.noteId;
|
||||||
|
this.setState({ noteId: noteId });
|
||||||
|
void this.loadNoteTags(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadNoteTags(noteId: string) {
|
||||||
|
const tags = await Tag.tagsByNoteId(noteId);
|
||||||
|
const tagIds = tags.map(t => t.id);
|
||||||
|
|
||||||
|
const tagListData = this.props.tags.map(tag => {
|
||||||
|
return {
|
||||||
|
id: tag.id,
|
||||||
|
title: tag.title,
|
||||||
|
selected: tagIds.indexOf(tag.id) >= 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
tagListData.sort((a, b) => {
|
||||||
|
if (a.selected === b.selected) return naturalCompare(a.title, b.title, { caseInsensitive: true });
|
||||||
|
else if (b.selected === true) return 1;
|
||||||
|
else return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ tagListData: tagListData });
|
||||||
|
}
|
||||||
|
|
||||||
|
private styles() {
|
||||||
|
const themeId = this.props.themeId;
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
|
||||||
|
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||||
|
this.styles_ = {};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tag: {
|
||||||
|
padding: 10,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: theme.dividerColor,
|
||||||
|
},
|
||||||
|
tagIconText: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
tagText: { ...theme.normalText },
|
||||||
|
tagCheckbox: {
|
||||||
|
marginRight: 8,
|
||||||
|
fontSize: 20,
|
||||||
|
color: theme.color,
|
||||||
|
},
|
||||||
|
tagBox: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: theme.dividerColor,
|
||||||
|
},
|
||||||
|
newTagBoxLabel: { ...theme.normalText, marginRight: 8 },
|
||||||
|
tagBoxInput: { ...theme.lineInput, flex: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.styles_[themeId] = styles;
|
||||||
|
return this.styles_[themeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override render() {
|
||||||
|
const theme = themeStyle(this.props.themeId);
|
||||||
|
|
||||||
|
const dialogContent = (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<View style={this.styles().tagBox}>
|
||||||
|
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
|
||||||
|
<TextInput
|
||||||
|
selectionColor={theme.textSelectionColor}
|
||||||
|
keyboardAppearance={theme.keyboardAppearance}
|
||||||
|
value={this.state.newTags}
|
||||||
|
onChangeText={value => {
|
||||||
|
this.setState({ newTags: value });
|
||||||
|
}}
|
||||||
|
style={this.styles().tagBoxInput}
|
||||||
|
placeholder={_('tag1, tag2, ...')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={this.styles().tagBox}>
|
||||||
|
<TextInput
|
||||||
|
selectionColor={theme.textSelectionColor}
|
||||||
|
keyboardAppearance={theme.keyboardAppearance}
|
||||||
|
value={this.state.tagFilter}
|
||||||
|
onChangeText={value => {
|
||||||
|
this.setState({ tagFilter: value });
|
||||||
|
}}
|
||||||
|
placeholder={_('Filter tags')}
|
||||||
|
style={this.styles().tagBoxInput}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<FlatList data={this.filterTags(this.state.tagListData)} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ModalDialog themeId={this.props.themeId} ContentComponent={dialogContent} title={_('Type new tags or select from list')} onOkPress={this.okButton_press} onCancelPress={this.cancelButton_press} buttonBarEnabled={!this.state.savingTags} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoteTagsDialog = connect((state: AppState) => {
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
tags: state.tags,
|
||||||
|
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||||
|
};
|
||||||
|
})(NoteTagsDialogComponent);
|
||||||
|
|
||||||
|
export default NoteTagsDialog;
|
Loading…
Reference in New Issue
Block a user