Journals: remove unused journals components.
parent
2f565994b7
commit
c69f5e27ad
@ -1,207 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
|
||||||
import DialogActions from "@material-ui/core/DialogActions";
|
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
|
||||||
import DialogContentText from "@material-ui/core/DialogContentText";
|
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
|
||||||
|
|
||||||
import Dropzone from "react-dropzone";
|
|
||||||
|
|
||||||
import LoadingIndicator from "../widgets/LoadingIndicator";
|
|
||||||
|
|
||||||
import { SyncInfoJournal } from "../SyncGate";
|
|
||||||
|
|
||||||
import { store, CredentialsData, UserInfoData } from "../store";
|
|
||||||
import { addEntries } from "../store/actions";
|
|
||||||
import { createJournalEntry } from "../etesync-helpers";
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
|
|
||||||
import * as uuid from "uuid";
|
|
||||||
import * as ICAL from "ical.js";
|
|
||||||
import { ContactType, EventType, TaskType, PimType } from "../pim-types";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
syncJournal: SyncInfoJournal;
|
|
||||||
open: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ImportDialog(props: PropsType) {
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
|
|
||||||
function onFileDropCommon(itemsCreator: (fileText: string) => PimType[], acceptedFiles: File[], rejectedFiles: File[]) {
|
|
||||||
// XXX: implement handling of rejectedFiles
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onabort = () => {
|
|
||||||
setLoading(false);
|
|
||||||
console.error("Import Aborted");
|
|
||||||
alert("file reading was aborted");
|
|
||||||
};
|
|
||||||
reader.onerror = (e) => {
|
|
||||||
setLoading(false);
|
|
||||||
console.error(e);
|
|
||||||
alert("file reading has failed");
|
|
||||||
};
|
|
||||||
reader.onload = async () => {
|
|
||||||
try {
|
|
||||||
const fileText = reader.result as string;
|
|
||||||
const items = itemsCreator(fileText);
|
|
||||||
|
|
||||||
const { syncJournal } = props;
|
|
||||||
const last = syncJournal.journalEntries.last() as EteSync.Entry;
|
|
||||||
const lastUid = last ? last.uid : null;
|
|
||||||
|
|
||||||
// XXX implement chunked push most likely...
|
|
||||||
let prevUid = lastUid;
|
|
||||||
const journalItems = items.map((item) => {
|
|
||||||
const ret = createJournalEntry(
|
|
||||||
props.etesync, props.userInfo, syncJournal.journal,
|
|
||||||
prevUid, EteSync.SyncEntryAction.Add, item.toIcal());
|
|
||||||
|
|
||||||
prevUid = ret.uid;
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.dispatch<any>(
|
|
||||||
addEntries(props.etesync, syncJournal.journal.uid, journalItems, lastUid)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("An error has occurred, please contact developers.");
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
if (props.onClose) {
|
|
||||||
setLoading(false);
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acceptedFiles.length > 0) {
|
|
||||||
setLoading(true);
|
|
||||||
acceptedFiles.forEach((file) => {
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert("Failed importing file. Is the file type supported?");
|
|
||||||
console.log("Failed importing files. Rejected:", rejectedFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileDropContact(acceptedFiles: File[], rejectedFiles: File[]) {
|
|
||||||
const itemsCreator = (fileText: string) => {
|
|
||||||
const mainComp = ICAL.parse(fileText);
|
|
||||||
return mainComp.map((comp) => {
|
|
||||||
const ret = new ContactType(new ICAL.Component(comp));
|
|
||||||
if (!ret.uid) {
|
|
||||||
ret.uid = uuid.v4();
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileDropEvent(acceptedFiles: File[], rejectedFiles: File[]) {
|
|
||||||
const itemsCreator = (fileText: string) => {
|
|
||||||
const calendarComp = new ICAL.Component(ICAL.parse(fileText));
|
|
||||||
return calendarComp.getAllSubcomponents("vevent").map((comp) => {
|
|
||||||
const ret = new EventType(comp);
|
|
||||||
if (!ret.uid) {
|
|
||||||
ret.uid = uuid.v4();
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileDropTask(acceptedFiles: File[], rejectedFiles: File[]) {
|
|
||||||
const itemsCreator = (fileText: string) => {
|
|
||||||
const calendarComp = new ICAL.Component(ICAL.parse(fileText));
|
|
||||||
return calendarComp.getAllSubcomponents("vtodo").map((comp) => {
|
|
||||||
const ret = new TaskType(comp);
|
|
||||||
if (!ret.uid) {
|
|
||||||
ret.uid = uuid.v4();
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClose() {
|
|
||||||
if (loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.onClose) {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { syncJournal } = props;
|
|
||||||
const collectionInfo = syncJournal.collection;
|
|
||||||
let acceptTypes;
|
|
||||||
let dropFunction;
|
|
||||||
|
|
||||||
if (collectionInfo.type === "ADDRESS_BOOK") {
|
|
||||||
acceptTypes = ["text/vcard", "text/directory", "text/x-vcard", ".vcf"];
|
|
||||||
dropFunction = onFileDropContact;
|
|
||||||
} else if (collectionInfo.type === "CALENDAR") {
|
|
||||||
acceptTypes = ["text/calendar", ".ics", ".ical"];
|
|
||||||
dropFunction = onFileDropEvent;
|
|
||||||
} else if (collectionInfo.type === "TASKS") {
|
|
||||||
acceptTypes = ["text/calendar", ".ics", ".ical"];
|
|
||||||
dropFunction = onFileDropTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Dialog
|
|
||||||
open={props.open}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<DialogTitle>Import entries from file?</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
{loading ?
|
|
||||||
<LoadingIndicator style={{ display: "block", margin: "auto" }} />
|
|
||||||
:
|
|
||||||
<Dropzone
|
|
||||||
onDrop={dropFunction}
|
|
||||||
multiple={false}
|
|
||||||
accept={acceptTypes}
|
|
||||||
>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<section>
|
|
||||||
<div {...getRootProps()}>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
<DialogContentText id="alert-dialog-description">
|
|
||||||
To import entries from a file, drag 'n' drop it here, or click to open the file selector.
|
|
||||||
</DialogContentText>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button disabled={loading} onClick={onClose} color="primary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
|
||||||
import IconEdit from "@material-ui/icons/Edit";
|
|
||||||
import IconMembers from "@material-ui/icons/People";
|
|
||||||
import IconImport from "@material-ui/icons/ImportExport";
|
|
||||||
|
|
||||||
|
|
||||||
import AppBarOverride from "../widgets/AppBarOverride";
|
|
||||||
import Container from "../widgets/Container";
|
|
||||||
|
|
||||||
import JournalEntries from "../components/JournalEntries";
|
|
||||||
import ImportDialog from "./ImportDialog";
|
|
||||||
|
|
||||||
import { SyncInfo, SyncInfoJournal } from "../SyncGate";
|
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import { routeResolver } from "../App";
|
|
||||||
|
|
||||||
import { CredentialsData, UserInfoData } from "../store";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
syncInfo: SyncInfo;
|
|
||||||
syncJournal: SyncInfoJournal;
|
|
||||||
isOwner: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Journal extends React.Component<PropsType> {
|
|
||||||
public state: {
|
|
||||||
tab: number;
|
|
||||||
importDialogOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.importDialogToggle = this.importDialogToggle.bind(this);
|
|
||||||
this.state = {
|
|
||||||
tab: 0,
|
|
||||||
importDialogOpen: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { isOwner, syncJournal } = this.props;
|
|
||||||
|
|
||||||
const journal = syncJournal.journal;
|
|
||||||
const collectionInfo = syncJournal.collection;
|
|
||||||
const syncEntries = syncJournal.entries;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<AppBarOverride title={collectionInfo.displayName}>
|
|
||||||
{isOwner &&
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
component={Link}
|
|
||||||
title="Edit"
|
|
||||||
{...{ to: routeResolver.getRoute("journals._id.edit", { journalUid: journal.uid }) }}
|
|
||||||
>
|
|
||||||
<IconEdit />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
component={Link}
|
|
||||||
title="Members"
|
|
||||||
{...{ to: routeResolver.getRoute("journals._id.members", { journalUid: journal.uid }) }}
|
|
||||||
>
|
|
||||||
<IconMembers />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<IconButton
|
|
||||||
title="Import"
|
|
||||||
onClick={this.importDialogToggle}
|
|
||||||
>
|
|
||||||
<IconImport />
|
|
||||||
</IconButton>
|
|
||||||
</AppBarOverride>
|
|
||||||
<Container>
|
|
||||||
<JournalEntries journal={journal} entries={syncEntries} />
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<ImportDialog
|
|
||||||
etesync={this.props.etesync}
|
|
||||||
userInfo={this.props.userInfo}
|
|
||||||
syncJournal={this.props.syncJournal}
|
|
||||||
open={this.state.importDialogOpen}
|
|
||||||
onClose={this.importDialogToggle}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private importDialogToggle() {
|
|
||||||
this.setState((state: any) => ({ importDialogOpen: !state.importDialogOpen }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Journal;
|
|
@ -1,214 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
import TextField from "@material-ui/core/TextField";
|
|
||||||
import Select from "@material-ui/core/Select";
|
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
|
||||||
import FormControl from "@material-ui/core/FormControl";
|
|
||||||
import InputLabel from "@material-ui/core/InputLabel";
|
|
||||||
import IconDelete from "@material-ui/icons/Delete";
|
|
||||||
import IconCancel from "@material-ui/icons/Clear";
|
|
||||||
import IconSave from "@material-ui/icons/Save";
|
|
||||||
import * as colors from "@material-ui/core/colors";
|
|
||||||
|
|
||||||
import AppBarOverride from "../widgets/AppBarOverride";
|
|
||||||
import Container from "../widgets/Container";
|
|
||||||
import ConfirmationDialog from "../widgets/ConfirmationDialog";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
import { SyncInfo } from "../SyncGate";
|
|
||||||
import ColorPicker from "../widgets/ColorPicker";
|
|
||||||
import { defaultColor, colorHtmlToInt, colorIntToHtml } from "../journal-processors";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
syncInfo: SyncInfo;
|
|
||||||
item?: EteSync.CollectionInfo;
|
|
||||||
onSave: (info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) => void;
|
|
||||||
onDelete: (info: EteSync.CollectionInfo) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormErrors {
|
|
||||||
displayName?: string;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalEdit(props: PropsType) {
|
|
||||||
const [errors, setErrors] = React.useState<FormErrors>({});
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
|
|
||||||
const [info, setInfo] = React.useState<EteSync.CollectionInfo>();
|
|
||||||
const [selectedColor, setSelectedColor] = React.useState("");
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (props.item !== undefined) {
|
|
||||||
setInfo(props.item);
|
|
||||||
if (props.item.color) {
|
|
||||||
setSelectedColor(colorIntToHtml(props.item.color));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setInfo({
|
|
||||||
uid: EteSync.genUid(),
|
|
||||||
type: "ADDRESS_BOOK",
|
|
||||||
displayName: "",
|
|
||||||
description: "",
|
|
||||||
} as EteSync.CollectionInfo);
|
|
||||||
}
|
|
||||||
}, [props.item]);
|
|
||||||
|
|
||||||
if (info === undefined) {
|
|
||||||
return <React.Fragment />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit(e: React.FormEvent<any>) {
|
|
||||||
e.preventDefault();
|
|
||||||
const saveErrors: FormErrors = {};
|
|
||||||
const fieldRequired = "This field is required!";
|
|
||||||
|
|
||||||
const { onSave } = props;
|
|
||||||
|
|
||||||
const displayName = info?.displayName;
|
|
||||||
const color = colorHtmlToInt(selectedColor);
|
|
||||||
|
|
||||||
if (!displayName) {
|
|
||||||
saveErrors.displayName = fieldRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedColor && !color) {
|
|
||||||
saveErrors.color = "Must be of the form #RRGGBB or #RRGGBBAA or empty";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(saveErrors).length > 0) {
|
|
||||||
setErrors(saveErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = new EteSync.CollectionInfo({ ...info, color: color });
|
|
||||||
onSave(item, props.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteRequest() {
|
|
||||||
setShowDeleteDialog(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { item, onDelete, onCancel } = props;
|
|
||||||
|
|
||||||
const pageTitle = (item !== undefined) ? item.displayName : "New Journal";
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
fullWidth: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
submit: {
|
|
||||||
marginTop: 40,
|
|
||||||
marginBottom: 20,
|
|
||||||
textAlign: "right" as any,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const journalTypes = {
|
|
||||||
ADDRESS_BOOK: "Address Book",
|
|
||||||
CALENDAR: "Calendar",
|
|
||||||
TASKS: "Task List",
|
|
||||||
};
|
|
||||||
let collectionColorBox: React.ReactNode;
|
|
||||||
switch (info.type) {
|
|
||||||
case "CALENDAR":
|
|
||||||
case "TASKS":
|
|
||||||
collectionColorBox = (
|
|
||||||
<ColorPicker
|
|
||||||
defaultColor={defaultColor}
|
|
||||||
color={selectedColor}
|
|
||||||
onChange={(color: string) => setSelectedColor(color)}
|
|
||||||
error={errors.color}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppBarOverride title={pageTitle} />
|
|
||||||
<Container style={{ maxWidth: "30rem" }}>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<FormControl disabled={props.item !== undefined} style={styles.fullWidth}>
|
|
||||||
<InputLabel>
|
|
||||||
Collection type
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
name="type"
|
|
||||||
required
|
|
||||||
value={info.type}
|
|
||||||
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, type: event.target.value })}
|
|
||||||
>
|
|
||||||
{Object.keys(journalTypes).map((x) => (
|
|
||||||
<MenuItem key={x} value={x}>{journalTypes[x]}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<TextField
|
|
||||||
name="displayName"
|
|
||||||
required
|
|
||||||
label="Display name of this collection"
|
|
||||||
value={info.displayName}
|
|
||||||
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, displayName: event.target.value })}
|
|
||||||
style={styles.fullWidth}
|
|
||||||
margin="normal"
|
|
||||||
error={!!errors.displayName}
|
|
||||||
helperText={errors.displayName}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
name="description"
|
|
||||||
label="Description (optional)"
|
|
||||||
value={info.description}
|
|
||||||
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, description: event.target.value })}
|
|
||||||
style={styles.fullWidth}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
{collectionColorBox}
|
|
||||||
|
|
||||||
<div style={styles.submit}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
<IconCancel style={{ marginRight: 8 }} />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{props.item &&
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
style={{ marginLeft: 15, backgroundColor: colors.red[500], color: "white" }}
|
|
||||||
onClick={onDeleteRequest}
|
|
||||||
>
|
|
||||||
<IconDelete style={{ marginRight: 8 }} />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
style={{ marginLeft: 15 }}
|
|
||||||
>
|
|
||||||
<IconSave style={{ marginRight: 8 }} />
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Container>
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Delete Confirmation"
|
|
||||||
labelOk="Delete"
|
|
||||||
open={showDeleteDialog}
|
|
||||||
onOk={() => onDelete(props.item!)}
|
|
||||||
onCancel={() => setShowDeleteDialog(false)}
|
|
||||||
>
|
|
||||||
Are you sure you would like to delete this journal?
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import TextField from "@material-ui/core/TextField";
|
|
||||||
import Checkbox from "@material-ui/core/Checkbox";
|
|
||||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
|
||||||
|
|
||||||
import LoadingIndicator from "../widgets/LoadingIndicator";
|
|
||||||
import ConfirmationDialog from "../widgets/ConfirmationDialog";
|
|
||||||
import PrettyFingerprint from "../widgets/PrettyFingerprint";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
|
|
||||||
import { CredentialsData } from "../store";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
info: EteSync.CollectionInfo;
|
|
||||||
onOk: (user: string, publicKey: string, readOnly: boolean) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalMemberAddDialog(props: PropsType) {
|
|
||||||
const [addUser, setAddUser] = React.useState("");
|
|
||||||
const [publicKey, setPublicKey] = React.useState("");
|
|
||||||
const [readOnly, setReadOnly] = React.useState(false);
|
|
||||||
const [userChosen, setUserChosen] = React.useState(false);
|
|
||||||
const [error, setError] = React.useState<Error>();
|
|
||||||
|
|
||||||
function onAddRequest(_user: string) {
|
|
||||||
setUserChosen(true);
|
|
||||||
|
|
||||||
const { etesync } = props;
|
|
||||||
|
|
||||||
const creds = etesync.credentials;
|
|
||||||
const apiBase = etesync.serviceApiUrl;
|
|
||||||
const userInfoManager = new EteSync.UserInfoManager(creds, apiBase);
|
|
||||||
userInfoManager.fetch(addUser).then((userInfo) => {
|
|
||||||
setPublicKey(userInfo.publicKey);
|
|
||||||
}).catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOk() {
|
|
||||||
props.onOk(addUser, publicKey, readOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { onClose } = props;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Error adding member"
|
|
||||||
labelOk="OK"
|
|
||||||
open
|
|
||||||
onOk={onClose}
|
|
||||||
onCancel={onClose}
|
|
||||||
>
|
|
||||||
User ({addUser}) not found. Have they setup their encryption password from one of the apps?
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publicKey) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Verify security fingerprint"
|
|
||||||
labelOk="OK"
|
|
||||||
open
|
|
||||||
onOk={onOk}
|
|
||||||
onCancel={onClose}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Verify {addUser}'s security fingerprint to ensure the encryption is secure.
|
|
||||||
</p>
|
|
||||||
<div style={{ textAlign: "center" }}>
|
|
||||||
<PrettyFingerprint publicKey={publicKey as any} />
|
|
||||||
</div>
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Add member"
|
|
||||||
labelOk="OK"
|
|
||||||
open={!userChosen}
|
|
||||||
onOk={onAddRequest}
|
|
||||||
onCancel={onClose}
|
|
||||||
>
|
|
||||||
{userChosen ?
|
|
||||||
<LoadingIndicator />
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<TextField
|
|
||||||
name="addUser"
|
|
||||||
type="email"
|
|
||||||
placeholder="User email"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
value={addUser}
|
|
||||||
onChange={(ev) => setAddUser(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={readOnly}
|
|
||||||
onChange={(event) => setReadOnly(event.target.checked)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Read only?"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import sjcl from "sjcl";
|
|
||||||
|
|
||||||
import { List, ListItem } from "../widgets/List";
|
|
||||||
|
|
||||||
import IconMemberAdd from "@material-ui/icons/PersonAdd";
|
|
||||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
|
||||||
|
|
||||||
import AppBarOverride from "../widgets/AppBarOverride";
|
|
||||||
import Container from "../widgets/Container";
|
|
||||||
import LoadingIndicator from "../widgets/LoadingIndicator";
|
|
||||||
import ConfirmationDialog from "../widgets/ConfirmationDialog";
|
|
||||||
|
|
||||||
import JournalMemberAddDialog from "./JournalMemberAddDialog";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
import { CredentialsData, UserInfoData } from "../store";
|
|
||||||
|
|
||||||
import { SyncInfoJournal } from "../SyncGate";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
syncJournal: SyncInfoJournal;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalMembers(props: PropsType) {
|
|
||||||
const [members, setMembers] = React.useState<EteSync.JournalMemberJson[] | null>(null);
|
|
||||||
const [revokeUser, setRevokeUser] = React.useState<string | null>(null);
|
|
||||||
const [addMemberOpen, setAddMemberOpen] = React.useState(false);
|
|
||||||
|
|
||||||
function fetchMembers() {
|
|
||||||
const { etesync, syncJournal } = props;
|
|
||||||
const info = syncJournal.collection;
|
|
||||||
|
|
||||||
const creds = etesync.credentials;
|
|
||||||
const apiBase = etesync.serviceApiUrl;
|
|
||||||
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, info.uid);
|
|
||||||
journalMembersManager.list().then((members) => {
|
|
||||||
setMembers(members);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
fetchMembers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function onRevokeRequest(user: string) {
|
|
||||||
setRevokeUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRevokeDo() {
|
|
||||||
const { etesync, syncJournal } = props;
|
|
||||||
const info = syncJournal.collection;
|
|
||||||
|
|
||||||
const creds = etesync.credentials;
|
|
||||||
const apiBase = etesync.serviceApiUrl;
|
|
||||||
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, info.uid);
|
|
||||||
journalMembersManager.delete({ user: revokeUser!, key: "" }).then(() => {
|
|
||||||
fetchMembers();
|
|
||||||
});
|
|
||||||
setRevokeUser(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMemberAdd(user: string, publicKey: string, readOnly: boolean) {
|
|
||||||
const { etesync, syncJournal, userInfo } = props;
|
|
||||||
const journal = syncJournal.journal;
|
|
||||||
const derived = props.etesync.encryptionKey;
|
|
||||||
|
|
||||||
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
|
|
||||||
const cryptoManager = journal.getCryptoManager(derived, keyPair);
|
|
||||||
|
|
||||||
const pubkeyBytes = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(publicKey));
|
|
||||||
const encryptedKey = sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(cryptoManager.getEncryptedKey(keyPair, pubkeyBytes)));
|
|
||||||
|
|
||||||
const creds = etesync.credentials;
|
|
||||||
const apiBase = etesync.serviceApiUrl;
|
|
||||||
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, journal.uid);
|
|
||||||
journalMembersManager.create({ user, key: encryptedKey, readOnly }).then(() => {
|
|
||||||
fetchMembers();
|
|
||||||
});
|
|
||||||
setAddMemberOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { syncJournal } = props;
|
|
||||||
|
|
||||||
const info = syncJournal.collection;
|
|
||||||
const sharingAllowed = syncJournal.journal.version > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppBarOverride title={`${info.displayName} - Members`} />
|
|
||||||
<Container style={{ maxWidth: "30rem" }}>
|
|
||||||
{members ?
|
|
||||||
<List>
|
|
||||||
<ListItem rightIcon={<IconMemberAdd />} onClick={() => setAddMemberOpen(true)}>
|
|
||||||
Add member
|
|
||||||
</ListItem>
|
|
||||||
{(members.length > 0 ?
|
|
||||||
members.map((member) => (
|
|
||||||
<ListItem
|
|
||||||
key={member.user}
|
|
||||||
onClick={() => onRevokeRequest(member.user)}
|
|
||||||
rightIcon={(member.readOnly) ? (<VisibilityIcon />) : undefined}
|
|
||||||
>
|
|
||||||
{member.user}
|
|
||||||
</ListItem>
|
|
||||||
))
|
|
||||||
:
|
|
||||||
<ListItem>
|
|
||||||
No members
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
:
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
</Container>
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Remove member"
|
|
||||||
labelOk="OK"
|
|
||||||
open={revokeUser !== null}
|
|
||||||
onOk={onRevokeDo}
|
|
||||||
onCancel={() => setRevokeUser(null)}
|
|
||||||
>
|
|
||||||
Would you like to revoke {revokeUser}'s access?<br />
|
|
||||||
Please be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.
|
|
||||||
</ConfirmationDialog>
|
|
||||||
|
|
||||||
{addMemberOpen &&
|
|
||||||
(sharingAllowed ?
|
|
||||||
<JournalMemberAddDialog
|
|
||||||
etesync={props.etesync}
|
|
||||||
info={info}
|
|
||||||
onOk={onMemberAdd}
|
|
||||||
onClose={() => setAddMemberOpen(false)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<ConfirmationDialog
|
|
||||||
title="Now Allowed"
|
|
||||||
labelOk="OK"
|
|
||||||
open
|
|
||||||
onOk={() => setAddMemberOpen(false)}
|
|
||||||
onClose={() => setAddMemberOpen(false)}
|
|
||||||
>
|
|
||||||
Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the "import" dialog. If you are experiencing any issues, please contact support.
|
|
||||||
</ConfirmationDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { History } from "history";
|
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
|
||||||
import IconAdd from "@material-ui/icons/Add";
|
|
||||||
import ContactsIcon from "@material-ui/icons/Contacts";
|
|
||||||
import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
|
|
||||||
import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted";
|
|
||||||
|
|
||||||
import { List, ListItem } from "../widgets/List";
|
|
||||||
|
|
||||||
import AppBarOverride from "../widgets/AppBarOverride";
|
|
||||||
import Container from "../widgets/Container";
|
|
||||||
|
|
||||||
import { routeResolver } from "../App";
|
|
||||||
|
|
||||||
import { JournalsData, UserInfoData, CredentialsData } from "../store";
|
|
||||||
import ColorBox from "../widgets/ColorBox";
|
|
||||||
import { colorIntToHtml } from "../journal-processors";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
journals: JournalsData;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
history: History;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalsList(props: PropsType) {
|
|
||||||
const derived = props.etesync.encryptionKey;
|
|
||||||
|
|
||||||
function journalClicked(journalUid: string) {
|
|
||||||
props.history.push(routeResolver.getRoute("journals._id", { journalUid }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const journalMap = props.journals.reduce(
|
|
||||||
(ret, journal) => {
|
|
||||||
const userInfo = props.userInfo;
|
|
||||||
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
|
|
||||||
const cryptoManager = journal.getCryptoManager(derived, keyPair);
|
|
||||||
const info = journal.getInfo(cryptoManager);
|
|
||||||
let colorBox: React.ReactElement | undefined;
|
|
||||||
switch (info.type) {
|
|
||||||
case "CALENDAR":
|
|
||||||
case "TASKS":
|
|
||||||
colorBox = (
|
|
||||||
<ColorBox size={24} color={colorIntToHtml(info.color)} />
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ret[info.type] = ret[info.type] || [];
|
|
||||||
ret[info.type].push(
|
|
||||||
<ListItem key={journal.uid} rightIcon={colorBox} insetChildren
|
|
||||||
onClick={() => journalClicked(journal.uid)}>
|
|
||||||
{info.displayName}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
CALENDAR: [],
|
|
||||||
ADDRESS_BOOK: [],
|
|
||||||
TASKS: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<AppBarOverride title="Journals">
|
|
||||||
<IconButton
|
|
||||||
component={Link}
|
|
||||||
title="New"
|
|
||||||
{...{ to: routeResolver.getRoute("journals.new") }}
|
|
||||||
>
|
|
||||||
<IconAdd />
|
|
||||||
</IconButton>
|
|
||||||
</AppBarOverride>
|
|
||||||
<List>
|
|
||||||
<ListItem
|
|
||||||
primaryText="Address Books"
|
|
||||||
leftIcon={<ContactsIcon />}
|
|
||||||
nestedItems={journalMap.ADDRESS_BOOK}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
primaryText="Calendars"
|
|
||||||
leftIcon={<CalendarTodayIcon />}
|
|
||||||
nestedItems={journalMap.CALENDAR}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
primaryText="Tasks"
|
|
||||||
leftIcon={<FormatListBulletedIcon />}
|
|
||||||
nestedItems={journalMap.TASKS}
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import ContactsIcon from "@material-ui/icons/Contacts";
|
|
||||||
import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
|
|
||||||
import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted";
|
|
||||||
|
|
||||||
import { List, ListItem } from "../widgets/List";
|
|
||||||
|
|
||||||
import AppBarOverride from "../widgets/AppBarOverride";
|
|
||||||
import Container from "../widgets/Container";
|
|
||||||
|
|
||||||
import { JournalsData, UserInfoData, CredentialsData } from "../store";
|
|
||||||
import ImportDialog from "./ImportDialog";
|
|
||||||
import { SyncInfo, SyncInfoJournal } from "../SyncGate";
|
|
||||||
import { colorIntToHtml } from "../journal-processors";
|
|
||||||
import ColorBox from "../widgets/ColorBox";
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
journals: JournalsData;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
syncInfo: SyncInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalsList(props: PropsType) {
|
|
||||||
const [selectedJournal, setSelectedJournal] = React.useState<SyncInfoJournal>();
|
|
||||||
const derived = props.etesync.encryptionKey;
|
|
||||||
|
|
||||||
const journalMap = props.journals.reduce(
|
|
||||||
(ret, journal) => {
|
|
||||||
const userInfo = props.userInfo;
|
|
||||||
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
|
|
||||||
const cryptoManager = journal.getCryptoManager(derived, keyPair);
|
|
||||||
const info = journal.getInfo(cryptoManager);
|
|
||||||
let colorBox: React.ReactElement | undefined;
|
|
||||||
switch (info.type) {
|
|
||||||
case "CALENDAR":
|
|
||||||
case "TASKS":
|
|
||||||
colorBox = (
|
|
||||||
<ColorBox size={24} color={colorIntToHtml(info.color)} />
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ret[info.type] = ret[info.type] || [];
|
|
||||||
ret[info.type].push(
|
|
||||||
<ListItem key={journal.uid} rightIcon={colorBox} insetChildren
|
|
||||||
onClick={() => setSelectedJournal(props.syncInfo.get(journal.uid))}>
|
|
||||||
{info.displayName} ({journal.uid.slice(0, 5)})
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
CALENDAR: [],
|
|
||||||
ADDRESS_BOOK: [],
|
|
||||||
TASKS: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<AppBarOverride title="Journals Import" />
|
|
||||||
<List>
|
|
||||||
<ListItem
|
|
||||||
primaryText="Address Books"
|
|
||||||
leftIcon={<ContactsIcon />}
|
|
||||||
nestedItems={journalMap.ADDRESS_BOOK}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
primaryText="Calendars"
|
|
||||||
leftIcon={<CalendarTodayIcon />}
|
|
||||||
nestedItems={journalMap.CALENDAR}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
primaryText="Tasks"
|
|
||||||
leftIcon={<FormatListBulletedIcon />}
|
|
||||||
nestedItems={journalMap.TASKS}
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
{selectedJournal && (
|
|
||||||
<ImportDialog
|
|
||||||
etesync={props.etesync}
|
|
||||||
userInfo={props.userInfo}
|
|
||||||
syncJournal={selectedJournal}
|
|
||||||
open={!!selectedJournal}
|
|
||||||
onClose={() => setSelectedJournal(undefined)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Location, History } from "history";
|
|
||||||
import { Route, Switch } from "react-router";
|
|
||||||
|
|
||||||
import Journal from "./Journal";
|
|
||||||
import JournalEdit from "./JournalEdit";
|
|
||||||
import JournalMembers from "./JournalMembers";
|
|
||||||
import JournalsList from "./JournalsList";
|
|
||||||
import JournalsListImport from "./JournalsListImport";
|
|
||||||
|
|
||||||
import { routeResolver } from "../App";
|
|
||||||
|
|
||||||
import { store, JournalsData, UserInfoData, CredentialsData } from "../store";
|
|
||||||
import { addJournal, deleteJournal, updateJournal } from "../store/actions";
|
|
||||||
import { SyncInfo } from "../SyncGate";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
|
|
||||||
class Journals extends React.PureComponent {
|
|
||||||
public props: {
|
|
||||||
etesync: CredentialsData;
|
|
||||||
journals: JournalsData;
|
|
||||||
userInfo: UserInfoData;
|
|
||||||
syncInfo: SyncInfo;
|
|
||||||
history: History;
|
|
||||||
location: Location;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.onCancel = this.onCancel.bind(this);
|
|
||||||
this.onItemDelete = this.onItemDelete.bind(this);
|
|
||||||
this.onItemSave = this.onItemSave.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals")}
|
|
||||||
exact
|
|
||||||
render={({ history }) => (
|
|
||||||
<>
|
|
||||||
<JournalsList
|
|
||||||
userInfo={this.props.userInfo}
|
|
||||||
etesync={this.props.etesync}
|
|
||||||
journals={this.props.journals}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals.import")}
|
|
||||||
exact
|
|
||||||
render={() => (
|
|
||||||
<>
|
|
||||||
<JournalsListImport
|
|
||||||
userInfo={this.props.userInfo}
|
|
||||||
etesync={this.props.etesync}
|
|
||||||
journals={this.props.journals}
|
|
||||||
syncInfo={this.props.syncInfo}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals.new")}
|
|
||||||
render={() => (
|
|
||||||
<JournalEdit
|
|
||||||
syncInfo={this.props.syncInfo}
|
|
||||||
onSave={this.onItemSave}
|
|
||||||
onDelete={this.onItemDelete}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id")}
|
|
||||||
render={({ match }) => {
|
|
||||||
const journalUid = match.params.journalUid;
|
|
||||||
|
|
||||||
const syncJournal = this.props.syncInfo.get(journalUid);
|
|
||||||
if (!syncJournal) {
|
|
||||||
return (<div>Journal not found!</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOwner = syncJournal.journal.owner === this.props.etesync.credentials.email;
|
|
||||||
|
|
||||||
const collectionInfo = syncJournal.collection;
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id.edit")}
|
|
||||||
render={() => (
|
|
||||||
<JournalEdit
|
|
||||||
syncInfo={this.props.syncInfo}
|
|
||||||
item={collectionInfo}
|
|
||||||
onSave={this.onItemSave}
|
|
||||||
onDelete={this.onItemDelete}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id.members")}
|
|
||||||
render={() => (
|
|
||||||
<JournalMembers
|
|
||||||
etesync={this.props.etesync}
|
|
||||||
syncJournal={syncJournal}
|
|
||||||
userInfo={this.props.userInfo}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id")}
|
|
||||||
render={() => (
|
|
||||||
<Journal
|
|
||||||
etesync={this.props.etesync}
|
|
||||||
userInfo={this.props.userInfo}
|
|
||||||
syncInfo={this.props.syncInfo}
|
|
||||||
syncJournal={syncJournal}
|
|
||||||
isOwner={isOwner}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onItemSave(info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) {
|
|
||||||
const syncJournal = this.props.syncInfo.get(info.uid);
|
|
||||||
|
|
||||||
const derived = this.props.etesync.encryptionKey;
|
|
||||||
const userInfo = this.props.userInfo;
|
|
||||||
const existingJournal = (syncJournal) ? syncJournal.journal.serialize() : { uid: info.uid };
|
|
||||||
const journal = new EteSync.Journal(existingJournal);
|
|
||||||
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
|
|
||||||
const cryptoManager = journal.getCryptoManager(derived, keyPair);
|
|
||||||
journal.setInfo(cryptoManager, info);
|
|
||||||
|
|
||||||
if (originalInfo) {
|
|
||||||
store.dispatch<any>(updateJournal(this.props.etesync, journal)).then(() =>
|
|
||||||
this.props.history.goBack()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
store.dispatch<any>(addJournal(this.props.etesync, journal)).then(() =>
|
|
||||||
this.props.history.goBack()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onItemDelete(info: EteSync.CollectionInfo) {
|
|
||||||
const syncJournal = this.props.syncInfo.get(info.uid);
|
|
||||||
|
|
||||||
const derived = this.props.etesync.encryptionKey;
|
|
||||||
const userInfo = this.props.userInfo;
|
|
||||||
const existingJournal = (syncJournal) ? syncJournal.journal.serialize() : { uid: info.uid };
|
|
||||||
const journal = new EteSync.Journal(existingJournal);
|
|
||||||
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
|
|
||||||
const cryptoManager = journal.getCryptoManager(derived, keyPair);
|
|
||||||
journal.setInfo(cryptoManager, info);
|
|
||||||
|
|
||||||
store.dispatch<any>(deleteJournal(this.props.etesync, journal)).then(() =>
|
|
||||||
this.props.history.push(routeResolver.getRoute("journals"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCancel() {
|
|
||||||
this.props.history.goBack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Journals;
|
|
@ -1,68 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Route, Switch, withRouter } from "react-router";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
|
|
||||||
import { routeResolver } from "../App";
|
|
||||||
|
|
||||||
import { History } from "history";
|
|
||||||
import { SyncInfo } from "../SyncGate";
|
|
||||||
|
|
||||||
function objValues(obj: any) {
|
|
||||||
return Object.keys(obj).map((x) => obj[x]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function journalView(JournalList: any, JournalItem: any) {
|
|
||||||
return withRouter(class extends React.PureComponent {
|
|
||||||
public props: {
|
|
||||||
journal: EteSync.Journal;
|
|
||||||
entries: {[key: string]: any};
|
|
||||||
syncInfo?: SyncInfo;
|
|
||||||
history?: History;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.itemClicked = this.itemClicked.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public itemClicked(contact: any) {
|
|
||||||
const uid = contact.uid;
|
|
||||||
|
|
||||||
this.props.history!.push(
|
|
||||||
routeResolver.getRoute("journals._id.items._id", { journalUid: this.props.journal.uid, itemUid: encodeURIComponent(uid) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const items = this.props.entries;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id")}
|
|
||||||
exact
|
|
||||||
render={() => (
|
|
||||||
<JournalList syncInfo={this.props.syncInfo} entries={objValues(items)} onItemClick={this.itemClicked} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routeResolver.getRoute("journals._id.items._id")}
|
|
||||||
exact
|
|
||||||
render={({ match }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<JournalItem item={items[`${match.params.journalUid}|${decodeURIComponent(match.params.itemUid)}`]} />
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default journalView;
|
|
@ -1,261 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as Immutable from "immutable";
|
|
||||||
|
|
||||||
import { AutoSizer, List as VirtualizedList } from "react-virtualized";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { List, ListItem } from "../widgets/List";
|
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
|
||||||
import DialogActions from "@material-ui/core/DialogActions";
|
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
import IconAdd from "@material-ui/icons/Add";
|
|
||||||
import IconDelete from "@material-ui/icons/Delete";
|
|
||||||
import IconEdit from "@material-ui/icons/Edit";
|
|
||||||
import IconError from "@material-ui/icons/Error";
|
|
||||||
|
|
||||||
import { TaskType, EventType, ContactType, parseString } from "../pim-types";
|
|
||||||
|
|
||||||
import * as EteSync from "etesync";
|
|
||||||
import LoadingIndicator from "../widgets/LoadingIndicator";
|
|
||||||
import { useCredentials } from "../login";
|
|
||||||
import { createJournalEntry } from "../etesync-helpers";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import { StoreState } from "../store";
|
|
||||||
import { addEntries } from "../store/actions";
|
|
||||||
|
|
||||||
interface RollbackToHereDialogPropsType {
|
|
||||||
journal: EteSync.Journal;
|
|
||||||
entries: Immutable.List<EteSync.SyncEntry>;
|
|
||||||
entryUid: string;
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RollbackToHereDialog(props: RollbackToHereDialogPropsType) {
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
const etesync = useCredentials()!;
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const userInfo = useSelector((state: StoreState) => state.cache.userInfo);
|
|
||||||
|
|
||||||
async function go() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const changes = new Map<string, EteSync.SyncEntry>();
|
|
||||||
|
|
||||||
for (const entry of props.entries.reverse()) {
|
|
||||||
const comp = parseString(entry.content);
|
|
||||||
const itemComp = comp.getFirstSubcomponent("vevent") ?? comp.getFirstSubcomponent("vtodo") ?? comp;
|
|
||||||
const itemUid = itemComp.getFirstPropertyValue("uid");
|
|
||||||
|
|
||||||
if (itemUid && !changes.has(itemUid)) {
|
|
||||||
changes.set(itemUid, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.uid === props.entryUid) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const last = props.entries.last(null);
|
|
||||||
const lastUid = last?.uid ? last.uid : null;
|
|
||||||
|
|
||||||
// XXX implement chunked push most likely...
|
|
||||||
let prevUid = lastUid;
|
|
||||||
const journalItems = [];
|
|
||||||
for (const syncEntry of changes.values()) {
|
|
||||||
if (syncEntry.action === EteSync.SyncEntryAction.Delete) {
|
|
||||||
const ret = createJournalEntry(etesync, userInfo, props.journal, prevUid, EteSync.SyncEntryAction.Add, syncEntry.content);
|
|
||||||
journalItems.push(ret);
|
|
||||||
|
|
||||||
prevUid = ret.uid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (journalItems.length > 0) {
|
|
||||||
await dispatch<any>(
|
|
||||||
addEntries(etesync, props.journal.uid, journalItems, lastUid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={props.open}
|
|
||||||
onClose={props.onClose}
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
|
||||||
Recover items
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
{loading ? (
|
|
||||||
<LoadingIndicator style={{ display: "block", margin: "auto" }} />
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
This function restores all of the deleted items that happened after this change entry. It will not modify any items that haven't been changed since the item was deleted.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
disabled={loading}
|
|
||||||
onClick={props.onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
disabled={loading}
|
|
||||||
onClick={go}
|
|
||||||
>
|
|
||||||
Go
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PropsType {
|
|
||||||
journal: EteSync.Journal;
|
|
||||||
entries: Immutable.List<EteSync.SyncEntry>;
|
|
||||||
uid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JournalEntries(props: PropsType) {
|
|
||||||
const [dialog, setDialog] = React.useState<EteSync.SyncEntry>();
|
|
||||||
const [rollbackDialogId, setRollbackDialogId] = React.useState<string>();
|
|
||||||
|
|
||||||
if (props.journal === undefined) {
|
|
||||||
return (<div>Loading</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowRenderer = (params: { index: number, key: string, style: React.CSSProperties }) => {
|
|
||||||
const { key, index, style } = params;
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
const syncEntry = props.entries.get(props.entries.size - index - 1)!;
|
|
||||||
let comp;
|
|
||||||
try {
|
|
||||||
comp = parseString(syncEntry.content);
|
|
||||||
} catch (e) {
|
|
||||||
const icon = (<IconError style={{ color: "red" }} />);
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
leftIcon={icon}
|
|
||||||
primaryText="Failed parsing item"
|
|
||||||
secondaryText="Unknown"
|
|
||||||
onClick={() => setDialog(syncEntry)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon;
|
|
||||||
if (syncEntry.action === EteSync.SyncEntryAction.Add) {
|
|
||||||
icon = (<IconAdd style={{ color: "#16B14B" }} />);
|
|
||||||
} else if (syncEntry.action === EteSync.SyncEntryAction.Change) {
|
|
||||||
icon = (<IconEdit style={{ color: "#FEB115" }} />);
|
|
||||||
} else if (syncEntry.action === EteSync.SyncEntryAction.Delete) {
|
|
||||||
icon = (<IconDelete style={{ color: "#F20C0C" }} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
let name;
|
|
||||||
let uid;
|
|
||||||
if (comp.name === "vcalendar") {
|
|
||||||
if (EventType.isEvent(comp)) {
|
|
||||||
const vevent = EventType.fromVCalendar(comp);
|
|
||||||
name = vevent.summary;
|
|
||||||
uid = vevent.uid;
|
|
||||||
} else {
|
|
||||||
const vtodo = TaskType.fromVCalendar(comp);
|
|
||||||
name = vtodo.summary;
|
|
||||||
uid = vtodo.uid;
|
|
||||||
}
|
|
||||||
} else if (comp.name === "vcard") {
|
|
||||||
const vcard = new ContactType(comp);
|
|
||||||
name = vcard.fn;
|
|
||||||
uid = vcard.uid;
|
|
||||||
} else {
|
|
||||||
name = "Error processing entry";
|
|
||||||
uid = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
if (props.uid && (props.uid !== uid)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
leftIcon={icon}
|
|
||||||
primaryText={name}
|
|
||||||
secondaryText={uid}
|
|
||||||
onClick={() => setDialog(syncEntry)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<RollbackToHereDialog
|
|
||||||
journal={props.journal}
|
|
||||||
entries={props.entries}
|
|
||||||
entryUid={rollbackDialogId!}
|
|
||||||
open={!!rollbackDialogId}
|
|
||||||
onClose={() => setRollbackDialogId(undefined)}
|
|
||||||
/>
|
|
||||||
<Dialog
|
|
||||||
open={dialog !== undefined}
|
|
||||||
onClose={() => setDialog(undefined)}
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
|
||||||
Raw Content
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<div>Entry UID: <pre className="d-inline-block">{dialog?.uid}</pre></div>
|
|
||||||
<div>Content:
|
|
||||||
<pre>{dialog?.content}</pre>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setDialog(undefined);
|
|
||||||
setRollbackDialogId(dialog?.uid);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Recover items until here
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onClick={() => setDialog(undefined)}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
<List style={{ height: "calc(100vh - 300px)" }}>
|
|
||||||
<AutoSizer>
|
|
||||||
{({ height, width }) => (
|
|
||||||
<VirtualizedList
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
rowCount={props.entries.size}
|
|
||||||
rowHeight={56}
|
|
||||||
rowRenderer={rowRenderer}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in New Issue