mirror of https://github.com/laurent22/joplin.git
Desktop: Support for Joplin Cloud recursive linked notes
parent
a79bc69604
commit
9d9420a35c
|
@ -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'],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static supportsRecursiveLinkedNotes(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue