Desktop: Support for Joplin Cloud recursive linked notes

pull/6364/head^2
Laurent Cozic 2022-04-03 19:19:24 +01:00
parent a79bc69604
commit 9d9420a35c
8 changed files with 72 additions and 56 deletions

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
@ -15,6 +15,7 @@ import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '../../lib/SyncTargetRegistry';
const { clipboard } = require('electron');
interface Props {
@ -22,6 +23,7 @@ interface Props {
noteIds: Array<string>;
onClose: Function;
shares: StateShare[];
syncTargetId: number;
}
function styles_(props: Props) {
@ -69,9 +71,10 @@ export function ShareNoteDialog(props: Props) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
const [sharesState, setSharesState] = useState<string>('unknown');
// const [shares, setShares] = useState<SharesMap>({});
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
@ -102,7 +105,7 @@ export function ShareNoteDialog(props: Props) {
clipboard.writeText(links.join('\n'));
};
const shareLinkButton_click = async () => {
const shareLinkButton_click = useCallback(async () => {
const service = ShareService.instance();
let hasSynced = false;
@ -121,7 +124,7 @@ export function ShareNoteDialog(props: Props) {
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id);
const share = await service.shareNote(note.id, recursiveShare);
newShares.push(share);
}
@ -149,17 +152,7 @@ export function ShareNoteDialog(props: Props) {
break;
}
};
// const removeNoteButton_click = (event: any) => {
// const newNotes = [];
// for (let i = 0; i < notes.length; i++) {
// const n = notes[i];
// if (n.id === event.noteId) continue;
// newNotes.push(n);
// }
// setNotes(newNotes);
// };
}, [recursiveShare, notes]);
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
@ -171,22 +164,6 @@ export function ShareNoteDialog(props: Props) {
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
);
// const removeButton = notes.length <= 1 ? null : (
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
// );
// const unshareButton = !shares[note.id] ? null : (
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
// </button>
// );
// const removeButton = notes.length <= 1 ? null : (
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
// </button>
// );
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
@ -214,11 +191,26 @@ export function ShareNoteDialog(props: Props) {
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}
function renderContent() {
const onRecursiveShareChange = useCallback(() => {
setRecursiveShare(v => !v);
}, []);
const renderRecursiveShareCheckbox = () => {
if (!syncTargetInfo.supportsRecursiveLinkedNotes) return null;
return (
<div style={styles.root}>
<div className="form-input-group form-input-group-checkbox">
<input id="recursiveShare" name="recursiveShare" type="checkbox" checked={!!recursiveShare} onChange={onRecursiveShareChange} /> <label htmlFor="recursiveShare">{_('Also publish linked notes')}</label>
</div>
);
};
const renderContent = () => {
return (
<div style={styles.root} className="form">
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{renderEncryptionWarningMessage()}
@ -230,7 +222,7 @@ export function ShareNoteDialog(props: Props) {
/>
</div>
);
}
};
return (
<Dialog renderContent={renderContent}/>
@ -240,6 +232,7 @@ export function ShareNoteDialog(props: Props) {
const mapStateToProps = (state: AppState) => {
return {
shares: state.shareService.shares.filter(s => !!s.note_id),
syncTargetId: state.settings['sync.target'],
};
};

View File

@ -112,7 +112,15 @@ document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => event.preventDefault());
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
event.preventDefault();
});
app().start(bridge().processArgv()).then((result) => {
if (!result || !result.action) {

View File

@ -180,6 +180,22 @@ h2 {
margin-bottom: 10px;
}
.form > .form-input-group-checkbox {
display: flex;
flex-direction: row;
align-items: center;
}
.form > .form-input-group-checkbox > input {
display: flex;
margin-right: 6px;
}
.form > .form-input-group-checkbox > label {
display: flex;
margin-bottom: 0;
}
.bold {
font-weight: bold;
}

View File

@ -33,6 +33,10 @@ export default class BaseSyncTarget {
return true;
}
public static supportsRecursiveLinkedNotes(): boolean {
return false;
}
public option(name: string, defaultValue: any = null) {
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}

View File

@ -38,6 +38,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
return false;
}
public static supportsRecursiveLinkedNotes(): boolean {
return true;
}
public async isAuthenticated() {
return true;
}

View File

@ -4,33 +4,16 @@ export interface SyncTargetInfo {
label: string;
supportsSelfHosted: boolean;
supportsConfigCheck: boolean;
supportsRecursiveLinkedNotes: boolean;
description: string;
classRef: any;
}
// const syncTargetOrder = [
// 'joplinCloud',
// 'dropbox',
// 'onedrive',
// ];
export default class SyncTargetRegistry {
private static reg_: Record<number, SyncTargetInfo> = {};
private static get reg() {
// if (!this.reg_[0]) {
// this.reg_[0] = {
// id: 0,
// name: SyncTargetNone.targetName(),
// label: SyncTargetNone.label(),
// classRef: SyncTargetNone,
// description: SyncTargetNone.description(),
// supportsSelfHosted: false,
// supportsConfigCheck: false,
// };
// }
return this.reg_;
}
@ -47,6 +30,10 @@ export default class SyncTargetRegistry {
throw new Error(`Unknown name: ${name}`);
}
public static infoById(id: number): SyncTargetInfo {
return this.infoByName(this.idToName(id));
}
public static addClass(SyncTargetClass: any) {
this.reg[SyncTargetClass.id()] = {
id: SyncTargetClass.id(),
@ -56,6 +43,7 @@ export default class SyncTargetRegistry {
description: SyncTargetClass.description(),
supportsSelfHosted: SyncTargetClass.supportsSelfHosted(),
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
supportsRecursiveLinkedNotes: SyncTargetClass.supportsRecursiveLinkedNotes(),
};
}

View File

@ -53,7 +53,7 @@ describe('ShareService', function() {
},
});
await msleep(1);
await service.shareNote(note.id);
await service.shareNote(note.id, false);
function checkTimestamps(previousNote: NoteEntity, newNote: NoteEntity) {
// After sharing or unsharing, only the updated_time property should

View File

@ -228,11 +228,14 @@ export default class ShareService {
}
}
public async shareNote(noteId: string): Promise<StateShare> {
public async shareNote(noteId: string, recursive: boolean): Promise<StateShare> {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
const share = await this.api().exec('POST', 'api/shares', {}, { note_id: noteId });
const share = await this.api().exec('POST', 'api/shares', {}, {
note_id: noteId,
recursive: recursive ? 1 : 0,
});
await Note.save({
id: note.id,