You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Support for Joplin Cloud recursive linked notes
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user