You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Clipper: Resolves #681: Allow adding tags from Web Clipper
This commit is contained in:
		| @@ -82,6 +82,7 @@ | ||||
| 				base_url: baseUrl(), | ||||
| 				url: location.origin + location.pathname + location.search, | ||||
| 				parent_id: command.parent_id, | ||||
| 				tags: command.tags || '', | ||||
| 			};			 | ||||
| 		} | ||||
|  | ||||
| @@ -214,6 +215,7 @@ | ||||
| 						crop_rect: selectionArea, | ||||
| 						url: location.origin + location.pathname, | ||||
| 						parent_id: command.parent_id, | ||||
| 						tags: command.tags, | ||||
| 					}; | ||||
|  | ||||
| 					browser_.runtime.sendMessage({ | ||||
|   | ||||
| @@ -100,21 +100,40 @@ | ||||
| 	flex: 0; | ||||
| } | ||||
|  | ||||
| .App .Folders { | ||||
| .App .Folders, | ||||
| .App .Tags { | ||||
| 	display: flex; | ||||
| 	flex-direction: row; | ||||
| 	align-items: center; | ||||
| 	align-items: top; | ||||
| 	padding: 5px 0; | ||||
| } | ||||
|  | ||||
| .App .Folders label { | ||||
| .App .Folders label, | ||||
| .App .Tags label { | ||||
| 	flex: 0; | ||||
| 	white-space: nowrap; | ||||
| 	margin-right: .5em; | ||||
| } | ||||
|  | ||||
| .App .Folders select { | ||||
| .App .Folders select, | ||||
| .App .Tags .AwesompleteInput { | ||||
| 	flex: 1; | ||||
| 	margin-left: 10px; | ||||
| } | ||||
|  | ||||
| .App .Tags .AwesompleteInput, .awesomplete { | ||||
| 	display: flex !important; | ||||
| 	flex: 1; | ||||
| } | ||||
|  | ||||
| .App .Tags input { | ||||
| 	display: inline-block; | ||||
| 	flex: 1; | ||||
| 	margin-bottom: .5em; | ||||
| } | ||||
|  | ||||
| .App .ClearTagButton { | ||||
| 	margin-left: .5em; | ||||
| 	text-decoration: none; | ||||
| } | ||||
|  | ||||
| .App .StatusBar { | ||||
|   | ||||
| @@ -14,6 +14,7 @@ class AppComponent extends Component { | ||||
|  | ||||
| 		this.state = ({ | ||||
| 			contentScriptLoaded: false, | ||||
| 			selectedTags: [], | ||||
| 		}); | ||||
|  | ||||
| 		this.confirm_click = () => { | ||||
| @@ -31,6 +32,7 @@ class AppComponent extends Component { | ||||
| 			bridge().sendCommandToActiveTab({ | ||||
| 				name: 'simplifiedPageHtml', | ||||
| 				parent_id: this.props.selectedFolderId, | ||||
| 				tags: this.state.selectedTags.join(','), | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| @@ -38,6 +40,7 @@ class AppComponent extends Component { | ||||
| 			bridge().sendCommandToActiveTab({ | ||||
| 				name: 'completePageHtml', | ||||
| 				parent_id: this.props.selectedFolderId, | ||||
| 				tags: this.state.selectedTags.join(','), | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| @@ -45,6 +48,7 @@ class AppComponent extends Component { | ||||
| 			bridge().sendCommandToActiveTab({ | ||||
| 				name: 'selectedHtml', | ||||
| 				parent_id: this.props.selectedFolderId, | ||||
| 				tags: this.state.selectedTags.join(','), | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| @@ -56,6 +60,7 @@ class AppComponent extends Component { | ||||
| 					name: 'screenshot', | ||||
| 					api_base_url: baseUrl, | ||||
| 					parent_id: this.props.selectedFolderId, | ||||
| 					tags: this.state.selectedTags.join(','), | ||||
| 				}); | ||||
|  | ||||
| 				window.close(); | ||||
| @@ -74,6 +79,41 @@ class AppComponent extends Component { | ||||
| 				id: event.target.value, | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		this.tagCompChanged = this.tagCompChanged.bind(this); | ||||
| 		this.onAddTagClick = this.onAddTagClick.bind(this); | ||||
| 		this.onClearTagButtonClick = this.onClearTagButtonClick.bind(this); | ||||
| 	} | ||||
|  | ||||
| 	onAddTagClick(event) { | ||||
| 		const newTags = this.state.selectedTags.slice(); | ||||
| 		newTags.push(''); | ||||
| 		this.setState({ selectedTags: newTags }); | ||||
| 		this.focusNewTagInput_ = true; | ||||
| 	} | ||||
|  | ||||
| 	onClearTagButtonClick(event) { | ||||
| 		const index = event.target.getAttribute('data-index'); | ||||
| 		const newTags = this.state.selectedTags.slice(); | ||||
| 		newTags.splice(index, 1); | ||||
| 		this.setState({ selectedTags: newTags }); | ||||
| 	} | ||||
|  | ||||
| 	tagCompChanged(event) { | ||||
| 		const index = Number(event.target.getAttribute('data-index')); | ||||
| 		const value = event.target.value; | ||||
|  | ||||
| 		if (this.state.selectedTags.length <= index) { | ||||
| 			const newTags = this.state.selectedTags.slice(); | ||||
| 			newTags.push(value); | ||||
| 			this.setState({ selectedTags: newTags }); | ||||
| 		} else { | ||||
| 			if (this.state.selectedTags[index] !== value) { | ||||
| 				const newTags = this.state.selectedTags.slice(); | ||||
| 				newTags[index] = value; | ||||
| 				this.setState({ selectedTags: newTags });				 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async loadContentScripts() { | ||||
| @@ -89,6 +129,19 @@ class AppComponent extends Component { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	componentDidUpdate() { | ||||
| 		if (this.focusNewTagInput_) { | ||||
| 			this.focusNewTagInput_ = false; | ||||
| 			let lastRef = null; | ||||
| 			for (let i = 0; i < 100; i++) { | ||||
| 				const ref = this.refs['tagSelector' + i]; | ||||
| 				if (!ref) break; | ||||
| 				lastRef = ref; | ||||
| 			} | ||||
| 			if (lastRef) lastRef.focus(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if (!this.state.contentScriptLoaded) return 'Loading...'; | ||||
|  | ||||
| @@ -119,24 +172,17 @@ class AppComponent extends Component { | ||||
| 					<p className="Info">{ msg }</p> | ||||
| 				</div> | ||||
| 			); | ||||
| 		} else { | ||||
| 			if (hasContent) { | ||||
| 				previewComponent = ( | ||||
| 					<div className="Preview"> | ||||
| 						<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/> | ||||
| 						<div className={"BodyWrapper"}> | ||||
| 							<div className={"Body"} dangerouslySetInnerHTML={{__html: content.body_html}}></div> | ||||
| 						</div> | ||||
| 						<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a> | ||||
| 		} else if (hasContent) { | ||||
| 			previewComponent = ( | ||||
| 				<div className="Preview"> | ||||
| 					<h2>Preview:</h2> | ||||
| 					<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/> | ||||
| 					<div className={"BodyWrapper"}> | ||||
| 						<div className={"Body"} dangerouslySetInnerHTML={{__html: content.body_html}}></div> | ||||
| 					</div> | ||||
| 				); | ||||
| 			} else { | ||||
| 				previewComponent = ( | ||||
| 					<div className="Preview"> | ||||
| 						<p className="Info">(No preview yet)</p> | ||||
| 					</div> | ||||
| 				); | ||||
| 			} | ||||
| 					<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a> | ||||
| 				</div> | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		const clipperStatusComp = () => { | ||||
| @@ -166,8 +212,6 @@ class AppComponent extends Component { | ||||
| 			return <div className="StatusBar"><img alt={foundState} className="Led" src={led}/><span className="ServerStatus">{ msg }{ helpLink }</span></div> | ||||
| 		} | ||||
|  | ||||
| 		console.info(this.props.selectedFolderId); | ||||
|  | ||||
| 		const foldersComp = () => { | ||||
| 			const optionComps = []; | ||||
|  | ||||
| @@ -196,6 +240,37 @@ class AppComponent extends Component { | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		const tagsComp = () => { | ||||
| 			const comps = []; | ||||
| 			for (let i = 0; i < this.state.selectedTags.length; i++) { | ||||
| 				comps.push(<div> | ||||
| 					<input | ||||
| 						ref={'tagSelector' + i} | ||||
| 						data-index={i} | ||||
| 						key={i} | ||||
| 						type="text" | ||||
| 						list="tags" | ||||
| 						value={this.state.selectedTags[i]} | ||||
| 						onChange={this.tagCompChanged} | ||||
| 						onInput={this.tagCompChanged} | ||||
| 					/> | ||||
| 					<a data-index={i} href="#" className="ClearTagButton" onClick={this.onClearTagButtonClick}>[x]</a> | ||||
| 				</div>); | ||||
| 			} | ||||
| 			return ( | ||||
| 				<div> | ||||
| 					{comps} | ||||
| 					<a className="AddTagButton" href="#" onClick={this.onAddTagClick}>Add tag</a> | ||||
| 				</div> | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		const tagDataListOptions = []; | ||||
| 		for (let i = 0; i < this.props.tags.length; i++) { | ||||
| 			const tag = this.props.tags[i]; | ||||
| 			tagDataListOptions.push(<option key={tag.id}>{tag.title}</option>); | ||||
| 		} | ||||
|  | ||||
| 		return ( | ||||
| 			<div className="App"> | ||||
| 				<div className="Controls">			 | ||||
| @@ -207,8 +282,14 @@ class AppComponent extends Component { | ||||
| 					</ul> | ||||
| 				</div> | ||||
| 				{ foldersComp() } | ||||
| 				<div className="Tags"> | ||||
| 					<label>Tags:</label> | ||||
| 					{tagsComp()} | ||||
| 					<datalist id="tags"> | ||||
| 						{tagDataListOptions} | ||||
| 					</datalist> | ||||
| 				</div> | ||||
| 				{ warningComponent } | ||||
| 				<h2>Preview:</h2> | ||||
| 				{ previewComponent } | ||||
| 				{ clipperStatusComp() } | ||||
| 			</div> | ||||
| @@ -224,6 +305,7 @@ const mapStateToProps = (state) => { | ||||
| 		contentUploadOperation: state.contentUploadOperation, | ||||
| 		clipperServer: state.clipperServer, | ||||
| 		folders: state.folders, | ||||
| 		tags: state.tags, | ||||
| 		selectedFolderId: state.selectedFolderId, | ||||
| 	}; | ||||
| }; | ||||
|   | ||||
| @@ -28,6 +28,7 @@ class Bridge { | ||||
| 					base_url: command.base_url, | ||||
| 					source_url: command.url, | ||||
| 					parent_id: command.parent_id, | ||||
| 					tags: command.tags || '', | ||||
| 				}; | ||||
|  | ||||
| 				this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content }); | ||||
| @@ -122,6 +123,9 @@ class Bridge { | ||||
|  | ||||
| 					const folders = await this.folderTree(); | ||||
| 					this.dispatch({ type: 'FOLDERS_SET', folders: folders }); | ||||
|  | ||||
| 					const tags = await this.clipperApiExec('GET', 'tags'); | ||||
| 					this.dispatch({ type: 'TAGS_SET', tags: tags }); | ||||
| 					return; | ||||
| 				} | ||||
| 			} catch (error) { | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const defaultState = { | ||||
| 		port: null, | ||||
| 	}, | ||||
| 	folders: [], | ||||
| 	tags: [], | ||||
| 	selectedFolderId: null, | ||||
| 	env: 'prod', | ||||
| }; | ||||
| @@ -65,6 +66,11 @@ function reducer(state = defaultState, action) { | ||||
| 			newState.selectedFolderId = action.folders[0].id; | ||||
| 		} | ||||
|  | ||||
| 	} else if (action.type === 'TAGS_SET') { | ||||
|  | ||||
| 		newState = Object.assign({}, state); | ||||
| 		newState.tags = action.tags; | ||||
|  | ||||
| 	} else if (action.type === 'SELECTED_FOLDER_SET') { | ||||
|  | ||||
| 		newState = Object.assign({}, state); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ const urlParser = require("url"); | ||||
| const Note = require('lib/models/Note'); | ||||
| const Folder = require('lib/models/Folder'); | ||||
| const Resource = require('lib/models/Resource'); | ||||
| const Tag = require('lib/models/Tag'); | ||||
| const Setting = require('lib/models/Setting'); | ||||
| const { shim } = require('lib/shim'); | ||||
| const md5 = require('md5'); | ||||
| @@ -257,6 +258,10 @@ class ClipperServer { | ||||
| 					const structure = await Folder.allAsTree({ fields: ['id', 'parent_id', 'title'] }); | ||||
| 					return writeResponseJson(200, structure); | ||||
| 				} | ||||
|  | ||||
| 				if (url.pathname === '/tags') { | ||||
| 					return writeResponseJson(200, await Tag.all({ fields: ['id', 'title'] })); | ||||
| 				} | ||||
| 			} else if (request.method === 'POST') { | ||||
| 				if (url.pathname === '/notes') { | ||||
| 					let body = ''; | ||||
| @@ -278,6 +283,11 @@ class ClipperServer { | ||||
|  | ||||
| 							note = await Note.save(note); | ||||
|  | ||||
| 							if (requestNote.tags) { | ||||
| 								const tagTitles = requestNote.tags.split(','); | ||||
| 								await Tag.setNoteTagsByTitles(note.id, tagTitles); | ||||
| 							} | ||||
|  | ||||
| 							if (requestNote.image_data_url) { | ||||
| 								await this.attachImageFromDataUrl_(note, requestNote.image_data_url, requestNote.crop_rect); | ||||
| 							} | ||||
|   | ||||
| @@ -173,7 +173,16 @@ function shimInit() { | ||||
| 		if (shim.isElectron()) { | ||||
| 			const nativeImage = require('electron').nativeImage; | ||||
| 			let image = nativeImage.createFromDataURL(imageDataUrl); | ||||
| 			if (options.cropRect) image = image.crop(options.cropRect); | ||||
| 			if (image.isEmpty()) throw new Error('Could not convert data URL to image'); | ||||
| 			if (options.cropRect) { | ||||
| 				// Crop rectangle values need to be rounded or the crop() call will fail | ||||
| 				const c = options.cropRect; | ||||
| 				if ('x' in c) c.x = Math.round(c.x); | ||||
| 				if ('y' in c) c.y = Math.round(c.y); | ||||
| 				if ('width' in c) c.width = Math.round(c.width); | ||||
| 				if ('height' in c) c.height = Math.round(c.height); | ||||
| 				image = image.crop(c); | ||||
| 			} | ||||
| 			const mime = mimeUtils.fromDataUrl(imageDataUrl); | ||||
| 			await shim.writeImageToFile(image, mime, filePath); | ||||
| 		} else { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user