Skip to content

Commit

Permalink
Add possibility to change a note's collection
Browse files Browse the repository at this point in the history
  • Loading branch information
Kévin Commaille authored and tasn committed Feb 21, 2021
1 parent c772ed7 commit 312ffbe
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const rootStackScreens: RootStackScreens = {
NoteCreate: "new-note",
NoteEdit: "notebook/:colUid/note/:itemUid",
NoteProps: "notebook/:colUid/note/:itemUid/properties",
NoteMove: "notebook/:colUid/note/:itemUid/move",
Invitations: "invitations",
Settings: "settings",
About: "settings/about",
Expand Down
5 changes: 5 additions & 0 deletions src/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import DebugLogsScreen from "./screens/DebugLogsScreen";
import HomeScreen from "./screens/HomeScreen";
import NoteEditScreen from "./screens/NoteEditScreen";
import NotePropertiesScreen from "./screens/NotePropertiesScreen";
import NoteMoveScreen from "./screens/NoteMoveScreen";
import CollectionEditScreen from "./screens/CollectionEditScreen";
import CollectionChangelogScreen from "./screens/CollectionChangelogScreen";
import CollectionMembersScreen from "./screens/CollectionMembersScreen";
Expand Down Expand Up @@ -113,6 +114,10 @@ export default React.memo(function RootNavigator() {
name="NoteProps"
component={NotePropertiesScreen}
/>
<Stack.Screen
name="NoteMove"
component={NoteMoveScreen}
/>
<Stack.Screen
name="CollectionEdit"
component={CollectionEditScreen}
Expand Down
4 changes: 4 additions & 0 deletions src/RootStackParamList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export type RootStackParamList = {
colUid: string;
itemUid: string;
};
NoteMove: {
colUid: string;
itemUid: string;
};
Invitations: undefined;
Settings: undefined;
Password: undefined;
Expand Down
10 changes: 9 additions & 1 deletion src/screens/NoteEditScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default function NoteEditScreen(props: PropsType) {
onSave={onSave}
onEdit={() => navigation.navigate("NoteProps", { colUid, itemUid })}
onDelete={() => setNoteDeleteDialogShow(true)}
onMove={() => navigation.navigate("NoteMove", { colUid, itemUid })}
onShare={onShare}
changed={changed}
/>
Expand Down Expand Up @@ -246,10 +247,11 @@ interface RightActionViewProps {
onEdit: () => void;
onSave: () => void;
onDelete: () => void;
onMove: () => void;
onShare: () => void;
}

function RightAction({ viewMode, setViewMode, onSave, onEdit, onDelete, onShare, changed }: RightActionViewProps) {
function RightAction({ viewMode, setViewMode, onSave, onEdit, onDelete, onMove, onShare, changed }: RightActionViewProps) {
const [showMenu, setShowMenu] = React.useState(false);

return (
Expand All @@ -276,6 +278,12 @@ function RightAction({ viewMode, setViewMode, onSave, onEdit, onDelete, onShare,
onDelete();
}}
/>
<Menu.Item icon="share" title="Move"
onPress={() => {
setShowMenu(false);
onMove();
}}
/>
<Menu.Item icon="content-save" title="Save"
disabled={!changed}
onPress={() => {
Expand Down
197 changes: 197 additions & 0 deletions src/screens/NoteMoveScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: © 2019 EteSync Authors
// SPDX-License-Identifier: GPL-3.0-only

import * as React from "react";
import { useSelector } from "react-redux";
import { View } from "react-native";
import { Text, HelperText, Button, TouchableRipple } from "react-native-paper";
import { useNavigation, RouteProp } from "@react-navigation/native";
import { StackNavigationProp } from "@react-navigation/stack";

import { useSyncGate } from "../SyncGate";
import { useCredentials } from "../credentials";
import { StoreState, useAsyncDispatch } from "../store";
import { itemBatch, pushMessage } from "../store/actions";
import { CachedItem } from "../store/reducers";

import ScrollView from "../widgets/ScrollView";
import Container from "../widgets/Container";
import ErrorOrLoadingDialog from "../widgets/ErrorOrLoadingDialog";
import Select from "../widgets/Select";
import TextInputWithIcon from "../widgets/TextInputWithIcon";
import NotFound from "../widgets/NotFound";

import { useLoading } from "../helpers";
import { RootStackParamList } from "../RootStackParamList";

interface FormErrors {
notebook?: string;
}

type NavigationProp = StackNavigationProp<RootStackParamList, "NoteProps">;

interface PropsType {
route: RouteProp<RootStackParamList, "NoteMove">;
}

export default function NotePropertiesScreen(props: PropsType) {
const colUid = props.route.params?.colUid;
const itemUid = props.route.params?.itemUid;
const [errors, setErrors] = React.useState({} as FormErrors);
const [selectOpen, setSelectOpen] = React.useState(false);
const dispatch = useAsyncDispatch();
const cachedCollections = useSelector((state: StoreState) => state.cache.collections);
const cachedItems = useSelector((state: StoreState) => state.cache.items);
const [cachedItem, setCachedItem] = React.useState<CachedItem | undefined>();
const options = Array.from(
cachedCollections.map((val, uid) => ({ ...val, uid }))
.sort((a, b) => (a.meta!.name!.toUpperCase() >= b.meta!.name!.toUpperCase()) ? 1 : -1)
.values()
);
const [collection, setCollection] = React.useState((options.length > 0) ? options[0] : undefined);
const syncGate = useSyncGate();
const navigation = useNavigation<NavigationProp>();
const etebase = useCredentials()!;
const [loading, error, setPromise] = useLoading();

React.useEffect(() => {
if (syncGate) {
return;
}

if (colUid) {
const cachedCollection = cachedItems.get(colUid);

if (cachedCollection) {
setCollection(options.find((x) => x.uid === colUid) ?? options[0]);

if (itemUid) {
const cachedItem = cachedCollection.get(itemUid);
if (cachedItem) {
setCachedItem(cachedItem);
} else {
setCachedItem(undefined);
}
}
} else {
setCollection(undefined);
}
}

}, [syncGate, cachedItems, colUid, itemUid]);

React.useEffect(() => {
navigation.setOptions({
title: "Move Note",
});
}, [colUid, itemUid]);

if (syncGate) {
return syncGate;
}

if (colUid && !collection) {
return <NotFound />;
}
if (itemUid && !cachedItem) {
return <NotFound message="This note can't be found" />;
}

function onMove() {
setPromise(async () => {
const saveErrors: FormErrors = {};
const fieldRequired = "This field is required!";

if (!collection) {
saveErrors.notebook = fieldRequired;
} else if (collection.uid === colUid) {
saveErrors.notebook = "You must pick a different notebook to move the note!";
}

if (Object.keys(saveErrors).length > 0) {
setErrors(saveErrors);
return;
}

const colMgr = etebase.getCollectionManager();

const oldCollection = cachedCollections.get(colUid!)!;
const oldCol = colMgr.cacheLoad(oldCollection.cache);
const oldItemMgr = colMgr.getItemManager(oldCol);
const oldItem = oldItemMgr.cacheLoad(cachedItem!.cache);

const newCol = colMgr.cacheLoad(collection!.cache);
const newItemMgr = colMgr.getItemManager(newCol);

const meta = oldItem.getMeta();
meta.mtime = (new Date()).getTime();
const content = await oldItem.getContent();
const newItem = await newItemMgr.create(meta, content);
await dispatch(itemBatch(newCol, newItemMgr, [newItem]));

oldItem.setMeta(meta);
oldItem.delete(true);
await dispatch(itemBatch(oldCol, oldItemMgr, [oldItem]));

dispatch(pushMessage({ message: "Note moved", severity: "success" }));
navigation.navigate("NoteEdit", { colUid: collection!.uid, itemUid: newItem.uid });
});
}

return (
<ScrollView keyboardAware>
<Container>
<ErrorOrLoadingDialog
loading={loading}
error={error}
onDismiss={() => setPromise(undefined)}
/>

<View>
<Select
visible={selectOpen}
onDismiss={() => setSelectOpen(false)}
options={options ?? []}
titleAccossor={(col) => col.meta.name!}
onChange={(chosen) => {
setSelectOpen(false);
if (!chosen || chosen === collection) {
return;
}
setCollection(chosen);
}}
anchor={(
<TouchableRipple
onPress={() => setSelectOpen(true)}
>
<TextInputWithIcon
editable={false}
label="Move to Notebook"
accessibilityLabel="Move to Notebook"
value={collection?.meta.name ?? "No Notebooks"}
icon="plus"
iconAccessibilityLabel="Create Notebook"
iconOnPress={() => navigation.navigate("CollectionCreate")}
/>
</TouchableRipple>
)}
/>
<HelperText
type="error"
visible={!!errors?.notebook}
>
{errors?.notebook}
</HelperText>
</View>

<Button
mode="contained"
disabled={loading}
onPress={onMove}
>
<Text>{loading ? "Loading…" : "Move"}</Text>
</Button>
</Container>
</ScrollView>
);
}

0 comments on commit 312ffbe

Please sign in to comment.