├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── App.jsx ├── __redirects ├── bookmarklet.min.js ├── bookmarklet │ └── bookmarklet.js ├── components │ ├── BookmarkletLink.jsx │ ├── CaptureForm.jsx │ ├── ConfirmClear.jsx │ ├── IntegrationIcons.jsx │ ├── SensitiveInput.jsx │ ├── SettingsForm.jsx │ ├── SettingsModal.jsx │ └── SettingsToggler.jsx ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon.svg │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── functions │ └── send.js ├── hooks │ ├── useInput.jsx │ └── useLocalStorage.jsx ├── icons.jsx ├── index.css ├── index.html ├── index.jsx ├── logo.png ├── logo.svg ├── netlify.toml ├── package.json ├── robots.txt ├── theme.js ├── vite.config.js └── yarn.lock ├── core ├── capture.js ├── index.js ├── package.json ├── utils.js └── wf-cli.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | aliases.json 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | .env 32 | # Local Netlify folder 33 | .netlify -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original Copyright (c) 2014 Mike Robertson 4 | Modifications Copyright (c) 2016 Malcolm Ocean 5 | Modifications Copyright (c) 2021 Christian Miles 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | An unofficial way to send to WorkFlowy from anywhere. 5 | 6 | ## Intro 7 | 8 | This repository allows you to send to your WorkFlowy account from a variety of different sources: 9 | 10 | - 🕸 Website 11 | - 🔖 Bookmarklet 12 | - 📱 iOS Shortcut 13 | - 📱 Android (coming soon!) 14 | - ⌨️ Command Line 15 | 16 | It helps you save links and text for later so you can concentrate on the task in hand. 17 | 18 | ## ⚠️ Disclaimer ⚠️ 19 | 20 | WorkFlowy doesn't have an official API so `send-to-workflowy` needs some configuration to talk to WorkFlowy. It doesn't store any of your login information or WorkFlowy data but it's up to you to keep your Session ID secure. 21 | 22 | ## How to use 23 | 24 | For most users `send-to-workflowy` is a web application hosted at https://send-to-workflowy.netlify.app/ 25 | 26 | Simply add your Session ID and Parent ID on the Settings panel to send to your WorkFlowy. These IDs are stored in your browser cache for convenience. They are used to communicate with your WorkFlowy but otherwise are not tracked by `send-to-workflowy`. See [Configuration](#configuration) for details on finding these IDs. 27 | 28 | 29 | 30 | ## Configuration 31 | 32 | ### Session ID 33 | 34 | `send-to-workflowy` uses a session ID to talk to your WorkFlowy account. The easiest way to do this is to copy it from the browser with the following steps: 35 | 36 | - Open WorkFlowy in a browser 37 | - Open the Developer Tools (right click anywhere and click "Inspect") 38 | - Go to the Application tab (on Chrome) or the Storage tab (on FireFox) 39 | - Chrome: In the left hand side bar under "Storage", open up "Cookies" 40 | - In Chrome and FireFox select https://workflowy.com – `sessionid` should be listed 41 | - e.g. `ulz0qzfzv1l1izs0oa2wuol69jdwtvdj` 42 | 43 | Note that you should treat this session ID as if it were a password. It should not be shared with anyone or posted publicly. Each Session ID expires after a few months. 44 | 45 | ### Parent ID 46 | 47 | All nodes in WorkFlowy have a parent ID. You can provide this to `send-to-workflowy` to always place new nodes under a node with a particular parent ID. 48 | 49 | To find your parent ID: 50 | 51 | - Open WorkFlowy in a browser 52 | - Find the node you wish to send to 53 | - Open the developer tools. (With Chrome: Ctrl + Shift + J on Windows, Command + Opt + J on macOS) 54 | - Run `copy(WF.currentItem().data.id)` 55 | - Your parent ID will now be on your clipboard ready to be pasted into `send-to-workflowy` 56 | - It will look something like `d4d9fe09-f770-41ef-d826-cb8fa483424f` 57 | 58 | ## Integrations 59 | 60 | The Settings panel also provides a link to the iOS shortcut and bookmarklet to make it even easier to send to WorkFlowy from your phone or without going to the site. Once you've configured your Session ID and Parent ID the integration icons on the Settings page are ready to use. 61 | 62 | ### iOS Shortcut 63 | 64 | If you select the Apple link you'll be redirected to an iOS shortcut hosted in iCloud. Due to limitations this is the easiest way to create your new `send-to-workflowy` shortcut. 65 | 66 | Scroll all the way down to accept the shortcut. On the next page you'll be prompted to add your Session ID and Parent ID. The shortcut expects these in the following form 67 | 68 | ``` 69 | 70 | 71 | ``` 72 | 73 | The `send-to-workflowy` web app should have already put these on the clipboard for you so just paste them into the prompt. Note these will not be leaving your device, just used to configure the shortcut. 74 | 75 | ### Bookmarklet 76 | 77 | This bookmarklet can be dragged to your bookmark bar. Again it's configured using the Session and Parent IDs provided in the webapp (locally) – it will send the current URL to your WorkFlowy. 78 | 79 | ### Android 80 | 81 | This integration isn't ready yet. Please let me know if there's a preferred way to do this on Android. 82 | 83 | In the meantime it's possible to add bookmarklets to [Chrome on Android](https://paul.kinlan.me/use-bookmarklets-on-chrome-on-android/). 84 | 85 | ## Self-hosted 86 | 87 | The most secure way to use `send-to-workflowy` is to host it yourself. This repository can be deployed to Netlify with the following URL. 88 | 89 | The environment variables `SESSIONID` and `PARENTID` can be configured in the Netlify configuration to avoid the need to provide these to the web application in the browser. 90 | 91 | [![](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/cjlm/send-to-workflowy) 92 | 93 | ## Future Roadmap 94 | 95 | - [ ] Android integration 96 | - [ ] Date support 97 | - [ ] Multiple parent support 98 | - [ ] Bookmarklet: send page selection if highlighted 99 | - [ ] Bookmarklet: visual confirmation of success 100 | - [ ] email-to-workflowy? 101 | - [ ] text-to-workflowy? 102 | 103 | ### Dev Improvements 104 | 105 | - [ ] Add tests 106 | - [x] Move top-level code to workspaces 107 | 108 | ## Acknowledgements 109 | 110 | There's a decade of previous attempts at an unofficial API of sorts for WorkFlowy. I am most indebted to Malcolm Ocean's [opusfluxus](https://github.com/malcolmocean/opusfluxus) for the original code forked to make this repository. Thanks to him and his collaborators. 111 | 112 | Thanks to Jesse Patel & team for WorkFlowy ❤️ 113 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .snowpack 2 | build 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /app/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Box, Heading } from '@chakra-ui/react'; 3 | 4 | import { ArrowIcon } from './icons'; 5 | 6 | import CaptureForm from './components/CaptureForm'; 7 | import SettingsToggler from './components/SettingsToggler'; 8 | import SettingsModal from './components/SettingsModal'; 9 | 10 | import useInput from './hooks/useInput'; 11 | import useLocalStorage from './hooks/useLocalStorage'; 12 | import { useDisclosure } from '@chakra-ui/react'; 13 | 14 | function App() { 15 | const { 16 | value: sessionId, 17 | bind: bindSessionId, 18 | setValue: setSessionId, 19 | } = useInput('', 'sessionId'); 20 | const { 21 | value: parentId, 22 | bind: bindParentId, 23 | setValue: setParentId, 24 | } = useInput('', 'parentId'); 25 | const [top, setTop] = useLocalStorage('addToTop', true); 26 | 27 | const { isOpen, onOpen, onClose } = useDisclosure(); 28 | 29 | const modalProps = { 30 | sessionId, 31 | bindSessionId, 32 | setSessionId, 33 | parentId, 34 | bindParentId, 35 | setParentId, 36 | top, 37 | setTop, 38 | onClose, 39 | isOpen, 40 | }; 41 | 42 | return ( 43 | 49 | 58 | 59 | 60 | 61 | 62 | 63 | {'Send to WorkFlowy'} 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | export default App; 75 | -------------------------------------------------------------------------------- /app/__redirects: -------------------------------------------------------------------------------- 1 | /robots.txt /robots.txt 200 -------------------------------------------------------------------------------- /app/bookmarklet.min.js: -------------------------------------------------------------------------------- 1 | post("!URL",{sessionId:"!SESSION_ID",parentId:"!PARENT_ID",text:`${document.title}`,note:""});function post(t,{sessionId:e,parentId:o,text:n,note:d,priority:i=0}){fetch(t,{method:"POST",mode:"no-cors",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:n,note:d,sessionId:e,parentId:o,priority:i})})} 2 | -------------------------------------------------------------------------------- /app/bookmarklet/bookmarklet.js: -------------------------------------------------------------------------------- 1 | post('!URL', { 2 | sessionId: '!SESSION_ID', 3 | parentId: '!PARENT_ID', 4 | text: `${document.title}`, 5 | note: '', 6 | }); 7 | 8 | function post(url, { sessionId, parentId, text, note, priority = 0 }) { 9 | fetch(url, { 10 | method: 'POST', 11 | mode: 'no-cors', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | body: JSON.stringify({ text, note, sessionId, parentId, priority }), 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /app/components/BookmarkletLink.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link } from '@chakra-ui/react'; 3 | 4 | import bookmarkletRaw from '/bookmarklet.min.js?raw' 5 | 6 | export default function BookmarkletLink(props) { 7 | const { sessionId, parentId, children } = props; 8 | const [link, setLink] = useState(''); 9 | 10 | useEffect(() => { 11 | setLink( 12 | `javascript:${bookmarkletRaw 13 | .replace(`!SESSION_ID`, sessionId) 14 | .replace(`!PARENT_ID`, parentId) 15 | .replace(`!URL`, `${document.URL}send`)}` 16 | ); 17 | }, [sessionId, parentId]); 18 | 19 | return {children}; 20 | } 21 | -------------------------------------------------------------------------------- /app/components/CaptureForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import useInput from '../hooks/useInput'; 4 | import useLocalStorage from '../hooks/useLocalStorage'; 5 | import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/react'; 6 | 7 | import { Button, CircularProgress, Checkbox } from '@chakra-ui/react'; 8 | import { Input, Textarea, Box, Collapse } from '@chakra-ui/react'; 9 | 10 | import { useToast } from '@chakra-ui/react'; 11 | 12 | export default function CaptureForm(props) { 13 | const { parentId, sessionId, top } = props; 14 | 15 | const priority = top ? 0 : 10000000; 16 | 17 | const { value: text, bind: bindText, reset: resetText } = useInput(''); 18 | const { value: note, bind: bindNote, reset: resetNote } = useInput(''); 19 | 20 | const [status, setStatus] = useState(''); 21 | 22 | const [isNoteShown, toggleNote] = useLocalStorage('notes', false); 23 | 24 | const toast = useToast(); 25 | 26 | const showToast = (text, status) => { 27 | toast({ 28 | title: text, 29 | description: '', 30 | status, 31 | isClosable: false, 32 | duration: 4000, 33 | position: 'bottom', 34 | }); 35 | }; 36 | 37 | const keyboardSubmit = (evt) => { 38 | if (evt.keyCode === 13 && (evt.ctrlKey || evt.metaKey)) { 39 | handleSubmit(evt); 40 | } 41 | }; 42 | 43 | const handleSubmit = async (evt) => { 44 | evt.preventDefault(); 45 | setStatus('loading'); 46 | 47 | try { 48 | const response = await fetch('/send', { 49 | method: 'POST', 50 | mode: 'no-cors', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | body: JSON.stringify({ text, note, sessionId, parentId, priority }), 55 | }); 56 | if (response.ok) { 57 | setStatus('success'); 58 | 59 | resetText(); 60 | resetNote(); 61 | 62 | showToast('Sent!', 'success'); 63 | } else { 64 | throw new Error(`${response.status} - ${response.statusText}`); 65 | } 66 | } catch (err) { 67 | console.error(err); 68 | setStatus('error'); 69 | showToast( 70 | 'Error connecting to WorkFlowy, please check your configuration.', 71 | 'error' 72 | ); 73 | } 74 | }; 75 | return ( 76 | <> 77 |
78 | 79 | Text: 80 |