├── .nvmrc ├── .dockerignore ├── Procfile ├── test_helpers ├── fixtures │ ├── empty_file.org │ ├── schedule.org │ ├── email.org │ ├── more_tags.org │ ├── phonenumber.org │ ├── tags.org │ ├── url.org │ ├── content_but_no_headline.org │ ├── schedule_with_repeater.org │ ├── schedule_and_deadline.org │ ├── newlines.org │ ├── www_url.org │ ├── nested_header.org │ ├── schedule_and_timestamps.org │ ├── schedule_with_repeater_and_nologrepeat.org │ ├── properties.org │ ├── indented_list.org │ ├── planning_item_with_following_checkmark.org │ ├── schedule_with_repeater_and_nologrepeat_and_other_options.org │ ├── headline_filter.org │ ├── bold_text.org │ ├── properties_extended.org │ ├── multiple_headlines_with_timestamps_simple.org │ ├── checkboxes.org │ ├── various_todos.org │ ├── schedule_with_repeater_and_nologrepeat_property.org │ ├── table.org │ ├── logbook.org │ ├── multiple_headlines_with_timestamps.org │ ├── before-first-headline.org │ ├── clock_entries.org │ ├── large_table.org │ ├── todo_keywords_interspersed.org │ ├── logbook_and_log_notes.org │ └── main_test_file.org └── index.js ├── .prettierignore ├── src ├── components │ ├── OrgFile │ │ ├── components │ │ │ ├── FinderModal │ │ │ │ ├── stylesheet.css │ │ │ │ └── index.js │ │ │ ├── TableEditorModal │ │ │ │ ├── stylesheet.css │ │ │ │ ├── index.js │ │ │ │ └── components │ │ │ │ │ └── TableActionButtons │ │ │ │ │ └── stylesheet.css │ │ │ ├── DrawerActionBar │ │ │ │ ├── stylesheet.css │ │ │ │ └── components │ │ │ │ │ └── DrawerActionButtons │ │ │ │ │ └── stylesheet.css │ │ │ ├── Table │ │ │ │ ├── stylesheet.css │ │ │ │ ├── TableCell │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ └── index.js │ │ │ │ ├── TableCellEditContainer │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── AttributedString │ │ │ │ ├── components │ │ │ │ │ ├── TimestampPart │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ListPart │ │ │ │ │ │ ├── ListActionDrawer │ │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── stylesheet.css │ │ │ │ │ └── TablePart │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ └── stylesheet.css │ │ │ ├── NoteEditorModal │ │ │ │ ├── stylesheet.css │ │ │ │ └── index.js │ │ │ ├── HeaderContent │ │ │ │ ├── components │ │ │ │ │ ├── LogBookEntries │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── PropertyListItems │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ │ └── PlanningItems │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ └── stylesheet.css │ │ │ ├── HeaderList │ │ │ │ └── stylesheet.css │ │ │ ├── ActionDrawer │ │ │ │ ├── components │ │ │ │ │ └── ActionButton │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ └── stylesheet.css │ │ │ ├── TaskListModal │ │ │ │ ├── components │ │ │ │ │ └── TaskListView │ │ │ │ │ │ └── stylesheet.css │ │ │ │ └── stylesheet.css │ │ │ ├── SearchModal │ │ │ │ ├── components │ │ │ │ │ └── HeaderListView │ │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ │ └── index.js │ │ │ │ └── stylesheet.css │ │ │ ├── SyncConfirmationModal │ │ │ │ ├── stylesheet.css │ │ │ │ └── index.js │ │ │ ├── TimestampEditorModal │ │ │ │ ├── stylesheet.css │ │ │ │ └── components │ │ │ │ │ └── TimestampEditor │ │ │ │ │ └── stylesheet.css │ │ │ ├── AgendaModal │ │ │ │ ├── stylesheet.css │ │ │ │ └── components │ │ │ │ │ └── AgendaDay │ │ │ │ │ ├── stylesheet.css │ │ │ │ │ └── AgendaDay.unit.test.js │ │ │ ├── PropertyListEditorModal │ │ │ │ └── stylesheet.css │ │ │ ├── TitleLine │ │ │ │ ├── .index.js.swp │ │ │ │ └── stylesheet.css │ │ │ ├── CaptureModal │ │ │ │ └── stylesheet.css │ │ │ ├── Header │ │ │ │ ├── stylesheet.css │ │ │ │ └── components │ │ │ │ │ └── HeaderActionDrawer │ │ │ │ │ └── stylesheet.css │ │ │ ├── DescriptionEditorModal │ │ │ │ └── stylesheet.css │ │ │ ├── TagsEditorModal │ │ │ │ └── stylesheet.css │ │ │ └── TitleEditorModal │ │ │ │ └── stylesheet.css │ │ └── stylesheet.css │ ├── SyncServiceSignIn │ │ ├── google_drive.png │ │ └── stylesheet.css │ ├── FileSettingsEditor │ │ ├── stylesheet.css │ │ └── components │ │ │ └── FileSetting │ │ │ └── stylesheet.css │ ├── Landing │ │ ├── fonts │ │ │ └── metropolis │ │ │ │ ├── Metropolis-Black.otf │ │ │ │ ├── Metropolis-Bold.otf │ │ │ │ ├── Metropolis-Light.otf │ │ │ │ ├── Metropolis-Thin.otf │ │ │ │ ├── Metropolis-Medium.otf │ │ │ │ ├── Metropolis-Regular.otf │ │ │ │ ├── Metropolis-BoldItalic.otf │ │ │ │ ├── Metropolis-ExtraBold.otf │ │ │ │ ├── Metropolis-ExtraLight.otf │ │ │ │ ├── Metropolis-SemiBold.otf │ │ │ │ ├── Metropolis-ThinItalic.otf │ │ │ │ ├── Metropolis-BlackItalic.otf │ │ │ │ ├── Metropolis-LightItalic.otf │ │ │ │ ├── Metropolis-MediumItalic.otf │ │ │ │ ├── Metropolis-ExtraBoldItalic.otf │ │ │ │ ├── Metropolis-RegularItalic.otf │ │ │ │ ├── Metropolis-SemiBoldItalic.otf │ │ │ │ └── Metropolis-ExtraLightItalic.otf │ │ ├── Landing.test.js │ │ └── stylesheet.css │ ├── PrivacyPolicy │ │ └── stylesheet.css │ ├── UI │ │ ├── Checkbox │ │ │ ├── stylesheet.css │ │ │ └── index.js │ │ ├── ExternalLink │ │ │ └── index.js │ │ ├── Switch │ │ │ ├── stylesheet.css │ │ │ └── index.js │ │ ├── TabButtons │ │ │ ├── stylesheet.css │ │ │ └── index.js │ │ └── Drawer │ │ │ └── stylesheet.css │ ├── KeyboardShortcutsEditor │ │ ├── stylesheet.css │ │ ├── components │ │ │ └── ShortcutRow │ │ │ │ └── stylesheet.css │ │ └── index.js │ ├── Entry │ │ └── stylesheet.css │ ├── CaptureTemplatesEditor │ │ └── stylesheet.css │ ├── LoadingIndicator │ │ ├── stylesheet.css │ │ └── index.js │ ├── FileBrowser │ │ ├── stylesheet.css │ │ └── components │ │ │ └── ActionDrawer │ │ │ └── index.js │ ├── HeaderBar │ │ └── stylesheet.css │ ├── Settings │ │ └── stylesheet.css │ └── Turnout │ │ └── index.js ├── index.css ├── lib │ ├── id_generator.js │ ├── sample_capture_templates.js │ ├── capture_template_substitution.js │ ├── keybindings.js │ ├── browser_utils.js │ ├── share_utils.js │ └── clocking.js ├── index.js ├── migrations │ ├── migrate_access_token_to_dropbox_access_token.js │ ├── index.js │ ├── migrate_nonsense_values_in_localstorage.js │ └── migrate_store_in_dropbox_to_store_in_sync_backend.js ├── sync_backend_clients │ └── dropbox_sync_backend_client.test.js ├── service-worker.js ├── store.js ├── util │ ├── misc.js │ ├── settings_persister.test.js │ └── parse_query_string.js ├── middleware │ ├── toggle_color_scheme.js │ ├── live_sync.js │ └── sync_on_becoming_visible.js ├── reducers │ ├── sync_backend.js │ └── index.js ├── actions │ └── capture.js └── colors.css ├── .eslintignore ├── babel.config.json ├── public ├── favicon.ico ├── organice.png ├── organice-small.png ├── organice-192x192.png ├── organice-512x512.png ├── manifest.json └── index.html ├── .editorconfig ├── config └── jest │ ├── SvgComponent.js │ ├── fileTransform.js │ ├── cssTransform.js │ └── babelTransform.js ├── images ├── screenshot-overview.png ├── screenshot-settings.png └── screenshot-introduction.png ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── bin ├── entrypoint.sh ├── transient_env_vars.sh ├── compile_doc_and_upload.sh ├── compile_search_parser.sh ├── compile_and_upload.sh └── compile_doc.sh ├── .parcelrc ├── .prettierrc.json ├── .gitattributes ├── .env.sample ├── contrib ├── organice-k8s │ ├── service.yml │ ├── ingress.yml │ ├── README.org │ ├── deployment.yml │ └── load-balancer.yml ├── organice-pinephone │ ├── organice.service │ ├── webdav.service │ └── Dockerfile ├── organice-webdav-traefik │ ├── webdav │ │ ├── Dockerfile │ │ ├── webdav.conf │ │ └── run.sh │ ├── LICENSE │ └── docker-compose.yml ├── organice-caddy-webdav │ ├── Dockerfile │ └── docker-compose.yml ├── organice-webdav-post │ ├── organice_inbox.sh │ ├── home.php │ ├── inbox.php │ └── Readme.md ├── organice-nextcloud-nginx │ └── README.org ├── organice-nginx-cors │ └── README.org └── organice-on-android-with-webdav-server │ └── README.md ├── doc ├── org2html │ └── init.el ├── webdav │ ├── Dockerfile │ └── webdav.conf └── setupfile ├── __mocks__ └── fileMock.js ├── Makefile ├── .gitignore ├── .eslintrc.yml ├── docker-compose.yml ├── jest.config.json ├── flake.lock ├── Dockerfile └── flake.nix /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.17.0 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: serve build 2 | -------------------------------------------------------------------------------- /test_helpers/fixtures/empty_file.org: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/FinderModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TableEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | config/jest/ 3 | __mocks__/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-macros" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule.org: -------------------------------------------------------------------------------- 1 | * Spec header 2 | SCHEDULED: <2019-09-22 Sun> 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/organice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/public/organice.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [**.js{,on}] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /config/jest/SvgComponent.js: -------------------------------------------------------------------------------- 1 | export default function SvgComponent() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /test_helpers/fixtures/email.org: -------------------------------------------------------------------------------- 1 | * foo 2 | This is an e-mail foo.bar@baz.org in a line of text. 3 | -------------------------------------------------------------------------------- /public/organice-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/public/organice-small.png -------------------------------------------------------------------------------- /public/organice-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/public/organice-192x192.png -------------------------------------------------------------------------------- /public/organice-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/public/organice-512x512.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Verdana, sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /test_helpers/fixtures/more_tags.org: -------------------------------------------------------------------------------- 1 | * Tagged header :t1:t2:t3:spec_tag: 2 | -------------------------------------------------------------------------------- /images/screenshot-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/images/screenshot-overview.png -------------------------------------------------------------------------------- /images/screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/images/screenshot-settings.png -------------------------------------------------------------------------------- /src/lib/id_generator.js: -------------------------------------------------------------------------------- 1 | export default (() => { 2 | let nextId = 1; 3 | return () => nextId++; 4 | })(); 5 | -------------------------------------------------------------------------------- /test_helpers/fixtures/phonenumber.org: -------------------------------------------------------------------------------- 1 | * foo 2 | +Don't+ call me on: +49123456789 (should become a tel: link) 3 | -------------------------------------------------------------------------------- /test_helpers/fixtures/tags.org: -------------------------------------------------------------------------------- 1 | * Spec header :spec_tag: 2 | -------------------------------------------------------------------------------- /test_helpers/fixtures/url.org: -------------------------------------------------------------------------------- 1 | * foo 2 | an =URL= https://foo.bar.baz/xyz?a=b&d#foo and /more text/ and so 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [200ok-ch] 4 | patreon: [200ok] 5 | -------------------------------------------------------------------------------- /bin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bin/transient_env_vars.sh switch build serve 4 | 5 | serve -p 5000 -s build 6 | -------------------------------------------------------------------------------- /test_helpers/fixtures/content_but_no_headline.org: -------------------------------------------------------------------------------- 1 | This is a legit Org mode file, yet it has not a single headline. 2 | -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_with_repeater.org: -------------------------------------------------------------------------------- 1 | * TODO Header with repeater 2 | SCHEDULED: <2019-11-27 Wed +1d> 3 | -------------------------------------------------------------------------------- /images/screenshot-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/images/screenshot-introduction.png -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_and_deadline.org: -------------------------------------------------------------------------------- 1 | * Spec header 2 | SCHEDULED: <2019-08-01 Thu> DEADLINE: <2019-08-04 Sun> 3 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "url:*": ["@parcel/transformer-raw"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test_helpers/fixtures/newlines.org: -------------------------------------------------------------------------------- 1 | * Main header (required for parser atm) 2 | 3 | * Second header 4 | 5 | - Some item 6 | - Another item 7 | -------------------------------------------------------------------------------- /test_helpers/fixtures/www_url.org: -------------------------------------------------------------------------------- 1 | * Foo 2 | Parse www.example.com and www.service.example.com/signin. 3 | But not www.foo (missing TLD)! 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /test_helpers/fixtures/nested_header.org: -------------------------------------------------------------------------------- 1 | * Top level header 2 | ** A nested header 3 | *** A deep nested header 4 | ** A second nested header 5 | -------------------------------------------------------------------------------- /src/components/SyncServiceSignIn/google_drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/SyncServiceSignIn/google_drive.png -------------------------------------------------------------------------------- /src/components/FileSettingsEditor/stylesheet.css: -------------------------------------------------------------------------------- 1 | .no-file-setting-message { 2 | text-align: center; 3 | color: var(--base0); 4 | padding: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_and_timestamps.org: -------------------------------------------------------------------------------- 1 | * <2020-01-18 Sat> Spec header 2 | SCHEDULED: <2019-09-22 Sun> 3 | 4 | Description contains <2020-01-19 Sun> 5 | -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_with_repeater_and_nologrepeat.org: -------------------------------------------------------------------------------- 1 | #+STARTUP: nologrepeat 2 | 3 | * TODO Header with repeater 4 | SCHEDULED: <2019-11-27 Wed +1d> 5 | -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Black.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Bold.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Light.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Thin.otf -------------------------------------------------------------------------------- /test_helpers/fixtures/properties.org: -------------------------------------------------------------------------------- 1 | * TODO Test header 2 | SCHEDULED: <2019-12-08 Sun +1y> 3 | :PROPERTIES: 4 | :LAST_REPEAT: [2018-12-30 Sun 14:00] 5 | :END: 6 | -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Medium.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-Regular.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-BoldItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-ExtraBold.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-ExtraLight.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-SemiBold.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-ThinItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-ThinItalic.otf -------------------------------------------------------------------------------- /src/components/PrivacyPolicy/stylesheet.css: -------------------------------------------------------------------------------- 1 | .privacy-policy-container { 2 | margin-left: 10px; 3 | } 4 | 5 | .privacy-policy-container a { 6 | color: var(--base00); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-BlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-BlackItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-LightItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-MediumItalic.otf -------------------------------------------------------------------------------- /src/components/OrgFile/components/DrawerActionBar/stylesheet.css: -------------------------------------------------------------------------------- 1 | .static-action-bar { 2 | padding-top: 20px; 3 | padding-left: 20px; 4 | background-color: var(--base3); 5 | } -------------------------------------------------------------------------------- /test_helpers/fixtures/indented_list.org: -------------------------------------------------------------------------------- 1 | * Header 2 | - Indented list line 1 3 | - Indented list line 2 4 | * Header 2 5 | - Indented list line 1 6 | - Indented list line 2 7 | -------------------------------------------------------------------------------- /test_helpers/fixtures/planning_item_with_following_checkmark.org: -------------------------------------------------------------------------------- 1 | * WAITING Proofing "Announcement für EmacsConf" 2 | SCHEDULED: <2019-09-16 Mon> 3 | - [X] Write draft announcement 4 | -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-ExtraBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-ExtraBoldItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-RegularItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-RegularItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-SemiBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-SemiBoldItalic.otf -------------------------------------------------------------------------------- /src/components/Landing/fonts/metropolis/Metropolis-ExtraLightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/organice/HEAD/src/components/Landing/fonts/metropolis/Metropolis-ExtraLightItalic.otf -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_with_repeater_and_nologrepeat_and_other_options.org: -------------------------------------------------------------------------------- 1 | #+STARTUP: showeverything nologrepeat 2 | 3 | * TODO Header with repeater 4 | SCHEDULED: <2019-11-27 Wed +1d> 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have LF line endings on checkout. 5 | *.org text eol=lf 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Table/stylesheet.css: -------------------------------------------------------------------------------- 1 | .table-part { 2 | align-self: center; 3 | border-collapse: collapse; 4 | margin-top: 2.5px; 5 | margin-bottom: 3px; 6 | empty-cells: show; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/TimestampPart/stylesheet.css: -------------------------------------------------------------------------------- 1 | .attributed-string__timestamp-part { 2 | cursor: pointer; 3 | color: var(--base02); 4 | text-decoration: underline; 5 | } 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_DROPBOX_CLIENT_ID=your_dropbox_client_id 2 | REACT_APP_GITLAB_CLIENT_ID=your_gitlab_client_id 3 | REACT_APP_GITLAB_SECRET=your_gitlab_secret 4 | REACT_APP_WEBDAV_URL=your_default_webdav_server_if_desired 5 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/NoteEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .note-editor__done-btn { 4 | font-weight: bold; 5 | font-size: 18px; 6 | 7 | height: 60px; 8 | width: 100%; 9 | } -------------------------------------------------------------------------------- /test_helpers/fixtures/headline_filter.org: -------------------------------------------------------------------------------- 1 | * TODO Spec header :spec_tag:tag2:#technology: 2 | 3 | :PROPERTIES: 4 | :prop1: abc 5 | :prop1: def 6 | :prop2: xyz 7 | :prop3: 8 | :END: 9 | -------------------------------------------------------------------------------- /contrib/organice-k8s/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: organice 6 | spec: 7 | ports: 8 | - name: http 9 | port: 80 10 | targetPort: 5000 11 | selector: 12 | name: organice 13 | -------------------------------------------------------------------------------- /contrib/organice-pinephone/organice.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Organice 3 | After=webdav 4 | 5 | [Service] 6 | ExecStart=docker run --rm --name organice -p 3000:3000 organice:latest 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/LogBookEntries/stylesheet.css: -------------------------------------------------------------------------------- 1 | .logbook-entries__item-start, 2 | .logbook-entries__item-end { 3 | cursor: pointer; 4 | color: var(--base02); 5 | text-decoration: underline; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/UI/Checkbox/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../colors.css'; 2 | 3 | .checkbox__inner-container { 4 | display: flex; 5 | justify-content: center; 6 | } 7 | 8 | .checkbox__inner-container .fa-square { 9 | color: white; 10 | } 11 | -------------------------------------------------------------------------------- /test_helpers/index.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | export default function readFixture(name) { 7 | return fs.readFileSync(path.join(__dirname, `./fixtures/${name}.org`)).toString(); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/UI/ExternalLink/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default ({ href, children }) => { 3 | return ( 4 | 5 | {children ? children : href} 6 | 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /test_helpers/fixtures/bold_text.org: -------------------------------------------------------------------------------- 1 | * Main header (required for parser atm) 2 | *This is bold*: Hello spec! 3 | 4 | *Bold* text can also be just one word. 5 | 6 | *This is also bold*: And here goes for some more text which even 7 | includes more *bold* statements. 8 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/PropertyListItems/stylesheet.css: -------------------------------------------------------------------------------- 1 | .property-list__property { 2 | font-weight: bold; 3 | color: var(--yellow); 4 | 5 | margin-right: 5px; 6 | } 7 | 8 | .property-list__item-container { 9 | display: flex; 10 | } 11 | -------------------------------------------------------------------------------- /test_helpers/fixtures/properties_extended.org: -------------------------------------------------------------------------------- 1 | * Header 2 | :PROPERTIES: 3 | :foo: http://example.com 4 | :bar: xyz 5 | :baz: test 6 | :bay: since: [2019-12-24 Tue] 7 | :END: 8 | 9 | * Header 2 10 | :PROPERTIES: 11 | :foo2: abc 12 | :bar: zyx 13 | :baz: test 14 | :emptyprop: 15 | :END: 16 | -------------------------------------------------------------------------------- /src/components/KeyboardShortcutsEditor/stylesheet.css: -------------------------------------------------------------------------------- 1 | .keyboard-shortcuts-editor-container a { 2 | text-decoration: none; 3 | } 4 | 5 | .keyboard-shortcuts-editor__btn-container { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | 10 | margin-top: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Entry/stylesheet.css: -------------------------------------------------------------------------------- 1 | .entry-container { 2 | font-size: 16px; 3 | 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | min-height: 100vh; 8 | max-height: 100vh; 9 | overflow-x: hidden; 10 | } 11 | 12 | .entry-container--large-font { 13 | font-size: 20px; 14 | } 15 | -------------------------------------------------------------------------------- /contrib/organice-k8s/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: organice-ingress 5 | spec: 6 | rules: 7 | - host: org.johnhame.link 8 | http: 9 | paths: 10 | - path: / 11 | backend: 12 | serviceName: organice 13 | servicePort: 5000 14 | -------------------------------------------------------------------------------- /contrib/organice-pinephone/webdav.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WebDAV server 3 | 4 | [Service] 5 | ExecStart=docker run --rm -v /home/mobian/org:/srv/dav --name apache-webdav-app -p 8080:80 apache-webdav 6 | ExecStop=docker exec apache-webdav-app apachectl -k graceful-stop 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /contrib/organice-webdav-traefik/webdav/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --no-cache nginx-mod-http-dav-ext nginx gettext 4 | 5 | VOLUME /data 6 | VOLUME /var/log/nginx 7 | EXPOSE 80 8 | COPY webdav.conf /etc/nginx/nginx.template.conf 9 | 10 | COPY run.sh / 11 | RUN chmod +x run.sh && rm /etc/nginx/nginx.conf 12 | CMD /run.sh 13 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/PlanningItems/stylesheet.css: -------------------------------------------------------------------------------- 1 | .planning-items__item-container { 2 | display: flex; 3 | } 4 | 5 | .planning-item__type { 6 | color: var(--base0); 7 | } 8 | 9 | .planning-item__timestamp { 10 | cursor: pointer; 11 | color: var(--base02); 12 | text-decoration: underline; 13 | } 14 | -------------------------------------------------------------------------------- /test_helpers/fixtures/multiple_headlines_with_timestamps_simple.org: -------------------------------------------------------------------------------- 1 | #+TODO: TODO | DONE 2 | #+TODO: START INPROGRESS STALLED | FINISHED 3 | 4 | * DONE This is a scheduled header 5 | SCHEDULED: <2019-08-04 Sun> 6 | 7 | * START This is a deadline header 8 | DEADLINE: <2019-08-27 Tue> 9 | 10 | * INPROGRESS This is a header without timestamp 11 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Table/TableCell/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../colors.css'; 2 | 3 | .table-part__cell { 4 | border: 1px solid var(--base01); 5 | white-space: pre; 6 | vertical-align: top; 7 | min-width: 20px; 8 | height: 20px; 9 | 10 | } 11 | 12 | .table-part__cell--selected { 13 | background-color: var(--green-soft); 14 | } 15 | -------------------------------------------------------------------------------- /test_helpers/fixtures/checkboxes.org: -------------------------------------------------------------------------------- 1 | * Header with checkboxes 2 | - [ ] Box A 3 | - [ ] Box B 4 | - [X] Box C 5 | * Another header [1/3] 6 | - [X] Box D 7 | - [-] Box E [50%] 8 | - [ ] Box G 9 | - [ ] Box H 10 | - [X] Box I 11 | - [-] Box J 12 | - [ ] Box K 13 | - [-] Box L 14 | - [ ] Box M 15 | - [X] Box N 16 | - [ ] Box O 17 | - Not a checkbox 18 | -------------------------------------------------------------------------------- /doc/org2html/init.el: -------------------------------------------------------------------------------- 1 | (add-to-list 'load-path (file-name-directory load-file-name)) 2 | (require 'htmlize) 3 | (load-library "tramp-gvfs") 4 | (setq tramp-gvfs-enabled t) 5 | (require 'org) 6 | (org-babel-do-load-languages 7 | 'org-babel-load-languages 8 | '((shell . t) 9 | (emacs-lisp . t))) 10 | 11 | ;; Don’t ask to execute a code block. 12 | (setq org-confirm-babel-evaluate nil) 13 | -------------------------------------------------------------------------------- /test_helpers/fixtures/various_todos.org: -------------------------------------------------------------------------------- 1 | * DONE This is done 2 | * TODO Header with repeater 3 | * This is not a todo 4 | * TODO Task with active timestamp and repeater <2020-11-15 Sun +1d> 5 | * TODO Repeating task 6 | SCHEDULED: <2019-11-27 Wed +1d> 7 | * TODO Not done 8 | * TODO Not done with logs 9 | :LOGBOOK: 10 | CLOCK: [2025-06-01 Sun 00:29]--[2025-06-01 Sun 00:38] => 0:09 11 | :END: 12 | -------------------------------------------------------------------------------- /src/components/CaptureTemplatesEditor/stylesheet.css: -------------------------------------------------------------------------------- 1 | .capture-templates-container { 2 | position: relative; 3 | } 4 | 5 | .new-capture-template-button-container { 6 | display: flex; 7 | justify-content: flex-end; 8 | 9 | margin-top: 10px; 10 | margin-right: 10px; 11 | } 12 | 13 | .no-capture-templates-message { 14 | text-align: center; 15 | color: var(--base0); 16 | padding: 20px; 17 | } 18 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | return `module.exports = ${assetFilename};`; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test_helpers/fixtures/schedule_with_repeater_and_nologrepeat_property.org: -------------------------------------------------------------------------------- 1 | * Header 2 | :PROPERTIES: 3 | :LOGGING: nologrepeat 4 | :END: 5 | ** Intermediate 6 | *** TODO Leaf 7 | DEADLINE: <2019-11-25 Mon ++1d> 8 | * Header Two 9 | ** Middle 10 | :PROPERTIES: 11 | :LOGGING: logrepeat 12 | :END: 13 | *** Beep 14 | ** Middle 2 15 | :PROPERTIES: 16 | :LOGGING: nologrepeat 17 | :END: 18 | *** Boop 19 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Table/TableCellEditContainer/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../colors.css'; 2 | 3 | 4 | .table-cell__edit-container { 5 | width: 100%; 6 | } 7 | 8 | .table-cell__insert-timestamp-button { 9 | color: var(--base0); 10 | 11 | font-family: Courier; 12 | 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .insert-timestamp-icon { 18 | margin-right: 5px; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /contrib/organice-caddy-webdav/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CADDY_VERSION=2.6.4 2 | FROM caddy:${CADDY_VERSION}-builder AS builder 3 | 4 | RUN xcaddy build \ 5 | --with github.com/lucaslorentz/caddy-docker-proxy/v2@v2.8.4 \ 6 | --with github.com/mholt/caddy-webdav 7 | # --with 8 | 9 | FROM caddy:${CADDY_VERSION}-alpine 10 | 11 | COPY --from=builder /usr/bin/caddy /usr/bin/caddy 12 | 13 | CMD ["caddy", "docker-proxy"] 14 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderList/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .header-list-container { 4 | font-family: Courier; 5 | margin-top: 10px; 6 | 7 | z-index: 1; 8 | 9 | height: '100%'; 10 | overflow: 'auto'; 11 | } 12 | 13 | .header-list-container--narrowed { 14 | box-shadow: inset 0px 0px 5px 0px var(--base0-soft); 15 | margin: 0; 16 | margin-top: 10px; 17 | border-radius: 5px; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/LoadingIndicator/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../colors.css'; 2 | 3 | .loading-indicator { 4 | color: var(--base00); 5 | background-color: var(--base2); 6 | font-weight: bold; 7 | opacity: 0.9; 8 | padding: 18px; 9 | display: inline-block; 10 | position: fixed; 11 | font-size: 20px; 12 | left: 50%; 13 | top: 85px; 14 | width: 80%; 15 | text-align: center; 16 | transform: translateX(-50%); 17 | z-index: 1; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/ActionDrawer/components/ActionButton/stylesheet.css: -------------------------------------------------------------------------------- 1 | .action-drawer__btn--letter { 2 | font-size: 24px; 3 | } 4 | 5 | .action-drawer__btn--with-sub-icon { 6 | padding-right: 3px; 7 | padding-bottom: 3px; 8 | } 9 | 10 | .action-drawer__btn__sub-icon { 11 | position: absolute; 12 | bottom: 10px; 13 | right: 10px; 14 | } 15 | 16 | .action-drawer__btn__sub-icon--rotated { 17 | transform: rotate(270deg); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Landing/Landing.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import Landing from './'; 6 | 7 | afterEach(cleanup); 8 | 9 | test(' renders', () => { 10 | const { container } = render( 11 | 12 | 13 | 14 | ); 15 | 16 | expect(container).toMatchSnapshot(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/migrations/migrate_access_token_to_dropbox_access_token.js: -------------------------------------------------------------------------------- 1 | import { localStorageAvailable } from '../util/settings_persister'; 2 | 3 | export default () => { 4 | if (!localStorageAvailable) { 5 | return; 6 | } 7 | 8 | const accessToken = localStorage.getItem('accessToken'); 9 | if (!accessToken) { 10 | return; 11 | } 12 | 13 | localStorage.setItem('dropboxAccessToken', accessToken); 14 | localStorage.removeItem('accessToken'); 15 | }; 16 | -------------------------------------------------------------------------------- /src/sync_backend_clients/dropbox_sync_backend_client.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | dropboxDirectoryListing, 3 | dropboxDirectoryListingSorted, 4 | } from './fixtures/directory_listing'; 5 | 6 | const { filterAndSortDirectoryListing } = require('./dropbox_sync_backend_client'); 7 | 8 | test('Filters down to Org files and orders alphabetically', () => { 9 | expect(filterAndSortDirectoryListing(dropboxDirectoryListing)).toEqual( 10 | dropboxDirectoryListingSorted 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/UI/Switch/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../colors.css'; 2 | 3 | .switch { 4 | display: inline-block; 5 | border: 1px solid var(--base2); 6 | width: 90px; 7 | height: 34px; 8 | padding: 2px; 9 | box-sizing: border-box; 10 | } 11 | 12 | .switch__grabber { 13 | width: 42px; 14 | height: 28px; 15 | border: 1px solid var(--base2); 16 | box-sizing: border-box; 17 | box-shadow: 1px 1px 5px 0px var(--base0-soft); 18 | background-color: var(--base00); 19 | } 20 | -------------------------------------------------------------------------------- /contrib/organice-k8s/README.org: -------------------------------------------------------------------------------- 1 | * Running organice a Kubernetes cluster 2 | 3 | This uses the [[https://organice.200ok.ch/documentation.html#docker][official organice Docker image]], but adds deployment on a 4 | Kubernetes cluster. 5 | 6 | Author: 7 | - Github: https://github.com/johnhamelink 8 | - Website http://johnhame.link/ 9 | - Email: john@johnhamelink.com 10 | - Twitter: https://twitter.com/john_hamelink 11 | 12 | Upstream repository: https://git.sr.ht/~johnhamelink/organice-k8s 13 | -------------------------------------------------------------------------------- /contrib/organice-k8s/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: organice 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: organice 11 | template: 12 | metadata: 13 | labels: 14 | name: organice 15 | spec: 16 | containers: 17 | - name: organice 18 | imagePullPolicy: Always 19 | image: twohundredok/organice:latest 20 | ports: 21 | - containerPort: 5000 22 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | import { manifest, version } from '@parcel/service-worker'; 2 | 3 | async function install() { 4 | const cache = await caches.open(version); 5 | await cache.addAll(manifest); 6 | } 7 | addEventListener('install', (e) => e.waitUntil(install())); 8 | 9 | async function activate() { 10 | const keys = await caches.keys(); 11 | await Promise.all(keys.map((key) => key !== version && caches.delete(key))); 12 | } 13 | addEventListener('activate', (e) => e.waitUntil(activate())); 14 | -------------------------------------------------------------------------------- /src/migrations/index.js: -------------------------------------------------------------------------------- 1 | import migrateAccessTokenToDropboxAccessToken from './migrate_access_token_to_dropbox_access_token'; 2 | import migrateStoreInDropboxToStoreInSyncBackend from './migrate_store_in_dropbox_to_store_in_sync_backend'; 3 | import migrateNonsenseValuesInLocalstorage from './migrate_nonsense_values_in_localstorage'; 4 | 5 | export default () => { 6 | migrateAccessTokenToDropboxAccessToken(); 7 | migrateStoreInDropboxToStoreInSyncBackend(); 8 | migrateNonsenseValuesInLocalstorage(); 9 | }; 10 | -------------------------------------------------------------------------------- /src/migrations/migrate_nonsense_values_in_localstorage.js: -------------------------------------------------------------------------------- 1 | import { localStorageAvailable } from '../util/settings_persister'; 2 | 3 | export default () => { 4 | if (!localStorageAvailable) { 5 | return; 6 | } 7 | 8 | Object.entries(localStorage).forEach(([k, v], _) => { 9 | if (['null', 'undefined'].includes(v)) { 10 | console.warn(`localStorage contains a bogus entry: '${k}': '${v}'`); 11 | console.warn('Deleting the bogus entry.'); 12 | localStorage.removeItem(k); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /test_helpers/fixtures/table.org: -------------------------------------------------------------------------------- 1 | * A table 2 | #+BEGIN: clocktable :maxlevel 25 :scope subtree :formula "$3=$2*150;t" 3 | #+CAPTION: Clock summary at [2019-09-22 Sun 11:21] 4 | | Headline | Time | | 5 | |---------------------+--------+--------| 6 | | *Total time* | *4:10* | 625.00 | 7 | |---------------------+--------+--------| 8 | | Tables | 4:10 | 625.00 | 9 | | Something todo | 2:05 | 312.50 | 10 | | Something else todo | 2:05 | 312.50 | 11 | #+TBLFM: $3=$2*150;t 12 | #+END: 13 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TaskListModal/components/TaskListView/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Currently, the markup is the same as in the AgendaDay component. Hence */ 2 | /* the CSS rules from there apply. */ 3 | 4 | 5 | .task-list__header-planning-type { 6 | color: var(--base01); 7 | min-width: 8em; 8 | margin: 0 5px 0 0; 9 | display: inline-block; 10 | } 11 | 12 | .task-list__header-planning-date { 13 | display: inline-block; 14 | } 15 | 16 | .task-list__header-planning-date--overdue { 17 | color: var(--orange); 18 | } 19 | -------------------------------------------------------------------------------- /contrib/organice-k8s/load-balancer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx-ingress 5 | namespace: kube-system 6 | labels: 7 | app.kubernetes.io/name: ingress-nginx 8 | app.kubernetes.io/part-of: ingress-nginx 9 | spec: 10 | type: LoadBalancer 11 | ports: 12 | - port: 80 13 | name: http 14 | targetPort: 80 15 | - port: 443 16 | name: https 17 | targetPort: 443 18 | selector: 19 | app.kubernetes.io/name: ingress-nginx 20 | app.kubernetes.io/part-of: ingress-nginx 21 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .header-content-container { 4 | white-space: pre-wrap; 5 | overflow-x: auto; 6 | } 7 | 8 | .header-content__edit-icon { 9 | color: var(--base2); 10 | 11 | margin-top: 10px; 12 | margin-right: 5px; 13 | float: left; 14 | } 15 | 16 | .header-content__insert-timestamp-button { 17 | color: var(--base0); 18 | 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .insert-timestamp-icon { 24 | margin-right: 5px; 25 | } 26 | -------------------------------------------------------------------------------- /contrib/organice-webdav-post/organice_inbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | IFS=$'\n' 4 | 5 | DESC="" 6 | DATE=$(date +%Y-%m-%d) 7 | TITLE="$(echo $1 | sed -e 's/"//g' | sed -e "s/'//g")" 8 | 9 | for line in $2 10 | do 11 | if [ "$DESC" == "" ]; 12 | then 13 | DESC="$(echo $line | sed -e 's/"//g' | sed -e "s/'//g")" 14 | else 15 | DESC="$DESC\n $(echo $line | sed -e 's/"//g' | sed -e "s/'//g")" 16 | fi 17 | done 18 | 19 | echo -e "* TODO $TITLE 20 | DEADLINE: <$DATE> 21 | - $DESC" >> $3 22 | 23 | echo Added task $1 to $3 24 | -------------------------------------------------------------------------------- /src/components/Landing/stylesheet.css: -------------------------------------------------------------------------------- 1 | .main-image { 2 | max-height: 35em; 3 | } 4 | .testimonial-brand img { 5 | height: 3em; 6 | width: auto; 7 | } 8 | 9 | /* Icons with */ 10 | #icons img { 11 | opacity: 0.5; 12 | margin-bottom: 2em; 13 | width: 100%; 14 | max-height: 2em; 15 | } 16 | 17 | #icons img:hover { 18 | opacity: 1; 19 | } 20 | 21 | /* Icons with Fontawesome */ 22 | #icons svg:not(:root).svg-inline--fa { 23 | height: 3em; 24 | width: 100%; 25 | } 26 | 27 | #false-bottom-preventor { 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import liveSync from './middleware/live_sync'; 4 | import toggleColorScheme from './middleware/toggle_color_scheme'; 5 | import rootReducer from './reducers'; 6 | 7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 8 | 9 | export default (initialState) => 10 | createStore( 11 | rootReducer, 12 | initialState, 13 | composeEnhancers(applyMiddleware(thunk, liveSync, toggleColorScheme)) 14 | ); 15 | -------------------------------------------------------------------------------- /src/migrations/migrate_store_in_dropbox_to_store_in_sync_backend.js: -------------------------------------------------------------------------------- 1 | import { localStorageAvailable } from '../util/settings_persister'; 2 | 3 | export default () => { 4 | if (!localStorageAvailable) { 5 | return; 6 | } 7 | 8 | const shouldStoreSettingsInDropbox = localStorage.getItem('shouldStoreSettingsInDropbox'); 9 | if (!shouldStoreSettingsInDropbox) { 10 | return; 11 | } 12 | 13 | localStorage.setItem('shouldStoreSettingsInSyncBackend', shouldStoreSettingsInDropbox); 14 | localStorage.removeItem('shouldStoreSettingsInDropbox'); 15 | }; 16 | -------------------------------------------------------------------------------- /doc/webdav/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | RUN apt-get update -y -qq && \ 4 | apt-get install -y -qq \ 5 | apache2-utils \ 6 | apache2 7 | 8 | ADD doc/webdav/webdav.conf /etc/apache2/sites-available/webdav.conf 9 | 10 | RUN a2enmod headers 11 | RUN a2enmod dav* 12 | RUN a2enmod rewrite 13 | RUN a2ensite webdav 14 | 15 | 16 | RUN mkdir /srv/dav 17 | # RUN echo demo | htpasswd -ci /srv/dav/.htpasswd demo 18 | RUN chmod 770 /srv/dav 19 | RUN chown www-data. /srv/dav 20 | 21 | COPY sample.org /srv/dav/demo.org 22 | 23 | CMD apachectl -D FOREGROUND 24 | EXPOSE 80 -------------------------------------------------------------------------------- /src/components/OrgFile/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../colors.css'; 2 | 3 | .org-file__parsing-error-message { 4 | text-align: center; 5 | font-size: 16px; 6 | 7 | margin: 10px; 8 | } 9 | 10 | .org-file__btn { 11 | margin: 10px; 12 | text-align: center; 13 | } 14 | 15 | .dirty-indicator { 16 | padding: 3px; 17 | opacity: 0.6; 18 | color: var(--base2); 19 | background-color: var(--orange); 20 | position: fixed; 21 | bottom: 100px; 22 | right: 10px; 23 | font-weight: bold; 24 | } 25 | 26 | .error-message-container { 27 | padding: 10px; 28 | font-size: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /bin/transient_env_vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s globstar 4 | 5 | RVARS=$(cut -d = -f 1 .env.sample) 6 | 7 | case $1 in 8 | 9 | "bait") 10 | for KEY in $RVARS; do 11 | echo "$KEY=${KEY//REACT_APP_/ORGANICE_}" 12 | done 13 | ;; 14 | 15 | "switch") 16 | SRC=$2 17 | DST=$3 18 | 19 | rm -rf "$DST" 20 | cp -r "$SRC" "$DST" 21 | 22 | OVARS=${RVARS//REACT_APP_/ORGANICE_} 23 | 24 | for KEY in $OVARS; do 25 | VALUE=${!KEY} 26 | sed -i "s/$KEY/$VALUE/" "$DST"/**/*.js 27 | done 28 | ;; 29 | 30 | *) 31 | echo "Unknown command: $1" 32 | ;; 33 | esac 34 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "organice", 3 | "name": "organice", 4 | "icons": [ 5 | { 6 | "src": "./organice-512x512.png", 7 | "sizes": "512x512", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./organice-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./favicon.ico", 17 | "sizes": "64x64 32x32 24x24 16x16", 18 | "type": "image/x-icon" 19 | } 20 | ], 21 | "start_url": "/index.html", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /bin/compile_doc_and_upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Sanity check that all variables are set to upload to FTP 4 | [ -z ${FTP_HOST+x} ] && (echo "$FTP_HOST needs to be set for uploading to FTP."; exit 1) 5 | [ -z ${FTP_USER+x} ] && (echo "$FTP_USER needs to be set for uploading to FTP."; exit 1) 6 | [ -z ${FTP_PASSWD+x} ] && (echo "$FTP_PASSWD needs to be set for uploading to FTP."; exit 1) 7 | 8 | 9 | here=$(dirname $0) 10 | 11 | if $here/compile_doc.sh; then 12 | lftp -e "put documentation.html; exit" -u $FTP_USER,$FTP_PASSWD $FTP_HOST 13 | else 14 | echo >&2 "compile_doc.sh failed; skipping upload" 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/SearchModal/components/HeaderListView/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Currently, the markup is the same as in the AgendaDay component. Hence */ 2 | /* the CSS rules from there apply. */ 3 | 4 | .task-list__header-planning-type { 5 | color: var(--base01); 6 | min-width: 8em; 7 | margin: 0 5px 0 0; 8 | display: inline-block; 9 | } 10 | 11 | .task-list__header-planning-date { 12 | display: inline-block; 13 | } 14 | 15 | .task-list__header-planning-date--overdue { 16 | color: var(--orange); 17 | } 18 | 19 | .search__breadcrumbs { 20 | color: var(--base01); 21 | font-size: 12px; 22 | font-family: Courier; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/SyncConfirmationModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .sync-confirmation-modal__header { 4 | color: var(--base2); 5 | 6 | margin-top: 0; 7 | } 8 | 9 | .sync-confirmation-modal__last-sync-time { 10 | color: var(--base0); 11 | 12 | text-align: center; 13 | font-weight: bold; 14 | } 15 | 16 | .sync-confirmation-modal__buttons-container { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | 21 | margin-top: 20px; 22 | margin-bottom: 10px; 23 | } 24 | 25 | .sync-confirmation-modal__button { 26 | font-size: 18px; 27 | 28 | margin-top: 15px; 29 | } 30 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2014-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | // This is a custom Jest transformer turning style imports into empty objects. 12 | // http://facebook.github.io/jest/docs/en/webpack.html 13 | 14 | module.exports = { 15 | process() { 16 | return 'module.exports = {};'; 17 | }, 18 | getCacheKey() { 19 | // The output is always the same. 20 | return 'cssTransform'; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/ListPart/ListActionDrawer/stylesheet.css: -------------------------------------------------------------------------------- 1 | .list-action-drawer-container { 2 | color: lightgray; 3 | 4 | padding-bottom: 10px; 5 | 6 | padding-right: 20px; 7 | } 8 | 9 | .list-action-drawer__row { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-around; 13 | 14 | padding-top: 10px; 15 | } 16 | 17 | .list-action-drawer__edit-icon-container { 18 | position: relative; 19 | } 20 | 21 | .list-action-drawer__separator { 22 | background-color: lightgray; 23 | 24 | height: 15px; 25 | width: 1px; 26 | 27 | margin-left: 10px; 28 | margin-right: 10px; 29 | } 30 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // This is a workaround. We used to have CRA, now we have Parcel. 2 | 3 | // We used to have the raw.macro for reading files, but that's a babel 4 | // thing, so we had to switch for importing the parcel way. Now, that 5 | // code works well, again. Having said so, jest (which uses babel) 6 | // cannot read the files as parcel does. So we need a workaround. 7 | // Luckily Jest is very powerful and has moduleNameMapper 8 | // functionality where we defined in jest.config.json that we override 9 | // imports with `bundle-text` and `url` prefixes to this stubbed file. 10 | 11 | // Export a simple string stub. 12 | module.exports = 'test-file-stub'; 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | .DEFAULT_GOAL = run 4 | 5 | # ------------------------------------------------------------ 6 | # dev 7 | 8 | .PHONY: setup 9 | setup: 10 | yarn install --production=false 11 | 12 | .PHONY: run 13 | run: setup 14 | yarn start 15 | 16 | .PHONY: test 17 | test: setup 18 | yarn test 19 | 20 | .PHONY: test-update-snapshots 21 | test-update-snapshots: setup 22 | yarn test -u 23 | 24 | .PHONY: docs 25 | docs: 26 | ./bin/compile_doc.sh 27 | 28 | .PHONY: deploy-docs 29 | deploy-docs: docs 30 | ./bin/compile_doc_and_upload.sh 31 | 32 | .PHONY: deploy 33 | deploy: 34 | ./bin/compile_and_upload.sh 35 | ./bin/compile_doc_and_upload.sh 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | tags 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | /.log/ 24 | 25 | *~ 26 | 27 | .env 28 | /src/lib/headline_filter_parser.js 29 | 30 | /documentation.html 31 | /documentation.org 32 | 33 | .eslintcache 34 | /src/components/Landing/vendor_css/template.css 35 | /src/components/Landing/vendor_css/template.css.map 36 | 37 | .parcel-cache 38 | dist -------------------------------------------------------------------------------- /test_helpers/fixtures/logbook.org: -------------------------------------------------------------------------------- 1 | * Spec header 2 | :LOGBOOK: 3 | CLOCK: [2019-11-13 Wed 13:15] 4 | - State "DONE" from "TODO" CLOCK: [2019-11-13 Wed 13:15] 5 | 6 | CLOCK: [2019-11-13 Wed 13:15]--[2019-11-13 Wed 13:15] => 0:00 7 | 8 | some inter-entry text 9 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 13:20] => -0:55 10 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 14:25] => 0:10 11 | :END: 12 | * another one 13 | :LOGBOOK: 14 | CLOCK: [2019-11-13 Wed 13:15] 15 | CLOCK: [2019-11-13 Wed 13:15]--[2019-11-13 Wed 13:15] => 0:00 16 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 13:20] => -0:55 17 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 14:25] => 0:10 18 | :END: 19 | -------------------------------------------------------------------------------- /src/util/misc.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | // Hard-wrap long lines - from https://stackoverflow.com/a/51506718/252585 4 | export const formatTextWrap = (text, w) => { 5 | return text.replace(new RegExp(`(?![^\\n]{1,${w}}$)([^\\n]{1,${w}})\\s`, 'g'), '$1\n'); 6 | }; 7 | 8 | /* See adr-002 for details. 9 | `isLandingPage` is a function that can be used to create certain 10 | safeguards, i.e. not loading some CSS where not appropriate. */ 11 | 12 | export const isLandingPage = () => { 13 | const history = createBrowserHistory(); 14 | return ( 15 | !window.testRunner && 16 | history.location.pathname === '/' && 17 | !localStorage.authenticatedSyncService 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /test_helpers/fixtures/multiple_headlines_with_timestamps.org: -------------------------------------------------------------------------------- 1 | #+TODO: TODO | DONE 2 | #+TODO: START INPROGRESS STALLED | FINISHED 3 | 4 | * INPROGRESS This is a header without timestamp 5 | 6 | * DONE This is a scheduled header 7 | SCHEDULED: <2019-08-04 Sun> 8 | 9 | * START This is a deadline header 10 | DEADLINE: <2019-08-27 Tue> 11 | 12 | * START This is a header with both 13 | SCHEDULED: <2019-08-04 Sun> DEADLINE: <2019-08-27 Tue> 14 | 15 | * START This is a header with all three 16 | SCHEDULED: <2019-08-04 Sun> DEADLINE: <2019-08-27 Tue> CLOSED: [2019-08-27 Tue] 17 | 18 | * TODO <2020-01-01 Wed> This is a header with timestamp 19 | 20 | * TODO This is a header with timestamp in the text 21 | <2020-01-01 Wed 00:00> 22 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TaskListModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | /* Currently, the markup is the same as in the AgendaModal component. 4 | * Hence the CSS rules from there apply. */ 5 | 6 | .task-list__modal-title { 7 | width: 100%; 8 | height: 20px; 9 | min-height: 20px; 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | } 14 | 15 | .task-list__filter-input { 16 | box-sizing: border-box; 17 | width: 100%; 18 | } 19 | 20 | .task-list__input-container { 21 | flex-shrink: 0; 22 | display: flex; 23 | align-items: center; 24 | flex-direction: column; 25 | } 26 | 27 | .task-list__filter-input--invalid { 28 | border: 2px solid var(--red); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/TablePart/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .table-part { 4 | border-collapse: collapse; 5 | margin-top: 3px; 6 | margin-bottom: 3px; 7 | } 8 | 9 | .table-part__cell { 10 | border: 1px solid var(--base01); 11 | 12 | white-space: pre; 13 | 14 | vertical-align: top; 15 | } 16 | 17 | .table-part__cell--selected { 18 | background-color: var(--green-soft); 19 | } 20 | 21 | .table-cell__edit-container { 22 | width: 100%; 23 | } 24 | 25 | .table-cell__insert-timestamp-button { 26 | color: var(--base0); 27 | 28 | font-family: Courier; 29 | 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .insert-timestamp-icon { 35 | margin-right: 5px; 36 | } -------------------------------------------------------------------------------- /src/components/OrgFile/components/TimestampEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .timestamp-editor__title { 4 | color: var(--base01); 5 | 6 | margin-top: 0; 7 | } 8 | 9 | .timestamp-editor__button-container { 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .timestamp-editor__add-new-button { 15 | font-size: 18px; 16 | margin-top: 15px; 17 | } 18 | 19 | .timestamp-editor__separator { 20 | display: flex; 21 | align-items: center; 22 | justify-content: space-between; 23 | 24 | margin-top: 10px; 25 | margin-bottom: 10px; 26 | 27 | font-size: 22px; 28 | } 29 | 30 | .timestamp-editor__separator__margin-line { 31 | background-color: var(--base03); 32 | height: 1px; 33 | width: 40%; 34 | 35 | margin-top: 5px; 36 | } 37 | -------------------------------------------------------------------------------- /doc/webdav/webdav.conf: -------------------------------------------------------------------------------- 1 | Alias /webdav /srv/dav/ 2 | ServerName 127.0.0.1 3 | 4 | 5 | Options Indexes 6 | DAV On 7 | # AuthType Basic 8 | # AuthName "Restricted Content" 9 | # AuthUserFile /srv/dav/.htpasswd 10 | # 11 | # Require valid-user 12 | # 13 | 14 | Header always set Access-Control-Allow-Origin "*" 15 | Header always set Access-Control-Allow-Methods "GET,POST,OPTIONS,DELETE,PUT,PROPFIND" 16 | Header always set Access-Control-Allow-Headers "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization,X-CSRF-Token,Depth" 17 | Header always set Access-Control-Allow-Credentials true 18 | 19 | Require all granted 20 | 21 | -------------------------------------------------------------------------------- /doc/setupfile: -------------------------------------------------------------------------------- 1 | # -*- mode: org; -*- 2 | 3 | #+HTML_HEAD: 4 | #+HTML_HEAD: 5 | 6 | #+HTML_HEAD: 7 | #+HTML_HEAD: 8 | #+HTML_HEAD: 9 | #+HTML_HEAD: 10 | -------------------------------------------------------------------------------- /bin/compile_search_parser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npx pegjs -o src/lib/headline_filter_parser.{js,grammar.pegjs} 4 | 5 | # The generated code of pegjs throws eslint warnings. Since it's not 6 | # code that we're writing and we want to keep eslint warnings at 0, 7 | # we're telling eslint to ignore the generated file. 8 | PARSER_FILE=src/lib/headline_filter_parser.js 9 | 10 | # Prepend the generated parser code with a `eslint-disable` comment. 11 | # We don't use `sed` or `ex` because some Linux/UNIX distros don't 12 | # support them. For example MacOS ships with BSD sed which doesn't 13 | # support `-i` and some barebones Linux distros don't ship `ex`. 14 | # `mktemp` works under Linux and MacOS, though. 15 | tmp=$(mktemp) 16 | echo '/* eslint-disable */' > "$tmp" && cat "$PARSER_FILE" >> "$tmp" && mv "$tmp" "$PARSER_FILE" 17 | -------------------------------------------------------------------------------- /src/components/UI/TabButtons/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../colors.css'; 2 | 3 | .tab-buttons { 4 | border: 1px solid var(--base2); 5 | height: 34px; 6 | box-sizing: border-box; 7 | display: inline-flex; 8 | align-items: center; 9 | } 10 | 11 | .tab-buttons--equal-width-tabs { 12 | display: grid; 13 | grid-template-columns: repeat(3, 1fr); 14 | } 15 | 16 | .tab-buttons__btn { 17 | cursor: pointer; 18 | box-sizing: border-box; 19 | height: 100%; 20 | padding: 7px; 21 | border-right: 1px solid var(--base01); 22 | 23 | text-align: center; 24 | } 25 | 26 | .tab-buttons__btn:last-of-type { 27 | border-right: none; 28 | } 29 | 30 | .entry-container--large-font .tab-buttons__btn { 31 | padding: 5px; 32 | } 33 | 34 | .tab-buttons__btn--selected { 35 | background-color: var(--base2); 36 | color: var(--base03); 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before creating a new issue, please read the "How we work with issues" section in the documentation: https://organice.200ok.ch/documentation.html#how_we_work_with_issues 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /src/util/settings_persister.test.js: -------------------------------------------------------------------------------- 1 | import Store from '../store'; 2 | import { readInitialState, subscribeToChanges } from './settings_persister'; 3 | 4 | describe('Settings persister', () => { 5 | let store; 6 | beforeEach(() => { 7 | const initialState = readInitialState(); 8 | store = Store(initialState); 9 | subscribeToChanges(store)(); 10 | }); 11 | 12 | afterEach(() => { 13 | localStorage.clear(); 14 | }); 15 | 16 | test('Do not persist nonsense like like "false" for settings without default', () => { 17 | expect(localStorage.getItem('showClockDisplay')).not.toBe('false'); 18 | expect(localStorage.getItem('showClockDisplay')).toBe(null); 19 | }); 20 | 21 | test('Does persist given default values, for example colorScheme', () => { 22 | expect(localStorage.getItem('colorScheme')).toBe('OS'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AgendaModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .agenda__title { 4 | color: var(--base01); 5 | 6 | margin-top: 0; 7 | } 8 | 9 | .agenda__tab-container { 10 | justify-content: center; 11 | } 12 | 13 | .agenda__timeframe-header-container { 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | 18 | margin: 2em 0 1.5em 0; 19 | 20 | color: var(--base01); 21 | } 22 | 23 | .agenda__timeframe-header-container > i { 24 | cursor: pointer; 25 | } 26 | 27 | .agenda__timeframe-header { 28 | font-weight: bold; 29 | font-size: 18px; 30 | 31 | text-align: center; 32 | 33 | margin-left: 15px; 34 | margin-right: 15px; 35 | 36 | min-width: 230px; 37 | } 38 | 39 | .agenda__days-container { 40 | margin-top: 15px; 41 | padding-left: 10px; 42 | padding-right: 10px; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .attributed-string__cookie-part { 4 | font-weight: bold; 5 | color: var(--red); 6 | } 7 | 8 | .attributed-string__cookie-part--complete { 9 | color: var(--green); 10 | } 11 | 12 | .attributed-string__inline-markup--inline-code, 13 | .attributed-string__inline-markup--verbatim { 14 | color: var(--base03); 15 | background-color: var(--base2); 16 | border: 1px solid var(--base1); 17 | padding: 2px; 18 | } 19 | 20 | .attributed-string__inline-markup--bold { 21 | font-weight: bold; 22 | } 23 | 24 | .attributed-string__inline-markup--italic { 25 | font-style: italic; 26 | } 27 | 28 | .attributed-string__inline-markup--strikethrough { 29 | text-decoration: line-through; 30 | } 31 | 32 | .attributed-string__inline-markup--underline { 33 | text-decoration: underline; 34 | } 35 | -------------------------------------------------------------------------------- /src/middleware/toggle_color_scheme.js: -------------------------------------------------------------------------------- 1 | import { setColorScheme } from '../actions/base'; 2 | 3 | export default (store) => (next) => (action) => { 4 | // Watch if the user changes the preferred color scheme through the 5 | // OS or browser. 6 | 7 | // Returns a MediaQueryList object 8 | const prefersColorSchemeMediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); 9 | 10 | // Feature detection. If there's no dark mode (i.e. iOS <14), then 11 | // the `matchMedia` query above does not resolve in anything that 12 | // can be observed. 13 | if ('addEventListener' in prefersColorSchemeMediaQueryList) { 14 | prefersColorSchemeMediaQueryList.addEventListener('change', (e) => { 15 | const selectedColorScheme = e.matches ? 'Dark' : 'Light'; 16 | store.dispatch(setColorScheme(selectedColorScheme)); 17 | }); 18 | } 19 | 20 | return next(action); 21 | }; 22 | -------------------------------------------------------------------------------- /test_helpers/fixtures/before-first-headline.org: -------------------------------------------------------------------------------- 1 | #+SETUPFILE: orgmode-export.conf 2 | #+TITLE: Testing what happens before the first headline 3 | #+AUTHOR: \includegraphics[height=2cm]{images/organice.png} 4 | #+EMAIL: 5 | #+DATE: \today 6 | #+DESCRIPTION: Introduction 7 | #+KEYWORDS: beamer org orgmode 8 | #+LANGUAGE: en 9 | #+STARTUP: beamer showeverything 10 | #+LaTeX_CLASS: beamer 11 | #+LaTeX_CLASS_OPTIONS: [bigger] 12 | #+BEAMER_THEME: metropolis 13 | #+OPTIONS: H:2 toc:t ^:{} 14 | 15 | All this content will not be visible in organice, however, it will be 16 | properly exported back! 17 | 18 | A paragraph before the first headline, weirdly indented. 19 | 20 | Another paragraph before the first headline, not indented. 21 | 22 | A list: 23 | 24 | - 1 25 | - 2 26 | - 3 27 | 28 | 29 | 30 | 31 | * This is the first headline 32 | - 1 33 | - 2 34 | - 3 35 | * Second headline 36 | -------------------------------------------------------------------------------- /src/lib/sample_capture_templates.js: -------------------------------------------------------------------------------- 1 | import generateId from './id_generator'; 2 | 3 | import { fromJS } from 'immutable'; 4 | 5 | export default fromJS([ 6 | { 7 | description: 'Groceries', 8 | headerPaths: ['Capture', 'Groceries'], 9 | iconName: 'lemon', 10 | id: generateId(), 11 | isAvailableInAllOrgFiles: false, 12 | letter: '', 13 | orgFilesWhereAvailable: [], 14 | shouldPrepend: false, 15 | template: '* TODO %?', 16 | isSample: true, 17 | }, 18 | { 19 | description: 'Deeply nested header', 20 | headerPaths: ['Capture', 'Deeply', 'Nested', 'Headers', 'Work', 'Too!'], 21 | iconName: 'fighter-jet', 22 | id: generateId(), 23 | isAvailableInAllOrgFiles: false, 24 | letter: '', 25 | orgFilesWhereAvailable: [], 26 | shouldPrepend: true, 27 | template: '* You can insert timestamps too! %T %?', 28 | isSample: true, 29 | }, 30 | ]); 31 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/TimestampPart/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import { renderAsText } from '../../../../../../lib/timestamps'; 6 | 7 | export default ({ part, subPartDataAndHandlers: { onTimestampClick, shouldDisableActions } }) => { 8 | const handleClick = () => (shouldDisableActions ? void 0 : onTimestampClick(part.get('id'))); 9 | 10 | const firstTimestamp = part.get('firstTimestamp'); 11 | const secondTimestamp = part.get('secondTimestamp'); 12 | 13 | return ( 14 | 15 | {!!firstTimestamp && renderAsText(firstTimestamp)} 16 | {!!secondTimestamp && ( 17 | 18 | {'--'} 19 | {renderAsText(secondTimestamp)} 20 | 21 | )} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/middleware/live_sync.js: -------------------------------------------------------------------------------- 1 | import { sync } from '../actions/org'; 2 | import { persistIsDirty, saveFileToLocalStorage } from '../util/file_persister'; 3 | import { determineAffectedFiles } from '../reducers/org'; 4 | 5 | export default (store) => (next) => (action) => { 6 | // middleware is run before the reducer. to persist the result of the action, 7 | // save and sync are done in a callback so they happen after the state is changed 8 | setTimeout(() => { 9 | let dirtyFiles = determineAffectedFiles(store.getState().org.present, action); 10 | 11 | dirtyFiles.forEach((path) => saveFileToLocalStorage(store.getState(), path)); 12 | dirtyFiles.forEach((path) => persistIsDirty(true, path)); 13 | 14 | if (store.getState().base.get('shouldLiveSync')) { 15 | dirtyFiles.forEach((path) => store.dispatch(sync({ shouldSuppressMessages: true, path }))); 16 | } 17 | }, 0); 18 | 19 | return next(action); 20 | }; 21 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2014-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | const babelJest = require('babel-jest'); 12 | 13 | const hasJsxRuntime = (() => { 14 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 15 | return false; 16 | } 17 | 18 | try { 19 | require.resolve('react/jsx-runtime'); 20 | return true; 21 | } catch (e) { 22 | return false; 23 | } 24 | })(); 25 | 26 | module.exports = babelJest.createTransformer({ 27 | presets: [ 28 | [ 29 | require.resolve('babel-preset-react-app'), 30 | { 31 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 32 | }, 33 | ], 34 | ], 35 | babelrc: false, 36 | configFile: false, 37 | }); 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | extends: 5 | - 'eslint:recommended' 6 | - 'plugin:jest/recommended' 7 | - 'plugin:react/recommended' 8 | - 'plugin:react-redux/recommended' 9 | globals: 10 | Atomics: readonly 11 | SharedArrayBuffer: readonly 12 | parser: babel-eslint 13 | parserOptions: 14 | ecmaFeatures: 15 | jsx: true 16 | ecmaVersion: 2018 17 | sourceType: module 18 | plugins: 19 | - jest 20 | - react 21 | - react-redux 22 | rules: 23 | no-extra-boolean-cast: off 24 | no-empty: off 25 | no-case-declarations: off 26 | no-unused-vars: 27 | - error 28 | - varsIgnorePattern: '_.+' 29 | argsIgnorePattern: '_.*|reject' 30 | react/no-unescaped-entities: off 31 | react/display-name: off 32 | react/prop-types: off 33 | react-redux/prefer-separate-component-file: off 34 | jest/no-disabled-tests: off 35 | no-redeclare: 36 | - 'error' 37 | - builtinGlobals: false 38 | settings: 39 | react: 40 | version: detect 41 | -------------------------------------------------------------------------------- /bin/compile_and_upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Sanity check that all variables are set to upload to FTP 6 | [ -z ${FTP_HOST+x} ] && (echo "$FTP_HOST needs to be set for uploading to FTP."; exit 1) 7 | [ -z ${FTP_USER+x} ] && (echo "$FTP_USER needs to be set for uploading to FTP."; exit 1) 8 | [ -z ${FTP_PASSWD+x} ] && (echo "$FTP_PASSWD needs to be set for uploading to FTP."; exit 1) 9 | 10 | # Configure available back-end API keys 11 | cp .env.sample .env 12 | [ -z ${REACT_APP_DROPBOX_CLIENT_ID+x} ] || sed -i "s/your_dropbox_client_id/${REACT_APP_DROPBOX_CLIENT_ID}/" .env 13 | 14 | [ -z ${REACT_APP_GITLAB_CLIENT_ID+x} ] || sed -i "s/your_gitlab_client_id/${REACT_APP_GITLAB_CLIENT_ID}/" .env 15 | [ -z ${REACT_APP_GITLAB_SECRET+x} ] || sed -i "s/your_gitlab_secret/${REACT_APP_GITLAB_SECRET}/" .env 16 | 17 | yarn install 18 | yarn run build 19 | cd dist 20 | lftp "$FTP_HOST" < { 6 | return ( 7 | 8 | 9 | {table.get('contents').map((row, rowIndex) => { 10 | return ( 11 | 12 | {row.get('contents').map((cell, columnIndex) => { 13 | const cellId = cell.get('id'); 14 | const cellProps = { 15 | filePath, 16 | headerIndex, 17 | descriptionItemIndex, 18 | cellId, 19 | row: rowIndex, 20 | column: columnIndex, 21 | }; 22 | return ; 23 | })} 24 | 25 | ); 26 | })} 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default Table; 33 | -------------------------------------------------------------------------------- /contrib/organice-pinephone/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | ARG USER_ID=1000 4 | ARG GROUP_ID=1000 5 | 6 | RUN userdel -f www-data &&\ 7 | if getent group www-data ; then groupdel www-data; fi &&\ 8 | groupadd -g ${GROUP_ID} www-data &&\ 9 | useradd -l -u ${USER_ID} -g www-data www-data &&\ 10 | install -d -m 0755 -o www-data -g www-data /home/www-data 11 | 12 | RUN apt-get update -y -qq && \ 13 | apt-get install -y -qq \ 14 | apache2-utils \ 15 | apache2 16 | 17 | RUN ls -ld /var/cache/apache2/mod_cache_disk 18 | RUN chown www-data. /var/cache/apache2/mod_cache_disk 19 | RUN ls -ld /var/cache/apache2/mod_cache_disk 20 | 21 | ADD doc/webdav/webdav.conf /etc/apache2/sites-available/webdav.conf 22 | 23 | RUN a2enmod headers 24 | RUN a2enmod dav* 25 | RUN a2enmod rewrite 26 | RUN a2ensite webdav 27 | 28 | 29 | RUN mkdir /srv/dav 30 | # RUN echo demo | htpasswd -ci /srv/dav/.htpasswd demo 31 | RUN chmod 770 /srv/dav 32 | RUN chown www-data. /srv/dav 33 | 34 | COPY sample.org /srv/dav 35 | 36 | CMD apachectl -D FOREGROUND 37 | EXPOSE 80 38 | -------------------------------------------------------------------------------- /test_helpers/fixtures/clock_entries.org: -------------------------------------------------------------------------------- 1 | * No clock entries 2 | * With clock entries within LOGBOOK 3 | :LOGBOOK: 4 | CLOCK: [2020-10-10 Sat 17:49]--[2020-10-10 Sat 18:49] => 1:00 5 | CLOCK: [2020-10-10 Sat 12:49]--[2020-10-10 Sat 18:49] => 6:00 6 | :END: 7 | * With clock entries without LOGBOOK 8 | CLOCK: [2020-10-10 Sat 17:49]--[2020-10-10 Sat 18:49] => 1:00 9 | CLOCK: [2020-10-10 Sat 12:49]--[2020-10-10 Sat 18:49] => 6:00 10 | 11 | organice only has partial support for this. It'll display the 12 | timestamps correctly and can edit them as a timestamp range. However, 13 | a change will not reflect in updating the time distance. The reason is 14 | clock entries without a drawer are not the default in Emacs for quite 15 | a while. 16 | 17 | * With clock entries within custom drawer 18 | :timedrawer: 19 | CLOCK: [2020-10-10 Sat 17:49]--[2020-10-10 Sat 18:49] => 1:00 20 | CLOCK: [2020-10-10 Sat 12:49]--[2020-10-10 Sat 18:49] => 6:00 21 | :END: 22 | 23 | organice does support this only in the same way as it does partially 24 | support clock entries without LOGBOOK. 25 | -------------------------------------------------------------------------------- /src/util/parse_query_string.js: -------------------------------------------------------------------------------- 1 | const parseQueryString = (str) => { 2 | var ret = Object.create(null); 3 | 4 | if (typeof str !== 'string') { 5 | return ret; 6 | } 7 | 8 | str = str.trim().replace(/^(\?|#|&)/, ''); 9 | 10 | if (!str) { 11 | return ret; 12 | } 13 | 14 | str.split('&').forEach(function (param) { 15 | var parts = param.replace(/\+/g, ' ').split('='); 16 | // Firefox (pre 40) decodes `%3D` to `=` 17 | // https://github.com/sindresorhus/query-string/pull/37 18 | var key = parts.shift(); 19 | var val = parts.length > 0 ? parts.join('=') : undefined; 20 | 21 | key = decodeURIComponent(key); 22 | 23 | // missing `=` should be `null`: 24 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters 25 | val = val === undefined ? null : decodeURIComponent(val); 26 | 27 | if (ret[key] === undefined) { 28 | ret[key] = val; 29 | } else if (Array.isArray(ret[key])) { 30 | ret[key].push(val); 31 | } else { 32 | ret[key] = [ret[key], val]; 33 | } 34 | }); 35 | 36 | return ret; 37 | }; 38 | 39 | export default parseQueryString; 40 | -------------------------------------------------------------------------------- /contrib/organice-webdav-traefik/webdav/webdav.conf: -------------------------------------------------------------------------------- 1 | include /etc/nginx/modules/10_http_dav_ext.conf; 2 | user ${USER} ${GROUP}; 3 | worker_processes auto; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '${LOG_FORMAT}'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | 25 | server { 26 | listen 80; 27 | access_log /var/log/nginx/access.log; 28 | error_log /var/log/nginx/error.log info; 29 | client_max_body_size 0; 30 | location ~ ^/${WEBDAV_PREFIX}(?:/(.*))?$ { 31 | alias /data/$1; 32 | autoindex on; 33 | dav_methods ${DAV_METHODS}; # put "off" for readonly else PUT DELETE MKCOL COPY MOVE 34 | dav_access user:rw group:rw all:r; 35 | dav_ext_methods PROPFIND OPTIONS; 36 | } 37 | } 38 | 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/CaptureModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .capture-modal-header { 4 | display: flex; 5 | align-items: center; 6 | } 7 | 8 | .capture-modal-header-path { 9 | font-family: Courier; 10 | color: var(--base0); 11 | 12 | margin-top: 5px; 13 | } 14 | 15 | .capture-modal-textarea { 16 | border: 1px solid var(--base01); 17 | font-family: Courier; 18 | overflow: scroll; 19 | 20 | margin-top: 0.5em; 21 | 22 | width: calc(100% - 2.25em); 23 | margin-left: auto; 24 | margin-right: auto; 25 | display: block; 26 | 27 | -webkit-appearance: none; 28 | border-radius: 0; 29 | } 30 | 31 | .capture-modal-button-container { 32 | display: flex; 33 | justify-content: space-between; 34 | 35 | margin-top: 10px; 36 | } 37 | 38 | .capture-modal-prepend-container { 39 | display: flex; 40 | align-items: center; 41 | } 42 | 43 | .capture-modal-prepend-label { 44 | margin-right: 10px; 45 | } 46 | 47 | .capture-modal-button { 48 | font-size: 16px; 49 | } 50 | 51 | .capture-modal-error-message { 52 | color: var(--red); 53 | 54 | text-align: center; 55 | 56 | margin-top: 10px; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TitleLine/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .title-line { 4 | margin-top: -18px; 5 | display: flex; 6 | } 7 | 8 | .entry-container--large-font .title-line { 9 | margin-top: -24px; 10 | } 11 | 12 | .todo-keyword { 13 | font-weight: bold; 14 | margin-right: 5px; 15 | color: var(--orange); 16 | } 17 | 18 | .todo-keyword--done-state { 19 | color: var(--green); 20 | } 21 | 22 | .todo-keyword--none { 23 | color: var(--base01); 24 | } 25 | 26 | .header-tag { 27 | border: 1px solid var(--base01); 28 | padding: 1px 2px; 29 | margin: 2px; 30 | display: inline-block; 31 | border-radius: 5px; 32 | } 33 | 34 | .title-line__edit-container { 35 | width: 100%; 36 | } 37 | 38 | .title-line__insert-timestamp-button { 39 | font-family: sans-serif; 40 | } 41 | 42 | .title-line__insert-timestamp-button { 43 | color: var(--base0); 44 | 45 | font-family: Courier; 46 | 47 | display: flex; 48 | align-items: center; 49 | } 50 | 51 | .insert-timestamp-icon { 52 | margin-right: 5px; 53 | } 54 | 55 | .title-line-text { 56 | width: 100%; 57 | display: flex; 58 | justify-content: space-between; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/FileBrowser/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../colors.css'; 2 | 3 | .file-browser-container a { 4 | text-decoration: none; 5 | color: var(--base03); 6 | } 7 | 8 | .file-browser__header { 9 | margin: 5px; 10 | } 11 | 12 | .file-browser__file-list { 13 | list-style-type: none; 14 | padding-left: 0; 15 | } 16 | 17 | .file-browser__file-list__icon { 18 | color: var(--base02); 19 | } 20 | 21 | .file-browser__file-list__element { 22 | border-top: 1px solid var(--base2); 23 | padding: 10px; 24 | font-family: Courier; 25 | } 26 | 27 | .file-browser__file-list__element:last-of-type { 28 | border-bottom: 1px solid var(--base2); 29 | } 30 | 31 | .file-browser__file-list__element--load-more-row:last-of-type { 32 | border-bottom: none; 33 | text-align: center; 34 | color: var(--base2); 35 | font-weight: bold; 36 | } 37 | 38 | .file-browser__file-list__loading-more-container { 39 | display: flex; 40 | justify-content: center; 41 | 42 | color: var(--base2); 43 | 44 | margin-top: 10px; 45 | } 46 | 47 | .file-browser__file-list__icon--not-org { 48 | color: var(--base01); 49 | } 50 | 51 | .file-browser__file-list__icon--directory { 52 | color: var(--base00); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/UI/Switch/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Motion, spring } from 'react-motion'; 3 | 4 | import { interpolateColors, rgbaObject, rgbaString } from '../../../lib/color'; 5 | 6 | import './stylesheet.css'; 7 | 8 | export default ({ isEnabled, onToggle }) => { 9 | const disabledColor = rgbaObject(255, 255, 255, 1); 10 | const enabledColor = rgbaObject(238, 232, 213, 1); 11 | 12 | const switchStyle = { 13 | colorFactor: spring(isEnabled ? 1 : 0, { stiffness: 300 }), 14 | }; 15 | 16 | const grabberStyle = { 17 | marginLeft: spring(isEnabled ? 42 : 0, { stiffness: 300 }), 18 | }; 19 | 20 | return ( 21 | 22 | {(style) => { 23 | const backgroundColor = rgbaString( 24 | interpolateColors(disabledColor, enabledColor, style.colorFactor) 25 | ); 26 | 27 | return ( 28 |
29 | 30 | {(style) =>
} 31 | 32 |
33 | ); 34 | }} 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /contrib/organice-webdav-post/home.php: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /contrib/organice-webdav-traefik/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gallo Feliz 4 | Copyright (c) 2020 Edgar Vincent 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/ActionDrawer/stylesheet.css: -------------------------------------------------------------------------------- 1 | .action-drawer-container { 2 | position: fixed; 3 | bottom: 20px; 4 | left: 10px; 5 | right: 10px; 6 | white-space: nowrap; 7 | 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | 12 | z-index: 2; 13 | pointer-events: none; /* Allow clicks to pass through to elements below */ 14 | } 15 | 16 | .action-drawer__btn { 17 | outline: 0; 18 | box-shadow: 2px 2px 5px 0px var(--base0-soft); 19 | 20 | position: relative; 21 | 22 | transition-property: opacity; 23 | transition-duration: 0.2s; 24 | pointer-events: all !important; 25 | } 26 | 27 | .action-drawer__done-btn { 28 | font-weight: bold; 29 | font-size: 18px; 30 | 31 | height: 60px; 32 | width: 100%; 33 | } 34 | 35 | .action-drawer__arrow-buttons-container { 36 | position: relative; 37 | } 38 | 39 | .action-drawer__arrow-button { 40 | position: absolute; 41 | z-index: 0; 42 | } 43 | 44 | .action-drawer__main-arrow-button { 45 | position: relative; 46 | z-index: 1; 47 | } 48 | 49 | .action-drawer__capture-buttons-container { 50 | position: relative; 51 | } 52 | 53 | .active-clock-indicator { 54 | background-color: var(--orange); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/LoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Motion, spring } from 'react-motion'; 3 | 4 | import './stylesheet.css'; 5 | 6 | export default ({ message }) => { 7 | const [shouldRenderIndicator, setShouldRenderIndicator] = useState(true); 8 | const [lastMessage, setLastMessage] = useState(message); 9 | 10 | useEffect(() => { 11 | if (!!message) { 12 | setLastMessage(message); 13 | setShouldRenderIndicator(true); 14 | } 15 | }, [message]); 16 | 17 | const handleAnimationRest = () => { 18 | if (!!message) { 19 | return; 20 | } 21 | 22 | setShouldRenderIndicator(false); 23 | setLastMessage(null); 24 | }; 25 | 26 | const handleClick = () => setShouldRenderIndicator(false); 27 | 28 | if (!shouldRenderIndicator) { 29 | return null; 30 | } 31 | 32 | const style = { 33 | opacity: spring(!!message ? 0.9 : 0, { stiffness: 300 }), 34 | }; 35 | 36 | return ( 37 | 38 | {(style) => ( 39 |
40 | {lastMessage} 41 |
42 | )} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AgendaModal/components/AgendaDay/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .agenda-day__title { 4 | color: var(--base01); 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | font-size: 18px; 9 | 10 | position: relative; 11 | } 12 | 13 | .agenda-day__today-indicator { 14 | position: absolute; 15 | left: -13px; 16 | top: 7px; 17 | 18 | width: 7px; 19 | height: 7px; 20 | background-color: var(--base2); 21 | border-radius: 50%; 22 | } 23 | 24 | .agenda-day__header-planning-date--overdue { 25 | color: var(--orange); 26 | } 27 | 28 | .agenda-day__title__day-name { 29 | font-weight: bold; 30 | } 31 | 32 | .agenda-day__headers-container { 33 | margin-top: 5px; 34 | margin-left: 20px; 35 | } 36 | 37 | .agenda-day__header-container { 38 | display: flex; 39 | flex-direction: column; 40 | 41 | font-family: Courier; 42 | 43 | margin-bottom: 8px; 44 | } 45 | 46 | .agenda-day__header__header-container { 47 | width: 100%; 48 | margin-top: 18px; 49 | } 50 | 51 | .agenda-day__header__planning-item-container { 52 | font-family: Courier; 53 | } 54 | 55 | .agenda-day__header-planning-type { 56 | min-width: 135px; 57 | color: var(--base01); 58 | } 59 | -------------------------------------------------------------------------------- /test_helpers/fixtures/large_table.org: -------------------------------------------------------------------------------- 1 | * Dogs 2 | | Dog Name | Age | Weight | Parent | Score | 3 | |----------+-----+--------+------------------+-------| 4 | | Bauschan | 3 | 20 | Thomas Mann | 160 | 5 | |----------+-----+--------+------------------+-------| 6 | | Bendico | 15 | 40 | Fabrizio Salina | 170 | 7 | |----------+-----+--------+------------------+-------| 8 | | Argos | 20 | 45 | Odysseus | 180 | 9 | |----------+-----+--------+------------------+-------| 10 | | Gyp | 1 | 55 | Adam Bede | 150 | 11 | |----------+-----+--------+------------------+-------| 12 | | Blue | 6 | 4.8 | Luster | 190 | 13 | |----------+-----+--------+------------------+-------| 14 | | Pilot | 25 | 25 | Edward Rochester | 200 | 15 | |----------+-----+--------+------------------+-------| 16 | | Rufus | 14 | 4.8 | Anika R. | 210 | 17 | |----------+-----+--------+------------------+-------| 18 | | Cesar | 2 | 60 | Joe C. | 220 | 19 | |----------+-----+--------+------------------+-------| 20 | | Wise | 10 | 17 | Manon Bril | 230 | 21 | |----------+-----+--------+------------------+-------| 22 | | Tito | 7 | 11 | Charlie D. | 204 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Header/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .header { 4 | margin-bottom: 2px; 5 | margin-top: 5px; 6 | padding-top: 5px; 7 | position: relative; 8 | overflow-y: visible; 9 | overflow-x: visible; 10 | min-height: 23px; 11 | } 12 | 13 | .header--selected { 14 | background-color: var(--base1-soft); 15 | } 16 | 17 | .header--removing { 18 | overflow-y: hidden; 19 | min-height: 0; 20 | margin-bottom: 0; 21 | padding-top: 0; 22 | margin-top: -2px; 23 | } 24 | 25 | .left-swipe-action-container { 26 | top: 0; 27 | right: 100%; 28 | height: 100%; 29 | position: absolute; 30 | 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | .right-swipe-action-container { 36 | top: 0; 37 | right: 0; 38 | height: 100%; 39 | position: absolute; 40 | 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .swipe-action-container__icon { 46 | position: absolute; 47 | color: var(--base00); 48 | } 49 | 50 | .swipe-action-container__icon--left { 51 | right: 10px; 52 | } 53 | 54 | .swipe-action-container__icon--right { 55 | left: 10px; 56 | } 57 | 58 | .header-deadline { 59 | padding-right: 0.5em; 60 | } 61 | 62 | .header-deadline--overdue { 63 | color: var(--red); 64 | } 65 | -------------------------------------------------------------------------------- /contrib/organice-webdav-traefik/webdav/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f /etc/nginx/nginx.conf ]; then 4 | 5 | echo "Configuring WebDAV" 6 | 7 | export DAV_METHODS=${DAV_METHODS:=PUT DELETE MKCOL COPY MOVE} 8 | PUID=${PUID} 9 | PGID=${PGID} 10 | export WEBDAV_PREFIX=${WEBDAV_PREFIX:=/} 11 | #TZ 12 | 13 | export USER 14 | export GROUP 15 | 16 | if [ -n "$PGID" ]; then 17 | echo "Creating group" 18 | addgroup -g "$PGID" group 19 | GROUP=group 20 | fi 21 | 22 | if [ -n "$PUID" ]; then 23 | echo "Creating user" 24 | if [ -n "$PGID" ]; then 25 | adduser --disabled-password --uid "$PUID" --ingroup group user 26 | else 27 | adduser --disabled-password --uid "$PUID" user 28 | fi 29 | USER=user 30 | else 31 | USER=nginx 32 | GROUP=nginx 33 | fi 34 | 35 | export LOG_FORMAT='$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"' 36 | 37 | chown "$USER":"$GROUP" /var/lib/nginx /var/lib/nginx/tmp 38 | 39 | envsubst < /etc/nginx/nginx.template.conf > /etc/nginx/nginx.conf 40 | 41 | fi 42 | 43 | echo "Starting!" 44 | 45 | exec nginx -g "daemon off;" 46 | -------------------------------------------------------------------------------- /src/components/SyncServiceSignIn/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../colors.css'; 2 | 3 | #webdavLogin h2 { 4 | font-size: 3.5em; 5 | margin: 0; 6 | } 7 | 8 | .sync-service-container form { 9 | display: table; 10 | width: 100%; 11 | } 12 | .sync-service-container form p { 13 | display: table-row; 14 | } 15 | .sync-service-container form label { 16 | display: table-cell; 17 | } 18 | .sync-service-container form input:not([type='submit']) { 19 | display: table-cell; 20 | width: calc(100% - 10px); 21 | } 22 | 23 | .sync-service-sign-in-container { 24 | max-width: 500px; 25 | margin-left: auto; 26 | margin-right: auto; 27 | } 28 | 29 | .sync-service-sign-in__help-text { 30 | padding-left: 5px; 31 | padding-right: 5px; 32 | font-weight: bold; 33 | 34 | font-size: 18px; 35 | color: var(--base0); 36 | 37 | text-align: center; 38 | } 39 | 40 | .sync-service-container { 41 | border-bottom: 1px solid var(--base01); 42 | 43 | padding: 20px 10px; 44 | text-align: center; 45 | } 46 | 47 | /* Sync back-end logos or titles are the same height. */ 48 | .sync-service-container img, 49 | .sync-service-container h2 { 50 | max-height: 87px; 51 | } 52 | 53 | .sync-service-container:last-of-type { 54 | border-bottom: none; 55 | } 56 | 57 | .dropbox-logo { 58 | width: 80%; 59 | } 60 | -------------------------------------------------------------------------------- /test_helpers/fixtures/todo_keywords_interspersed.org: -------------------------------------------------------------------------------- 1 | #+TODO: NEXT | DONE 2 | * Top level header 3 | ** A nested header 4 | ** TODO A todo item with schedule and deadline 5 | DEADLINE: <2018-10-05 Fri> SCHEDULED: <2019-09-19 Thu> 6 | * Another top level header 7 | Some description content 8 | ** TODO A repeating todo 9 | SCHEDULED: <2020-04-05 Sun +1d> 10 | 11 | * A header with tags :tag1:tag2: 12 | * A header with [[https://organice.200ok.ch][a link]] 13 | * orgmode settings in middle of file 14 | #+TYP_TODO: START(s!/!) | FINISHED(f@) 15 | * A header with a link to a local .org file as content 16 | 17 | [[file:schedule_and_timestamps.org][a local .org file]] 18 | * A header with a URL, mail address and phone number as content 19 | 20 | This is a URL https://foo.bar.baz/xyz?a=b&d#foo in a line of text. 21 | 22 | This is an e-mail foo.bar@baz.org in a line of text. 23 | 24 | +Don't+ call me on: +49123456789 25 | ** PROJECT Foo 26 | *** DONE A headline that's done since a loong time 27 | SCHEDULED: <2001-01-03 Wed> 28 | *** DONE A headline that's done a day earlier even 29 | SCHEDULED: <2001-01-02 Tue> 30 | * FINISHED A header with a custom todo sequence in DONE state 31 | * orgmode settings at end of file 32 | #+SEQ_TODO: PROJECT(p) | PROJDONE 33 | -------------------------------------------------------------------------------- /src/lib/capture_template_substitution.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import format from 'date-fns/format'; 3 | import _ from 'lodash'; 4 | 5 | export default (templateString, customVariables = Map()) => { 6 | if (!templateString) { 7 | return ['', null]; 8 | } 9 | 10 | const substitutions = { 11 | '%t': `<${format(new Date(), 'yyyy-MM-dd eee')}>`, 12 | '%T': `<${format(new Date(), 'yyyy-MM-dd eee HH:mm')}>`, 13 | '%u': `[${format(new Date(), 'yyyy-MM-dd eee')}]`, 14 | '%U': `[${format(new Date(), 'yyyy-MM-dd eee HH:mm')}]`, 15 | '%r': `${format(new Date(), 'yyyy-MM-dd eee')}`, 16 | '%R': `${format(new Date(), 'yyyy-MM-dd eee HH:mm')}`, 17 | '%y': `${format(new Date(), 'yyyy')}`, 18 | }; 19 | 20 | customVariables.entrySeq().forEach(([key, value]) => { 21 | substitutions[`%${key}`] = value; 22 | }); 23 | 24 | let substitutedString = templateString; 25 | _.entries(substitutions).forEach( 26 | ([formatString, value]) => 27 | (substitutedString = substitutedString.replace(RegExp(formatString, 'g'), value)) 28 | ); 29 | 30 | const cursorIndex = substitutedString.includes('%?') ? substitutedString.indexOf('%?') : null; 31 | substitutedString = substitutedString.replace(/%\?/, ''); 32 | 33 | return [substitutedString, cursorIndex]; 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/keybindings.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_BINDINGS = [ 2 | ['Select next header', 'selectNextVisibleHeader', 'ctrl+down'], 3 | ['Select previous header', 'selectPreviousVisibleHeader', 'ctrl+up'], 4 | ['Toggle header opened', 'toggleHeaderOpened', 'tab'], 5 | ['Advance todo state', 'advanceTodo', 'alt+t'], 6 | ['Edit title', 'editTitle', 'ctrl+h'], 7 | ['Edit description', 'editDescription', 'ctrl+d'], 8 | ['Exit edit mode', 'exitEditMode', 'alt+enter'], 9 | ['Add header', 'addHeader', 'ctrl+enter'], 10 | ['Remove header', 'removeHeader', 'backspace'], 11 | ['Move header up', 'moveHeaderUp', 'alt+up'], 12 | ['Move header down', 'moveHeaderDown', 'alt+down'], 13 | ['Move header left', 'moveHeaderLeft', 'alt+shift+left'], 14 | ['Move header right', 'moveHeaderRight', 'alt+shift+right'], 15 | ['Undo', 'undo', 'ctrl+/'], 16 | ]; 17 | 18 | export const calculateNamedKeybindings = (customKeybindings) => 19 | DEFAULT_BINDINGS.map(([bindingName, _bindingAction, binding]) => [ 20 | bindingName, 21 | customKeybindings.get(bindingName, binding), 22 | ]); 23 | 24 | export const calculateActionedKeybindings = (customKeybindings) => 25 | DEFAULT_BINDINGS.map(([bindingName, bindingAction, binding]) => [ 26 | bindingAction, 27 | customKeybindings.get(bindingName, binding), 28 | ]); 29 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/DrawerActionBar/components/DrawerActionButtons/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .drawer-action-button--selected { 4 | color: var(--orange); 5 | } 6 | 7 | .header-action-drawer-container { 8 | color: var(--base01); 9 | 10 | padding-bottom: 10px; 11 | 12 | padding-right: 20px; 13 | } 14 | 15 | .header-action-drawer__row { 16 | display: flex; 17 | justify-content: space-between; 18 | 19 | padding-top: 10px; 20 | } 21 | 22 | .header-action-drawer__ff-click-catcher-container { 23 | position: relative; 24 | cursor: pointer; 25 | } 26 | 27 | .header-action-drawer__ff-click-catcher-container:hover { 28 | color: var(--base02); 29 | } 30 | 31 | .header-action-drawer__ff-click-catcher { 32 | height: 20px; 33 | width: 20px; 34 | 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | } 39 | 40 | .header-action-drawer__separator { 41 | background-color: var(--base01); 42 | 43 | height: 15px; 44 | width: 1px; 45 | 46 | margin-left: 10px; 47 | margin-right: 10px; 48 | } 49 | 50 | .header-action-drawer__deadline-scheduled-button { 51 | cursor: pointer; 52 | text-transform: uppercase; 53 | width: 90px; 54 | text-align: center; 55 | } 56 | 57 | .header-action-drawer__deadline-scheduled-button:hover { 58 | color: var(--base02); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/UI/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Motion, spring } from 'react-motion'; 3 | 4 | import { interpolateColors, rgbaObject, rgbaString } from '../../../lib/color'; 5 | 6 | import './stylesheet.css'; 7 | 8 | export default ({ state, onClick }) => { 9 | const uncheckedColor = rgbaObject(255, 255, 255, 1); 10 | const checkedColor = rgbaObject(238, 232, 213, 1); 11 | 12 | const checkboxStyle = { 13 | colorFactor: spring( 14 | { 15 | checked: 1, 16 | partial: 1, 17 | unchecked: 0, 18 | }[state], 19 | { stiffness: 300 } 20 | ), 21 | }; 22 | 23 | return ( 24 | 25 | {(style) => { 26 | const backgroundColor = rgbaString( 27 | interpolateColors(uncheckedColor, checkedColor, style.colorFactor) 28 | ); 29 | 30 | return ( 31 |
32 |
33 | {state === 'checked' && } 34 | {state === 'partial' && } 35 | {state === 'unchecked' && } 36 |
37 |
38 | ); 39 | }} 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/reducers/sync_backend.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | const signOut = (state) => state.set('isAuthenticated', false).set('client', null); 4 | 5 | const setCurrentFileBrowserDirectoryListing = (state, action) => 6 | state 7 | .set( 8 | 'currentFileBrowserDirectoryListing', 9 | Map({ 10 | listing: action.directoryListing, 11 | hasMore: action.hasMore, 12 | additionalSyncBackendState: action.additionalSyncBackendState, 13 | }) 14 | ) 15 | .set('currentPath', action.path); 16 | 17 | const setIsLoadingMoreDirectoryListing = (state, action) => 18 | state 19 | .update('currentFileBrowserDirectoryListing', (currentFileBrowserDirectoryListing) => 20 | !!currentFileBrowserDirectoryListing ? currentFileBrowserDirectoryListing : Map() 21 | ) 22 | .setIn(['currentFileBrowserDirectoryListing', 'isLoadingMore'], action.isLoadingMore); 23 | 24 | export default (state = Map(), action) => { 25 | switch (action.type) { 26 | case 'SIGN_OUT': 27 | return signOut(state); 28 | case 'SET_CURRENT_FILE_BROWSER_DIRECTORY_LISTING': 29 | return setCurrentFileBrowserDirectoryListing(state, action); 30 | case 'SET_IS_LOADING_MORE_DIRECTORY_LISTING': 31 | return setIsLoadingMoreDirectoryListing(state, action); 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/SearchModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | /* Currently, the markup is the same as in the AgendaModal component. 4 | * Hence the CSS rules from there apply. */ 5 | 6 | .search-input-container{ 7 | display: flex; 8 | margin-bottom: 1em; 9 | } 10 | 11 | .search-input-line { 12 | width: 100%; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | .bookmark__icon { 19 | margin-left: 10px; 20 | 21 | cursor: pointer; 22 | } 23 | 24 | .bookmark__icon__enabled { 25 | color: var(--magenta); 26 | } 27 | 28 | .task-list__filter-input, 29 | .task-list__filter-input-invalid { 30 | box-sizing: border-box; 31 | width: 100%; 32 | } 33 | 34 | .search__input-container { 35 | flex-shrink: 0; 36 | flex-grow: 1; 37 | display: flex; 38 | align-items: center; 39 | flex-direction: column; 40 | } 41 | 42 | .task-list__filter-input-invalid { 43 | border: 2px solid var(--red); 44 | } 45 | 46 | .task-list__modal-title { 47 | width: 100%; 48 | height: 20px; 49 | min-height: 20px; 50 | display: flex; 51 | justify-content: space-between; 52 | align-items: center; 53 | } 54 | 55 | .task-list__modal-title_search { 56 | width: 100%; 57 | height: 20px; 58 | min-height: 20px; 59 | display: flex; 60 | justify-content: right; 61 | align-items: center; 62 | } 63 | -------------------------------------------------------------------------------- /contrib/organice-nextcloud-nginx/README.org: -------------------------------------------------------------------------------- 1 | * Allow organice as an origin for Nextcloud running behind nginx-proxy 2 | 3 | The following assumes that the Nextcloud server is at =webdav.my.domain= and is running behind nginx-proxy. 4 | Also it assumes that the organice webapp resides at =organice.my.domain=. 5 | 6 | It has been confirmed to work with Nextcloud 21.0.1 7 | 8 | ** Configuring nginx proxy 9 | Create the file =/etc/nginx/vhost.d/webdav.my.domain_location= with the following contents. 10 | #+begin_example 11 | add_header "Access-Control-Allow-Origin" https://organice.my.domain always; 12 | add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND" always; 13 | add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept, DNT, X-CustomHeader, Keep-Alive,User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Range, Range, Depth" always; 14 | #+end_example 15 | 16 | ** Setting organice.my.domain as an allowed origin for nextcloud 17 | Log into the Nextcloud instance as an admininstrator and install the [[https://apps.nextcloud.com/apps/webapppassword][WebAppPassword]] app. 18 | Go to =Settings=, =Administration=, =WebAppPassword= 19 | 20 | Add =https://organice.my.domain,http://organice.my.domain= to the =Allowed Origins= field. 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/UI/Drawer/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../colors.css'; 2 | 3 | .drawer-outer-container { 4 | position: fixed; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | 10 | z-index: 3; 11 | } 12 | 13 | .drawer-outer-container--visible { 14 | background-color: var(--base1-soft); 15 | } 16 | 17 | .drawer-inner-container { 18 | flex: 1; 19 | display: flex; 20 | flex-direction: column; 21 | position: fixed; 22 | bottom: -1px; 23 | left: -1px; 24 | right: -1px; 25 | 26 | padding: 15px 10px; 27 | 28 | box-shadow: 0px -1px 35px 0px var(--base03); 29 | border-bottom: none; 30 | border-top-left-radius: 20px; 31 | border-top-right-radius: 20px; 32 | 33 | background-color: var(--base3); 34 | max-height: 92%; 35 | 36 | overflow: auto; 37 | } 38 | 39 | .drawer__close-button { 40 | position: absolute; 41 | top: 15px; 42 | right: 15px; 43 | 44 | color: var(--base2); 45 | 46 | border: 0; 47 | background-color: transparent; 48 | padding: 0; 49 | } 50 | 51 | .drawer__grabber { 52 | width: 35px; 53 | height: 5px; 54 | background-color: var(--base0); 55 | flex-shrink: 0; 56 | 57 | border-radius: 50px; 58 | 59 | margin-left: auto; 60 | margin-right: auto; 61 | 62 | margin-top: -5px; 63 | margin-bottom: 5px; 64 | } 65 | 66 | .drawer-modal__title { 67 | color: var(--base2); 68 | 69 | margin-top: 0; 70 | } 71 | -------------------------------------------------------------------------------- /src/components/UI/TabButtons/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import classNames from 'classnames'; 6 | 7 | export default ({ buttons, titles, values, selectedButton, useEqualWidthTabs, onSelect }) => { 8 | const handleButtonClick = (buttonName) => () => onSelect(buttonName); 9 | 10 | const containerClassName = classNames('tab-buttons', { 11 | 'tab-buttons--equal-width-tabs': useEqualWidthTabs, 12 | }); 13 | 14 | const style = { 15 | gridTemplateColumns: useEqualWidthTabs ? `repeat(${buttons.length}, 1fr)` : null, 16 | }; 17 | 18 | return ( 19 |
20 | {buttons.map((buttonName, index) => { 21 | const value = values ? values[index] : buttonName; 22 | const className = classNames('tab-buttons__btn', { 23 | 'tab-buttons__btn--selected': value === selectedButton, 24 | }); 25 | // Optionally add a title 26 | let title = ''; 27 | if (titles) { 28 | title = titles[index]; 29 | } 30 | 31 | return ( 32 |
38 | {buttonName} 39 |
40 | ); 41 | })} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/actions/capture.js: -------------------------------------------------------------------------------- 1 | export const addNewEmptyCaptureTemplate = () => ({ 2 | type: 'ADD_NEW_EMPTY_CAPTURE_TEMPLATE', 3 | }); 4 | 5 | export const updateTemplateFieldPathValue = (templateId, fieldPath, newValue) => ({ 6 | type: 'UPDATE_TEMPLATE_FIELD_PATH_VALUE', 7 | templateId, 8 | fieldPath, 9 | newValue, 10 | }); 11 | 12 | export const addNewTemplateOrgFileAvailability = (templateId) => ({ 13 | type: 'ADD_NEW_TEMPLATE_ORG_FILE_AVAILABILITY', 14 | templateId, 15 | }); 16 | 17 | export const removeTemplateOrgFileAvailability = (templateId, orgFileAvailabilityIndex) => ({ 18 | type: 'REMOVE_TEMPLATE_ORG_FILE_AVAILABILITY', 19 | templateId, 20 | orgFileAvailabilityIndex, 21 | }); 22 | 23 | export const addNewTemplateHeaderPath = (templateId) => ({ 24 | type: 'ADD_NEW_TEMPLATE_HEADER_PATH', 25 | templateId, 26 | }); 27 | 28 | export const removeTemplateHeaderPath = (templateId, headerPathIndex) => ({ 29 | type: 'REMOVE_TEMPLATE_HEADER_PATH', 30 | templateId, 31 | headerPathIndex, 32 | }); 33 | 34 | export const deleteTemplate = (templateId) => ({ 35 | type: 'DELETE_TEMPLATE', 36 | templateId, 37 | }); 38 | 39 | export const restoreCaptureSettings = (newSettings) => ({ 40 | type: 'RESTORE_CAPTURE_SETTINGS', 41 | newSettings, 42 | }); 43 | 44 | export const reorderCaptureTemplate = (fromIndex, toIndex) => ({ 45 | type: 'REORDER_CAPTURE_TEMPLATE', 46 | fromIndex, 47 | toIndex, 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/PlanningItems/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import { renderAsText } from '../../../../../../lib/timestamps'; 6 | import { isRegularPlanningItem } from '../../../../../../lib/org_utils'; 7 | 8 | import _ from 'lodash'; 9 | 10 | export default class PlanningItems extends PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | _.bindAll(this, ['handleTimestampClick']); 15 | } 16 | 17 | handleTimestampClick(planningType, planningItemIndex) { 18 | return () => this.props.onClick(planningType, planningItemIndex); 19 | } 20 | 21 | render() { 22 | const { planningItems } = this.props; 23 | 24 | const planningItemsToRender = planningItems.filter((x) => isRegularPlanningItem(x)); 25 | if (planningItemsToRender.isEmpty()) return null; 26 | 27 | return ( 28 |
29 | {planningItemsToRender.map((planningItem, index) => ( 30 |
31 |
{planningItem.get('type')}:
32 |
36 | {renderAsText(planningItem.get('timestamp'))} 37 |
38 |
39 | ))} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/browser_utils.js: -------------------------------------------------------------------------------- 1 | import Bowser from 'bowser'; 2 | 3 | const browser = Bowser.getParser(window.navigator.userAgent); 4 | 5 | /** Is the browser Mobile Safari with iOS version of at least 13, but 6 | less than 13.3 */ 7 | export const isMobileSafari13 = (() => { 8 | return ( 9 | browser.satisfies({ 10 | mobile: { 11 | safari: '>=13', 12 | }, 13 | }) && 14 | browser.satisfies({ 15 | mobile: { 16 | safari: '<13.0.4', 17 | }, 18 | }) 19 | ); 20 | })(); 21 | 22 | /** Is the OS iOS or Android? */ 23 | export const isMobileBrowser = (() => { 24 | return browser.getPlatformType() !== 'desktop'; 25 | })(); 26 | 27 | export const isIos = () => { 28 | return browser.getOS().name === 'iOS'; 29 | }; 30 | 31 | /** Is iPhone Model X (tested with Xs) */ 32 | export const isIphoneX = window.matchMedia 33 | ? window.matchMedia('(max-device-width: 812px) and (-webkit-device-pixel-ratio : 3)').matches 34 | : false; 35 | 36 | /** Is iPhone Model 6, 7 or 8 (tested with 6s) */ 37 | export const isIphone678 = window.matchMedia 38 | ? window.matchMedia('(min-device-width: 375px) and (-webkit-device-pixel-ratio : 2)').matches 39 | : false; 40 | 41 | /** Is running in standalone mode (not in Mobile Safari) */ 42 | export const isRunningAsPWA = 'standalone' in window.navigator && window.navigator.standalone; 43 | 44 | /** Is running in Landscape Mode (as opposed to Portrait Mode) */ 45 | export function isInLandscapeMode() { 46 | return [90, -90].includes(window.orientation); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/DescriptionEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .no-tags-message { 4 | color: var(--base0); 5 | text-align: center; 6 | } 7 | 8 | .tag-container { 9 | display: flex; 10 | 11 | padding: 3px 5px; 12 | } 13 | 14 | .tag-container--dragging { 15 | background-color: var(--base1); 16 | } 17 | 18 | .tag-container__drag-handle { 19 | user-select: none; 20 | } 21 | 22 | .tag-container__textfield { 23 | flex: 3; 24 | } 25 | 26 | .tag-container__actions-container { 27 | flex: 1; 28 | 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | 33 | padding-left: 10px; 34 | 35 | color: var(--base1); 36 | } 37 | 38 | .tags-editor__add-new-container { 39 | display: flex; 40 | justify-content: flex-end; 41 | 42 | margin-top: 10px; 43 | } 44 | 45 | .tags-editor__separator { 46 | width: calc(100% - 20px); 47 | margin-top: 20px; 48 | margin-bottom: 20px; 49 | 50 | border: none; 51 | border-bottom: 1px solid var(--base2); 52 | } 53 | 54 | .all-tags-container { 55 | display: flex; 56 | flex-wrap: wrap; 57 | 58 | margin-bottom: 15px; 59 | } 60 | 61 | .all-tags__tag { 62 | cursor: pointer; 63 | color: var(--base00); 64 | background-color: var(--base2); 65 | 66 | padding: 8px; 67 | margin: 5px; 68 | } 69 | 70 | .all-tags__tag:hover { 71 | color: var(--base2); 72 | background-color: var(--base00); 73 | } 74 | 75 | .all-tags__tag--in-use { 76 | background-color: var(--base01); 77 | color: var(--base2); 78 | } -------------------------------------------------------------------------------- /src/components/OrgFile/components/TagsEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .no-tags-message { 4 | color: var(--base0); 5 | text-align: center; 6 | } 7 | 8 | .tag-container { 9 | display: flex; 10 | 11 | padding: 3px 5px; 12 | } 13 | 14 | .tag-container--dragging { 15 | background-color: var(--base1); 16 | } 17 | 18 | .tag-container__drag-handle { 19 | user-select: none; 20 | } 21 | 22 | .tag-container__textfield { 23 | flex: 3; 24 | } 25 | 26 | .tag-container__actions-container { 27 | flex: 1; 28 | 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | 33 | padding-left: 10px; 34 | 35 | color: var(--base1); 36 | } 37 | 38 | .tags-editor__add-new-container { 39 | display: flex; 40 | justify-content: flex-end; 41 | 42 | margin-top: 10px; 43 | } 44 | 45 | .tags-editor__separator { 46 | width: calc(100% - 20px); 47 | margin-top: 20px; 48 | margin-bottom: 20px; 49 | 50 | border: none; 51 | border-bottom: 1px solid var(--base2); 52 | } 53 | 54 | .all-tags-container { 55 | display: flex; 56 | flex-wrap: wrap; 57 | 58 | margin-bottom: 15px; 59 | } 60 | 61 | .all-tags__tag { 62 | cursor: pointer; 63 | color: var(--base00); 64 | background-color: var(--base2); 65 | 66 | padding: 8px; 67 | margin: 5px; 68 | } 69 | 70 | .all-tags__tag:hover { 71 | color: var(--base2); 72 | background-color: var(--base00); 73 | } 74 | 75 | .all-tags__tag--in-use { 76 | background-color: var(--base01); 77 | color: var(--base2); 78 | } 79 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "collectCoverageFrom": [ 6 | "src/**/*.{js,jsx,ts,tsx}", 7 | "!src/**/*.d.ts" 8 | ], 9 | "setupFilesAfterEnv": [], 10 | "testMatch": [ 11 | "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", 12 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" 13 | ], 14 | "testEnvironment": "jsdom", 15 | "testRunner": "./node_modules/jest-circus/runner.js", 16 | "transform": { 17 | "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "/config/jest/babelTransform.js", 18 | "^.+\\.css$": "/config/jest/cssTransform.js", 19 | "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" 20 | }, 21 | "transformIgnorePatterns": [ 22 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$", 23 | "^.+\\.module\\.(css|sass|scss)$" 24 | ], 25 | "modulePaths": [], 26 | "moduleNameMapper": { 27 | "^react-native$": "react-native-web", 28 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", 29 | "^jsx:.+\\.svg": "/config/jest/SvgComponent.js", 30 | "^bundle-text:(.*)$": "/__mocks__/fileMock.js", 31 | "^url:(.*)$": "/__mocks__/fileMock.js" 32 | }, 33 | "moduleFileExtensions": [ 34 | "web.js", 35 | "js", 36 | "web.ts", 37 | "ts", 38 | "web.tsx", 39 | "tsx", 40 | "json", 41 | "web.jsx", 42 | "jsx", 43 | "node" 44 | ], 45 | "watchPlugins": [ 46 | "jest-watch-typeahead/filename", 47 | "jest-watch-typeahead/testname" 48 | ], 49 | "resetMocks": true, 50 | "coverageReporters": [ 51 | "text" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /bin/compile_doc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! [ -e README.org ]; then 4 | echo >&2 "Error: Please run this script from top of the organice repository." 5 | exit 1 6 | fi 7 | 8 | if ! which pandoc >/dev/null 2>&1; then 9 | echo >&2 "Error: You need pandoc installed to build the docs." 10 | exit 1 11 | fi 12 | 13 | # The SETUPFILE is a verbatim copy of: 14 | # https://raw.githubusercontent.com/fniessen/org-html-themes/master/org/theme-readtheorg.setup 15 | # It normally wouldn't be necessary to download it into this 16 | # repository, however, CircleCI at some point refused to download the 17 | # file for months. Therefore, now it's included in the repository. 18 | echo "#+SETUPFILE: doc/setupfile" > documentation.org 19 | 20 | # Replace absolute links with anchors with only the anchor part (#787). 21 | cat README.org | \ 22 | grep -v "^Documentation: https://organice.200ok.ch/documentation.html" | \ 23 | sed 's/https:\/\/organice.200ok.ch\/documentation.html#/#/g' \ 24 | >> documentation.org 25 | 26 | sed -i 's/# REPO_PLACEHOLDER/Code repository: https:\/\/github.com\/200ok-ch\/organice/' documentation.org 27 | cat WIKI.org >> documentation.org 28 | cat CONTRIBUTING.org >> documentation.org 29 | pandoc CODE_OF_CONDUCT.md -o CODE_OF_CONDUCT.org 30 | cat CODE_OF_CONDUCT.org >> documentation.org 31 | rm CODE_OF_CONDUCT.org 32 | emacs documentation.org -l ./doc/org2html/init.el --batch --funcall org-html-export-to-html --kill 33 | 34 | html="`pwd`/documentation.html" 35 | if [ -e "$html" ]; then 36 | echo "Documentation written to file://$html" 37 | else 38 | echo >&2 "Error: failed to build documentation" 39 | exit 1 40 | fi 41 | -------------------------------------------------------------------------------- /test_helpers/fixtures/logbook_and_log_notes.org: -------------------------------------------------------------------------------- 1 | * Simple header with log notes and logbook 2 | - Note taken on [2020-08-29 Sat 18:01] \\ 3 | another note 4 | :LOGBOOK: 5 | CLOCK: [2019-11-13 Wed 13:15]--[2019-11-13 Wed 13:15] => 0:00 6 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 13:20] => -0:55 7 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 14:25] => 0:10 8 | :END: 9 | description text 10 | * Complex header with repeating timestamp, properties, log notes, logbook, and description 11 | SCHEDULED: <2020-08-29 Sat +1w> 12 | :PROPERTIES: 13 | :LAST_REPEAT: [2020-08-29 Sat 18:02] 14 | :END: 15 | - Note taken on [2020-08-29 Sat 18:04] \\ 16 | new note 17 | - State "DONE" from "TODO" [2020-08-29 Sat 18:02] 18 | - Note taken on [2020-08-29 Sat 18:01] \\ 19 | test note here 20 | - Note taken on [2020-08-29 Sat 18:01] \\ 21 | another note 22 | :LOGBOOK: 23 | CLOCK: [2019-11-13 Wed 13:15]--[2019-11-13 Wed 13:15] => 0:00 24 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 13:20] => -0:55 25 | CLOCK: [2019-11-12 Tue 14:15]--[2019-11-12 Tue 14:25] => 0:10 26 | :END: 27 | 28 | description text 29 | 30 | * Header with =LOGBOOK= drawer and note with standard spacing in between 31 | :LOGBOOK: 32 | CLOCK: [2020-10-11 Sun 18:15]--[2020-10-11 Sun 18:25] => 0:10 33 | :END: 34 | 35 | 36 | - Note taken on [2020-10-11 Sun 18:25] \\ 37 | bar 38 | * Header with =LOGBOOK= drawer and note without spacing in between 39 | :LOGBOOK: 40 | CLOCK: [2020-10-11 Sun 18:15]--[2020-10-11 Sun 18:25] => 0:10 41 | :END: 42 | - Note taken on [2020-10-11 Sun 18:25] \\ 43 | bar 44 | * Boring header 45 | no text 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 14 | 15 | 16 | 17 | 23 | organice - keep your life nicely organized 24 | 25 | 26 | 27 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/ListPart/ListActionDrawer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | export default ({ 6 | subPartDataAndHandlers: { 7 | onEnterListTitleEditMode, 8 | onEnterListContentsEditMode, 9 | onAddNewListItem, 10 | onRemoveListItem, 11 | }, 12 | }) => { 13 | return ( 14 |
15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 38 |
39 | 40 | 41 | 42 |
43 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/HeaderBar/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../colors.css'; 2 | 3 | .header-bar { 4 | background-color: var(--base2); 5 | height: 30px; 6 | min-height: 30px; 7 | padding: 10px; 8 | 9 | display: grid; 10 | grid-template-columns: minmax(2em, 3.5fr) minmax(1em, 3fr) minmax(1em, 3.5fr); 11 | grid-template-areas: 'back title actions'; 12 | align-items: center; 13 | 14 | position: sticky; 15 | top: 0; 16 | 17 | z-index: 1; 18 | } 19 | 20 | .header-bar--with-logo { 21 | grid-template-columns: 5fr 1fr 5fr; 22 | } 23 | 24 | .header-bar__logo-container { 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .header-bar__logo { 30 | width: 30px; 31 | } 32 | 33 | .header-bar__app-name { 34 | color: var(--base01); 35 | margin: 0; 36 | margin-left: 5px; 37 | } 38 | 39 | .header-bar__title { 40 | color: var(--base01); 41 | text-align: center; 42 | font-size: 20px; 43 | height: 100%; 44 | grid-area: title; 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | cursor: pointer; 48 | } 49 | 50 | .header-bar__back-button { 51 | color: var(--base01); 52 | text-decoration: none; 53 | 54 | text-overflow: ellipsis; 55 | overflow: hidden; 56 | white-space: nowrap; 57 | grid-area: back; 58 | } 59 | 60 | .header-bar__back-button__directory-path { 61 | margin-left: 0.25em; 62 | } 63 | 64 | .header-bar__actions { 65 | margin-left: auto; 66 | display: flex; 67 | align-items: center; 68 | cursor: pointer; 69 | 70 | color: var(--base01); 71 | grid-area: actions; 72 | } 73 | 74 | .header-bar__actions__item { 75 | color: var(--base01); 76 | 77 | margin-left: 0.5em; 78 | } 79 | 80 | .header-bar__actions__item--disabled { 81 | opacity: 0.5; 82 | } 83 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1744232761, 24 | "narHash": "sha256-gbl9hE39nQRpZaLjhWKmEu5ejtQsgI5TWYrIVVJn30U=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "f675531bc7e6657c10a18b565cfebd8aa9e24c14", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /contrib/organice-nginx-cors/README.org: -------------------------------------------------------------------------------- 1 | * Setting up Nginx with CORS 2 | 3 | This is a snippet configuring the official instance of 4 | https://organice.200ok.ch together with a custom instance of Nginx as 5 | WebDAV server with the necessary CORS headers. 6 | 7 | #+begin_example 8 | location /dav { 9 | root /data/www; 10 | client_body_temp_path /tmp; 11 | client_max_body_size 0; 12 | dav_methods PUT DELETE MKCOL COPY MOVE; 13 | dav_ext_methods PROPFIND OPTIONS; 14 | #create_full_put_path on; 15 | dav_access user:rw group:rw all:r; 16 | autoindex on; 17 | 18 | add_header 'Access-Control-Allow-Origin' 'https://organice.200ok.ch' always; 19 | add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS, POST, PROPFIND, PUT' always; 20 | add_header 'Access-Control-Allow-Headers' 'Authorization,Depth,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; 21 | add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; 22 | if ($request_method = 'OPTIONS') { 23 | return 204; 24 | } 25 | 26 | auth_basic "Restricted access"; 27 | auth_basic_user_file auth/.htpasswd; 28 | } 29 | #+end_example 30 | 31 | This snippet was contributed by @Varajada:matrix.org in the 32 | [[https://matrix.to/#/!DfVpGxoYxpbfAhuimY:matrix.org?via=matrix.org&via=ungleich.ch][#organice:matrix.org]] channel for public use. 33 | 34 | Here you can find more information on organice and WebDAV: https://organice.200ok.ch/documentation.html#faq_webdav 35 | 36 | We also have an official WebDAV test server which uses Apache. 37 | More information on that here: https://organice.200ok.ch/documentation.html#webdav_faq_test_server 38 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/SyncConfirmationModal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import { customFormatDistanceToNow } from '../../../../lib/org_utils'; 6 | import format from 'date-fns/format'; 7 | 8 | export default ({ lastServerModifiedAt, lastSyncAt, path, onPull, onPush, onCancel }) => { 9 | return ( 10 | <> 11 |

Sync conflict

12 | Since you last pulled {path}, a newer version of the file has been pushed to the server. The 13 | newer version is from: 14 |
15 |   16 |
17 |
18 | {format(lastServerModifiedAt, 'MMMM do, yyyy [at] h:mm:ss a')} 19 |
({customFormatDistanceToNow(lastServerModifiedAt)}) 20 |
21 |
22 |   23 |
24 | While your version is from: 25 |
26 |   27 |
28 |
29 | {format(lastSyncAt, 'MMMM do, yyyy [at] h:mm:ss a')} 30 |
({customFormatDistanceToNow(lastSyncAt)}) 31 |
32 |
33 | 36 | 39 | 42 |
43 |
44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage Dockerfile for organice 2 | # Supports both development and production builds 3 | 4 | FROM node:20.17.0-bookworm AS base 5 | 6 | WORKDIR /opt/organice 7 | 8 | # Copy package files 9 | COPY package.json yarn.lock /opt/organice/ 10 | 11 | # Development stage 12 | FROM base AS development 13 | 14 | # Copy source code 15 | COPY . /opt/organice 16 | 17 | # Generate environment variables 18 | RUN bin/transient_env_vars.sh bait >> .env 19 | 20 | # Create non-root user 21 | RUN groupadd organice \ 22 | && useradd -g organice organice \ 23 | && chown -R organice: /opt/organice 24 | 25 | USER organice 26 | 27 | # Install dependencies 28 | RUN yarn install 29 | 30 | ENV NODE_ENV=development 31 | EXPOSE 3000 32 | CMD ["/bin/bash"] 33 | 34 | # Build stage 35 | FROM base AS build 36 | 37 | # Copy source code 38 | COPY . /opt/organice 39 | 40 | # Install dependencies, including devDependencies like Parcel 41 | RUN yarn install --frozen-lockfile 42 | 43 | # Generate environment variables 44 | RUN bin/transient_env_vars.sh bait >> .env 45 | 46 | # Build the application 47 | RUN yarn build 48 | 49 | # Production stage 50 | FROM node:20.17.0-bookworm-slim AS production 51 | 52 | RUN npm install -g serve 53 | 54 | WORKDIR /opt/organice 55 | 56 | # Copy built application and necessary files from build stage 57 | COPY --from=build /opt/organice/dist ./build/ 58 | COPY --from=build /opt/organice/bin ./bin/ 59 | COPY --from=build /opt/organice/package.json . 60 | COPY --from=build /opt/organice/.env . 61 | 62 | # Create non-root user 63 | RUN groupadd organice \ 64 | && useradd -g organice organice \ 65 | && chown -R organice: /opt/organice 66 | 67 | USER organice 68 | 69 | ENV NODE_ENV=production 70 | EXPOSE 5000 71 | ENTRYPOINT ["./bin/entrypoint.sh"] -------------------------------------------------------------------------------- /src/components/KeyboardShortcutsEditor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | import { withRouter } from 'react-router-dom'; 6 | 7 | import { Map } from 'immutable'; 8 | 9 | import ShortcutRow from './components/ShortcutRow'; 10 | 11 | import * as baseActions from '../../actions/base'; 12 | 13 | import { calculateNamedKeybindings } from '../../lib/keybindings'; 14 | 15 | import './stylesheet.css'; 16 | 17 | const KeyboardShortcutsEditor = ({ customKeybindings, base }) => { 18 | const handleBindingChange = (bindingName, newBinding) => { 19 | const alreadyInUseBinding = calculateNamedKeybindings(customKeybindings).filter( 20 | ([_, binding]) => binding === newBinding 21 | )[0]; 22 | 23 | if (!!alreadyInUseBinding) { 24 | alert(`That binding is already in use for "${alreadyInUseBinding[0]}"`); 25 | return; 26 | } 27 | 28 | base.setCustomKeybinding(bindingName, newBinding); 29 | }; 30 | 31 | return ( 32 |
33 | {calculateNamedKeybindings(customKeybindings).map(([name, binding]) => ( 34 | 40 | ))} 41 |
42 | ); 43 | }; 44 | 45 | const mapStateToProps = (state) => { 46 | return { 47 | customKeybindings: state.base.get('customKeybindings') || Map(), 48 | }; 49 | }; 50 | 51 | const mapDispatchToProps = (dispatch) => { 52 | return { 53 | base: bindActionCreators(baseActions, dispatch), 54 | }; 55 | }; 56 | 57 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(KeyboardShortcutsEditor)); 58 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TableEditorModal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { is, Map } from 'immutable'; 4 | import { curry } from 'lodash/fp'; 5 | 6 | import './stylesheet.css'; 7 | import Table from '../Table/index'; 8 | import TableActionButtons from './components/TableActionButtons'; 9 | import { closePopup } from '../../../../actions/base'; 10 | import { getTable } from '../../../../lib/org_utils'; 11 | 12 | const getFilePath = (state) => state.org.present.get('path'); 13 | const getFile = curry((filePath, state) => { 14 | return state.org.present.getIn(['files', filePath], Map()); 15 | }); 16 | 17 | const TableEditorModal = () => { 18 | const dispatch = useDispatch(); 19 | const filePath = useSelector(getFilePath); 20 | const file = useSelector(getFile(filePath), is); 21 | const headerIndex = file.get('selectedHeaderIndex'); 22 | const descriptionItemIndex = file.get('selectedDescriptionItemIndex'); 23 | const tableGetter = getTable({ filePath, headerIndex, descriptionItemIndex }); 24 | const table = useSelector(tableGetter, is); 25 | 26 | const tableProps = { 27 | filePath, 28 | table, 29 | headerIndex, 30 | descriptionItemIndex, 31 | }; 32 | 33 | const handlePopupClose = () => { 34 | dispatch(closePopup()); 35 | }; 36 | 37 | if (table.get('contents').size === 0 || table.getIn(['contents', 0, 'contents']).size === 0) { 38 | handlePopupClose(); 39 | } 40 | 41 | return ( 42 | <> 43 |

Edit table

44 |
45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default TableEditorModal; 53 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/PropertyListItems/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import AttributedString from '../../../AttributedString'; 6 | 7 | export default ({ propertyListItems, shouldDisableActions, onTimestampClick, onEdit }) => { 8 | const [isDrawerCollapsed, setIsDrawerCollapsed] = useState(true); 9 | 10 | const handleCollapseToggle = () => setIsDrawerCollapsed(!isDrawerCollapsed); 11 | 12 | return propertyListItems.size === 0 ? null : ( 13 |
14 |
15 | :PROPERTIES: 16 | {isDrawerCollapsed ? '...' : ''} 17 |
18 | 19 | {!isDrawerCollapsed && ( 20 | 21 | {propertyListItems.map((propertyListItem) => ( 22 |
23 |
27 | :{propertyListItem.get('property')}: 28 |
29 |
30 | 37 |
38 |
39 | ))} 40 | 41 |
42 | :END: 43 |
44 |
45 | )} 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TitleEditorModal/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .todo-container { 4 | display: flex; 5 | justify-content: space-around; 6 | flex-wrap: wrap; 7 | margin-top: -15px; 8 | margin-bottom: 10px; 9 | margin-right: 5px; 10 | margin-left: 5px; 11 | } 12 | 13 | .todo-editor__icon { 14 | color: var(--magenta); 15 | cursor: pointer; 16 | } 17 | 18 | .no-tags-message { 19 | color: var(--base0); 20 | text-align: center; 21 | } 22 | 23 | .tag-container { 24 | display: flex; 25 | 26 | padding: 3px 5px; 27 | } 28 | 29 | .tag-container--dragging { 30 | background-color: var(--base1); 31 | } 32 | 33 | .tag-container__drag-handle { 34 | user-select: none; 35 | } 36 | 37 | .tag-container__textfield { 38 | flex: 3; 39 | } 40 | 41 | .tag-container__actions-container { 42 | flex: 1; 43 | 44 | display: flex; 45 | justify-content: space-between; 46 | align-items: center; 47 | 48 | padding-left: 10px; 49 | 50 | color: var(--base1); 51 | } 52 | 53 | .tags-editor__add-new-container { 54 | display: flex; 55 | justify-content: flex-end; 56 | 57 | margin-top: 10px; 58 | } 59 | 60 | .tags-editor__separator { 61 | width: calc(100% - 20px); 62 | margin-top: 20px; 63 | margin-bottom: 20px; 64 | 65 | border: none; 66 | border-bottom: 1px solid var(--base2); 67 | } 68 | 69 | .all-tags-container { 70 | display: flex; 71 | flex-wrap: wrap; 72 | 73 | margin-bottom: 15px; 74 | } 75 | 76 | .all-tags__tag { 77 | cursor: pointer; 78 | color: var(--base00); 79 | background-color: var(--base2); 80 | 81 | padding: 8px; 82 | margin: 5px; 83 | } 84 | 85 | .all-tags__tag:hover { 86 | color: var(--base2); 87 | background-color: var(--base00); 88 | } 89 | 90 | .all-tags__tag--in-use { 91 | background-color: var(--base01); 92 | color: var(--base2); 93 | } 94 | -------------------------------------------------------------------------------- /src/colors.css: -------------------------------------------------------------------------------- 1 | /* INFO: These variables constitute the 16 colors of Solarized bright: 2 | https://ethanschoonover.com/solarized/ */ 3 | /* They are used throughout the stylesheets of the components. */ 4 | /* 5 | ------------ 6 | usage guide: 7 | ------------ 8 | 9 | this is supposed to document current usage, 10 | it's not complete, not necessarily prescriptive and 11 | some usages might need to be reconsidered to be more consistent 12 | 13 | --base03: text in search/agenda/tasklist, selected buttons 14 | --base02: timestamps 15 | --base01: title text 16 | --base00: non-user provided text (e.g. footer, privacy policy) 17 | --base0: help text, actionable text (e.g. 'insert timestamp') 18 | --base1: elements being dragged 19 | --base2: highlights like borders and dividers 20 | --base3: background 21 | 22 | --base0-soft: box shadows 23 | --base1-soft: special surfaces (selected header, things behind modal, glow) 24 | 25 | header text based on indentation level: 26 | --blue 27 | --green: DONE keyword 28 | --cyan 29 | --yellow 30 | 31 | additional colors: 32 | --orange: TODO keyword, overdue planning dates, active clock indicator 33 | --red: error messages, changelog 34 | --magenta: action button 35 | --violet: unused 36 | 37 | --green-soft: highlight selected table cell 38 | */ 39 | :root { 40 | --base03: #002b36; 41 | --base02: #073642; 42 | --base01: #586e75; 43 | --base00: #657b83; 44 | --base0: #839496; 45 | --base0-soft: rgba(131, 148, 150, 0.75); 46 | --base1: #93a1a1; 47 | --base1-soft: rgba(147, 161, 161, 0.4); 48 | --base2: #eee8d5; 49 | --base3: #fdf6e3; 50 | --yellow: #b58900; 51 | --orange: #cb4b16; 52 | --red: #dc322f; 53 | --magenta: #d33682; 54 | --violet: #6c71c4; 55 | --blue: #268bd2; 56 | --cyan: #2aa198; 57 | --green: #859900; 58 | --green-soft: rgba(133, 153, 0, 0.28); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/ActionDrawer/components/ActionButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import classNames from 'classnames'; 6 | import _ from 'lodash'; 7 | 8 | export default class ActionButton extends PureComponent { 9 | constructor(props) { 10 | super(props); 11 | 12 | _.bindAll(this, ['handleClick']); 13 | } 14 | 15 | handleClick() { 16 | const { onClick, isDisabled } = this.props; 17 | 18 | if (isDisabled) { 19 | return; 20 | } 21 | 22 | onClick(); 23 | } 24 | 25 | render() { 26 | const { 27 | iconName, 28 | subIconName, 29 | isDisabled, 30 | shouldRotateSubIcon, 31 | letter, 32 | additionalClassName, 33 | style, 34 | tooltip, 35 | shouldSpinSubIcon, 36 | onRef, 37 | } = this.props; 38 | 39 | const className = classNames( 40 | 'btn', 41 | 'btn--circle', 42 | 'action-drawer__btn', 43 | additionalClassName || '', 44 | { 45 | fas: !letter, 46 | 'fa-lg': !letter, 47 | [`fa-${iconName}`]: !letter, 48 | 'action-drawer__btn--with-sub-icon': !!subIconName, 49 | 'btn--disabled': isDisabled, 50 | 'action-drawer__btn--letter': !!letter, 51 | } 52 | ); 53 | 54 | const subIconClassName = classNames( 55 | 'fas', 56 | 'fa-xs', 57 | `fa-${subIconName}`, 58 | 'action-drawer__btn__sub-icon', 59 | { 60 | 'action-drawer__btn__sub-icon--rotated': shouldRotateSubIcon, 61 | 'fa-spin': shouldSpinSubIcon, 62 | } 63 | ); 64 | 65 | return ( 66 | 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TimestampEditorModal/components/TimestampEditor/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .timestamp-editor__render { 4 | font-family: Courier; 5 | font-size: 14px; 6 | color: var(--base01); 7 | 8 | text-align: center; 9 | } 10 | 11 | .timestamp-editor__date-time-fields-container { 12 | display: grid; 13 | grid-template-rows: repeat(2, 1fr); 14 | grid-template-columns: repeat(2, 1fr); 15 | } 16 | 17 | .timestamp-editor__field-container { 18 | margin-top: 10px; 19 | } 20 | 21 | .timestamp-editor__field-container--inline { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: flex-start; 25 | } 26 | 27 | .timestamp-editor__field-title { 28 | text-transform: uppercase; 29 | font-size: 12px; 30 | color: var(--base01); 31 | 32 | margin-bottom: 5px; 33 | } 34 | 35 | .timestamp-editor__field { 36 | cursor: pointer; 37 | font-family: Courier; 38 | 39 | flex: 1; 40 | 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .timestamp-editor__icon { 46 | color: var(--base0); 47 | } 48 | 49 | .delay-repeater-value-input { 50 | width: 30px; 51 | height: 22px; 52 | 53 | margin-left: 10px; 54 | margin-right: 10px; 55 | } 56 | 57 | .timestamp-editor__icon--add { 58 | height: 34px; 59 | padding-right: 12px; 60 | padding-left: 12px; 61 | 62 | background-color: var(--magenta); 63 | color: var(--base3); 64 | 65 | font-size: 14px; 66 | 67 | display: flex; 68 | align-items: center; 69 | } 70 | 71 | .timestamp-editor__date-input, .timestamp-editor__time-input { 72 | background-color: var(--magenta); 73 | color: var(--base3); 74 | 75 | border: none; 76 | border-radius: 0; 77 | font-size: 12px; 78 | padding: 8px 0 8px 16px; 79 | margin: 0; 80 | height: 18px; 81 | 82 | -webkit-appearance: none; 83 | } 84 | 85 | .timestamp-editor__icon--remove { 86 | color: var(--magenta); 87 | margin-left: 15px; 88 | } 89 | 90 | .timestamp-editor__delay-repeater-type { 91 | min-width: 95px; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/TableEditorModal/components/TableActionButtons/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .table-action-drawer { 4 | display: flex; 5 | justify-content: space-around; 6 | min-height: '100px'; 7 | } 8 | 9 | .table-action-drawer-container { 10 | color: var(--base01); 11 | 12 | width: 50%; 13 | 14 | display: grid; 15 | grid-template-columns: repeat(3, 1fr); 16 | grid-template-rows: repeat(2, 1fr); 17 | } 18 | 19 | .table-action-drawer__edit-icon-container { 20 | grid-row-start: span 2; 21 | 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | 27 | .table-action-drawer__sub-icon-container { 28 | position: relative; 29 | margin: 15px 0; 30 | } 31 | 32 | .table-action-drawer__main-icon { 33 | padding-right: 3px; 34 | padding-bottom: 3px; 35 | } 36 | 37 | .table-action-drawer__sub-icon { 38 | position: absolute; 39 | top: 10px; 40 | left: 15px; 41 | } 42 | 43 | .table-action-drawer__sub-icon--rotated { 44 | transform: rotate(270deg); 45 | } 46 | 47 | .table-action-movement-container { 48 | color: var(--base01); 49 | 50 | width: 50%; 51 | 52 | display: grid; 53 | grid-template-columns: repeat(3, 1fr); 54 | grid-template-rows: repeat(3, 1fr); 55 | } 56 | 57 | .table-action-movement__up { 58 | grid-column-start: 2; 59 | grid-row-start: 1; 60 | 61 | display: flex; 62 | justify-content: center; 63 | align-items: center; 64 | } 65 | 66 | .table-action-movement__down { 67 | grid-column-start: 2; 68 | grid-row-start: 3; 69 | 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | } 74 | 75 | .table-action-movement__left { 76 | grid-column-start: 1; 77 | grid-row-start: 2; 78 | 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | } 83 | 84 | .table-action-movement__right { 85 | grid-column-start: 3; 86 | grid-row-start: 2; 87 | 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | } 92 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A very basic flake for organice"; 3 | 4 | # Type 'nix develop' to enter a development shell with all programs 5 | # and dependencies available. 6 | 7 | # Using the exact same node version as in .nvmrc would be good. 8 | # But it's hard to implement in Nix and takes a long time to compile. 9 | 10 | # Other problems that the development shell fixes automatically (see shellHook): 11 | # 1. In package.json, change engines node version to "" in order to 12 | # allow newer versions of node. 13 | 14 | inputs = { 15 | nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 16 | flake-utils.url = "github:numtide/flake-utils"; 17 | }; 18 | 19 | outputs = { self, nixpkgs, flake-utils }: 20 | flake-utils.lib.eachDefaultSystem (system: 21 | let 22 | pkgs = nixpkgs.legacyPackages.${system}; 23 | in { 24 | devShells.default = pkgs.mkShell { 25 | name = "dev-shell"; 26 | buildInputs = with pkgs; [ 27 | nodejs 28 | # Install sass this way and remove it from package.json because that one requires gyp which doesn't work on nix: 29 | nodePackages.sass 30 | yarn 31 | # Required for compile_docs.sh: 32 | emacs 33 | pandoc 34 | # Required to upload docs: 35 | lftp 36 | # Required in transient_env_vars.sh 37 | gnused 38 | # Required in entrypoint.sh 39 | nodePackages.serve 40 | ]; 41 | shellHook = '' 42 | echo 43 | read -rp "Apply changes in package.json to work on NixOS? [Y/n] " ans 44 | if [[ $ans =~ ^([Yy]|)$ ]]; then 45 | sed -i \ 46 | -e 's/"node": ".*"/"node": ""/' \ 47 | package.json 48 | echo 49 | echo "Note: Be careful to not accidentally commit this change!" 50 | echo 51 | fi 52 | ''; 53 | }; 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/NoteEditorModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | import './stylesheet.css'; 6 | 7 | import * as baseActions from '../../../../actions/base'; 8 | import * as orgActions from '../../../../actions/org'; 9 | 10 | import _ from 'lodash'; 11 | 12 | class NoteEditorModal extends PureComponent { 13 | constructor(props) { 14 | super(props); 15 | 16 | _.bindAll(this, ['handleTextareaRef', 'addNote', 'handleDescriptionChange']); 17 | 18 | this.state = { 19 | allTags: props.allTags, 20 | note: '', 21 | }; 22 | } 23 | 24 | handleTextareaRef(textarea) { 25 | this.textarea = textarea; 26 | } 27 | 28 | addNote() { 29 | let { note } = this.state; 30 | if (note !== null) note = note.trim(); 31 | if (!note) return; 32 | 33 | this.props.org.addNote(note, new Date()); 34 | this.setState({ note: '' }); 35 | } 36 | 37 | handleNoteFieldClick(event) { 38 | event.stopPropagation(); 39 | } 40 | 41 | handleDescriptionChange(event) { 42 | this.setState({ note: event.target.value }); 43 | } 44 | 45 | render() { 46 | return ( 47 | <> 48 |

Add note

49 |
Enter a note to add to the header:
50 |

21 | 22 | 23 | 24 | Close popup'; 43 | $navigation = true; 44 | } 45 | } 46 | 47 | $voice = false; 48 | if (isset($_REQUEST['voice'])) { 49 | if ($_REQUEST['voice'] == "yes") { 50 | $voice = true; 51 | } 52 | } 53 | 54 | if ( (!$navigation) && (!$voice) ) { 55 | echo '

Add another task'; 56 | } 57 | } 58 | ?> 59 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/HeaderContent/components/LogBookEntries/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import { renderAsText, timestampDuration } from '../../../../../../lib/timestamps'; 6 | 7 | export default ({ logBookEntries, onTimestampClick, shouldDisableActions }) => { 8 | const [isDrawerCollapsed, setIsDrawerCollapsed] = useState(true); 9 | 10 | const handleCollapseToggle = () => setIsDrawerCollapsed(!isDrawerCollapsed); 11 | 12 | const onClick = (entryIndex, type) => () => 13 | shouldDisableActions ? void 0 : onTimestampClick(entryIndex, type); 14 | 15 | return logBookEntries.size === 0 ? null : ( 16 |

17 |
18 | :LOGBOOK: 19 | {isDrawerCollapsed ? '...' : ''} 20 |
21 | {!isDrawerCollapsed && ( 22 | 23 | {logBookEntries.map((entry, index) => 24 | entry.get('raw') !== undefined ? ( 25 |
26 | {entry.get('raw') || ' '} 27 |
28 | ) : ( 29 |
30 | CLOCK: 31 | 32 | {renderAsText(entry.get('start'))} 33 | 34 | {entry.get('end') === null ? ( 35 | '' 36 | ) : ( 37 | 38 | -- 39 | 40 | {renderAsText(entry.get('end'))} 41 | 42 | 43 | {'=>'} {timestampDuration(entry.get('start'), entry.get('end'))} 44 | 45 | 46 | )} 47 |
48 | ) 49 | )} 50 |
:END:
51 |
52 | )} 53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/FileSettingsEditor/components/FileSetting/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../colors.css'; 2 | 3 | .file-setting-container { 4 | border-bottom: 2px solid var(--base2); 5 | padding: 15px 5px; 6 | 7 | -webkit-tap-highlight-color: var(--base03); 8 | } 9 | 10 | .file-setting-container--dragging { 11 | background-color: var(--base1); 12 | } 13 | 14 | .file-setting-container__header { 15 | display: flex; 16 | align-items: center; 17 | 18 | color: var(--base01); 19 | } 20 | 21 | .file_setting-container__header__title { 22 | font-size: 20px; 23 | margin-left: 10px; 24 | 25 | max-width: calc(100% - 120px); 26 | } 27 | 28 | .file-setting-container__header__caret { 29 | margin-right: 15px; 30 | margin-left: 5px; 31 | 32 | transition-property: transform; 33 | transition-duration: 0.15s; 34 | } 35 | 36 | .file-setting-container__header__caret--rotated { 37 | transform: rotate(90deg); 38 | } 39 | 40 | .file-setting-container__header__drag-handle { 41 | margin-left: auto; 42 | margin-right: 20px; 43 | 44 | user-select: none; 45 | } 46 | 47 | .file-setting-container__content { 48 | margin-top: 10px; 49 | } 50 | 51 | .file-setting__field-container { 52 | margin-bottom: 5px; 53 | border-bottom: 1px solid var(--base2); 54 | padding-bottom: 5px; 55 | } 56 | 57 | .file-setting__field-container:last-of-type { 58 | border-bottom: none; 59 | padding-bottom: 0; 60 | } 61 | 62 | .file-setting__field { 63 | display: flex; 64 | align-items: center; 65 | justify-content: space-between; 66 | } 67 | 68 | .file-setting__help-text { 69 | color: var(--base0); 70 | font-size: 14px; 71 | margin-top: 5px; 72 | margin-bottom: 5px; 73 | } 74 | 75 | .file-setting__delete-button-container { 76 | display: flex; 77 | justify-content: center; 78 | } 79 | 80 | .file-setting__delete-button { 81 | background-color: var(--red); 82 | 83 | margin-top: 15px; 84 | margin-bottom: 0; 85 | } 86 | 87 | .file-setting-icons { 88 | display: grid; 89 | grid-template-columns: repeat(3, 20px [col-start]); 90 | grid-template-rows: repeat(2, 20px [row-start]); 91 | } 92 | 93 | .file-setting-icon { 94 | font-size: small; 95 | } 96 | 97 | .load-on-startup-icon { 98 | grid-column-start: 1; 99 | grid-column-end: 1; 100 | grid-row-start: 1; 101 | grid-row-end: span 2; 102 | align-self: center; 103 | } -------------------------------------------------------------------------------- /src/components/OrgFile/components/Header/components/HeaderActionDrawer/stylesheet.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../../colors.css'; 2 | 3 | .header-action-drawer-container { 4 | color: var(--base01); 5 | padding-bottom: 10px; 6 | padding-right: 20px; 7 | } 8 | 9 | .header-action-drawer__row { 10 | display: flex; 11 | justify-content: space-between; 12 | padding-top: 10px; 13 | } 14 | 15 | .header-action-drawer__ff-click-catcher-container { 16 | position: relative; 17 | cursor: pointer; 18 | /* Base transition for transform, color, and box-shadow */ 19 | transition: transform 0.1s ease-in-out, color 0.1s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.1s ease-in-out; 20 | } 21 | 22 | .header-action-drawer__ff-click-catcher-container:hover { 23 | color: var(--base02); 24 | } 25 | 26 | .header-action-drawer__ff-click-catcher-container.header-action-drawer__long-press-feedback { 27 | transform: scale(1.05); 28 | color: var(--base02); /* Or a specific feedback color if preferred */ 29 | background-color: rgba(0, 0, 0, 0.05); /* Subtle background change */ 30 | border-radius: 4px; 31 | /* Assuming var(--base02) is a greyish color like rgb(131, 148, 150) for the shadow */ 32 | box-shadow: 0 0 0 18px rgba(131, 148, 150, 0.25); /* Increased visible shadow during press */ 33 | } 34 | 35 | .header-action-drawer__ff-click-catcher-container.header-action-drawer__long-press-success { 36 | transform: scale(1.15); 37 | color: var(--base0B); /* Success color for the icon */ 38 | background-color: rgba(0, 0, 0, 0.05); /* Can use a success-themed background if available, e.g., var(--base0B-alpha-low) */ 39 | border-radius: 4px; 40 | /* Assuming var(--base0B) is a success/greenish color like rgb(133, 153, 0) for the shadow */ 41 | box-shadow: 0 0 0 28px rgba(133, 153, 0, 0.3); /* Increased larger, more prominent shadow on success */ 42 | } 43 | 44 | .header-action-drawer__ff-click-catcher { 45 | height: 20px; 46 | width: 20px; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | } 51 | 52 | .header-action-drawer__separator { 53 | background-color: var(--base01); 54 | height: 15px; 55 | width: 1px; 56 | margin-left: 10px; 57 | margin-right: 10px; 58 | } 59 | 60 | .header-action-drawer__deadline-scheduled-button { 61 | cursor: pointer; 62 | text-transform: uppercase; 63 | width: 90px; 64 | text-align: center; 65 | } 66 | 67 | .header-action-drawer__deadline-scheduled-button:hover { 68 | color: var(--base02); 69 | } 70 | -------------------------------------------------------------------------------- /contrib/organice-caddy-webdav/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | caddy: 4 | # image: lucaslorentz/caddy-docker-proxy 5 | build: ./caddy/src/ 6 | container_name: caddy 7 | ports: 8 | - 80:80 9 | - 443:443 10 | environment: 11 | - CADDY_INGRESS_NETWORKS=caddy 12 | volumes: 13 | - /var/run/docker.sock:/var/run/docker.sock 14 | - ./caddy/caddy-data:/data/ 15 | - ./caddy/caddy-config:/config/ 16 | # Replace '/path/to/org-dir' with the path to the 17 | # directory in which your Org files are stored. 18 | - /path/to/org-dir:/srv/dav/Org/ 19 | restart: unless-stopped 20 | networks: 21 | - caddy 22 | 23 | organice: 24 | image: 'twohundredok/organice:latest' 25 | container_name: organice 26 | # ports: 27 | # - '5000:3000' 28 | logging: 29 | driver: json-file 30 | environment: 31 | # Replace 'organice.example.org' with your domain. 32 | - ORGANICE_WEBDAV_URL=https://organice.example.org/dav 33 | # /srv/dav/ volume is bound in caddy container 34 | labels: 35 | # Replace 'organice.example.org' with your domain. 36 | caddy: organice.example.org 37 | # Secure your server, either with IP whitelisting, 38 | # by uncommenting the next three lines (obviously, 39 | # change the value of the IP subnet): 40 | # caddy.@blocked.not: "remote_ip 192.168.2.0/24" 41 | # caddy.@blocked.path: "*" 42 | # caddy.route.respond: "@blocked Forbidden. 403" 43 | # Or with HTTP basic auth, by uncommenting the next two 44 | # lines, replacing 'myser' with the desired username and 45 | # 'mypassword' with a hashed password, which you can 46 | # generate with 'docker run -it caddy caddy hash-password'. 47 | # Don't forget to escape the dollar signs ('$') in the hash 48 | # by doubling them (i.e '$foo$bar' should become '$$foo$$bar'). 49 | # caddy.route.basicauth: "*" 50 | # caddy.route.basicauth.myuser: mypassword 51 | # If you want to restrict the access to the Webdav server 52 | # only and keep your Organice instance public, replace the 53 | # '*' in the examples above with '/dav/*'. 54 | caddy.@proxy.not: "path /dav/*" 55 | caddy.route.reverse_proxy: "@proxy {{upstreams 5000}}" 56 | caddy.route.rewrite: /dav /dav/ 57 | caddy.route.webdav: "/dav/*" 58 | caddy.route.webdav.prefix: /dav 59 | caddy.route.webdav.root: "/srv/dav/Org" 60 | networks: 61 | - caddy 62 | 63 | networks: 64 | caddy: 65 | name: caddy 66 | -------------------------------------------------------------------------------- /src/components/Turnout/index.js: -------------------------------------------------------------------------------- 1 | // organice renders different kinds of components: 2 | // - Static pages like the landing page 3 | // - The actual application 4 | // These components do not have many things in common, neither CSS, 5 | // nor structure. See adr-002 for details 6 | // ensures rendering either a static page or a dynamic application 7 | // component. 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | 12 | import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; 13 | 14 | import Landing from '../Landing'; 15 | import Entry from '../Entry'; 16 | import PrivacyPolicy from '../PrivacyPolicy'; 17 | import SyncServiceSignIn from '../SyncServiceSignIn'; 18 | import OrgFile from '../OrgFile'; 19 | import HeaderBar from '../HeaderBar'; 20 | 21 | const Turnout = ({ isAuthenticated }) => { 22 | if (isAuthenticated) return ; 23 | 24 | if (!isAuthenticated) 25 | return ( 26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 | 37 | 46 |
47 |
48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | ); 63 | }; 64 | 65 | const mapStateToProps = (state) => { 66 | return { 67 | isAuthenticated: state.syncBackend.get('isAuthenticated'), 68 | }; 69 | }; 70 | 71 | export default withRouter(connect(mapStateToProps)(Turnout)); 72 | -------------------------------------------------------------------------------- /test_helpers/fixtures/main_test_file.org: -------------------------------------------------------------------------------- 1 | #+TODO: TODO | DONE 2 | #+TODO: START(s!/!) | FINISHED(f@) 3 | 4 | * Top level header 5 | ** A nested header 6 | ** TODO A todo item with schedule and deadline 7 | DEADLINE: <2018-10-05 Fri> SCHEDULED: <2019-09-19 Thu> 8 | * Another top level header 9 | Some description content 10 | ** TODO A repeating todo 11 | SCHEDULED: <2020-04-05 Sun +1d> 12 | 13 | * A header with tags :tag1:tag2: 14 | * A header with [[https://organice.200ok.ch][a link]] 15 | * A header with various links as content 16 | Relative link to [[file:schedule_and_timestamps.org][an existing .org file in the same directory]] 17 | Relative link to [[file:subdir][subdir]] 18 | Relative link to [[file:subdir/][subdir/]] 19 | Relative link to [[file:subdir/foo.org][a fictitious .org file in a sub-directory]] 20 | Relative link to [[file:../foo.org_archive][a fictitious .org file in a parent directory]] 21 | Relative link to [[file:../subdir][../subdir]] 22 | Relative link to [[file:../subdir/][../subdir/]] 23 | Relative link to [[file:../../foo.org][a fictitious .org file in a grand-parent directory]] 24 | Relative link to [[file:../../../../too-high-to-access-file.org][a fictitious .org file in a too-high ancestor directory]] 25 | Relative link to [[file:../../../../too-high-to-access-directory][a too-high ancestor directory]] 26 | Absolute link to [[file:~/foo/bar/baz.org][a fictitious .org file in home directory]] 27 | Absolute link to [[file:/foo/bar/baz.org][a fictitious .org file]] 28 | * A header with a URL, mail address and phone number as content 29 | 30 | This is a URL https://foo.bar.baz/xyz?a=b&d#foo in a line of text. 31 | 32 | This is an e-mail foo.bar@baz.org in a line of text. 33 | 34 | Canonical format +xxxxxxxxx phone numbers: 35 | 36 | - +49123456789 37 | 38 | US phone numbers: 39 | - 123-456-7890 40 | - (123) 456-7890 41 | - 123 456 7890 42 | - 123.456.7890 43 | - +91 (123) 456-7890 44 | 45 | Swiss phone numbers: 46 | - 0783268674 47 | - 078 326 86 74 48 | - 041783268675 49 | - 0041783268674 50 | - +41783268676 51 | - +41783268677 52 | 53 | These are not phone numbers: 54 | - 05 05 05 55 | 56 | ** PROJECT Foo 57 | *** DONE A headline that's done since a loong time 58 | SCHEDULED: <2001-01-03 Wed> 59 | *** DONE A headline that's done a day earlier even 60 | SCHEDULED: <2001-01-02 Tue> 61 | * A header with plain list items 62 | 63 | - Plain list item 1 64 | - Plain list item 2 65 | 66 | * FINISHED A header with a custom todo sequence in DONE state 67 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/FinderModal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | import './stylesheet.css'; 6 | 7 | import TabButtons from '../../../UI/TabButtons'; 8 | import TaskListModal from '../TaskListModal'; 9 | import SearchModal from '../SearchModal'; 10 | 11 | import * as baseActions from '../../../../actions/base'; 12 | import * as orgActions from '../../../../actions/org'; 13 | import { determineIncludedFiles } from '../../../../reducers/org'; 14 | 15 | function FinderModal(props) { 16 | const { finderTab, onClose, headers, activeClocks } = props; 17 | 18 | function handleTabChange(finderTab) { 19 | props.base.setFinderTab(finderTab); 20 | } 21 | 22 | function renderTab(finderTab) { 23 | if (!activeClocks && finderTab === 'Clock List') { 24 | props.base.setFinderTab('Search'); 25 | return; 26 | } 27 | switch (finderTab) { 28 | case 'Search': 29 | return ; 30 | case 'Task List': 31 | return ; 32 | case 'Clock List': 33 | return ; 34 | default: 35 | return

Error

; 36 | } 37 | } 38 | 39 | return ( 40 | <> 41 |
42 | 48 |
49 | {renderTab(finderTab)} 50 |
51 | 52 | ); 53 | } 54 | 55 | const mapStateToProps = (state) => { 56 | const path = state.org.present.get('path'); 57 | const files = state.org.present.get('files'); 58 | const fileSettings = state.org.present.get('fileSettings'); 59 | const searchFiles = determineIncludedFiles(files, fileSettings, path, 'includeInSearch', false); 60 | const activeClocks = Object.values( 61 | searchFiles.map((f) => (f.get('headers').size ? f.get('activeClocks') : 0)).toJS() 62 | ).reduce((acc, val) => (typeof val === 'number' ? acc + val : acc), 0); 63 | return { 64 | finderTab: state.base.get('finderTab'), 65 | activeClocks, 66 | }; 67 | }; 68 | 69 | const mapDispatchToProps = (dispatch) => ({ 70 | org: bindActionCreators(orgActions, dispatch), 71 | base: bindActionCreators(baseActions, dispatch), 72 | }); 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(FinderModal); 75 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AgendaModal/components/AgendaDay/AgendaDay.unit.test.js: -------------------------------------------------------------------------------- 1 | import { parseOrg } from '../../../../../../lib/parse_org'; 2 | 3 | import AgendaDay from './index'; 4 | 5 | import { Map } from 'immutable'; 6 | import { parseISO } from 'date-fns'; 7 | 8 | import readFixture from '../../../../../../../test_helpers/index'; 9 | 10 | describe('Unit Tests for AgendaDay', () => { 11 | const component = new AgendaDay(); 12 | test('Given some headlines, it constructs a datastructure for the relevant ones for the "today" view', () => { 13 | let input = { 14 | headers: [], 15 | todoKeywordSets: [], 16 | date: parseISO('2019-08-27T15:50:32.624Z'), 17 | agendaDefaultDeadlineDelayValue: 5, 18 | agendaDefaultDeadlineDelayUnit: 'd', 19 | dateStart: parseISO('2019-08-26T22:00:00.000Z'), 20 | dateEnd: parseISO('2019-08-27T21:59:59.999Z'), 21 | }; 22 | // INFO: The output here is stringified output from the algorithm, 23 | // taken from a manually verified run of `AgendaDay` in the 24 | // application. It's not necessarily meant to manually update it 25 | // in the future, instead insert new valid data from the web app 26 | // itself. 27 | const output = [ 28 | [ 29 | { 30 | type: 'DEADLINE', 31 | timestamp: { month: '08', dayName: 'Tue', isActive: true, day: '27', year: '2019' }, 32 | id: 7, 33 | }, 34 | { 35 | titleLine: { 36 | title: [{ type: 'text', contents: 'This is a deadline header' }], 37 | rawTitle: 'This is a deadline header', 38 | todoKeyword: 'START', 39 | tags: [], 40 | }, 41 | rawDescription: '\n', 42 | description: [{ type: 'text', contents: '\n' }], 43 | opened: false, 44 | path: '/testfile.org', 45 | id: 4, 46 | logNotes: [], 47 | nestingLevel: 1, 48 | logBookEntries: [], 49 | planningItems: [ 50 | { 51 | type: 'DEADLINE', 52 | timestamp: { month: '08', dayName: 'Tue', isActive: true, day: '27', year: '2019' }, 53 | id: 7, 54 | }, 55 | ], 56 | propertyListItems: [], 57 | totalTimeLogged: 0, 58 | totalTimeLoggedRecursive: 0, 59 | }, 60 | ], 61 | ]; 62 | const testOrgFile = readFixture('multiple_headlines_with_timestamps_simple'); 63 | const parsedOrgFile = parseOrg(testOrgFile); 64 | input.files = Map({ '/testfile.org': parsedOrgFile }); 65 | 66 | expect(JSON.parse(JSON.stringify(component.getPlanningItemsAndHeaders(input)))).toEqual(output); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /contrib/organice-webdav-post/Readme.md: -------------------------------------------------------------------------------- 1 | * Receive an HTTP post with a message to post to an Org file in organice 2 | 3 | These scripts *directly* update Org files on your WebDAV server. You can use this script directly from your web browser or through automation tools (such as Mailgun or IFTTT) to post tasks directly to Org files. 4 | 5 | *NOTES*: 6 | - This set of scripts was designed to be modular and used 7 | - There is limited content checking - so please be careful with what you post 8 | - Watch out for race conditions if you edit your Org files in multiple places simultaneously 9 | - The =organice_inbox.sh= script assumes a deadline of today for convenience 10 | 11 | ** Configuring the scripts 12 | 13 | In this example, the file =home.php= contains all of the configuration information. There should be no reason to edit the other files, unless you wish to change some default behaviours. 14 | 15 | The file =inbox.php= does the actual parsing of the web traffic. You can create multiple =home.php= files - each with different configurations - and they can all include =inbox.php= safely to get incoming tasks processed. 16 | 17 | ** Enabling the script 18 | 19 | You should put the file =organice-inbox.sh= in a directoory *outside* of the directories of your web server. The script must be executable by the same user that runs your web-server. This user must also have write access to the Org file where the task will be captured. 20 | 21 | ** Using the script 22 | 23 | Once you have these scripts properly configured and added to your web-server, you can use them directly in a browser or POST data to be immediately added. For this example, let's assume the URL of https://my.organice.org/home.php 24 | 25 | Loading this URL in a browser will present an HTML form where you can add a task with a title and description. 26 | 27 | You can also POST content to the URL to automatically add a task to Organice without further confirmation. For POSTing data, the following fields are used: 28 | - *Subject* _(required)_ - the title of the task to be included in the headline 29 | - *body-plain* _(optional)_ - additional text to be included in the description of the task 30 | - *token* _(optional)_ - if you wish to pass a token to be authenticate requests later (*NOTE:* Validating the token is not currently in this script) 31 | - *bookmarklet* _(optional)_ - setting any value here will tell the script that it was called from a bookmarklet popup - and the link button after inserting a task will cause the popup to close 32 | - *voice* _(optional)_ - setting any value here will tell the script that it was called from a voice command - so you have a way to customize the response to be read back by Siri, Google Assistant or another voice assistant 33 | -------------------------------------------------------------------------------- /contrib/organice-on-android-with-webdav-server/README.md: -------------------------------------------------------------------------------- 1 | # Setting up a local version of organice on Android 2 | 3 | Author: 4 | - Matrix: @AppAraat:matrix.org 5 | - https://matrix.to/#/!DfVpGxoYxpbfAhuimY:matrix.org/$D-xaDZAoRjIOgJrancXIy7Yf2_cq0IeMf1UtSRQRyyY?via=matrix.org&via=kde.org&via=chat.physics.ac 6 | 7 | Suppose you know you're going to a place with no network connectivity. If you just use the [hosted version of organice](https://organice.200ok.ch/), you might be in a bad situation if you decide to reboot your phone (or if you (or your Android) decide to kill your browser). You won't be able to reach https://organice.200ok.ch/ because... you have no network connectivity. So what now? 8 | 9 | Luckily you can build a locally hosted version of organice on your Android. All it requires is Termux and a few packages that are going to be installed through it. 10 | 11 | Termux does not adhere to the FHS, so there are 2 main directories to be aware of in Termux: 12 | 13 | * `/data/data/com.termux/files/usr/` - This replaces directories such as `/bin/`, `/etc/`, `/usr/` or `/var/`. This is where packages are installed and is assigned to the environment variable `$PREFIX`. 14 | * `/data/data/com.termux/files/home/` - This is your home directory, also assigned to `$HOME` variable and accessible through simply executing `cd`. 15 | * `/data/data/com.termux/files/home/storage/` (a.k.a. `~/storage/`) - Contains symlinks to your storage framework. Only accessible if you correctly run `termux-setup-storage`. [More info here.](https://wiki.termux.com/wiki/Internal_and_external_storage) 16 | 17 | 18 | ### Installation of packages 19 | First we have to install all the necessary packages: 20 | 21 | * `pkg install git nodejs-lts yarn` 22 | * Note that as of 2020-08-28, the required version of Node.js for compiling organice is `v12.13.1`, however, the `nodejs-lts` package is on `v12.18.3`. This may lead to some undefined behavior. To install a specific Node.js version in Termux means that you have to build it yourself: https://wiki.termux.com/wiki/Building_packages 23 | * `git clone --depth=1 https://github.com/200ok-ch/organice` 24 | 25 | ### Now we build! 26 | * `cd organice` 27 | * `yarn install` - This should build organice. 28 | * `yarn start` - This should run it. This might take a while though. Your browser should automatically open up. 29 | 30 | 31 | ## Building `hacdias/webdav` 32 | If you want to also have a local WebDAV server installed, follow these steps: 33 | 34 | * `pkg install golang` 35 | * `git clone --depth=1 https://github.com/hacdias/webdav` 36 | * `cd webdav` 37 | * `go build` - After this there should appear a binary file called `webdav`. 38 | * `./webdav -c /path/to/your/webdav-config.yaml` - I recommend these settings in your config: http://ix.io/2vuX 39 | -------------------------------------------------------------------------------- /src/components/FileBrowser/components/ActionDrawer/index.js: -------------------------------------------------------------------------------- 1 | // INFO: There's an component within the 2 | // component, as well. 3 | 4 | import React, { Fragment } from 'react'; 5 | import { connect } from 'react-redux'; 6 | import { bindActionCreators } from 'redux'; 7 | import _ from 'lodash'; 8 | 9 | import './../../../OrgFile/components/ActionDrawer/stylesheet.css'; 10 | 11 | import * as orgActions from '../../../../actions/org'; 12 | import * as syncActions from '../../../../actions/sync_backend'; 13 | 14 | import ActionButton from '../../../OrgFile/components/ActionDrawer/components/ActionButton'; 15 | 16 | const ensureCompleteFilename = (fileName) => { 17 | return fileName.endsWith('.org') ? fileName : `${fileName}.org`; 18 | }; 19 | 20 | const ActionDrawer = ({ org, files, syncBackend, path }) => { 21 | const handleAddNewOrgFileClick = () => { 22 | const content = '* First header\nExtend the file from here.'; 23 | let fileName = prompt('New filename:'); 24 | 25 | if (!fileName) return; 26 | 27 | fileName = ensureCompleteFilename(fileName); 28 | let newPath = `${path}/${fileName}`; 29 | 30 | if (_.includes(files, newPath)) { 31 | alert('File already exists. Aborting.'); 32 | } else { 33 | syncBackend.createFile(newPath, content); 34 | org.addNewFile(newPath, content); 35 | } 36 | }; 37 | 38 | const mainButtonStyle = { 39 | opacity: 1, 40 | position: 'relative', 41 | zIndex: 1, 42 | }; 43 | 44 | return ( 45 |
46 | { 47 | 48 |
55 | 62 |
63 |
64 | } 65 |
66 | ); 67 | }; 68 | 69 | const mapStateToProps = (state) => { 70 | const path = state.syncBackend.get('currentPath'); 71 | let files = state.syncBackend.getIn(['currentFileBrowserDirectoryListing', 'listing']); 72 | files = files ? files.map((e) => e.get('id')).toJS() : []; 73 | return { 74 | path, 75 | files, 76 | }; 77 | }; 78 | 79 | const mapDispatchToProps = (dispatch) => { 80 | return { 81 | org: bindActionCreators(orgActions, dispatch), 82 | syncBackend: bindActionCreators(syncActions, dispatch), 83 | }; 84 | }; 85 | 86 | export default connect(mapStateToProps, mapDispatchToProps)(ActionDrawer); 87 | -------------------------------------------------------------------------------- /src/lib/share_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if Web Share API is available 3 | * @returns {boolean} True if Web Share API is supported 4 | */ 5 | export const isWebShareSupported = () => { 6 | return navigator.share && typeof navigator.share === 'function'; 7 | }; 8 | 9 | /** 10 | * Share content using Web Share API with fallback 11 | * @param {Object} options - Share options 12 | * @param {string} options.title - Title for the share 13 | * @param {string} options.text - Text content to share 14 | * @param {string} options.url - URL to share (optional) 15 | * @param {Array} options.files - Files to share (optional) 16 | * @returns {Promise} Result object with success status and method used 17 | */ 18 | export const shareContent = async (options) => { 19 | const { title, text, url, files } = options; 20 | 21 | if (isWebShareSupported()) { 22 | try { 23 | const shareData = {}; 24 | if (title) shareData.title = title; 25 | if (text) shareData.text = text; 26 | if (url) shareData.url = url; 27 | if (files) shareData.files = files; 28 | 29 | await navigator.share(shareData); 30 | return { success: true, method: 'web-share' }; 31 | } catch (error) { 32 | if (error.name === 'AbortError') { 33 | return { success: false, method: 'web-share', error: 'user-cancelled' }; 34 | } 35 | // Fall through to fallback 36 | } 37 | } 38 | 39 | // Fallback to email 40 | return fallbackToEmail(options); 41 | }; 42 | 43 | /** 44 | * Fallback to email sharing 45 | * @param {Object} options - Share options 46 | * @param {string} options.title - Title for the email subject 47 | * @param {string} options.text - Text content for the email body 48 | * @returns {Object} Result object 49 | */ 50 | const fallbackToEmail = ({ title, text }) => { 51 | const subject = title || 'Shared from organice'; 52 | const body = text || ''; 53 | const mailtoURI = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent( 54 | body 55 | )}`; 56 | 57 | window.open(mailtoURI); 58 | return { success: true, method: 'email' }; 59 | }; 60 | 61 | /** 62 | * Create downloadable file 63 | * @param {string} content - File content 64 | * @param {string} filename - Name of the file 65 | * @param {string} mimeType - MIME type of the file (default: 'text/plain') 66 | */ 67 | export const createDownloadableFile = (content, filename, mimeType = 'text/plain') => { 68 | const blob = new Blob([content], { type: mimeType }); 69 | const url = URL.createObjectURL(blob); 70 | 71 | const link = document.createElement('a'); 72 | link.href = url; 73 | link.download = filename; 74 | document.body.appendChild(link); 75 | link.click(); 76 | document.body.removeChild(link); 77 | 78 | URL.revokeObjectURL(url); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/AttributedString/components/TablePart/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react'; 2 | 3 | import './stylesheet.css'; 4 | 5 | import AttributedString from '../../../AttributedString'; 6 | 7 | import _ from 'lodash'; 8 | import classNames from 'classnames'; 9 | import { Map } from 'immutable'; 10 | 11 | export default class TablePart extends PureComponent { 12 | constructor(props) { 13 | super(props); 14 | 15 | _.bindAll(this, ['handleTableSelect']); 16 | 17 | this.state = { 18 | rawCellValues: this.generateCellValueMap(props.table), 19 | shouldIgnoreBlur: false, 20 | }; 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | const { 25 | subPartDataAndHandlers: { onTableCellValueUpdate, selectedTableCellId, inTableEditMode }, 26 | } = this.props; 27 | const { rawCellValues } = this.state; 28 | 29 | if (prevProps.subPartDataAndHandlers.inTableEditMode && !inTableEditMode) { 30 | if (rawCellValues.has(selectedTableCellId)) { 31 | onTableCellValueUpdate(selectedTableCellId, rawCellValues.get(selectedTableCellId)); 32 | } 33 | } 34 | 35 | if (this.props.table !== prevProps.table) { 36 | this.setState({ rawCellValues: this.generateCellValueMap(this.props.table) }); 37 | } 38 | } 39 | 40 | generateCellValueMap(table) { 41 | return Map( 42 | table 43 | .get('contents') 44 | .map((row) => row.get('contents').map((cell) => [cell.get('id'), cell.get('rawContents')])) 45 | .flatten() 46 | ); 47 | } 48 | 49 | handleTableSelect(tableId) { 50 | this.props.subPartDataAndHandlers.onTableSelect(tableId, this.props.descriptionItemIndex); 51 | } 52 | 53 | render() { 54 | const { table } = this.props; 55 | return ( 56 | 57 |
this.handleTableSelect(table.get('id'))}> 58 | 59 | {table.get('contents').map((row) => ( 60 | 61 | {row.get('contents').map((cell) => { 62 | const className = classNames('table-part__cell'); 63 | return ( 64 | 74 | ); 75 | })} 76 | 77 | ))} 78 | 79 |
65 | {cell.get('contents').size > 0 ? ( 66 | 70 | ) : ( 71 | ' ' 72 | )} 73 |
80 | 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/clocking.js: -------------------------------------------------------------------------------- 1 | import { subheadersOfHeaderWithIndex } from './org_utils'; 2 | import { dateForTimestamp } from './timestamps'; 3 | 4 | export const hasActiveClock = (header) => { 5 | const logBookEntries = header.get('logBookEntries', []); 6 | const activeEntries = logBookEntries.filter((entry) => entry.get('start') && !entry.get('end')); 7 | return activeEntries.size !== 0; 8 | }; 9 | 10 | const totalTimeLogged = (header) => { 11 | const logBookEntries = header.get('logBookEntries', []); 12 | 13 | const times = logBookEntries.map((entry) => 14 | entry.get('start') && entry.get('end') 15 | ? dateForTimestamp(entry.get('end')) - dateForTimestamp(entry.get('start')) 16 | : 0 17 | ); 18 | 19 | const addedTimes = times.reduce((acc, val) => acc + val, 0); 20 | return addedTimes; 21 | }; 22 | 23 | export const totalFilteredTimeLogged = (filters, header) => { 24 | const clocks = header 25 | .get('logBookEntries', []) 26 | .map((l) => [l.get('start'), l.get('end')]) 27 | .filter(([start, end]) => start && end) 28 | .filter((ts) => ts.some((t) => filters.every((f) => f(t)))); 29 | 30 | const times = clocks.map(([start, end]) => dateForTimestamp(end) - dateForTimestamp(start)); 31 | 32 | const addedTimes = times.reduce((acc, val) => acc + val, 0); 33 | return addedTimes; 34 | }; 35 | 36 | export const updateHeadersTotalTimeLoggedRecursive = (headers) => { 37 | if (!headers) { 38 | return headers; 39 | } 40 | const headersWithtotalTimeLogged = headers.map((header) => 41 | header.set('totalTimeLogged', totalTimeLogged(header)) 42 | ); 43 | const headersWithtotalTimeLoggedRecursive = headersWithtotalTimeLogged.map((header, index) => { 44 | const subheaders = subheadersOfHeaderWithIndex(headersWithtotalTimeLogged, index); 45 | const totalTimeLoggedRecursive = subheaders.reduce( 46 | (acc, val) => acc + val.get('totalTimeLogged'), 47 | header.get('totalTimeLogged') 48 | ); 49 | return header.set('totalTimeLoggedRecursive', totalTimeLoggedRecursive); 50 | }); 51 | return headersWithtotalTimeLoggedRecursive; 52 | }; 53 | 54 | export const updateHeadersTotalFilteredTimeLoggedRecursive = (filters, headers) => { 55 | if (!headers) { 56 | return headers; 57 | } 58 | const headersWithtotalTimeLogged = headers.map((header) => 59 | header.set('totalFilteredTimeLogged', totalFilteredTimeLogged(filters, header)) 60 | ); 61 | const headersWithtotalTimeLoggedRecursive = headersWithtotalTimeLogged.map((header, index) => { 62 | const subheaders = subheadersOfHeaderWithIndex(headersWithtotalTimeLogged, index); 63 | const totalTimeLoggedRecursive = subheaders.reduce( 64 | (acc, val) => acc + val.get('totalFilteredTimeLogged'), 65 | header.get('totalFilteredTimeLogged') 66 | ); 67 | return header.set('totalFilteredTimeLoggedRecursive', totalTimeLoggedRecursive); 68 | }); 69 | return headersWithtotalTimeLoggedRecursive; 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Table/TableCell/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | import { is } from 'immutable'; 5 | import { curry } from 'lodash/fp'; 6 | import AttributedString from '../../AttributedString'; 7 | import TableCellEditContainer from '../TableCellEditContainer/index'; 8 | import { getTableCell } from '../../../../../lib/org_utils'; 9 | import { activatePopup } from '../../../../../actions/base'; 10 | import { setSelectedTableCellId, advanceCheckboxState } from '../../../../../actions/org'; 11 | import './stylesheet.css'; 12 | 13 | const getInTableEditMode = curry( 14 | (filePath, state) => state.org.present.getIn(['files', filePath, 'editMode']) === 'table' 15 | ); 16 | const getSelectedCellId = curry((filePath, state) => { 17 | return state.org.present.getIn(['files', filePath, 'selectedTableCellId']); 18 | }); 19 | const TableCell = ({ 20 | props: { filePath, headerIndex, descriptionItemIndex, cellId, row, column }, 21 | }) => { 22 | const dispatch = useDispatch(); 23 | const inTableEditMode = useSelector(getInTableEditMode(filePath)); 24 | const selectedCellId = useSelector(getSelectedCellId(filePath)); 25 | 26 | const [isCellSelected, setIsCellSelected] = useState(cellId === selectedCellId); 27 | 28 | const tableCellGetter = getTableCell({ 29 | filePath, 30 | headerIndex, 31 | descriptionItemIndex, 32 | row, 33 | column, 34 | }); 35 | const cell = useSelector(tableCellGetter, is); 36 | const cellContents = cell.get('contents'); 37 | const cellRawContents = cell.get('rawContents'); 38 | 39 | useEffect(() => { 40 | if (cellId == selectedCellId) { 41 | setIsCellSelected(true); 42 | } else { 43 | setIsCellSelected(false); 44 | } 45 | }, [selectedCellId, isCellSelected, cellId]); 46 | 47 | const className = classNames('table-part__cell', { 48 | 'table-part__cell--selected': isCellSelected, 49 | }); 50 | 51 | const handleCellSelect = () => { 52 | setIsCellSelected(true); 53 | dispatch(setSelectedTableCellId(cellId)); 54 | }; 55 | 56 | const handleCheckboxClick = (listItemId) => { 57 | dispatch(advanceCheckboxState(listItemId)); 58 | }; 59 | 60 | const handleTimestampClick = (timestampId) => { 61 | dispatch(activatePopup('timestamp-editor', { timestampId })); 62 | }; 63 | 64 | const subPartDataAndHandlers = { 65 | inTableEditMode: inTableEditMode, 66 | onCheckboxClick: handleCheckboxClick, 67 | onTimestampClick: handleTimestampClick, 68 | }; 69 | 70 | return ( 71 | 72 | {isCellSelected && inTableEditMode ? ( 73 | 74 | ) : ( 75 | 76 | )} 77 | 78 | ); 79 | }; 80 | 81 | export default TableCell; 82 | -------------------------------------------------------------------------------- /contrib/organice-webdav-traefik/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | traefik: 5 | container_name: traefik 6 | image: "traefik:v2.10" 7 | command: 8 | - --entrypoints.websecure.address=:443 9 | - --entrypoints.web.address=:80 10 | - --entrypoints.web.http.redirections.entrypoint.to=websecure 11 | - --entrypoints.web.http.redirections.entrypoint.scheme=https 12 | - --providers.docker=true 13 | - --providers.docker.exposedbydefault=false 14 | - --certificatesresolvers.le.acme.email=${EMAIL} 15 | - --certificatesresolvers.le.acme.storage=/acme.json 16 | - --certificatesresolvers.le.acme.tlschallenge=true 17 | - --api 18 | - --log.filePath=/var/log/error.log 19 | - --log.level=INFO 20 | - --accesslog.filepath=/var/log/access.log 21 | - --accesslog=true 22 | ports: 23 | - "80:80" 24 | - "443:443" 25 | volumes: 26 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 27 | - "./traefik/acme.json:/acme.json" 28 | - "./traefik/log:/var/log" 29 | labels: 30 | - "traefik.http.routers.traefik.tls.certresolver=le" 31 | - "traefik.http.routers.traefik.entrypoints=websecure" 32 | restart: unless-stopped 33 | 34 | organice: 35 | image: "twohundredok/organice:latest" 36 | container_name: organice 37 | restart: unless-stopped 38 | evironment: 39 | - ORGANICE_WEBDAV_URL=https://${DOMAIN}/${WEBDAV_PREFIX} 40 | labels: 41 | - "traefik.enable=true" 42 | - "traefik.http.routers.organice.rule=Host(`${DOMAIN}`)" 43 | # Remove either of the following lines to disable IP whitelisting or basic auth. 44 | - "traefik.http.middlewares.auth.basicauth.users=${HTTP_AUTH}" 45 | - "traefik.http.middlewares.ipwl.ipwhitelist.sourcerange=127.0.0.1/32, 192.168.2.0/24" 46 | # Remove either 'auth' or 'ipwl' below accordingly. Eg: 47 | # - "traefik.http.routers.organice.middlewares=auth" 48 | - "traefik.http.routers.organice.middlewares=auth, ipwl" 49 | - "traefik.http.routers.organice.tls.certresolver=le" 50 | - "traefik.http.routers.organice.entrypoints=websecure" 51 | - "traefik.http.services.organice.loadbalancer.server.port=5000" 52 | 53 | webdav: 54 | build: 55 | context: ./webdav/ 56 | container_name: webdav 57 | environment: 58 | - "PUID=${PUID}" 59 | - "PGID=${PGID}" 60 | - "WEBDAV_PREFIX=${WEBDAV_PREFIX}" 61 | volumes: 62 | - "${ORG_DIR}:/data" 63 | - "./webdav/log:/var/log/nginx" 64 | restart: unless-stopped 65 | labels: 66 | - "traefik.enable=true" 67 | - "traefik.http.routers.webdav.rule=Host(`${DOMAIN}`) && PathPrefix(`/${WEBDAV_PREFIX}`)" 68 | - "traefik.http.routers.webdav.tls.certresolver=le" 69 | - "traefik.http.routers.webdav.entrypoints=websecure" 70 | - "traefik.http.services.webdav.loadbalancer.server.port=80" 71 | # See above comments to configure IP whitelisting. 72 | - "traefik.http.routers.webdav.middlewares=auth" 73 | -------------------------------------------------------------------------------- /src/components/OrgFile/components/Table/TableCellEditContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { curry } from 'lodash/fp'; 4 | 5 | import { getCurrentTimestampAsText } from '../../../../../lib/timestamps'; 6 | import { exitEditMode, updateTableCellValue } from '../../../../../actions/org'; 7 | import './stylesheet.css'; 8 | 9 | const getSelectedCellId = curry((filePath, state) => { 10 | return state.org.present.getIn(['files', filePath, 'selectedTableCellId']); 11 | }); 12 | 13 | const CellEditContainer = ({ filePath, cellValue, cellId }) => { 14 | const dispatch = useDispatch(); 15 | const selectedCellId = useSelector(getSelectedCellId(filePath)); 16 | const [isCellSelected, setIsCellSelected] = useState(cellId == selectedCellId); 17 | const [currentCellValue, setCurrentCellValue] = useState(cellValue); 18 | const [shouldIgnoreBlur, setShouldIgnoreBlur] = useState(false); 19 | const textareaRef = useRef(null); 20 | 21 | useEffect(() => { 22 | if (cellId == selectedCellId) { 23 | setIsCellSelected(true); 24 | } else { 25 | setIsCellSelected(false); 26 | handleTableCellValueUpdate(cellId, currentCellValue); 27 | } 28 | }, [selectedCellId, isCellSelected, cellId, currentCellValue]); 29 | 30 | const handleTableCellValueUpdate = (cellId, newValue) => { 31 | dispatch(updateTableCellValue(cellId, newValue)); 32 | }; 33 | 34 | const handleExitTableEditMode = () => { 35 | dispatch(updateTableCellValue(cellId, currentCellValue)); 36 | dispatch(exitEditMode()); 37 | }; 38 | 39 | const handleCellChange = (event) => setCurrentCellValue(event.target.value); 40 | 41 | const handleInsertTimestamp = () => { 42 | setShouldIgnoreBlur(true); 43 | const insertionIndex = textareaRef.current.selectionStart; 44 | const newValue = 45 | currentCellValue.substring(0, insertionIndex) + 46 | getCurrentTimestampAsText() + 47 | currentCellValue.substring(textareaRef.current.selectionEnd || insertionIndex); 48 | 49 | textareaRef.current.value = newValue; 50 | setCurrentCellValue(newValue); 51 | 52 | setShouldIgnoreBlur(false); 53 | textareaRef.current.focus(); 54 | }; 55 | 56 | const handleTextareaBlur = () => { 57 | if (!shouldIgnoreBlur) { 58 | handleExitTableEditMode(); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 |