├── .babelrc ├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── etc └── default │ ├── dev.env │ └── staging.env ├── makefiles ├── docker │ ├── cleanup.Makefile │ └── compose.Makefile └── help.Makefile ├── package.json ├── public ├── bookmarklet.js ├── favicon.ico ├── humans.txt ├── icons │ ├── icon-144x144.png │ ├── icon-192x192.png │ ├── icon-48x48.png │ ├── icon-72x72.png │ └── icon-96x96.png ├── index.html ├── keycloak.json ├── logo.svg ├── manifest.json └── robots.txt ├── screenshot.png └── src ├── App.jsx ├── Routes.jsx ├── api ├── client.js ├── common │ └── AbstractApi.js ├── document.js ├── export.js ├── graveyard.js ├── label.js ├── profile.js ├── sharing.js └── webhook.js ├── components ├── .gitkeep ├── AppBar │ ├── index.jsx │ └── styles.css ├── AppKeyboardHandlers │ └── index.jsx ├── AppMenu │ ├── index.jsx │ └── styles.css ├── AppModal │ ├── index.jsx │ └── styles.css ├── AppNotification │ ├── index.jsx │ └── styles.css ├── AppSignPanel │ ├── index.jsx │ └── styles.css ├── ColorSwatch │ ├── colors.json │ ├── index.jsx │ └── styles.css ├── DocumentContent │ └── index.jsx ├── DocumentContextMenu │ └── index.jsx ├── DocumentLabels │ ├── index.jsx │ └── styles.css ├── DocumentRibbon │ ├── index.jsx │ └── styles.css ├── DocumentTile │ ├── index.jsx │ └── styles.css ├── DocumentTitleModal │ └── index.jsx ├── DocumentUrlModal │ └── index.jsx ├── DocumentsContextMenu │ └── index.jsx ├── InfiniteGrid │ └── index.jsx ├── KeymapHelpModal │ ├── index.jsx │ └── styles.css ├── ProfilePanel │ ├── index.jsx │ └── styles.css └── SearchBarItem │ └── index.jsx ├── helpers ├── AuthProvider.js └── DateHelper.js ├── index.js ├── layouts ├── MainLayout.jsx ├── PublicLayout.jsx ├── RootLayout.jsx └── styles.css ├── middlewares ├── .gitkeep ├── Authentication.jsx └── Context.jsx ├── node_modules ├── api ├── components ├── helpers ├── layouts ├── middlewares ├── store └── views ├── store ├── client │ ├── actions.js │ └── index.js ├── clients │ ├── actions.js │ └── index.js ├── createStore.js ├── exports │ ├── actions.js │ └── index.js ├── helper.js ├── label │ ├── actions.js │ └── index.js ├── labels │ ├── actions.js │ └── index.js ├── modules │ ├── auth.js │ ├── document.js │ ├── documents.js │ ├── graveyard.js │ ├── index.js │ ├── layout.js │ ├── notification.js │ ├── sharing.js │ ├── titleModal.js │ └── urlModal.js ├── profile │ ├── actions.js │ └── index.js ├── reducers.js ├── webhook │ ├── actions.js │ └── index.js └── webhooks │ ├── actions.js │ └── index.js ├── styles ├── _base.scss ├── _bugfix.scss ├── _readable.scss ├── _scroll.scss ├── main.css ├── main.scss └── vendor │ └── _normalize.scss └── views ├── AboutView └── index.jsx ├── ApiClientView └── index.jsx ├── BookmarkletView ├── index.jsx └── styles.css ├── DocumentView ├── index.jsx └── styles.css ├── DocumentsView └── index.jsx ├── GraveyardView └── index.jsx ├── LabelDocumentsView └── index.jsx ├── LabelView └── index.jsx ├── PublicDocumentView └── index.jsx ├── PublicDocumentsView └── index.jsx ├── SettingsView ├── ApiClientsTab │ └── index.jsx ├── ApiKeyTab │ └── index.jsx ├── BookmarkletTab │ ├── index.jsx │ └── styles.css ├── ExportTab │ └── index.jsx ├── WebhooksTab │ └── index.jsx └── index.jsx ├── ShareLabelView └── index.jsx ├── SharedDocumentsView └── index.jsx ├── SharingListView └── index.jsx └── WebhookView └── index.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | // NOTE: These options are overriden by the babel-loader configuration 2 | // for webpack, which can be found in ~/build/webpack-environments/_base 3 | // and ~/build/webpack-environments/production. 4 | // 5 | // Why? The react-transform-hmr plugin depends on HMR (and throws if 6 | // module.hot is disbled, so keeping it and related plugins contained 7 | // within webpack helps prevent unexpected errors. 8 | { 9 | "presets": ["es2015", "react", "stage-0"], 10 | "plugins": ["transform-runtime", "add-module-exports"] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ROOT=https://api.nunux.org/keeper/v2 2 | REACT_APP_LOGIN_ROOT=https://login.nunux.org 3 | APP_SRC_DIR=/usr/src/app 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | dist/** 4 | *.spec.js 5 | src/index.html 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # vim:ft=yaml: 2 | 3 | # So parent files don't get applied 4 | root: true 5 | 6 | env: 7 | browser: true 8 | 9 | extends: 10 | - standard 11 | - standard-react 12 | 13 | rules: 14 | semi: [2, never] 15 | 16 | parser: babel-eslint 17 | 18 | globals: 19 | __DEV__ : false 20 | __PROD__ : false 21 | __MOCK__ : false 22 | __DEBUG__ : false 23 | __DEBUG_NEW_WINDOW__ : false 24 | __BASENAME__ : false 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | 3 | cache: 4 | paths: 5 | - node_modules/ 6 | 7 | pages: 8 | before_script: 9 | - npm install 10 | script: 11 | - npm run build 12 | artifacts: 13 | paths: 14 | - build 15 | only: 16 | - master -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "5.0" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run test 15 | - NODE_ENV=development npm run deploy 16 | - NODE_ENV=staging npm run deploy 17 | - NODE_ENV=production npm run deploy 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2.0.0 5 | ----- 6 | 7 | ### Features 8 | * Authentication delegated to external identity provider (with Keycloak) 9 | * Create text or HTML documents (from scratch or from an URL) 10 | * Edit online documents 11 | * Search and visualize documents (also in raw mode) 12 | * Create labels (name, color) 13 | * Classify documents with labels 14 | 15 | ### Improvements 16 | * Based on a complete new stack (ES6, React, Redux, Webpack) 17 | * New modern GUI thanks to Semantic UI 18 | * Better navigation with semantic routes and full history usage 19 | * Better Bookmarklet visual 20 | * Complete separation with the API backend (support full mocked setup) 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address 8 | the issue mentioned. Likewise, please check for issues related to update, as 9 | someone else may be working on the issue in a branch or fork. 10 | 11 | * Non-trivial changes should be discussed in an issue first 12 | * Develop in a topic branch, not master 13 | * Squash your commits 14 | 15 | ### Linting 16 | 17 | Please check your code using `npm run lint` before submitting your pull 18 | requests, as the CI build will fail if `eslint` fails. 19 | 20 | ### Commit Message Format 21 | 22 | Each commit message should include a **type**, a **scope** and a **subject**: 23 | 24 | ``` 25 | (): 26 | ``` 27 | 28 | Lines should not exceed 100 characters. This allows the message to be easier to 29 | read on github as well as in various git tools and produces a nice, neat commit 30 | log ie: 31 | 32 | ``` 33 | #271 feat(standard): add style config and refactor to match 34 | #270 fix(config): only override publicPath when served by webpack 35 | #269 feat(eslint-config-defaults): replace eslint-config-airbnb 36 | #268 feat(config): allow user to configure webpack stats output 37 | ``` 38 | 39 | #### Type 40 | 41 | Must be one of the following: 42 | 43 | * **feat**: A new feature 44 | * **fix**: A bug fix 45 | * **doc**: Documentation only changes 46 | * **style**: Changes that do not affect the meaning of the code (white-space, 47 | formatting, missing semi-colons, etc) 48 | * **refactor**: A code change that neither fixes a bug or adds a feature 49 | * **test**: Adding missing tests 50 | * **chore**: Changes to the build process or auxiliary tools and libraries such 51 | as documentation generation 52 | 53 | #### Scope 54 | 55 | The scope could be anything specifying place of the commit change. For example 56 | `document`, `label`, etc... 57 | 58 | #### Subject 59 | 60 | The subject contains succinct description of the change: 61 | 62 | * use the imperative, present tense: "change" not "changed" nor "changes" 63 | * don't capitalize first letter 64 | * no dot (.) at the end 65 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Nunux Keeper web app. 2 | # 3 | # VERSION 2.0 4 | 5 | FROM node:6-onbuild 6 | 7 | MAINTAINER Nicolas Carlier 8 | 9 | # Ports 10 | EXPOSE 3000 11 | 12 | ENTRYPOINT ["/usr/local/bin/npm"] 13 | 14 | CMD ["start"] 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | 3 | # Image name 4 | USERNAME:=ncarlier 5 | APPNAME:=keeper-web-app 6 | env?=dev 7 | 8 | # Compose files 9 | COMPOSE_FILES?=-f docker-compose.yml 10 | 11 | # Deploy directory 12 | DEPLOY_DIR:=/var/www/html/app.nunux.org/keeper 13 | 14 | # Include common Make tasks 15 | root_dir:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 16 | makefiles:=$(root_dir)/makefiles 17 | include $(makefiles)/help.Makefile 18 | include $(makefiles)/docker/compose.Makefile 19 | 20 | all: help 21 | 22 | # Get Docker binaries version 23 | infos: 24 | echo "Using $(shell docker --version)" 25 | echo "Using $(shell docker-compose --version)" 26 | .PHONY: infos 27 | 28 | ## Build Docker image 29 | build: 30 | docker build --rm -t $(USERNAME)/$(APPNAME) . 31 | .PHONY: build 32 | 33 | ## Run the container in test mode 34 | test: 35 | echo "Running tests..." 36 | CMD=test docker-compose $(COMPOSE_FILES) up --no-deps --no-build --abort-on-container-exit --exit-code-from app app 37 | .PHONY: test 38 | 39 | ## Run the container in foreground 40 | start: 41 | echo "Running container..." 42 | docker-compose $(COMPOSE_FILES) up --no-deps --no-build --abort-on-container-exit --exit-code-from app app 43 | .PHONY: start 44 | 45 | ## Start required services 46 | deploy: infos compose-up 47 | .PHONY: up 48 | 49 | ## Stop all services 50 | undeploy: compose-down-force 51 | .PHONY: down 52 | 53 | ## Show services logs 54 | logs: compose-logs 55 | .PHONY: logs 56 | 57 | ## Install as a service (needs root privileges) 58 | install: build 59 | echo "Install generated files at deployment location..." 60 | mkdir -p $(DEPLOY_DIR) 61 | docker run --rm -v $(DEPLOY_DIR):/usr/src/app/build $(USERNAME)/$(APPNAME) run build 62 | .PHONY: install 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NUNUX Keeper Web App 2 | 3 | > Your personal content curation service. 4 | 5 | Nunux Keeper allow you to collect, organize, and display web documents. 6 | 7 | **This project is the official web frontend.** 8 | 9 | ![Screenshot](screenshot.png) 10 | 11 | ## Table of Contents 12 | 1. [Requirements](#requirements) 13 | 1. [Features](#features) 14 | 1. [Installation](#installation) 15 | 1. [Development server](#development-server) 16 | 1. [Other commands](#other-commands) 17 | 1. [Under the hood](#under-the-hood) 18 | 1. [Structure](#structure) 19 | 20 | ## Requirements 21 | 22 | Docker OR Node `^5.0.0` 23 | 24 | ## Features 25 | 26 | * Welcome page 27 | * Login with external identity provider (Google, Twitter, etc.) 28 | * Manage labels to organize documents 29 | * Create document from scratch or from a remote location 30 | * Create document from another website thanks to the bookmarklet 31 | * Search documents with a powerful search engine 32 | * Share documents 33 | 34 | ## Configuration 35 | 36 | Basic project configuration can be found in `etc/dev.env`. Here you'll be able 37 | to redefine some parameters: 38 | 39 | * REACT_APP_API_ROOT: Nunux Keeper API endpoint 40 | * REACT_APP_DEBUG: Activate debug mode 41 | 42 | ## Installation 43 | 44 | > Note that this project is "only" the web front end of the backend API of Linux 45 | > Keeper. If you want to use your own API server you have to install first this 46 | > project: [keeper-core-api](https://github.com/nunux-keeper/keeper-core-api) 47 | 48 | Once configured for your needs (see section above), you can build the static 49 | Web App into the directory of your choice: 50 | 51 | ```bash 52 | $ git clone https://github.com/nunux-keeper/keeper-web-app.git 53 | $ cd keeper-web-app 54 | $ make install DEPLOY_DIR=/var/www/html 55 | ``` 56 | 57 | Then, you can serve this directory with your favorite HTTP server. 58 | 59 | ## Development server 60 | 61 | With Node: 62 | 63 | ```shell 64 | $ git clone https://github.com/nunux-keeper/keeper-web-app.git 65 | $ cd keeper-web-app 66 | $ npm install # Install Node modules listed in ./package.json (may take a 67 | # while the first time) 68 | $ npm start # Compile and launch 69 | ``` 70 | 71 | Or with Docker: 72 | 73 | ```shell 74 | $ git clone https://github.com/nunux-keeper/keeper-web-app.git 75 | $ cd keeper-web-app 76 | $ make build start # Build Docker image and start it 77 | ``` 78 | 79 | ## Other commands 80 | 81 | Here's a brief summary of available Docker commands: 82 | 83 | * `make help` - Show available commands. 84 | * `make build` - Build Docker image. 85 | * `make test` - Start container with tests. 86 | * `make start` - Start container in foreground. 87 | * `make deploy` - Start container in background. 88 | * `make undeploy` - Stop container in background. 89 | * `make logs` - View container logs. 90 | * `make install` - Install generated site into the deployment directory. 91 | 92 | Here's a brief summary of available NPM commands: 93 | 94 | * `npm start` - Start development server. 95 | * `npm run build` - Compiles the application to disk (`~/build`). 96 | * `npm run test` - Runs unit tests. 97 | * `npm run build-css`- Runs SASS to generate CSS file. 98 | 99 | ## Under the hood 100 | 101 | * [React](https://github.com/facebook/react) 102 | * [Redux](http://redux.js.org/) 103 | * [React Router](https://github.com/ReactTraining/react-router) 104 | * [React Create App](https://github.com/facebookincubator/create-react-app) 105 | * [Sass](http://sass-lang.com/) 106 | * [ESLint](http://eslint.org) 107 | 108 | 109 | ## Structure 110 | 111 | Here the folder structure: 112 | 113 | ``` 114 | . 115 | ├── build # Builded website 116 | ├── etc # Configuration 117 | ├── makefiles # Makefiles 118 | ├── public # Static files to serve as is 119 | ├── src # Application source code 120 | │ ├── api # Backend API connector (real and mock) 121 | │ ├── components # App components 122 | │ ├── layouts # Components that dictate major page structure 123 | │ ├── middlewares # Components that provide context and AuthN 124 | │ ├── store # Redux store 125 | │ │ └── modules # Redux modules 126 | │ ├── styles # Application-wide styles 127 | │ ├── views # Components that live at a route 128 | │ ├── App.js # Application bootstrap and rendering 129 | │ ├── Routes.js # Application routes 130 | │ └── index.js # Application entry point 131 | └── test # Unit tests 132 | ``` 133 | 134 | ---------------------------------------------------------------------- 135 | 136 | NUNUX Keeper 137 | 138 | Copyright (c) 2017 Nicolas CARLIER (https://github.com/ncarlier) 139 | 140 | This program is free software: you can redistribute it and/or modify 141 | it under the terms of the GNU General Public License as published by 142 | the Free Software Foundation, either version 3 of the License. 143 | 144 | This program is distributed in the hope that it will be useful, 145 | but WITHOUT ANY WARRANTY; without even the implied warranty of 146 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 147 | GNU General Public License for more details. 148 | 149 | You should have received a copy of the GNU General Public License 150 | along with this program. If not, see . 151 | 152 | ---------------------------------------------------------------------- 153 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | ####################################### 4 | # Web App 5 | ####################################### 6 | app: 7 | image: "ncarlier/keeper-web-app:latest" 8 | env_file: "etc/default/${ENV:-dev}.env" 9 | command: "${CMD:-start}" 10 | volumes: 11 | - ${PWD}:${APP_SRC_DIR:-/usr/src/app_src} 12 | ports: 13 | - "${PORT:-3000}:3000" 14 | -------------------------------------------------------------------------------- /etc/default/dev.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ROOT=https://api.nunux.org/keeper/v2 2 | REACT_APP_LOGIN_ROOT=https://login.nunux.org 3 | -------------------------------------------------------------------------------- /etc/default/staging.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ROOT=https://api.nunux.org/keeper/v2 2 | REACT_APP_LOGIN_ROOT=https://login.nunux.org 3 | -------------------------------------------------------------------------------- /makefiles/docker/cleanup.Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | 3 | # Remove dangling Docker images 4 | cleanup: 5 | echo "Removing dangling docker images..." 6 | -docker images -q --filter 'dangling=true' | xargs docker rmi 7 | .PHONY: cleanup 8 | 9 | -------------------------------------------------------------------------------- /makefiles/docker/compose.Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | 3 | # Docker compose configuration files 4 | COMPOSE_FILES?=-f docker-compose.yml 5 | 6 | # Wait until a service ($$service) is up and running (needs health run flag) 7 | compose-wait: 8 | sid=`docker-compose $(COMPOSE_FILES) ps -q $(service)`;\ 9 | n=30;\ 10 | while [ $${n} -gt 0 ] ; do\ 11 | status=`docker inspect --format "{{json .State.Health.Status }}" $${sid}`;\ 12 | if [ -z $${status} ]; then echo "No status informations."; exit 1; fi;\ 13 | echo "Waiting for $(service) up and ready ($${status})...";\ 14 | if [ "\"healthy\"" = $${status} ]; then exit 0; fi;\ 15 | sleep 2;\ 16 | n=`expr $$n - 1`;\ 17 | done;\ 18 | echo "Timeout" && exit 1 19 | .PHONY: compose-wait 20 | 21 | # Build services (or single service with $$service) 22 | compose-build: 23 | echo "Building services ..." 24 | docker-compose $(COMPOSE_FILES) build $(service) 25 | .PHONY: compose-build 26 | 27 | # Config a service ($$service) 28 | compose-config: compose-wait 29 | echo "Configuring $(service)..." 30 | $(MAKE) config-$(service) 31 | .PHONY: compose-config 32 | 33 | # Deploy compose stack 34 | compose-up: 35 | echo "Deploying compose stack..." 36 | -cat .env 37 | docker-compose $(COMPOSE_FILES) up -d 38 | echo "Congrats! Compose stack deployed." 39 | .PHONY: compose-up 40 | 41 | # Un-deploy compose stack 42 | compose-down: 43 | echo "Un-deploying compose stack..." 44 | @while [ -z "$$CONTINUE" ]; do \ 45 | read -r -p "Are you sure? [y/N]: " CONTINUE; \ 46 | done ; \ 47 | [ $$CONTINUE = "y" ] || [ $$CONTINUE = "Y" ] || (echo "Exiting."; exit 1;) 48 | docker-compose $(COMPOSE_FILES) down 49 | echo "Compose stack un-deployed." 50 | .PHONY: compose-down 51 | 52 | # Un-deploy compose stack (without user confirmation) 53 | compose-down-force: 54 | echo "Un-deploying compose stack..." 55 | docker-compose $(COMPOSE_FILES) down --remove-orphans 56 | echo "Compose stack un-deployed." 57 | .PHONY: compose-down-force 58 | 59 | # Stop a service ($$service) 60 | compose-stop: 61 | echo "Stoping service: $(service) ..." 62 | docker-compose $(COMPOSE_FILES) stop $(service) 63 | .PHONY: compose-stop 64 | 65 | # Stop a service ($$service) 66 | compose-start: 67 | echo "Starting service: $(service) ..." 68 | docker-compose $(COMPOSE_FILES) up -d $(service) 69 | .PHONY: compose-start 70 | 71 | # Restart a service ($$service) 72 | compose-restart: 73 | echo "Restarting service: $(service) ..." 74 | docker-compose $(COMPOSE_FILES) restart $(service) 75 | .PHONY: compose-restart 76 | 77 | # View service logs ($$service) 78 | compose-logs: 79 | echo "Viewing $(service) service logs ..." 80 | docker-compose $(COMPOSE_FILES) logs -f $(service) 81 | .PHONY: compose-logs 82 | 83 | # View services status 84 | compose-ps: 85 | echo "Viewing services status ..." 86 | docker-compose $(COMPOSE_FILES) ps 87 | .PHONY: compose-ps 88 | 89 | -------------------------------------------------------------------------------- /makefiles/help.Makefile: -------------------------------------------------------------------------------- 1 | .SILENT: 2 | 3 | ## This help screen 4 | help: 5 | printf "Available targets:\n\n" 6 | awk '/^[a-zA-Z\-\_0-9]+:/ { \ 7 | helpMessage = match(lastLine, /^## (.*)/); \ 8 | if (helpMessage) { \ 9 | helpCommand = substr($$1, 0, index($$1, ":")); \ 10 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 11 | printf "%-15s %s\n", helpCommand, helpMessage; \ 12 | } \ 13 | } \ 14 | { lastLine = $$0 }' $(MAKEFILE_LIST) 15 | .PHONY: help 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keeper", 3 | "version": "1.0.0", 4 | "description": "Nunux Keeper webapp.", 5 | "main": "src/index.jsx", 6 | "engines": { 7 | "node": ">=6.0.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject", 15 | "build-css": "sass src/styles/main.scss src/styles/main.css", 16 | "watch-css": "sass src/styles/main.scss src/styles/main.css -w" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@gitlab.com/nunux-keeper/keeper-web-app.git" 21 | }, 22 | "author": "Nicolas Carlier ", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://gitlab.com/nunux-keeper/keeper-web-app/issues" 26 | }, 27 | "homepage": "https://app.nunux.org/keeper", 28 | "dependencies": { 29 | "event-source-polyfill": "0.0.12", 30 | "mousetrap": "^1.6.0", 31 | "nprogress": "^0.2.0", 32 | "react": "^15.4.1", 33 | "react-addons-css-transition-group": "^15.4.1", 34 | "react-dom": "^15.4.1", 35 | "react-modal": "^1.6.1", 36 | "react-redux": "^4.4.6", 37 | "react-router": "^3.0.0", 38 | "react-router-redux": "^4.0.7", 39 | "react-tinymce": "^0.6.0", 40 | "redux": "^3.6.0", 41 | "redux-actions": "^1.1.0", 42 | "redux-thunk": "^2.1.0", 43 | "semantic-ui-css": "^2.2.12", 44 | "semantic-ui-react": "^0.71.5", 45 | "timeago.js": "^2.0.4" 46 | }, 47 | "devDependencies": { 48 | "react-scripts": "^0.9.0", 49 | "sass": "^1.49.9" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/bookmarklet.js: -------------------------------------------------------------------------------- 1 | window.kBookmarklet = function () { 2 | const cid = 'KPR_K__CONTAINER' 3 | const frameId = 'KPR_K__FRAME' 4 | let $c = document.getElementById(cid) 5 | 6 | let popup 7 | 8 | if (!$c) { 9 | $c = document.createElement('div') 10 | $c.id = cid 11 | $c.style.position = 'fixed' 12 | $c.style.background = '#fff' 13 | $c.style.bottom = '20px' 14 | $c.style.left = '20px' 15 | $c.style.width = '200px' 16 | $c.style.height = '200px' 17 | $c.style.zIndex = 999999999 18 | $c.style['box-shadow'] = '#000 4px 4px 20px' 19 | $c.style['border-radius'] = '4px' 20 | var $o = document.createElement('div') 21 | $o.style.position = 'absolute' 22 | $o.style.height = '160px' 23 | $o.style.top = '40px' 24 | $o.style.right = 0 25 | $o.style.width = '200px' 26 | $o.style.background = '#ffffff01' 27 | $o.style.cursor = 'pointer' 28 | $o.addEventListener('dragenter', function (e) { 29 | popup.postMessage(JSON.stringify({ _type: 'onDragEnter' }), window.K_REALM) 30 | }) 31 | $o.addEventListener('dragover', function (e) { 32 | if (e.preventDefault) { 33 | e.preventDefault() 34 | } 35 | return false 36 | }) 37 | $o.addEventListener('dragleave', function (e) { 38 | popup.postMessage(JSON.stringify({ _type: 'onDragLeave' }), window.K_REALM) 39 | }) 40 | $o.addEventListener('drop', function (e) { 41 | if (e.preventDefault) { 42 | e.preventDefault() 43 | } 44 | var data = e.dataTransfer.getData('text/html') 45 | popup.postMessage(JSON.stringify({ _type: 'onDropData', data: data }), window.K_REALM) 46 | return false 47 | }) 48 | $o.addEventListener('click', function (e) { 49 | popup.postMessage(JSON.stringify({ _type: 'onClick' }), window.K_REALM) 50 | }) 51 | $c.appendChild($o) 52 | 53 | var $ifrm = document.createElement('iframe') 54 | $ifrm.setAttribute('id', frameId) 55 | $ifrm.setAttribute('name', frameId) 56 | $ifrm.style.width = '100%' 57 | $ifrm.style.height = '100%' 58 | $ifrm.style.border = 'none' 59 | $ifrm.style.margin = 0 60 | 61 | $c.appendChild($ifrm) 62 | document.body.appendChild($c) 63 | } 64 | 65 | var url = window.K_REALM.replace(/\/$/, '') + '/bookmarklet?url=' + 66 | encodeURIComponent(window.location.href) + 67 | '&title=' + encodeURIComponent(window.document.title) 68 | 69 | popup = window.open(url, frameId) 70 | if (!popup) alert('Unable to load bookmarklet.') 71 | 72 | var receiveMessage = function (e) { 73 | let msg 74 | try { 75 | msg = JSON.parse(e.data) 76 | } catch (e) { 77 | console.error('Unable to parse received event data', e) 78 | return 79 | } 80 | if (msg._type === 'close' && window.K_REALM.indexOf(e.origin) === 0) { 81 | $c.parentNode.removeChild($c) 82 | } else if (msg._type === 'redirect') { 83 | console.log('Redirecting to ', msg.payload) 84 | document.location.replace(msg.payload) 85 | } 86 | } 87 | window.addEventListener('message', receiveMessage, false) 88 | setInterval(function () { 89 | popup.postMessage(JSON.stringify({ _type: 'ping' }), window.K_REALM) 90 | }, 2000) 91 | } 92 | 93 | window.kBookmarklet() 94 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/favicon.ico -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | /* TEAM */ 4 | 5 | Author: Nicolas Carlier 6 | Site: http://ncarlier.github.io/ 7 | Twitter: @ncarlier 8 | From: Tours, Centre, France 9 | 10 | /* SITE */ 11 | Language: English 12 | Doctype: HTML5 13 | IDE: vim 14 | -------------------------------------------------------------------------------- /public/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/icons/icon-48x48.png -------------------------------------------------------------------------------- /public/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/public/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nunux Keeper 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
Loading...
18 |
19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/keycloak.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "nunux-keeper", 3 | "realm-public-key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7kgrRHj3XvKylJmt4uoqXN/EU\nK8+GCuWEvWk2uaOoIJhsqqTsxR7Y4CLkdAQS05y3YJK/FT4wNIMmyMZGCCvZd6iO\nLA4u7DuoQs3/WTQfwQzu7ZLDK1xvo9Pk8wMZdAQ8uCYaeffNpVLOcSzX3v7mEoBa\nMOsTa4smYTcSs9/G3QIDAQAB\n-----END PUBLIC KEY-----\n", 4 | "auth-server-url": "https://login.nunux.org/auth", 5 | "ssl-required": "external", 6 | "resource": "nunux-keeper-app", 7 | "public-client": true, 8 | "enable-cors": true 9 | } 10 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 74 | 75 | 81 | 86 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nunux Keeper", 3 | "short_name": "Keeper", 4 | "description": "Your personal content curation service", 5 | "developer": { 6 | "name": "Nicolas Carlier", 7 | "url": "http://ncarlier.github.io" 8 | }, 9 | "icons": [ 10 | { 11 | "src": "icons/icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "icons/icon-72x72.png", 17 | "sizes": "72x72", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "icons/icon-96x96.png", 22 | "sizes": "96x96", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "icons/icon-192x192.png", 32 | "sizes": "192x192", 33 | "type": "image/png" 34 | } 35 | ], 36 | "lang": "en_US", 37 | "display": "standalone", 38 | "background_color": "#ececec", 39 | "theme_color": "#2185d0" 40 | } 41 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/screenshot.png -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | import { Router, browserHistory } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import { useBasename } from 'history' 6 | 7 | import createStore from 'store/createStore' 8 | import makeRoutes from './Routes' 9 | 10 | import './styles/main.css' 11 | import 'semantic-ui-css/semantic.min.css' 12 | import 'nprogress/nprogress.css' 13 | 14 | import * as NProgress from 'nprogress' 15 | 16 | NProgress.configure({ showSpinner: false }) 17 | 18 | // ======================================================== 19 | // Store and History Instantiation 20 | // ======================================================== 21 | // Create redux store and sync with react-router-redux. We have installed the 22 | // react-router-redux reducer under the routerKey "router" in src/routes/index.js, 23 | // so we need to provide a custom `selectLocationState` to inform 24 | // react-router-redux of its location. 25 | const basename = process.env.PUBLIC_URL || '' 26 | const basenameHistory = useBasename(() => browserHistory)({ basename }) 27 | const store = createStore({}, basenameHistory) 28 | const history = syncHistoryWithStore(basenameHistory, store, { 29 | selectLocationState: (state) => state.router 30 | }) 31 | const routes = makeRoutes(store) 32 | 33 | class App extends Component { 34 | render () { 35 | return ( 36 | 37 | 38 | {routes} 39 | 40 | 41 | ) 42 | } 43 | } 44 | 45 | export default App 46 | 47 | -------------------------------------------------------------------------------- /src/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, IndexRedirect, Redirect } from 'react-router' 3 | 4 | import RootLayout from 'layouts/RootLayout' 5 | import MainLayout from 'layouts/MainLayout' 6 | import PublicLayout from 'layouts/PublicLayout' 7 | import LabelView from 'views/LabelView' 8 | import ShareLabelView from 'views/ShareLabelView' 9 | import DocumentView from 'views/DocumentView' 10 | import LabelDocumentsView from 'views/LabelDocumentsView' 11 | import SharedDocumentsView from 'views/SharedDocumentsView' 12 | import PublicDocumentsView from 'views/PublicDocumentsView' 13 | import PublicDocumentView from 'views/PublicDocumentView' 14 | import DocumentsView from 'views/DocumentsView' 15 | import BookmarkletView from 'views/BookmarkletView' 16 | import GraveyardView from 'views/GraveyardView' 17 | import SettingsView from 'views/SettingsView' 18 | import WebhookView from 'views/WebhookView' 19 | import ApiClientView from 'views/ApiClientView' 20 | import SharingListView from 'views/SharingListView' 21 | import AboutView from 'views/AboutView' 22 | 23 | import { requireAuthentication } from 'middlewares/Authentication' 24 | 25 | import { 26 | createNewDocument, 27 | fetchDocument, 28 | fetchDocuments, 29 | fetchLabel, 30 | fetchLabelAndSharing, 31 | fetchLabelAndDocument, 32 | fetchLabelAndDocuments, 33 | fetchSharedDocuments, 34 | fetchSharedDocument, 35 | fetchPublicDocuments, 36 | fetchPublicDocument, 37 | fetchSharing, 38 | fetchGraveyard 39 | } from 'middlewares/Context' 40 | 41 | const WebhookCreateView = props => 42 | const WebhookEditView = props => 43 | 44 | const ApiClientCreateView = props => 45 | const ApiClientEditView = props => 46 | 47 | export default (store) => ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | -------------------------------------------------------------------------------- /src/api/client.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class ClientApi extends AbstractApi { 4 | all (params) { 5 | return this.fetch('/clients') 6 | } 7 | 8 | get (id) { 9 | return this.fetch(`/clients/${id}`) 10 | } 11 | 12 | create (client) { 13 | return this.fetch('/clients', { 14 | method: 'post', 15 | body: JSON.stringify(client) 16 | }) 17 | } 18 | 19 | update (client, update) { 20 | return this.fetch(`/clients/${client.id}`, { 21 | method: 'put', 22 | body: JSON.stringify(update) 23 | }) 24 | } 25 | 26 | remove (client) { 27 | return this.fetch(`/clients/${client.id}`, { 28 | method: 'delete' 29 | }) 30 | } 31 | } 32 | 33 | const instance = new ClientApi() 34 | export default instance 35 | -------------------------------------------------------------------------------- /src/api/common/AbstractApi.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import authProvider from 'helpers/AuthProvider' 3 | import 'event-source-polyfill' 4 | 5 | export default class AbstractApi { 6 | constructor () { 7 | this.firstCall = true 8 | this.apiRoot = process.env.REACT_APP_API_ROOT 9 | } 10 | 11 | buildQueryString (query) { 12 | if (query) { 13 | const params = Object.keys(query).reduce((acc, key) => { 14 | if (query.hasOwnProperty(key) && query[key] != null) { 15 | acc.push( 16 | encodeURIComponent(key) + '=' + encodeURIComponent(query[key]) 17 | ) 18 | } 19 | return acc 20 | }, []) 21 | return params.length ? '?' + params.join('&') : '' 22 | } else { 23 | return '' 24 | } 25 | } 26 | 27 | resolveUrl (url, query) { 28 | return this.apiRoot + url + this.buildQueryString(query) 29 | } 30 | 31 | sse (url, params) { 32 | params = Object.assign({ 33 | headers: { 34 | Accept: 'application/json' 35 | }, 36 | credentials: 'include' 37 | }, params) 38 | const {headers, query} = params 39 | let authz = Promise.resolve() 40 | if (params.credentials !== 'none') { 41 | authz = authProvider.updateToken().then((updated) => { 42 | if (updated || this.firstCall) { 43 | // Token was updated or it's the first API call. 44 | // Authorization header is set in order to update the API cookie. 45 | headers['Authorization'] = `Bearer ${authProvider.getToken()}` 46 | this.firstCall = false 47 | } 48 | return Promise.resolve() 49 | }, (err) => { 50 | // Fatal error from keycloak server. Mainly due to CORS. 51 | // Forced to reload the page. 52 | // FIXME Find a better way to handle Keycloak errors. 53 | console.error('Fatal error when updating the token', err) 54 | location.reload() 55 | }) 56 | } 57 | 58 | const _url = this.resolveUrl(url, query) 59 | return authz.then(() => { 60 | const source = new EventSource(_url, {headers, withCredentials: params.credentials !== 'none'}) 61 | return Promise.resolve(source) 62 | }) 63 | } 64 | 65 | fetch (url, params) { 66 | params = Object.assign({ 67 | method: 'get', 68 | headers: { 69 | Accept: 'application/json' 70 | }, 71 | credentials: 'include' 72 | }, params) 73 | const {method, body, headers, query} = params 74 | let {credentials} = params 75 | if (method === 'post' || method === 'put' || method === 'patch') { 76 | headers['Content-Type'] = 'application/json' 77 | } 78 | 79 | let authz = Promise.resolve() 80 | if (credentials !== 'none') { 81 | authz = authProvider.updateToken().then((updated) => { 82 | if (updated || this.firstCall) { 83 | // Token was updated or it's the first API call. 84 | // Authorization header is set in order to update the API cookie. 85 | headers['Authorization'] = `Bearer ${authProvider.getToken()}` 86 | this.firstCall = false 87 | } 88 | return Promise.resolve() 89 | }, (err) => { 90 | // Fatal error from keycloak server. Mainly due to CORS. 91 | // Forced to reload the page. 92 | // FIXME Find a better way to handle Keycloak errors. 93 | console.error('Fatal error when updating the token', err) 94 | location.reload() 95 | }) 96 | } else { 97 | credentials = undefined 98 | } 99 | 100 | const _url = this.resolveUrl(url, query) 101 | return authz.then(() => fetch(_url, {method, body, headers, credentials})) 102 | .then(response => { 103 | if (response.status === 204 || response.status === 205) { 104 | return Promise.resolve() 105 | } else if (response.status >= 200 && response.status < 300) { 106 | return response.json() 107 | } else { 108 | return response.json().then(err => Promise.reject(err)) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/api/document.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class DocumentApi extends AbstractApi { 4 | search (params) { 5 | const {from, size, order, label} = params 6 | let {q} = params 7 | if (label && q) { 8 | q = `labels:${label} AND ${q}` 9 | } else if (label) { 10 | q = `labels:${label}` 11 | } 12 | return this.fetch('/documents', { 13 | query: {q, from, size, order} 14 | }) 15 | } 16 | 17 | searchShared (params) { 18 | const {q, from, size, order, sharingId} = params 19 | return this.fetch(`/sharing/${sharingId}`, { 20 | query: {q, from, size, order} 21 | }) 22 | } 23 | 24 | searchPublic (params) { 25 | const {q, from, size, order, sharingId} = params 26 | return this.fetch(`/public/${sharingId}`, { 27 | query: {q, from, size, order}, 28 | credentials: 'none' 29 | }) 30 | } 31 | 32 | get (id) { 33 | return this.fetch(`/documents/${id}`) 34 | } 35 | 36 | getShared (id, sharingId) { 37 | return this.fetch(`/sharing/${sharingId}/${id}`) 38 | } 39 | 40 | getPublic (id, sharingId) { 41 | return this.fetch(`/public/${sharingId}/${id}`, { 42 | credentials: 'none' 43 | }) 44 | } 45 | 46 | create (doc) { 47 | return this.fetch('/documents', { 48 | method: 'post', 49 | body: JSON.stringify(doc) 50 | }) 51 | } 52 | 53 | update (doc, update) { 54 | return this.fetch(`/documents/${doc.id}`, { 55 | method: 'put', 56 | body: JSON.stringify(update) 57 | }) 58 | } 59 | 60 | remove (doc) { 61 | return this.fetch(`/documents/${doc.id}`, { 62 | method: 'delete' 63 | }) 64 | } 65 | 66 | restore (doc) { 67 | return this.fetch(`/graveyard/documents/${doc.id}`, { 68 | method: 'put' 69 | }) 70 | } 71 | } 72 | 73 | const instance = new DocumentApi() 74 | export default instance 75 | -------------------------------------------------------------------------------- /src/api/export.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class ExportApi extends AbstractApi { 4 | 5 | getStatus (onProgress) { 6 | return this.sse('/exports/status', {}) 7 | .then(source => { 8 | return new Promise((resolve, reject) => { 9 | source.addEventListener('progress', evt => { 10 | // console.log('EventSource data:', evt.data) 11 | onProgress(evt.data) 12 | }, false) 13 | source.addEventListener('complete', evt => { 14 | source.close() 15 | return resolve(evt.data) 16 | }, false) 17 | source.addEventListener('error', (evt) => { 18 | console.log('EventSource error:', evt) 19 | source.close() 20 | return reject(evt.data || 'Unable to get export status') 21 | }, false) 22 | }) 23 | }) 24 | } 25 | 26 | schedule () { 27 | return this.fetch('/exports', { 28 | method: 'post' 29 | }) 30 | } 31 | 32 | getDownloadUrl () { 33 | return this.resolveUrl('/exports') 34 | } 35 | } 36 | 37 | const instance = new ExportApi() 38 | export default instance 39 | -------------------------------------------------------------------------------- /src/api/graveyard.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class GraveyardApi extends AbstractApi { 4 | search (params) { 5 | const {from, size, order, label} = params 6 | let {q} = params 7 | if (label && q) { 8 | q = `labels:${label} AND ${q}` 9 | } else if (label) { 10 | q = `labels:${label}` 11 | } 12 | return this.fetch('/graveyard/documents', { 13 | query: {q, from, size, order} 14 | }) 15 | } 16 | 17 | empty () { 18 | return this.fetch('/graveyard/documents', { 19 | method: 'delete' 20 | }) 21 | } 22 | 23 | remove (doc) { 24 | return this.fetch(`/graveyard/documents/${doc.id}`, { 25 | method: 'delete' 26 | }) 27 | } 28 | } 29 | 30 | const instance = new GraveyardApi() 31 | export default instance 32 | -------------------------------------------------------------------------------- /src/api/label.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class LabelApi extends AbstractApi { 4 | all (params) { 5 | return this.fetch('/labels') 6 | } 7 | 8 | get (id) { 9 | return this.fetch(`/labels/${id}`) 10 | } 11 | 12 | create (label) { 13 | return this.fetch('/labels', { 14 | method: 'post', 15 | body: JSON.stringify(label) 16 | }) 17 | } 18 | 19 | update (label, update) { 20 | return this.fetch(`/labels/${label.id}`, { 21 | method: 'put', 22 | body: JSON.stringify(update) 23 | }) 24 | } 25 | 26 | remove (label) { 27 | return this.fetch(`/labels/${label.id}`, { 28 | method: 'delete' 29 | }) 30 | } 31 | 32 | restore (label) { 33 | return this.fetch(`/graveyard/labels/${label.id}`, { 34 | method: 'put' 35 | }) 36 | } 37 | } 38 | 39 | const instance = new LabelApi() 40 | export default instance 41 | -------------------------------------------------------------------------------- /src/api/profile.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class ProfileApi extends AbstractApi { 4 | get (query) { 5 | return this.fetch('/profiles/current', {query}) 6 | } 7 | 8 | update (update) { 9 | return this.fetch('/profiles/current', { 10 | method: 'put', 11 | body: JSON.stringify(update) 12 | }) 13 | } 14 | 15 | } 16 | 17 | const instance = new ProfileApi() 18 | export default instance 19 | -------------------------------------------------------------------------------- /src/api/sharing.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class SharingApi extends AbstractApi { 4 | all (params) { 5 | return this.fetch('/sharing') 6 | } 7 | 8 | get (label) { 9 | return this.fetch(`/labels/${label.id}/sharing`) 10 | } 11 | 12 | create (label, sharing) { 13 | return this.fetch(`/labels/${label.id}/sharing`, { 14 | method: 'post', 15 | body: JSON.stringify(sharing) 16 | }) 17 | } 18 | 19 | update (label, update) { 20 | return this.fetch(`/labels/${label.id}/sharing`, { 21 | method: 'put', 22 | body: JSON.stringify(update) 23 | }) 24 | } 25 | 26 | remove (label) { 27 | return this.fetch(`/labels/${label.id}/sharing`, { 28 | method: 'delete' 29 | }) 30 | } 31 | } 32 | 33 | const instance = new SharingApi() 34 | export default instance 35 | -------------------------------------------------------------------------------- /src/api/webhook.js: -------------------------------------------------------------------------------- 1 | import AbstractApi from 'api/common/AbstractApi' 2 | 3 | export class WebhookApi extends AbstractApi { 4 | all (params) { 5 | return this.fetch('/webhooks') 6 | } 7 | 8 | get (id) { 9 | return this.fetch(`/webhooks/${id}`) 10 | } 11 | 12 | create (webhook) { 13 | return this.fetch('/webhooks', { 14 | method: 'post', 15 | body: JSON.stringify(webhook) 16 | }) 17 | } 18 | 19 | update (webhook, update) { 20 | return this.fetch(`/webhooks/${webhook.id}`, { 21 | method: 'put', 22 | body: JSON.stringify(update) 23 | }) 24 | } 25 | 26 | remove (webhook) { 27 | return this.fetch(`/webhooks/${webhook.id}`, { 28 | method: 'delete' 29 | }) 30 | } 31 | } 32 | 33 | const instance = new WebhookApi() 34 | export default instance 35 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/src/components/.gitkeep -------------------------------------------------------------------------------- /src/components/AppBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { bindActions } from 'store/helper' 5 | 6 | import { routerActions } from 'react-router-redux' 7 | import { actions as layoutActions } from 'store/modules/layout' 8 | 9 | import { Sizes } from 'store/modules/layout' 10 | 11 | import { Menu } from 'semantic-ui-react' 12 | 13 | import './styles.css' 14 | 15 | export class AppBar extends React.Component { 16 | static propTypes = { 17 | actions: PropTypes.object.isRequired, 18 | children: PropTypes.node, 19 | title: PropTypes.node, 20 | hideTitleOnMobile: PropTypes.bool, 21 | modal: PropTypes.bool, 22 | styles: PropTypes.object, 23 | location: PropTypes.object.isRequired, 24 | layout: PropTypes.object.isRequired 25 | }; 26 | 27 | static defaultProps = { 28 | title: '', 29 | hideTitleOnMobile: false, 30 | modal: false 31 | }; 32 | 33 | constructor (props) { 34 | super(props) 35 | this.handleCloseClick = this.handleCloseClick.bind(this) 36 | this.handleMenuClick = this.handleMenuClick.bind(this) 37 | } 38 | 39 | get sidebarIcon () { 40 | const { modal } = this.props 41 | if (modal) { 42 | return ( 43 | 44 | ) 45 | } else { 46 | const { layout } = this.props 47 | if (layout.size < Sizes.LARGE) { 48 | return ( 49 | 50 | ) 51 | } 52 | } 53 | } 54 | 55 | get title () { 56 | const { hideTitleOnMobile, title, layout } = this.props 57 | if (layout.size === Sizes.SMALL && hideTitleOnMobile) { 58 | return null 59 | } 60 | return ( 61 | 62 | {title} 63 | 64 | ) 65 | } 66 | 67 | render () { 68 | const { children, styles } = this.props 69 | return ( 70 | 71 | {this.sidebarIcon} 72 | {this.title} 73 | {children} 74 | 75 | ) 76 | } 77 | 78 | handleCloseClick () { 79 | const {actions, location} = this.props 80 | if (location.state.returnTo) { 81 | const {pathname, search} = location.state.returnTo 82 | actions.router.push({ 83 | pathname: pathname, 84 | search: search, 85 | state: { 86 | backFromModal: true 87 | } 88 | }) 89 | } 90 | } 91 | 92 | handleMenuClick () { 93 | const {actions} = this.props 94 | actions.layout.toggleSidebar() 95 | } 96 | } 97 | 98 | const mapStateToProps = (state) => ({ 99 | label: state.label, 100 | location: state.router.locationBeforeTransitions, 101 | layout: state.layout 102 | }) 103 | 104 | const mapActionsToProps = (dispatch) => (bindActions({ 105 | router: routerActions, 106 | layout: layoutActions 107 | }, dispatch)) 108 | 109 | export default connect(mapStateToProps, mapActionsToProps)(AppBar) 110 | -------------------------------------------------------------------------------- /src/components/AppBar/styles.css: -------------------------------------------------------------------------------- 1 | 2 | #AppBar .item { 3 | border-right: 1px solid rgba(255, 255, 255, 0.2); 4 | } 5 | 6 | #AppBar > .item.title { 7 | flex: 0 1 auto; 8 | white-space: nowrap; 9 | overflow-x: overlay; 10 | } 11 | 12 | #AppBar > .right.menu { 13 | flex: 1 1 auto; 14 | justify-content: flex-end; 15 | margin-left: 0 !important; 16 | } 17 | 18 | #AppBar .item.search { 19 | flex: 1 0 auto; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/components/AppKeyboardHandlers/index.jsx: -------------------------------------------------------------------------------- 1 | /*eslint new-cap: ["error", { "newIsCap": false }]*/ 2 | 3 | import React, { PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | import { routerActions as RouterActions } from 'react-router-redux' 6 | import * as Mousetrap from 'mousetrap' 7 | 8 | import { bindActions } from 'store/helper' 9 | 10 | export class AppKeyboardHandlers extends React.Component { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | actions: PropTypes.object.isRequired 14 | }; 15 | 16 | mousetrap = new Mousetrap.default(); 17 | 18 | goto = (pathname) => this.props.actions.router.push({pathname}); 19 | 20 | gotoDocuments = (e) => this.goto('/documents'); 21 | gotoTrash = (e) => this.goto('/trash'); 22 | gotoSharing = (e) => this.goto('/sharing'); 23 | gotoSettings = (e) => this.goto('/settings'); 24 | gotoAbout = (e) => this.goto('/about'); 25 | 26 | componentDidMount () { 27 | this.mousetrap.bind(['g d'], this.gotoDocuments) 28 | this.mousetrap.bind(['g t'], this.gotoTrash) 29 | this.mousetrap.bind(['g r'], this.gotoSharing) 30 | this.mousetrap.bind(['g s'], this.gotoSettings) 31 | this.mousetrap.bind(['g a'], this.gotoAbout) 32 | } 33 | 34 | componentWillUnmount () { 35 | this.mousetrap.unbind(['g d'], this.gotoDocuments) 36 | this.mousetrap.unbind(['g t'], this.gotoTrash) 37 | this.mousetrap.unbind(['g r'], this.gotoSharing) 38 | this.mousetrap.unbind(['g s'], this.gotoSettings) 39 | this.mousetrap.unbind(['g a'], this.gotoAbout) 40 | } 41 | 42 | render () { 43 | return (this.props.children) 44 | } 45 | } 46 | 47 | const mapActionsToProps = (dispatch) => (bindActions({ 48 | router: RouterActions 49 | }, dispatch)) 50 | 51 | export default connect(null, mapActionsToProps)(AppKeyboardHandlers) 52 | 53 | -------------------------------------------------------------------------------- /src/components/AppMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActions } from 'store/helper' 4 | import { Menu, Header, Icon } from 'semantic-ui-react' 5 | import { Link } from 'react-router' 6 | 7 | import labelsActions from 'store/labels/actions' 8 | import { actions as layoutActions } from 'store/modules/layout' 9 | 10 | import { Sizes } from 'store/modules/layout' 11 | 12 | import ProfilePanel from 'components/ProfilePanel' 13 | 14 | import './styles.css' 15 | 16 | export class AppMenu extends React.Component { 17 | static propTypes = { 18 | actions: PropTypes.object.isRequired, 19 | labels: PropTypes.object.isRequired, 20 | layout: PropTypes.object.isRequired, 21 | location: PropTypes.object.isRequired 22 | }; 23 | 24 | constructor () { 25 | super() 26 | this.handleItemClick = this.handleItemClick.bind(this) 27 | } 28 | 29 | componentDidMount () { 30 | const { actions } = this.props 31 | actions.labels.fetchLabels() 32 | } 33 | 34 | get spinner () { 35 | const { isProcessing } = this.props.labels 36 | if (isProcessing) { 37 | return ( 38 |
39 |
40 |
41 | ) 42 | } 43 | } 44 | 45 | get labels () { 46 | const { isProcessing, current } = this.props.labels 47 | if (isProcessing && current.labels.length === 0) { 48 | return ( 49 | 50 | 51 | 52 | ) 53 | } 54 | return current.labels.map( 55 | (label) => 62 | {this.getLabelIcon(label)} 63 | {label.label} 64 | 65 | ) 66 | } 67 | 68 | getLabelIcon (label) { 69 | if (label.sharing) { 70 | return ( 71 | 72 | 73 | 74 | 75 | ) 76 | } else { 77 | return ( 78 | 79 | ) 80 | } 81 | } 82 | 83 | handleItemClick (event) { 84 | // console.log(event) 85 | const { actions, layout } = this.props 86 | if (layout.size < Sizes.LARGE) { 87 | actions.layout.toggleSidebar() 88 | } 89 | } 90 | 91 | render () { 92 | const { 93 | location 94 | } = this.props 95 | 96 | return ( 97 | 98 | 99 |
100 | 101 | Nunux Keeper 102 |
103 | 104 |
105 | 110 | 111 | Documents 112 | 113 | 114 | 115 | 116 | Labels 117 | 121 | 122 | 123 | 124 | 125 | {this.labels} 126 | 127 | 128 | 133 | 134 | Sharing 135 | 136 | 141 | 142 | Trash 143 | 144 | 149 | 150 | Settings 151 | 152 | 157 | 158 | About 159 | 160 |
161 | ) 162 | } 163 | } 164 | 165 | const mapStateToProps = (state) => ({ 166 | location: state.router.locationBeforeTransitions, 167 | labels: state.labels, 168 | layout: state.layout 169 | }) 170 | 171 | const mapActionsToProps = (dispatch) => (bindActions({ 172 | labels: labelsActions, 173 | layout: layoutActions 174 | }, dispatch)) 175 | 176 | export default connect(mapStateToProps, mapActionsToProps)(AppMenu) 177 | -------------------------------------------------------------------------------- /src/components/AppMenu/styles.css: -------------------------------------------------------------------------------- 1 | 2 | #AppMenu { 3 | border: none; 4 | border-radius: 0; 5 | } 6 | 7 | #AppMenu .header { 8 | font-weight: normal; 9 | } 10 | 11 | #AppMenu > .header.item { 12 | background: #2185d0; 13 | color: rgba(255,255,255,.9); 14 | margin: 0; 15 | padding: 1em 0; 16 | border-radius: 0; 17 | } 18 | 19 | #AppMenu > .header.item h2 { 20 | width: 100%; 21 | color: rgba(255,255,255,.5); 22 | font-variant: small-caps; 23 | font-weight: bold; 24 | } 25 | 26 | #AppMenu .header > a { 27 | float: right; 28 | color: inherit; 29 | } 30 | 31 | #AppMenu .tags, 32 | #AppMenu .menu a > .icon, #AppMenu .menu a > .icons, 33 | #AppMenu > a.item > .icon, #AppMenu > a.item > .icons { 34 | margin: 0 .5em 0 0; 35 | float: none; 36 | font-size: 1.2em; 37 | width: 1em; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/components/AppModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { bindActions } from 'store/helper' 5 | 6 | import { routerActions as RouterActions } from 'react-router-redux' 7 | 8 | import Modal from 'react-modal' 9 | 10 | import { Sizes } from 'store/modules/layout' 11 | 12 | import './styles.css' 13 | 14 | const customStyles = { 15 | overlay: { 16 | backgroundColor: 'rgba(0, 0, 0, 0.90)' 17 | }, 18 | content: { 19 | padding: 0, 20 | right: 'initial', 21 | left: 'calc(50% - 450px)', 22 | width: '900px', 23 | border: '1px solid #4c4c4c' 24 | } 25 | } 26 | 27 | const customMobileStyles = { 28 | content: { 29 | top: 0, 30 | bottom: 0, 31 | left: 0, 32 | right: 0, 33 | padding: 0, 34 | border: 'none', 35 | borderRadius: 'initial' 36 | } 37 | } 38 | 39 | export class AppModal extends React.Component { 40 | static propTypes = { 41 | actions: PropTypes.object.isRequired, 42 | children: PropTypes.node.isRequired, 43 | returnTo: PropTypes.object, 44 | layout: PropTypes.object.isRequired 45 | }; 46 | 47 | constructor (props) { 48 | super(props) 49 | this.handleClose = this.handleClose.bind(this) 50 | } 51 | 52 | handleClose () { 53 | const { returnTo, actions } = this.props 54 | if (returnTo) { 55 | actions.router.push({ 56 | pathname: returnTo.pathname, 57 | search: returnTo.search, 58 | state: { 59 | backFromModal: true 60 | } 61 | }) 62 | } 63 | } 64 | 65 | render () { 66 | const { children, layout } = this.props 67 | const styles = layout.size < Sizes.LARGE ? customMobileStyles : customStyles 68 | return ( 69 | 74 | {children} 75 | 76 | ) 77 | } 78 | } 79 | 80 | const mapStateToProps = (state) => ({ 81 | layout: state.layout 82 | }) 83 | 84 | const mapActionsToProps = (dispatch) => (bindActions({ 85 | router: RouterActions 86 | }, dispatch)) 87 | 88 | export default connect(mapStateToProps, mapActionsToProps)(AppModal) 89 | 90 | -------------------------------------------------------------------------------- /src/components/AppModal/styles.css: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | -webkit-perspective: 600; 3 | perspective: 600; 4 | opacity: 0; 5 | z-index: 5; 6 | } 7 | 8 | .ReactModal__Overlay--after-open { 9 | opacity: 1; 10 | transition: opacity 150ms ease-out; 11 | } 12 | 13 | .ReactModal__Content { 14 | -webkit-transform: scale(0.5) rotateX(-30deg); 15 | transform: scale(0.5) rotateX(-30deg); 16 | } 17 | 18 | .ReactModal__Content--after-open { 19 | -webkit-transform: scale(1) rotateX(0deg); 20 | transform: scale(1) rotateX(0deg); 21 | transition: all 150ms ease-in; 22 | } 23 | 24 | .ReactModal__Overlay--before-close { 25 | opacity: 0; 26 | } 27 | 28 | .ReactModal__Content--before-close { 29 | -webkit-transform: scale(0.5) rotateX(30deg); 30 | transform: scale(0.5) rotateX(30deg); 31 | transition: all 150ms ease-in; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/components/AppNotification/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { actions as notificationActions } from 'store/modules/notification' 5 | import { Message, Button } from 'semantic-ui-react' 6 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group' 7 | 8 | import './styles.css' 9 | 10 | export class AppNotification extends React.Component { 11 | static propTypes = { 12 | layout: PropTypes.object.isRequired, 13 | notification: PropTypes.object.isRequired, 14 | hideNotification: PropTypes.func.isRequired 15 | }; 16 | 17 | constructor () { 18 | super() 19 | this.handleAction = this.handleAction.bind(this) 20 | this.handleClose = this.handleClose.bind(this) 21 | this.timeout = null 22 | } 23 | 24 | componentDidUpdate (prevProps, prevState) { 25 | const { level, visible } = this.props.notification 26 | if (visible !== prevProps.notification.visible) { 27 | if (this.timeout) { 28 | clearTimeout(this.timeout) 29 | this.timeout = null 30 | } 31 | if (level !== 'error' && visible) { 32 | this.timeout = setTimeout(this.handleClose, 5000) 33 | } 34 | } 35 | } 36 | 37 | get header () { 38 | const { header } = this.props.notification 39 | if (header) { 40 | return ( 41 | {header} 42 | ) 43 | } 44 | } 45 | 46 | get actionButton () { 47 | const { actionLabel } = this.props.notification 48 | if (actionLabel) { 49 | return ( 50 | 80 | 83 | 84 | 85 | ) 86 | } 87 | 88 | handleChange (event, {name, value}) { 89 | this.setState({[name]: value}) 90 | } 91 | 92 | handleClose () { 93 | const { actions } = this.props 94 | actions.titleModal.hideTitleModal() 95 | } 96 | 97 | handleSubmit (e) { 98 | e.preventDefault() 99 | if (!this.isValidTitle) { 100 | return false 101 | } 102 | const { actions, modal } = this.props 103 | actions.document.updateDocument(modal.doc, this.state) 104 | .then(() => this.handleClose(), (err) => { 105 | this.setState({err}) 106 | }) 107 | } 108 | } 109 | 110 | const mapStateToProps = (state) => ({ 111 | modal: state.titleModal, 112 | doc: state.document 113 | }) 114 | 115 | const mapActionsToProps = (dispatch) => (bindActions({ 116 | document: DocumentActions, 117 | titleModal: TitleModalActions 118 | }, dispatch)) 119 | 120 | export default connect(mapStateToProps, mapActionsToProps)(DocumentTitleModal) 121 | -------------------------------------------------------------------------------- /src/components/DocumentUrlModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Form, Button, Modal, Header, Message } from 'semantic-ui-react' 4 | 5 | import { bindActions } from 'store/helper' 6 | 7 | import { routerActions as RouterActions } from 'react-router-redux' 8 | import { actions as DocumentActions } from 'store/modules/document' 9 | import { actions as UrlModalActions } from 'store/modules/urlModal' 10 | 11 | export class DocumentUrlModal extends React.Component { 12 | static propTypes = { 13 | modal: PropTypes.object.isRequired, 14 | actions: PropTypes.object.isRequired, 15 | location: PropTypes.object.isRequired, 16 | doc: PropTypes.object.isRequired 17 | }; 18 | 19 | constructor (props) { 20 | super(props) 21 | this.handleSubmit = this.handleSubmit.bind(this) 22 | this.handleClose = this.handleClose.bind(this) 23 | this.handleChange = this.handleChange.bind(this) 24 | this.state = { 25 | url: '', 26 | method: 'default' 27 | } 28 | } 29 | 30 | get isValidUrl () { 31 | return this.state.url !== '' 32 | } 33 | 34 | get urlForm () { 35 | const { doc: {isProcessing} } = this.props 36 | const error = this.state.err ? this.state.err.error : null 37 | return ( 38 |
39 | 44 | 54 | 55 | 56 | 57 | 63 | 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | render () { 76 | const { open } = this.props.modal 77 | const disabled = !this.isValidUrl 78 | return ( 79 | 83 |
84 | 85 | {this.urlForm} 86 | 87 | 88 | 91 | 94 | 95 | 96 | ) 97 | } 98 | 99 | handleChange (event, {name, value}) { 100 | this.setState({[name]: value}) 101 | } 102 | 103 | handleClose () { 104 | const { actions } = this.props 105 | actions.urlModal.hideUrlModal() 106 | } 107 | 108 | handleSubmit (e) { 109 | e.preventDefault() 110 | if (!this.isValidUrl) { 111 | return false 112 | } 113 | 114 | const {url, method} = this.state 115 | const u = method !== 'default' ? `${method}+${url}` : url 116 | 117 | const {actions} = this.props 118 | actions.document.createDocument({origin: u}) 119 | .then((doc) => { 120 | actions.router.push({ 121 | pathname: `/document/${doc.id}` 122 | // FIXME When modal documents view is crushed by the create view 123 | // state: { modal: true, returnTo: location } 124 | }) 125 | actions.urlModal.hideUrlModal() 126 | }, (err) => { 127 | this.setState({err}) 128 | }) 129 | } 130 | } 131 | 132 | const mapStateToProps = (state) => ({ 133 | location: state.router.locationBeforeTransitions, 134 | modal: state.urlModal, 135 | doc: state.document 136 | }) 137 | 138 | const mapActionsToProps = (dispatch) => (bindActions({ 139 | document: DocumentActions, 140 | router: RouterActions, 141 | urlModal: UrlModalActions 142 | }, dispatch)) 143 | 144 | export default connect(mapStateToProps, mapActionsToProps)(DocumentUrlModal) 145 | -------------------------------------------------------------------------------- /src/components/InfiniteGrid/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | export default class InfiniteGrid extends React.Component { 4 | static propTypes = { 5 | size: PropTypes.string, 6 | hasMore: PropTypes.bool, 7 | loadMore: PropTypes.func.isRequired, 8 | children: PropTypes.node, 9 | threshold: PropTypes.number 10 | }; 11 | 12 | static defaultProps = { 13 | size: 'five', 14 | hasMore: false, 15 | threshold: 50 16 | }; 17 | 18 | constructor () { 19 | super() 20 | this.scrollListener = this.scrollListener.bind(this) 21 | this.state = { 22 | listening: false 23 | } 24 | } 25 | 26 | componentDidMount () { 27 | // console.debug('InfiniteGrid::componentDidMount', this.state) 28 | this.attachScrollListener() 29 | } 30 | 31 | componentWillUnmount () { 32 | // console.debug('InfiniteGrid::componentWillUnmount', this.state) 33 | this.detachScrollListener() 34 | } 35 | 36 | componentDidUpdate (prevProps) { 37 | if (!prevProps.hasMore && this.props.hasMore) { 38 | // console.debug('InfiniteGrid::componentDidUpdate', this.state) 39 | this.attachScrollListener() 40 | } 41 | } 42 | 43 | render () { 44 | const {children, size} = this.props 45 | return ( 46 |
47 | {children} 48 |
49 | ) 50 | } 51 | 52 | scrollListener () { 53 | const $el = this.refs.grid 54 | const delta = $el.parentElement.scrollHeight - $el.parentElement.scrollTop - $el.parentElement.offsetHeight 55 | // console.debug('scroll delta: ', delta) 56 | if (delta < Number(this.props.threshold)) { 57 | this.detachScrollListener() 58 | this.props.loadMore().then(this.attachScrollListener.bind(this)) 59 | } 60 | } 61 | 62 | attachScrollListener () { 63 | if (!this.state.listening && this.props.hasMore) { 64 | console.debug('InfiniteGrid::attachScrollListener', this.state) 65 | this.setState({listening: true}) 66 | const $el = this.refs.grid 67 | $el.parentElement.addEventListener('scroll', this.scrollListener) 68 | $el.parentElement.addEventListener('resize', this.scrollListener) 69 | } 70 | } 71 | 72 | detachScrollListener () { 73 | console.debug('InfiniteGrid::detachScrollListener', this.state) 74 | const $el = this.refs.grid 75 | $el.parentElement.removeEventListener('scroll', this.scrollListener) 76 | $el.parentElement.removeEventListener('resize', this.scrollListener) 77 | this.setState({listening: false}) 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/components/KeymapHelpModal/index.jsx: -------------------------------------------------------------------------------- 1 | /*eslint new-cap: ["error", { "newIsCap": false }]*/ 2 | 3 | import React from 'react' 4 | import * as Mousetrap from 'mousetrap' 5 | 6 | import { Icon, Button, Modal, Header } from 'semantic-ui-react' 7 | 8 | import './styles.css' 9 | 10 | export default class KeymapHelpModal extends React.Component { 11 | state = { isOpen: false }; 12 | 13 | mousetrap = new Mousetrap.default(); 14 | 15 | handleOpen = (e) => this.setState({ 16 | isOpen: true 17 | }); 18 | 19 | handleClose = (e) => this.setState({ 20 | isOpen: false 21 | }); 22 | 23 | componentDidMount () { 24 | this.mousetrap.bind(['?'], this.handleOpen) 25 | } 26 | componentWillUnmount () { 27 | this.mousetrap.unbind(['?'], this.handleOpen) 28 | } 29 | 30 | render () { 31 | return ( 32 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 |
Site wide shortcuts
46 | gd 47 | Go to Documents
52 | gt 53 | Go to Trash
58 | gr 59 | Go to Sharing
64 | gs 65 | Go to Settings
Documents
76 | r 77 | Refresh documents
82 | o 83 | Reverse sort order
88 | shift n 89 | Create new empty document
94 | shift u 95 | Create new document from an URL
100 |
101 | 102 | 105 | 106 | 107 | ) 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /src/components/KeymapHelpModal/styles.css: -------------------------------------------------------------------------------- 1 | .keyboard-mappings { 2 | line-height: 1.5; 3 | } 4 | 5 | .keyboard-mappings tr td:first-child { 6 | padding-right: 10px; 7 | color: #586069; 8 | text-align: right; 9 | white-space: nowrap; 10 | } 11 | 12 | .keyboard-mappings kbd { 13 | display: inline-block; 14 | padding: 3px 5px; 15 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 16 | line-height: 10px; 17 | color: #444d56; 18 | vertical-align: middle; 19 | background-color: #fcfcfc; 20 | border: solid 1px #c6cbd1; 21 | border-bottom-color: #959da5; 22 | border-radius: 3px; 23 | box-shadow: inset 0 -1px 0 #959da5; 24 | margin-right: 0.2em; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ProfilePanel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Image, Icon } from 'semantic-ui-react' 4 | import { default as Timeago } from 'timeago.js' 5 | 6 | import authProvider from 'helpers/AuthProvider' 7 | 8 | import './styles.css' 9 | 10 | class ProfilePanel extends React.Component { 11 | static propTypes = { 12 | profile: PropTypes.object.isRequired 13 | }; 14 | 15 | render () { 16 | const { current } = this.props.profile 17 | if (current) { 18 | const gravatar = `https://www.gravatar.com/avatar/${current.hash}` 19 | const memberAgo = new Timeago().format(current.date) 20 | const accountUrl = authProvider.getAccountUrl() 21 | return ( 22 |
23 | You on Gravatar 24 | 25 | {current.name} 26 | Member {memberAgo} 27 | 28 | 31 | 32 | 33 |
34 | ) 35 | } 36 | return
37 | } 38 | } 39 | 40 | const mapStateToProps = (state) => ({ 41 | profile: state.profile 42 | }) 43 | 44 | export default connect(mapStateToProps)(ProfilePanel) 45 | -------------------------------------------------------------------------------- /src/components/ProfilePanel/styles.css: -------------------------------------------------------------------------------- 1 | .ProfilePanel { 2 | padding: 0 1em; 3 | } 4 | 5 | .ProfilePanel a { 6 | color: rgba(255,255,255,.5); 7 | float: right; 8 | margin-top: 0.5em; 9 | } 10 | 11 | .ProfilePanel a:hover { 12 | color: white; 13 | } 14 | 15 | .ProfilePanel > span { 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | .ProfilePanel > span strong { 21 | display: block; 22 | } 23 | 24 | .ProfilePanel > span small { 25 | color: rgba(255,255,255,.5); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/components/SearchBarItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { routerActions } from 'react-router-redux' 5 | import { Menu, Input } from 'semantic-ui-react' 6 | 7 | export class SearchBarItem extends React.Component { 8 | static propTypes = { 9 | placeholder: PropTypes.string.isRequired, 10 | push: PropTypes.func, 11 | location: PropTypes.object.isRequired 12 | }; 13 | 14 | constructor (props) { 15 | super(props) 16 | this.handleChange = this.handleChange.bind(this) 17 | this.handleUpdateSearchQuery = this.handleUpdateSearchQuery.bind(this) 18 | 19 | const { query } = props.location 20 | this.state = { 21 | q: query ? query.q : '' 22 | } 23 | } 24 | 25 | handleChange (event) { 26 | this.setState({q: event.target.value}) 27 | } 28 | 29 | handleUpdateSearchQuery (e) { 30 | if (e.keyCode === 13) { 31 | const { location } = this.props 32 | this.props.push({ 33 | pathname: location.pathname, 34 | query: { 35 | q: e.target.value 36 | } 37 | }) 38 | } 39 | } 40 | 41 | render () { 42 | const {placeholder} = this.props 43 | return ( 44 | 45 | 52 | 53 | ) 54 | } 55 | } 56 | 57 | const mapStateToProps = (state) => ({ 58 | location: state.router.locationBeforeTransitions 59 | }) 60 | 61 | const mapDispatchToProps = (dispatch) => ( 62 | bindActionCreators(Object.assign({}, routerActions), dispatch) 63 | ) 64 | 65 | export default connect(mapStateToProps, mapDispatchToProps)(SearchBarItem) 66 | -------------------------------------------------------------------------------- /src/helpers/AuthProvider.js: -------------------------------------------------------------------------------- 1 | const keycloak = new window.Keycloak(process.env.PUBLIC_URL + '/keycloak.json') 2 | 3 | const facade = { 4 | init: () => { 5 | return new Promise((resolve, reject) => { 6 | keycloak 7 | .init({onLoad: 'check-sso'}) 8 | .success(resolve) 9 | .error(reject) 10 | }) 11 | }, 12 | updateToken: () => { 13 | return new Promise((resolve, reject) => { 14 | keycloak 15 | .updateToken(30) 16 | .success(resolve) 17 | .error(reject) 18 | }) 19 | }, 20 | getToken: () => keycloak.token, 21 | getAccountUrl: () => keycloak.createAccountUrl(), 22 | getLoginUrl: (params) => keycloak.createLoginUrl(params), 23 | getRealmUrl: () => `${keycloak.authServerUrl}/realms/${encodeURIComponent(keycloak.realm)}` 24 | } 25 | 26 | export default facade 27 | 28 | -------------------------------------------------------------------------------- /src/helpers/DateHelper.js: -------------------------------------------------------------------------------- 1 | 2 | export default class DateHelper { 3 | constructor (date) { 4 | this._date = date 5 | } 6 | 7 | static build (date = new Date()) { 8 | return new this(date) 9 | } 10 | 11 | addDays (days) { 12 | this._date.setDate(this._date.getDate() + days) 13 | return this 14 | } 15 | 16 | addHours (hours) { 17 | this._date.setHours(this._date.getHours() + hours) 18 | return this 19 | } 20 | 21 | get () { 22 | return this._date 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | function getParameterByName (name, url) { 6 | if (!url) { 7 | url = window.location.href 8 | } 9 | name = name.replace(/[\[\]]/g, '\\$&') 10 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') 11 | const results = regex.exec(url) 12 | if (!results) return null 13 | if (!results[2]) return '' 14 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) 15 | } 16 | 17 | // Redirect if require by the query params 18 | const redirect = getParameterByName('redirect') 19 | if (redirect) { 20 | console.log(`Redirecting to ${redirect} ...`) 21 | document.location.replace(redirect) 22 | } else { 23 | ReactDOM.render( 24 | , 25 | document.getElementById('root') 26 | ) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActions } from 'store/helper' 4 | 5 | import AppMenu from 'components/AppMenu' 6 | import AppModal from 'components/AppModal' 7 | import AppNotification from 'components/AppNotification' 8 | import AppKeyboardHandlers from 'components/AppKeyboardHandlers' 9 | import DocumentTitleModal from 'components/DocumentTitleModal' 10 | import DocumentUrlModal from 'components/DocumentUrlModal' 11 | import KeymapHelpModal from 'components/KeymapHelpModal' 12 | 13 | import { actions as layoutActions } from 'store/modules/layout' 14 | 15 | import { Sizes } from 'store/modules/layout' 16 | import { Sidebar } from 'semantic-ui-react' 17 | 18 | import './styles.css' 19 | 20 | export class MainLayout extends React.Component { 21 | static propTypes = { 22 | children: PropTypes.node, 23 | location: PropTypes.object, 24 | layout: PropTypes.object.isRequired, 25 | actions: PropTypes.object.isRequired 26 | }; 27 | 28 | constructor () { 29 | super() 30 | this.handleDimmerClick = this.handleDimmerClick.bind(this) 31 | } 32 | 33 | componentWillReceiveProps (nextProps) { 34 | if (nextProps.location !== this.props.location) { 35 | // if we changed routes... 36 | if ( 37 | nextProps.location.state && 38 | nextProps.location.state.modal 39 | ) { 40 | // save the old children (just like animation) 41 | this.previousChildren = this.props.children 42 | } else { 43 | this.previousChildren = null 44 | } 45 | } 46 | } 47 | 48 | shouldComponentUpdate (nextProps, nextState) { 49 | return nextProps.location !== this.props.location || 50 | nextProps.layout !== this.props.layout 51 | } 52 | 53 | handleDimmerClick (event) { 54 | // console.log(event) 55 | const { actions, layout } = this.props 56 | if (layout.size < Sizes.LARGE && layout.sidebar.visible) { 57 | actions.layout.toggleSidebar() 58 | } 59 | } 60 | 61 | renderModal () { 62 | if (this.previousChildren) { 63 | const { location, children } = this.props 64 | return ( 65 | 66 | {children} 67 | 68 | ) 69 | } 70 | } 71 | 72 | renderMobileLayout () { 73 | const { children, layout } = this.props 74 | 75 | return ( 76 | 77 | 78 | 79 | 80 | 81 |
82 | {this.previousChildren || children} 83 | {this.renderModal()} 84 | 85 | 86 | 87 | 88 |
89 |
90 |
91 | ) 92 | } 93 | 94 | renderDesktopLayout () { 95 | const { children } = this.props 96 | 97 | return ( 98 |
99 | 100 |
101 | {this.previousChildren || children} 102 | 103 | 104 | 105 | 106 |
107 | {this.renderModal()} 108 |
109 | ) 110 | } 111 | 112 | render () { 113 | const { layout } = this.props 114 | return ( 115 | 116 | {layout.size < Sizes.LARGE ? this.renderMobileLayout() : this.renderDesktopLayout()} 117 | 118 | ) 119 | } 120 | } 121 | 122 | const mapStateToProps = (state) => ({ 123 | location: state.router.locationBeforeTransitions, 124 | layout: state.layout 125 | }) 126 | 127 | const mapActionsToProps = (dispatch) => (bindActions({ 128 | layout: layoutActions 129 | }, dispatch)) 130 | 131 | export default connect(mapStateToProps, mapActionsToProps)(MainLayout) 132 | 133 | -------------------------------------------------------------------------------- /src/layouts/PublicLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | import './styles.css' 4 | 5 | const basename = process.env.PUBLIC_URL || '' 6 | 7 | export default class PublicLayout extends React.Component { 8 | static propTypes = { 9 | children: PropTypes.node 10 | }; 11 | 12 | render () { 13 | const { children } = this.props 14 | 15 | return ( 16 | 30 | ) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/layouts/RootLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { actions as layoutActions } from 'store/modules/layout' 4 | 5 | export class RootLayout extends React.Component { 6 | static propTypes = { 7 | children: PropTypes.node, 8 | resize: PropTypes.func 9 | }; 10 | 11 | componentDidMount () { 12 | const { resize } = this.props 13 | window.addEventListener('resize', resize) 14 | } 15 | 16 | componentWillUnmount () { 17 | const { resize } = this.props 18 | window.removeEventListener('resize', resize) 19 | } 20 | 21 | render () { 22 | return (this.props.children) 23 | } 24 | } 25 | 26 | export default connect(null, layoutActions)(RootLayout) 27 | 28 | -------------------------------------------------------------------------------- /src/layouts/styles.css: -------------------------------------------------------------------------------- 1 | #PublicLayout { 2 | background-color: #0c1d2b; 3 | } 4 | 5 | #PublicLayout h1 { 6 | text-align: center; 7 | padding: 1em 0; 8 | margin: 0; 9 | text-shadow: 2px 2px 5px #000; 10 | font-weight: 400; 11 | } 12 | 13 | #PublicLayout h1 .content { 14 | text-align: left; 15 | } 16 | 17 | #PublicLayout h1 .content small { 18 | font-size: small; 19 | } 20 | 21 | #PublicLayout .view.page { 22 | max-width: 900px; 23 | margin: 0 auto; 24 | background: white; 25 | padding: 1em; 26 | border-radius: 1em; 27 | } 28 | 29 | #DesktopLayout { 30 | height: 100vh; 31 | } 32 | 33 | #DesktopLayout #AppMenu { 34 | position: fixed; 35 | top: 0px; 36 | bottom: 0px; 37 | left: 0px; 38 | width: 260px !important; 39 | overflow-y: auto; 40 | margin: 0; 41 | z-index: 2; 42 | box-shadow: 0 0 20px rgba(34,36,38,.15); 43 | } 44 | 45 | #DesktopLayout > .main { 46 | margin-left: 260px; 47 | height: 100vh; 48 | } 49 | 50 | .ReactModalPortal .view { 51 | display: flex; 52 | flex-direction: column; 53 | position: relative; 54 | height: 100%; 55 | } 56 | 57 | #MobileLayout .view, 58 | #DesktopLayout .view { 59 | display: flex; 60 | flex-direction: column; 61 | position: relative; 62 | height: 100vh; 63 | } 64 | 65 | .ReactModalPortal .view > #AppBar, 66 | #MobileLayout .view > #AppBar, 67 | #DesktopLayout .view > #AppBar { 68 | margin-bottom: 0; 69 | border-radius: 0; 70 | } 71 | 72 | .ReactModalPortal .view > .viewContent, 73 | #MobileLayout .view > .viewContent, 74 | #DesktopLayout .view > .viewContent { 75 | flex: 1; 76 | overflow: auto; 77 | background: #fff; 78 | position: relative; 79 | } 80 | 81 | #MobileLayout .view > .documents, 82 | #MobileLayout .view > .trash, 83 | #DesktopLayout .view > .documents, 84 | #DesktopLayout .view > .trash { 85 | background: #ECECEC; 86 | } 87 | 88 | .viewContent { 89 | padding: 1em; 90 | } 91 | -------------------------------------------------------------------------------- /src/middlewares/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-web-app/f266905fb1293b26cdb299aca739ed74948da33f/src/middlewares/.gitkeep -------------------------------------------------------------------------------- /src/middlewares/Authentication.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | 5 | import authProvider from 'helpers/AuthProvider' 6 | import { actions as authActions } from 'store/modules/auth' 7 | import profileActions from 'store/profile/actions' 8 | 9 | export function requireAuthentication (Component) { 10 | class AuthenticatedComponent extends React.Component { 11 | static propTypes = { 12 | initAuthentication: PropTypes.func.isRequired, 13 | fetchProfile: PropTypes.func.isRequired, 14 | profile: PropTypes.object.isRequired, 15 | authenticated: PropTypes.bool 16 | }; 17 | 18 | componentWillMount () { 19 | const {authenticated} = this.props 20 | if (!authenticated) { 21 | const {initAuthentication, profile} = this.props 22 | initAuthentication().then((res) => { 23 | if (res.payload.authenticated) { 24 | if (!profile.current) { 25 | const {fetchProfile} = this.props 26 | fetchProfile() 27 | } 28 | } else { 29 | document.location.replace(authProvider.getLoginUrl()) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | render () { 36 | const {authenticated} = this.props 37 | return authenticated ? : null 38 | } 39 | } 40 | 41 | const mapStateToProps = (state) => ({ 42 | authenticated: state.auth.authenticated, 43 | profile: state.profile 44 | }) 45 | 46 | const mapDispatchToProps = (dispatch) => ( 47 | bindActionCreators(Object.assign({}, authActions, profileActions), dispatch) 48 | ) 49 | 50 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent) 51 | } 52 | -------------------------------------------------------------------------------- /src/node_modules/api: -------------------------------------------------------------------------------- 1 | ../api -------------------------------------------------------------------------------- /src/node_modules/components: -------------------------------------------------------------------------------- 1 | ../components -------------------------------------------------------------------------------- /src/node_modules/helpers: -------------------------------------------------------------------------------- 1 | ../helpers -------------------------------------------------------------------------------- /src/node_modules/layouts: -------------------------------------------------------------------------------- 1 | ../layouts -------------------------------------------------------------------------------- /src/node_modules/middlewares: -------------------------------------------------------------------------------- 1 | ../middlewares -------------------------------------------------------------------------------- /src/node_modules/store: -------------------------------------------------------------------------------- 1 | ../store -------------------------------------------------------------------------------- /src/node_modules/views: -------------------------------------------------------------------------------- 1 | ../views -------------------------------------------------------------------------------- /src/store/client/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import ClientApi from 'api/client' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_CLIENT = 'FETCH_CLIENT' 14 | export const CREATE_CLIENT = 'CREATE_CLIENT' 15 | export const UPDATE_CLIENT = 'UPDATE_CLIENT' 16 | export const REMOVE_CLIENT = 'REMOVE_CLIENT' 17 | export const RESET_CLIENT = 'RESET_CLIENT' 18 | 19 | // -------------------------------------- 20 | // Fetch client actions 21 | // -------------------------------------- 22 | const fetchClientRequest = createRequestAction(FETCH_CLIENT) 23 | const fetchClientFailure = createFailureAction(FETCH_CLIENT) 24 | const fetchClientSuccess = createSuccessAction(FETCH_CLIENT) 25 | 26 | export const fetchClient = (id) => { 27 | return (dispatch, getState) => { 28 | const { client } = getState() 29 | if (client.isProcessing) { 30 | console.warn('Unable to fetch client. An action is pending...') 31 | return Promise.resolve(null) 32 | } 33 | console.debug('Fetching client:', id) 34 | dispatch(fetchClientRequest()) 35 | return ClientApi.get(id) 36 | .then( 37 | res => dispatchAction(dispatch, fetchClientSuccess(res)), 38 | err => dispatchAction(dispatch, fetchClientFailure(err)) 39 | ) 40 | } 41 | } 42 | 43 | // -------------------------------------- 44 | // Create client actions 45 | // -------------------------------------- 46 | const createClientRequest = createRequestAction(CREATE_CLIENT) 47 | const createClientFailure = createFailureAction(CREATE_CLIENT) 48 | const createClientSuccess = createSuccessAction(CREATE_CLIENT) 49 | 50 | export const createClient = (client) => { 51 | return (dispatch, getState) => { 52 | console.debug('Creating client:', client) 53 | dispatch(createClientRequest()) 54 | return ClientApi.create(client) 55 | .then( 56 | res => dispatchAction(dispatch, createClientSuccess(res)), 57 | err => dispatchAction(dispatch, createClientFailure(err)) 58 | ) 59 | } 60 | } 61 | 62 | // -------------------------------------- 63 | // Update client actions 64 | // -------------------------------------- 65 | const updateClientRequest = createRequestAction(UPDATE_CLIENT) 66 | const updateClientFailure = createFailureAction(UPDATE_CLIENT) 67 | const updateClientSuccess = createSuccessAction(UPDATE_CLIENT) 68 | 69 | export const updateClient = (update) => { 70 | return (dispatch, getState) => { 71 | const { client } = getState() 72 | if (client.isProcessing) { 73 | console.warn('Unable to update client. An action is pending...') 74 | return Promise.resolve(null) 75 | } 76 | console.debug('Updating client:', client.current) 77 | dispatch(updateClientRequest()) 78 | return ClientApi.update(client.current, update) 79 | .then( 80 | res => dispatchAction(dispatch, updateClientSuccess(res)), 81 | err => dispatchAction(dispatch, updateClientFailure(err)) 82 | ) 83 | } 84 | } 85 | 86 | // -------------------------------------- 87 | // Remove client actions 88 | // -------------------------------------- 89 | const removeClientRequest = createRequestAction(REMOVE_CLIENT) 90 | const removeClientFailure = createFailureAction(REMOVE_CLIENT) 91 | const removeClientSuccess = createSuccessAction(REMOVE_CLIENT) 92 | 93 | export const removeClient = (client) => { 94 | return (dispatch, getState) => { 95 | console.debug('Removing client:', client.id) 96 | dispatch(removeClientRequest()) 97 | return ClientApi.remove(client) 98 | .then( 99 | res => dispatchAction(dispatch, removeClientSuccess(client)), 100 | err => dispatchAction(dispatch, removeClientFailure(err)) 101 | ) 102 | } 103 | } 104 | 105 | // -------------------------------------- 106 | // Reset client action 107 | // -------------------------------------- 108 | const resetClient = createRequestAction(RESET_CLIENT) 109 | 110 | const actions = { 111 | fetchClient, 112 | createClient, 113 | updateClient, 114 | removeClient, 115 | resetClient 116 | } 117 | 118 | export default actions 119 | 120 | -------------------------------------------------------------------------------- /src/store/client/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { 6 | FETCH_CLIENT, 7 | CREATE_CLIENT, 8 | UPDATE_CLIENT, 9 | REMOVE_CLIENT, 10 | RESET_CLIENT 11 | } from './actions' 12 | 13 | const defaultState = { 14 | isProcessing: false, 15 | current: { 16 | id: null, 17 | name: '', 18 | clientId: '', 19 | clientSecret: '', 20 | redirectUris: [], 21 | webOrigins: [], 22 | cdate: null, 23 | mdate: null 24 | }, 25 | error: null 26 | } 27 | 28 | // -------------------------------------- 29 | // Reducer 30 | // -------------------------------------- 31 | export default handleActions({ 32 | [FETCH_CLIENT]: commonActionHandler, 33 | [CREATE_CLIENT]: commonActionHandler, 34 | [UPDATE_CLIENT]: commonActionHandler, 35 | [REMOVE_CLIENT]: commonActionHandler, 36 | [RESET_CLIENT]: (state, action) => { 37 | if (state.isProcessing) { 38 | console.warn('Unable to reset client state. An action is pending...') 39 | return state 40 | } 41 | return Object.assign({}, defaultState) 42 | } 43 | }, defaultState) 44 | 45 | -------------------------------------------------------------------------------- /src/store/clients/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import ClientApi from 'api/client' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_CLIENTS = 'FETCH_CLIENTS' 14 | 15 | // -------------------------------------- 16 | // Fetch clients actions 17 | // -------------------------------------- 18 | const fetchClientsRequest = createRequestAction(FETCH_CLIENTS) 19 | const fetchClientsFailure = createFailureAction(FETCH_CLIENTS) 20 | const fetchClientsSuccess = createSuccessAction(FETCH_CLIENTS) 21 | 22 | export const fetchClients = () => { 23 | return (dispatch, getState) => { 24 | const { clients } = getState() 25 | if (clients.isProcessing) { 26 | console.warn('Unable to fetch clients. An action is pending...') 27 | return Promise.resolve(null) 28 | } 29 | console.debug('Fetching clients...') 30 | dispatch(fetchClientsRequest()) 31 | return ClientApi.all() 32 | .then( 33 | res => dispatchAction(dispatch, fetchClientsSuccess(res)), 34 | err => dispatchAction(dispatch, fetchClientsFailure(err)) 35 | ) 36 | } 37 | } 38 | 39 | const actions = { 40 | fetchClients 41 | } 42 | 43 | export default actions 44 | 45 | -------------------------------------------------------------------------------- /src/store/clients/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { FETCH_CLIENTS } from './actions' 6 | 7 | import { 8 | CREATE_CLIENT, 9 | UPDATE_CLIENT, 10 | REMOVE_CLIENT 11 | } from '../client/actions' 12 | 13 | const addToListActionHandler = (state, action) => { 14 | const { success } = action.meta 15 | if (success) { 16 | const update = { 17 | current: { 18 | clients: [action.payload, ...state.current.clients] 19 | } 20 | } 21 | return Object.assign({}, state, update) 22 | } 23 | return state 24 | } 25 | 26 | const updateListActionHandler = (state, action) => { 27 | const { success } = action.meta 28 | if (success) { 29 | const update = {current: {}} 30 | let updated = false 31 | update.current.clients = state.current.clients.reduce((acc, item, index) => { 32 | if (item.id === action.payload.id) { 33 | item = action.payload 34 | updated = true 35 | } 36 | acc.push(item) 37 | return acc 38 | }, []) 39 | if (updated) { 40 | return Object.assign({}, state, update) 41 | } 42 | } 43 | return state 44 | } 45 | 46 | const rmFromListActionHandler = (state, action) => { 47 | const { success } = action.meta 48 | if (success) { 49 | const update = {current: {}} 50 | let removed = null 51 | update.current.clients = state.current.clients.reduce((acc, item, index) => { 52 | if (item.id === action.payload.id) { 53 | removed = item 54 | } else { 55 | acc.push(item) 56 | } 57 | return acc 58 | }, []) 59 | if (removed) { 60 | return Object.assign({}, state, update) 61 | } 62 | } 63 | return state 64 | } 65 | 66 | // -------------------------------------- 67 | // Reducer 68 | // -------------------------------------- 69 | export default handleActions({ 70 | [FETCH_CLIENTS]: commonActionHandler, 71 | [CREATE_CLIENT]: addToListActionHandler, 72 | [UPDATE_CLIENT]: updateListActionHandler, 73 | [REMOVE_CLIENT]: rmFromListActionHandler 74 | }, { 75 | isProcessing: false, 76 | current: { 77 | clients: [] 78 | }, 79 | error: null 80 | }) 81 | 82 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import thunk from 'redux-thunk' 4 | import makeRootReducer from './reducers' 5 | 6 | const __DEBUG__ = process.env.REACT_APP_DEBUG === 'true' 7 | 8 | export default (initialState = {}, history) => { 9 | // ====================================================== 10 | // Middleware Configuration 11 | // ====================================================== 12 | const middleware = [thunk, routerMiddleware(history)] 13 | 14 | // ====================================================== 15 | // Store Enhancers 16 | // ====================================================== 17 | const enhancers = [] 18 | if (__DEBUG__) { 19 | const devToolsExtension = window.devToolsExtension 20 | if (typeof devToolsExtension === 'function') { 21 | enhancers.push(devToolsExtension()) 22 | } 23 | } 24 | 25 | // ====================================================== 26 | // Store Instantiation and HMR Setup 27 | // ====================================================== 28 | const store = createStore( 29 | makeRootReducer(), 30 | initialState, 31 | compose( 32 | applyMiddleware(...middleware), 33 | ...enhancers 34 | ) 35 | ) 36 | store.asyncReducers = {} 37 | 38 | if (module.hot) { 39 | module.hot.accept('./reducers', () => { 40 | const reducers = require('./reducers').default 41 | store.replaceReducer(reducers) 42 | }) 43 | } 44 | 45 | return store 46 | } 47 | -------------------------------------------------------------------------------- /src/store/exports/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | createProgressAction, 6 | dispatchAction 7 | } from 'store/helper' 8 | 9 | import ExportApi from 'api/export' 10 | 11 | // ------------------------------------ 12 | // Constants 13 | // ------------------------------------ 14 | export const SCHEDULE_EXPORT = 'SCHEDULE_EXPORT' 15 | export const EXPORT_STATUS = 'EXPORT_STATUS' 16 | 17 | // ------------------------------------ 18 | // Schedule export actions 19 | // ------------------------------------ 20 | const scheduleExportRequest = createRequestAction(SCHEDULE_EXPORT) 21 | const scheduleExportFailure = createFailureAction(SCHEDULE_EXPORT) 22 | const scheduleExportSuccess = createSuccessAction(SCHEDULE_EXPORT) 23 | 24 | export const scheduleExport = () => { 25 | return (dispatch, getState) => { 26 | const {exports} = getState() 27 | if (exports.isProcessing) { 28 | console.warn('Unable to schedule an export. An action is pending...') 29 | return Promise.resolve(null) 30 | } 31 | console.debug('Scheduling and export...') 32 | dispatch(scheduleExportRequest()) 33 | return ExportApi.schedule() 34 | .then( 35 | res => dispatchAction(dispatch, scheduleExportSuccess(res)), 36 | err => dispatchAction(dispatch, scheduleExportFailure(err)) 37 | ) 38 | } 39 | } 40 | 41 | // ------------------------------------ 42 | // Get export status 43 | // ------------------------------------ 44 | const getExportStatusRequest = createRequestAction(EXPORT_STATUS) 45 | const getExportStatusFailure = createFailureAction(EXPORT_STATUS) 46 | const getExportStatusSuccess = createSuccessAction(EXPORT_STATUS) 47 | const getExportStatusProgress = createProgressAction(EXPORT_STATUS) 48 | 49 | export const getExportStatus = () => { 50 | return (dispatch, getState) => { 51 | const {exports} = getState() 52 | if (exports.isProcessing) { 53 | console.warn('Unable to get export status. An action is pending...') 54 | return Promise.resolve(null) 55 | } 56 | console.debug('Getting export status...') 57 | dispatch(getExportStatusRequest()) 58 | return ExportApi.getStatus((data) => { 59 | dispatch(getExportStatusProgress(data)) 60 | }).then( 61 | res => dispatchAction(dispatch, getExportStatusSuccess(res)), 62 | err => dispatchAction(dispatch, getExportStatusFailure(err)) 63 | ) 64 | } 65 | } 66 | 67 | const actions = { 68 | scheduleExport, 69 | getExportStatus 70 | } 71 | 72 | export default actions 73 | 74 | -------------------------------------------------------------------------------- /src/store/exports/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { SCHEDULE_EXPORT, EXPORT_STATUS } from './actions' 4 | 5 | const scheduleActionHandler = function (state, action) { 6 | const update = { 7 | isProcessing: false, 8 | error: null, 9 | progress: -1 10 | } 11 | const {request, failure} = action.meta 12 | if (failure) { 13 | update.error = action.payload 14 | } else if (request) { 15 | update.isProcessing = true 16 | } 17 | return Object.assign({}, state, update) 18 | } 19 | 20 | const statusActionHandler = function (state, action) { 21 | const update = { 22 | isProcessing: false, 23 | error: null 24 | } 25 | const {request, progress, success, failure} = action.meta 26 | if (failure) { 27 | update.error = action.payload 28 | } else if (success) { 29 | update.progress = 100 30 | update.exported = action.payload 31 | update.total = action.payload 32 | } else if (request) { 33 | update.isProcessing = true 34 | // update.progress = 0 35 | // update.exported = 0 36 | } else if (progress) { 37 | const p = action.payload.split('|') 38 | update.progress = parseInt(p[0], 10) 39 | if (update.progress % 5 !== 0) { 40 | // Drop too tiny progress 41 | return state 42 | } 43 | update.exported = p[1] 44 | update.total = p[2] 45 | update.isProcessing = true 46 | } 47 | return Object.assign({}, state, update) 48 | } 49 | 50 | // ------------------------------------ 51 | // Reducer 52 | // ------------------------------------ 53 | export default handleActions({ 54 | [SCHEDULE_EXPORT]: scheduleActionHandler, 55 | [EXPORT_STATUS]: statusActionHandler 56 | }, { 57 | isProcessing: false, 58 | progress: -1, 59 | exported: 0, 60 | total: 0, 61 | error: null 62 | }) 63 | -------------------------------------------------------------------------------- /src/store/helper.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { createAction } from 'redux-actions' 3 | 4 | // Create wrappers for typed action: 5 | // - request action 6 | // - success action 7 | // - failure action 8 | // - progress action 9 | export const createRequestAction = name => createAction(name, null, () => ({request: true})) 10 | export const createSuccessAction = name => createAction(name, null, () => ({success: true})) 11 | export const createFailureAction = name => createAction(name, null, () => ({failure: true})) 12 | export const createProgressAction = name => createAction(name, null, () => ({progress: true})) 13 | 14 | /** 15 | * Dispatch an action and return a rpromise related to the action payload type. 16 | * @param {Function} dispatch The dispatcher 17 | * @param {Object} action The action to dispatch 18 | * @return {Promise} promise of the action 19 | */ 20 | export const dispatchAction = function (dispatch, action) { 21 | // The action is dispatched... 22 | dispatch(action) 23 | // If action is a failure 24 | // then the promise is rejected with this error 25 | if (action.meta.failure) { 26 | console.error(action.payload) 27 | return Promise.reject(action.payload) 28 | } 29 | // Otherwise the promise is resovled 30 | return Promise.resolve(action.payload) 31 | } 32 | 33 | /** 34 | * @deprecated 35 | */ 36 | export const errorHandler = function (err) { 37 | return {error: err} 38 | } 39 | 40 | /** 41 | * @deprecated 42 | */ 43 | export const payloadResponse = function (res) { 44 | if (res.payload.error) { 45 | return Promise.reject(res.payload.error) 46 | } else { 47 | const payload = res.payload.response || res.payload 48 | return Promise.resolve(payload) 49 | } 50 | } 51 | 52 | /** 53 | * Bind actions to the 'actions' property of an object. 54 | * @param {Array} actions Actions to bind 55 | * @param {Function} dispatch The dispatcher 56 | * @return {Object} Actions object 57 | */ 58 | export const bindActions = function (actions, dispatch) { 59 | const result = Object.keys(actions).reduce((acc, key) => { 60 | acc[key] = bindActionCreators(actions[key], dispatch) 61 | return acc 62 | }, {}) 63 | return {actions: result} 64 | } 65 | 66 | /** 67 | * Common action handler for CRUD operations. 68 | * Manage a classic CRUD state of a object. 69 | * @param {Object} state The global state 70 | * @param {Object} action The applied action 71 | * @return the altered store 72 | */ 73 | export const commonActionHandler = function (state, action) { 74 | const update = { 75 | isProcessing: false, 76 | error: null 77 | } 78 | const {request, success, failure} = action.meta 79 | if (failure) { 80 | update.error = action.payload 81 | } else if (success) { 82 | update.current = action.payload 83 | } else if (request) { 84 | update.isProcessing = true 85 | } 86 | return Object.assign({}, state, update) 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/store/label/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import LabelApi from 'api/label' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_LABEL = 'FETCH_LABEL' 14 | export const CREATE_LABEL = 'CREATE_LABEL' 15 | export const UPDATE_LABEL = 'UPDATE_LABEL' 16 | export const REMOVE_LABEL = 'REMOVE_LABEL' 17 | export const RESTORE_LABEL = 'RESTORE_LABEL' 18 | export const DISCARD_LABEL = 'DISCARD_LABEL' 19 | 20 | // -------------------------------------- 21 | // Fetch label actions 22 | // -------------------------------------- 23 | const fetchLabelRequest = createRequestAction(FETCH_LABEL) 24 | const fetchLabelFailure = createFailureAction(FETCH_LABEL) 25 | const fetchLabelSuccess = createSuccessAction(FETCH_LABEL) 26 | 27 | export const fetchLabel = (id) => { 28 | return (dispatch, getState) => { 29 | const {label: l} = getState() 30 | if (l.isProcessing) { 31 | console.warn('Unable to fetch label. An action is pending...') 32 | return Promise.resolve(null) 33 | } 34 | console.debug('Fetching label:', id) 35 | dispatch(fetchLabelRequest()) 36 | return LabelApi.get(id) 37 | .then( 38 | res => dispatchAction(dispatch, fetchLabelSuccess(res)), 39 | err => dispatchAction(dispatch, fetchLabelFailure(err)) 40 | ) 41 | } 42 | } 43 | 44 | // -------------------------------------- 45 | // Create label actions 46 | // -------------------------------------- 47 | const createLabelRequest = createRequestAction(CREATE_LABEL) 48 | const createLabelFailure = createFailureAction(CREATE_LABEL) 49 | const createLabelSuccess = createSuccessAction(CREATE_LABEL) 50 | 51 | export const createLabel = (label) => { 52 | return (dispatch, getState) => { 53 | console.debug('Creating label:', label) 54 | dispatch(createLabelRequest()) 55 | return LabelApi.create(label) 56 | .then( 57 | res => dispatchAction(dispatch, createLabelSuccess(res)), 58 | err => dispatchAction(dispatch, createLabelFailure(err)) 59 | ) 60 | } 61 | } 62 | 63 | // -------------------------------------- 64 | // Update label actions 65 | // -------------------------------------- 66 | const updateLabelRequest = createRequestAction(UPDATE_LABEL) 67 | const updateLabelFailure = createFailureAction(UPDATE_LABEL) 68 | const updateLabelSuccess = createSuccessAction(UPDATE_LABEL) 69 | 70 | export const updateLabel = (label, payload) => { 71 | return (dispatch, getState) => { 72 | const {label: l} = getState() 73 | if (l.isProcessing) { 74 | console.warn('Unable to update label. An action is pending...') 75 | return Promise.resolve(null) 76 | } 77 | console.debug('Updating label:', label) 78 | dispatch(updateLabelRequest()) 79 | return LabelApi.update(label, payload) 80 | .then( 81 | res => dispatchAction(dispatch, updateLabelSuccess(res)), 82 | err => dispatchAction(dispatch, updateLabelFailure(err)) 83 | ) 84 | } 85 | } 86 | 87 | // -------------------------------------- 88 | // Remove label actions 89 | // -------------------------------------- 90 | const removeLabelRequest = createRequestAction(REMOVE_LABEL) 91 | const removeLabelFailure = createFailureAction(REMOVE_LABEL) 92 | const removeLabelSuccess = createSuccessAction(REMOVE_LABEL) 93 | 94 | export const removeLabel = (label) => { 95 | return (dispatch, getState) => { 96 | console.debug('Removing label:', label.id) 97 | dispatch(removeLabelRequest()) 98 | return LabelApi.remove(label) 99 | .then( 100 | res => dispatchAction(dispatch, removeLabelSuccess(label)), 101 | err => dispatchAction(dispatch, removeLabelFailure(err)) 102 | ) 103 | } 104 | } 105 | 106 | // -------------------------------------- 107 | // Restore label actions 108 | // -------------------------------------- 109 | const restoreLabelRequest = createRequestAction(RESTORE_LABEL) 110 | const restoreLabelFailure = createFailureAction(RESTORE_LABEL) 111 | const restoreLabelSuccess = createSuccessAction(RESTORE_LABEL) 112 | 113 | export const restoreRemovedLabel = () => { 114 | return (dispatch, getState) => { 115 | const {labels} = getState() 116 | if (!labels.removed) { 117 | return Promise.reject({error: 'No label to restore.'}) 118 | } 119 | console.debug('Restoring label:', labels.removed.id) 120 | dispatch(restoreLabelRequest()) 121 | return LabelApi.restore(labels.removed) 122 | .then( 123 | res => dispatchAction(dispatch, restoreLabelSuccess(res)), 124 | err => dispatchAction(dispatch, restoreLabelFailure(err)) 125 | ) 126 | } 127 | } 128 | 129 | // -------------------------------------- 130 | // Discard label actions 131 | // -------------------------------------- 132 | const discardLabel = createRequestAction(DISCARD_LABEL) 133 | 134 | const actions = { 135 | fetchLabel, 136 | createLabel, 137 | updateLabel, 138 | removeLabel, 139 | restoreRemovedLabel, 140 | discardLabel 141 | } 142 | 143 | export default actions 144 | 145 | -------------------------------------------------------------------------------- /src/store/label/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { 6 | FETCH_LABEL, 7 | CREATE_LABEL, 8 | UPDATE_LABEL, 9 | REMOVE_LABEL, 10 | RESTORE_LABEL, 11 | DISCARD_LABEL 12 | } from './actions' 13 | 14 | // -------------------------------------- 15 | // Reducer 16 | // -------------------------------------- 17 | export default handleActions({ 18 | [FETCH_LABEL]: commonActionHandler, 19 | [CREATE_LABEL]: commonActionHandler, 20 | [UPDATE_LABEL]: commonActionHandler, 21 | [REMOVE_LABEL]: commonActionHandler, 22 | [RESTORE_LABEL]: commonActionHandler, 23 | [DISCARD_LABEL]: (state, action) => { 24 | return Object.assign({}, state, { 25 | isProcessing: false, 26 | current: null 27 | }) 28 | } 29 | }, { 30 | isProcessing: false, 31 | current: null, 32 | error: null 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /src/store/labels/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import LabelApi from 'api/label' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_LABELS = 'FETCH_LABELS' 14 | 15 | // -------------------------------------- 16 | // Fetch labels actions 17 | // -------------------------------------- 18 | const fetchLabelsRequest = createRequestAction(FETCH_LABELS) 19 | const fetchLabelsFailure = createFailureAction(FETCH_LABELS) 20 | const fetchLabelsSuccess = createSuccessAction(FETCH_LABELS) 21 | 22 | export const fetchLabels = () => { 23 | return (dispatch, getState) => { 24 | const { labels } = getState() 25 | if (labels.isProcessing) { 26 | console.warn('Unable to fetch labels. An action is pending...') 27 | return Promise.resolve(null) 28 | } 29 | console.debug('Fetching labels...') 30 | dispatch(fetchLabelsRequest()) 31 | return LabelApi.all() 32 | .then( 33 | res => dispatchAction(dispatch, fetchLabelsSuccess(res)), 34 | err => dispatchAction(dispatch, fetchLabelsFailure(err)) 35 | ) 36 | } 37 | } 38 | 39 | const actions = { 40 | fetchLabels 41 | } 42 | 43 | export default actions 44 | 45 | -------------------------------------------------------------------------------- /src/store/labels/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { FETCH_LABELS } from './actions' 6 | 7 | import { 8 | CREATE_LABEL, 9 | UPDATE_LABEL, 10 | REMOVE_LABEL 11 | } from '../label/actions' 12 | 13 | const addToListActionHandler = (state, action) => { 14 | const { success } = action.meta 15 | if (success) { 16 | const update = { 17 | current: { 18 | labels: [action.payload, ...state.current.labels] 19 | } 20 | } 21 | return Object.assign({}, state, update) 22 | } 23 | return state 24 | } 25 | 26 | const updateListActionHandler = (state, action) => { 27 | const { success } = action.meta 28 | if (success) { 29 | const update = {current: {}} 30 | let updated = false 31 | update.current.labels = state.current.labels.reduce((acc, item, index) => { 32 | if (item.id === action.payload.id) { 33 | item = action.payload 34 | updated = true 35 | } 36 | acc.push(item) 37 | return acc 38 | }, []) 39 | if (updated) { 40 | return Object.assign({}, state, update) 41 | } 42 | } 43 | return state 44 | } 45 | 46 | const rmFromListActionHandler = (state, action) => { 47 | const { success } = action.meta 48 | if (success) { 49 | const update = {current: {}} 50 | let removed = null 51 | update.current.labels = state.current.labels.reduce((acc, item, index) => { 52 | if (item.id === action.payload.id) { 53 | removed = item 54 | } else { 55 | acc.push(item) 56 | } 57 | return acc 58 | }, []) 59 | if (removed) { 60 | return Object.assign({}, state, update) 61 | } 62 | } 63 | return state 64 | } 65 | 66 | // -------------------------------------- 67 | // Reducer 68 | // -------------------------------------- 69 | export default handleActions({ 70 | [FETCH_LABELS]: commonActionHandler, 71 | [CREATE_LABEL]: addToListActionHandler, 72 | [UPDATE_LABEL]: updateListActionHandler, 73 | [REMOVE_LABEL]: rmFromListActionHandler 74 | }, { 75 | isProcessing: false, 76 | current: { 77 | labels: [] 78 | }, 79 | error: null 80 | }) 81 | 82 | -------------------------------------------------------------------------------- /src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | import authProvider from 'helpers/AuthProvider' 3 | 4 | // ------------------------------------ 5 | // Constants 6 | // ------------------------------------ 7 | export const INIT_AUTH = 'INIT_AUTH' 8 | 9 | // ------------------------------------ 10 | // Actions 11 | // ------------------------------------ 12 | export const initAuthenticationRequest = createAction(INIT_AUTH) 13 | export const initAuthenticationFailure = createAction(INIT_AUTH, (err) => { 14 | return { error: err } 15 | }) 16 | export const initAuthenticationSuccess = createAction(INIT_AUTH, (authenticated) => { 17 | return { authenticated } 18 | }) 19 | 20 | export const initAuthentication = () => { 21 | return (dispatch, getState) => { 22 | dispatch(initAuthenticationRequest()) 23 | return authProvider.init() 24 | .then((authenticated) => { 25 | return dispatch(initAuthenticationSuccess(authenticated)) 26 | }, (err) => { 27 | return dispatch(initAuthenticationFailure(err)) 28 | }) 29 | } 30 | } 31 | 32 | export const actions = { 33 | initAuthentication 34 | } 35 | 36 | // ------------------------------------ 37 | // Reducer 38 | // ------------------------------------ 39 | export default handleActions({ 40 | [INIT_AUTH]: (state, action) => { 41 | const update = { 42 | isProcessing: false 43 | } 44 | const {authenticated, error} = action.payload || {} 45 | if (error) { 46 | update.error = error 47 | } else if (authenticated) { 48 | update.authenticated = authenticated 49 | } else { 50 | update.isProcessing = true 51 | } 52 | return Object.assign({}, state, update) 53 | } 54 | }, { 55 | isProcessing: false, 56 | authenticated: false 57 | }) 58 | 59 | -------------------------------------------------------------------------------- /src/store/modules/documents.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | import DocumentApi from 'api/document' 3 | import { errorHandler, payloadResponse } from 'store/helper' 4 | 5 | // ------------------------------------ 6 | // Constants 7 | // ------------------------------------ 8 | export const FETCH_DOCUMENTS = 'FETCH_DOCUMENTS' 9 | export const CREATE_DOCUMENT = 'CREATE_DOCUMENT' 10 | export const UPDATE_DOCUMENT = 'UPDATE_DOCUMENT' 11 | export const REMOVE_DOCUMENT = 'REMOVE_DOCUMENT' 12 | export const RESTORE_DOCUMENT = 'RESTORE_DOCUMENT' 13 | 14 | // ------------------------------------ 15 | // Actions 16 | // ------------------------------------ 17 | export const fetchDocumentsRequest = createAction(FETCH_DOCUMENTS, (params) => { 18 | return {params} 19 | }) 20 | export const fetchDocumentsFailure = createAction(FETCH_DOCUMENTS, errorHandler) 21 | export const fetchDocumentsSuccess = createAction(FETCH_DOCUMENTS, (res) => { 22 | console.debug('Documents fetched:', res.total) 23 | return {response: {total: res.total, items: res.hits}} 24 | }) 25 | 26 | export const fetchDocuments = (params = {from: 0, size: 20}, type = 'user') => { 27 | params = Object.assign({ 28 | from: 0, 29 | size: 20, 30 | order: 'desc' 31 | }, params) 32 | return (dispatch, getState) => { 33 | const {documents} = getState() 34 | if (documents.isFetching || documents.isProcessing) { 35 | console.warn(`Unable to fetch ${type} documents. An action is pending...`) 36 | return Promise.resolve(null) 37 | } else if (documents.hasMore || params.from === 0) { 38 | console.debug(`Fetching ${type} documents:`, params) 39 | dispatch(fetchDocumentsRequest(params)) 40 | let fetched 41 | switch (true) { 42 | case type === 'shared': 43 | fetched = DocumentApi.searchShared(params) 44 | break 45 | case type === 'public': 46 | fetched = DocumentApi.searchPublic(params) 47 | break 48 | default: 49 | fetched = DocumentApi.search(params) 50 | } 51 | return fetched.then((res) => dispatch(fetchDocumentsSuccess(res))) 52 | .catch((err) => dispatch(fetchDocumentsFailure(err))) 53 | .then(payloadResponse) 54 | } else { 55 | console.warn(`Unable to fetch ${type} documents. No more documents`, params) 56 | return Promise.resolve(null) 57 | } 58 | } 59 | } 60 | 61 | export const actions = { 62 | fetchDocuments, 63 | fetchSharedDocuments: (params) => fetchDocuments(params, 'shared'), 64 | fetchPublicDocuments: (params) => fetchDocuments(params, 'public') 65 | } 66 | 67 | // ------------------------------------ 68 | // Reducer 69 | // ------------------------------------ 70 | export default handleActions({ 71 | [FETCH_DOCUMENTS]: (state, action) => { 72 | const update = { 73 | isProcessing: action.payload.params != null, 74 | isFetching: action.payload.params != null, 75 | error: null 76 | } 77 | const {error, response, params} = action.payload 78 | if (error) { 79 | update.error = error 80 | } else if (response) { 81 | const {items, total} = response 82 | update.total = total 83 | update.items = state.items.concat(items) 84 | update.hasMore = total > update.items.length 85 | } else if (params) { 86 | update.params = params 87 | if (params.from === 0) { 88 | update.items = [] 89 | update.total = 0 90 | update.hasMore = false 91 | } 92 | } 93 | return Object.assign({}, state, update) 94 | }, 95 | [CREATE_DOCUMENT]: (state, action) => { 96 | const {response} = action.payload || {} 97 | if (response) { 98 | // Add created document to the list 99 | const update = { 100 | items: [response, ...state.items], 101 | total: state.total + 1 102 | } 103 | return Object.assign({}, state, update) 104 | } 105 | return state 106 | }, 107 | [UPDATE_DOCUMENT]: (state, action) => { 108 | const {response} = action.payload || {} 109 | if (response) { 110 | // Update document into the list (if present) 111 | const doc = response 112 | const update = {} 113 | let updated = false 114 | update.items = state.items.reduce((acc, item, index) => { 115 | if (item.id === doc.id) { 116 | item.title = doc.title 117 | item.labels = doc.labels 118 | updated = true 119 | } 120 | acc.push(item) 121 | return acc 122 | }, []) 123 | if (updated) { 124 | return Object.assign({}, state, update) 125 | } 126 | } 127 | return state 128 | }, 129 | [REMOVE_DOCUMENT]: (state, action) => { 130 | const {response} = action.payload || {} 131 | if (response) { 132 | // Remove document from the list (if present) 133 | const doc = response 134 | const update = {} 135 | update.items = state.items.reduce((acc, item, index) => { 136 | if (item.id === doc.id) { 137 | update.removedIndex = index 138 | update.removed = item 139 | update.total = state.total - 1 140 | } else { 141 | acc.push(item) 142 | } 143 | return acc 144 | }, []) 145 | if (update.removed) { 146 | return Object.assign({}, state, update) 147 | } 148 | } 149 | return state 150 | }, 151 | [RESTORE_DOCUMENT]: (state, action) => { 152 | const {response} = action.payload || {} 153 | if (response) { 154 | // Restore document into the list 155 | const idx = state.removedIndex || 0 156 | const update = {} 157 | update.items = state.items.slice() 158 | update.items.splice(idx, 0, response) 159 | update.total = state.total + 1 160 | update.removed = null 161 | update.removedIndex = null 162 | return Object.assign({}, state, update) 163 | } 164 | return state 165 | } 166 | }, { 167 | isFetching: false, 168 | isProcessing: false, 169 | hasMore: true, 170 | removed: null, 171 | removedIndex: null, 172 | params: null, 173 | items: [], 174 | total: null, 175 | error: null 176 | }) 177 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | import auth from './auth' 2 | import layout from './layout' 3 | import document from './document' 4 | import documents from './documents' 5 | import graveyard from './graveyard' 6 | import sharing from './sharing' 7 | import notification from './notification' 8 | import titleModal from './titleModal' 9 | import urlModal from './urlModal' 10 | 11 | export default { 12 | auth, 13 | layout, 14 | document, 15 | documents, 16 | graveyard, 17 | sharing, 18 | notification, 19 | titleModal, 20 | urlModal 21 | } 22 | -------------------------------------------------------------------------------- /src/store/modules/layout.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | 3 | // ------------------------------------ 4 | // Constants 5 | // ------------------------------------ 6 | export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBARE' 7 | export const DEVICE_RESIZE = 'DEVICE_RESIZE' 8 | export const Sizes = { 9 | SMALL: 1, 10 | MEDIUM: 2, 11 | LARGE: 3 12 | } 13 | 14 | // ------------------------------------ 15 | // Actions 16 | // ------------------------------------ 17 | export const resize = createAction(DEVICE_RESIZE) 18 | export const toggleSidebar = createAction(TOGGLE_SIDEBAR) 19 | 20 | export const actions = { 21 | resize, 22 | toggleSidebar 23 | } 24 | 25 | // ------------------------------------ 26 | // Functions 27 | // ------------------------------------ 28 | function _getDeviceSize () { 29 | const width = window.innerWidth 30 | if (width >= 992) { 31 | return Sizes.LARGE 32 | } else if (width >= 768) { 33 | return Sizes.MEDIUM 34 | } else { // width < 768 35 | return Sizes.SMALL 36 | } 37 | } 38 | 39 | // ------------------------------------ 40 | // Reducer 41 | // ------------------------------------ 42 | export default handleActions({ 43 | [DEVICE_RESIZE]: (state) => { 44 | return Object.assign({}, state, { 45 | size: _getDeviceSize(), 46 | sidebar: { 47 | visible: _getDeviceSize() === Sizes.LARGE 48 | } 49 | }) 50 | }, 51 | [TOGGLE_SIDEBAR]: (state) => { 52 | return Object.assign({}, state, { 53 | sidebar: { 54 | visible: !state.sidebar.visible 55 | } 56 | }) 57 | } 58 | 59 | }, { 60 | size: _getDeviceSize(), 61 | sidebar: { 62 | visible: _getDeviceSize() === Sizes.LARGE 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /src/store/modules/notification.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | 3 | // ------------------------------------ 4 | // Constants 5 | // ------------------------------------ 6 | export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION' 7 | export const HIDE_NOTIFICATION = 'HIDE_NOTIFICATION' 8 | 9 | // ------------------------------------ 10 | // Actions 11 | // ------------------------------------ 12 | export const showNotification = createAction(SHOW_NOTIFICATION, (notification = {level: 'info'}) => notification) 13 | export const hideNotification = createAction(HIDE_NOTIFICATION) 14 | 15 | export const actions = { 16 | showNotification, 17 | hideNotification 18 | } 19 | 20 | // ------------------------------------ 21 | // Reducer 22 | // ------------------------------------ 23 | export default handleActions({ 24 | [SHOW_NOTIFICATION]: (state, {payload}) => { 25 | const {header, message, level, actionLabel, actionFn, visible} = Object.assign({ 26 | level: 'info', 27 | visible: true 28 | }, payload) 29 | return Object.assign({}, state, { 30 | header, message, level, actionLabel, actionFn, visible 31 | }) 32 | }, 33 | [HIDE_NOTIFICATION]: (state) => { 34 | return Object.assign({}, state, { 35 | visible: false 36 | }) 37 | } 38 | }, { 39 | visible: false, 40 | header: null, 41 | message: null, 42 | level: null, 43 | actionLabel: null, 44 | actionFn: null 45 | }) 46 | -------------------------------------------------------------------------------- /src/store/modules/titleModal.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | 3 | // ------------------------------------ 4 | // Constants 5 | // ------------------------------------ 6 | export const SHOW_TITLE_MODAL = 'SHOW_TITLE_MODAL' 7 | export const HIDE_TITLE_MODAL = 'HIDE_TITLE_MODAL' 8 | 9 | // ------------------------------------ 10 | // Actions 11 | // ------------------------------------ 12 | export const showTitleModal = createAction(SHOW_TITLE_MODAL, (doc) => doc) 13 | export const hideTitleModal = createAction(HIDE_TITLE_MODAL) 14 | 15 | export const actions = { 16 | showTitleModal, 17 | hideTitleModal 18 | } 19 | 20 | // ------------------------------------ 21 | // Reducer 22 | // ------------------------------------ 23 | export default handleActions({ 24 | [SHOW_TITLE_MODAL]: (state, action) => { 25 | return Object.assign({}, state, { 26 | doc: action.payload, 27 | open: true 28 | }) 29 | }, 30 | [HIDE_TITLE_MODAL]: (state, action) => { 31 | return Object.assign({}, state, { 32 | open: false 33 | }) 34 | } 35 | }, { 36 | open: false, 37 | doc: null 38 | }) 39 | -------------------------------------------------------------------------------- /src/store/modules/urlModal.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions' 2 | 3 | // ------------------------------------ 4 | // Constants 5 | // ------------------------------------ 6 | export const SHOW_URL_MODAL = 'SHOW_URL_MODAL' 7 | export const HIDE_URL_MODAL = 'HIDE_URL_MODAL' 8 | 9 | // ------------------------------------ 10 | // Actions 11 | // ------------------------------------ 12 | export const showUrlModal = createAction(SHOW_URL_MODAL) 13 | export const hideUrlModal = createAction(HIDE_URL_MODAL) 14 | 15 | export const actions = { 16 | showUrlModal, 17 | hideUrlModal 18 | } 19 | 20 | // ------------------------------------ 21 | // Reducer 22 | // ------------------------------------ 23 | export default handleActions({ 24 | [SHOW_URL_MODAL]: (state) => { 25 | return Object.assign({}, state, { 26 | open: true 27 | }) 28 | }, 29 | [HIDE_URL_MODAL]: (state) => { 30 | return Object.assign({}, state, { 31 | open: false 32 | }) 33 | } 34 | }, { 35 | open: false 36 | }) 37 | -------------------------------------------------------------------------------- /src/store/profile/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import ProfileApi from 'api/profile' 9 | 10 | // ------------------------------------ 11 | // Constants 12 | // ------------------------------------ 13 | export const FETCH_PROFILE = 'FETCH_PROFILE' 14 | export const UPDATE_PROFILE = 'UPDATE_PROFILE' 15 | 16 | // ------------------------------------ 17 | // Fetch profile actions 18 | // ------------------------------------ 19 | const fetchProfileRequest = createRequestAction(FETCH_PROFILE) 20 | const fetchProfileFailure = createFailureAction(FETCH_PROFILE) 21 | const fetchProfileSuccess = createSuccessAction(FETCH_PROFILE) 22 | 23 | export const fetchProfile = (withStats = false) => { 24 | return (dispatch, getState) => { 25 | const {profile} = getState() 26 | if (profile.isProcessing) { 27 | console.warn('Unable to fetch profile. An action is pending...') 28 | return Promise.resolve(null) 29 | } 30 | console.debug('Fetching profile...') 31 | dispatch(fetchProfileRequest()) 32 | return ProfileApi.get({withStats}) 33 | .then( 34 | res => dispatchAction(dispatch, fetchProfileSuccess(res)), 35 | err => dispatchAction(dispatch, fetchProfileFailure(err)) 36 | ) 37 | } 38 | } 39 | 40 | // ------------------------------------ 41 | // Update profile actions 42 | // ------------------------------------ 43 | const updateProfileRequest = createRequestAction(UPDATE_PROFILE) 44 | const updateProfileFailure = createFailureAction(UPDATE_PROFILE) 45 | const updateProfileSuccess = createSuccessAction(UPDATE_PROFILE) 46 | 47 | export const updateProfile = (update) => { 48 | return (dispatch, getState) => { 49 | const {profile} = getState() 50 | if (profile.isProcessing) { 51 | console.warn('Unable to update profile. An action is pending...') 52 | return Promise.resolve(null) 53 | } 54 | console.debug('Updating profile...') 55 | dispatch(updateProfileRequest()) 56 | return ProfileApi.update(update) 57 | .then( 58 | res => dispatchAction(dispatch, updateProfileSuccess(res)), 59 | err => dispatchAction(dispatch, updateProfileFailure(err)) 60 | ) 61 | } 62 | } 63 | 64 | const actions = { 65 | fetchProfile, 66 | updateProfile 67 | } 68 | 69 | export default actions 70 | 71 | -------------------------------------------------------------------------------- /src/store/profile/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { 6 | FETCH_PROFILE, 7 | UPDATE_PROFILE 8 | } from './actions' 9 | 10 | // ------------------------------------ 11 | // Reducer 12 | // ------------------------------------ 13 | export default handleActions({ 14 | [FETCH_PROFILE]: commonActionHandler, 15 | [UPDATE_PROFILE]: commonActionHandler 16 | }, { 17 | isProcessing: false, 18 | current: null, 19 | error: null 20 | }) 21 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | import syncReducers from './modules' 4 | 5 | import profile from './profile' 6 | import label from './label' 7 | import labels from './labels' 8 | import webhook from './webhook' 9 | import webhooks from './webhooks' 10 | import client from './client' 11 | import clients from './clients' 12 | import exports from './exports' 13 | 14 | export const makeRootReducer = (asyncReducers) => { 15 | return combineReducers({ 16 | router, 17 | ...syncReducers, 18 | profile, 19 | label, 20 | labels, 21 | webhook, 22 | webhooks, 23 | client, 24 | clients, 25 | exports, 26 | ...asyncReducers 27 | }) 28 | } 29 | 30 | export const injectReducer = (store, { key, reducer }) => { 31 | store.asyncReducers[key] = reducer 32 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 33 | } 34 | 35 | export default makeRootReducer 36 | -------------------------------------------------------------------------------- /src/store/webhook/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import WebhookApi from 'api/webhook' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_WEBHOOK = 'FETCH_WEBHOOK' 14 | export const CREATE_WEBHOOK = 'CREATE_WEBHOOK' 15 | export const UPDATE_WEBHOOK = 'UPDATE_WEBHOOK' 16 | export const REMOVE_WEBHOOK = 'REMOVE_WEBHOOK' 17 | export const RESET_WEBHOOK = 'RESET_WEBHOOK' 18 | 19 | // -------------------------------------- 20 | // Fetch webhook actions 21 | // -------------------------------------- 22 | const fetchWebhookRequest = createRequestAction(FETCH_WEBHOOK) 23 | const fetchWebhookFailure = createFailureAction(FETCH_WEBHOOK) 24 | const fetchWebhookSuccess = createSuccessAction(FETCH_WEBHOOK) 25 | 26 | export const fetchWebhook = (id) => { 27 | return (dispatch, getState) => { 28 | const { webhook } = getState() 29 | if (webhook.isProcessing) { 30 | console.warn('Unable to fetch webhook. An action is pending...') 31 | return Promise.resolve(null) 32 | } 33 | console.debug('Fetching webhook:', id) 34 | dispatch(fetchWebhookRequest()) 35 | return WebhookApi.get(id) 36 | .then( 37 | res => dispatchAction(dispatch, fetchWebhookSuccess(res)), 38 | err => dispatchAction(dispatch, fetchWebhookFailure(err)) 39 | ) 40 | } 41 | } 42 | 43 | // -------------------------------------- 44 | // Create webhook actions 45 | // -------------------------------------- 46 | const createWebhookRequest = createRequestAction(CREATE_WEBHOOK) 47 | const createWebhookFailure = createFailureAction(CREATE_WEBHOOK) 48 | const createWebhookSuccess = createSuccessAction(CREATE_WEBHOOK) 49 | 50 | export const createWebhook = (webhook) => { 51 | return (dispatch, getState) => { 52 | console.debug('Creating webhook:', webhook) 53 | dispatch(createWebhookRequest()) 54 | return WebhookApi.create(webhook) 55 | .then( 56 | res => dispatchAction(dispatch, createWebhookSuccess(res)), 57 | err => dispatchAction(dispatch, createWebhookFailure(err)) 58 | ) 59 | } 60 | } 61 | 62 | // -------------------------------------- 63 | // Update webhook actions 64 | // -------------------------------------- 65 | const updateWebhookRequest = createRequestAction(UPDATE_WEBHOOK) 66 | const updateWebhookFailure = createFailureAction(UPDATE_WEBHOOK) 67 | const updateWebhookSuccess = createSuccessAction(UPDATE_WEBHOOK) 68 | 69 | export const updateWebhook = (update) => { 70 | return (dispatch, getState) => { 71 | const { webhook } = getState() 72 | if (webhook.isProcessing) { 73 | console.warn('Unable to update webhook. An action is pending...') 74 | return Promise.resolve(null) 75 | } 76 | console.debug('Updating webhook:', webhook.current) 77 | dispatch(updateWebhookRequest()) 78 | return WebhookApi.update(webhook.current, update) 79 | .then( 80 | res => dispatchAction(dispatch, updateWebhookSuccess(res)), 81 | err => dispatchAction(dispatch, updateWebhookFailure(err)) 82 | ) 83 | } 84 | } 85 | 86 | // -------------------------------------- 87 | // Remove webhook actions 88 | // -------------------------------------- 89 | const removeWebhookRequest = createRequestAction(REMOVE_WEBHOOK) 90 | const removeWebhookFailure = createFailureAction(REMOVE_WEBHOOK) 91 | const removeWebhookSuccess = createSuccessAction(REMOVE_WEBHOOK) 92 | 93 | export const removeWebhook = (webhook) => { 94 | return (dispatch, getState) => { 95 | console.debug('Removing webhook:', webhook.id) 96 | dispatch(removeWebhookRequest()) 97 | return WebhookApi.remove(webhook) 98 | .then( 99 | res => dispatchAction(dispatch, removeWebhookSuccess(webhook)), 100 | err => dispatchAction(dispatch, removeWebhookFailure(err)) 101 | ) 102 | } 103 | } 104 | 105 | // -------------------------------------- 106 | // Reset webhook action 107 | // -------------------------------------- 108 | const resetWebhook = createRequestAction(RESET_WEBHOOK) 109 | 110 | const actions = { 111 | fetchWebhook, 112 | createWebhook, 113 | updateWebhook, 114 | removeWebhook, 115 | resetWebhook 116 | } 117 | 118 | export default actions 119 | 120 | -------------------------------------------------------------------------------- /src/store/webhook/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { 6 | FETCH_WEBHOOK, 7 | CREATE_WEBHOOK, 8 | UPDATE_WEBHOOK, 9 | REMOVE_WEBHOOK, 10 | RESET_WEBHOOK 11 | } from './actions' 12 | 13 | const defaultState = { 14 | isProcessing: false, 15 | current: { 16 | id: null, 17 | url: '', 18 | secret: '', 19 | events: [], 20 | labels: [] 21 | }, 22 | error: null 23 | } 24 | 25 | // -------------------------------------- 26 | // Reducer 27 | // -------------------------------------- 28 | export default handleActions({ 29 | [FETCH_WEBHOOK]: commonActionHandler, 30 | [CREATE_WEBHOOK]: commonActionHandler, 31 | [UPDATE_WEBHOOK]: commonActionHandler, 32 | [REMOVE_WEBHOOK]: commonActionHandler, 33 | [RESET_WEBHOOK]: (state, action) => { 34 | if (state.isProcessing) { 35 | console.warn('Unable to reset webhook state. An action is pending...') 36 | return state 37 | } 38 | return Object.assign({}, defaultState) 39 | } 40 | }, defaultState) 41 | 42 | -------------------------------------------------------------------------------- /src/store/webhooks/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRequestAction, 3 | createSuccessAction, 4 | createFailureAction, 5 | dispatchAction 6 | } from 'store/helper' 7 | 8 | import WebhookApi from 'api/webhook' 9 | 10 | // -------------------------------------- 11 | // Constants 12 | // -------------------------------------- 13 | export const FETCH_WEBHOOKS = 'FETCH_WEBHOOKS' 14 | 15 | // -------------------------------------- 16 | // Fetch webhooks actions 17 | // -------------------------------------- 18 | const fetchWebhooksRequest = createRequestAction(FETCH_WEBHOOKS) 19 | const fetchWebhooksFailure = createFailureAction(FETCH_WEBHOOKS) 20 | const fetchWebhooksSuccess = createSuccessAction(FETCH_WEBHOOKS) 21 | 22 | export const fetchWebhooks = () => { 23 | return (dispatch, getState) => { 24 | const { webhooks } = getState() 25 | if (webhooks.isProcessing) { 26 | console.warn('Unable to fetch webhooks. An action is pending...') 27 | return Promise.resolve(null) 28 | } 29 | console.debug('Fetching webhooks...') 30 | dispatch(fetchWebhooksRequest()) 31 | return WebhookApi.all() 32 | .then( 33 | res => dispatchAction(dispatch, fetchWebhooksSuccess(res)), 34 | err => dispatchAction(dispatch, fetchWebhooksFailure(err)) 35 | ) 36 | } 37 | } 38 | 39 | const actions = { 40 | fetchWebhooks 41 | } 42 | 43 | export default actions 44 | 45 | -------------------------------------------------------------------------------- /src/store/webhooks/index.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | import { commonActionHandler } from 'store/helper' 4 | 5 | import { FETCH_WEBHOOKS } from './actions' 6 | 7 | import { 8 | CREATE_WEBHOOK, 9 | UPDATE_WEBHOOK, 10 | REMOVE_WEBHOOK 11 | } from '../webhook/actions' 12 | 13 | const addToListActionHandler = (state, action) => { 14 | const { success } = action.meta 15 | if (success) { 16 | const update = { 17 | current: { 18 | webhooks: [action.payload, ...state.current.webhooks] 19 | } 20 | } 21 | return Object.assign({}, state, update) 22 | } 23 | return state 24 | } 25 | 26 | const updateListActionHandler = (state, action) => { 27 | const { success } = action.meta 28 | if (success) { 29 | const update = {current: {}} 30 | let updated = false 31 | update.current.webhooks = state.current.webhooks.reduce((acc, item, index) => { 32 | if (item.id === action.payload.id) { 33 | item = action.payload 34 | updated = true 35 | } 36 | acc.push(item) 37 | return acc 38 | }, []) 39 | if (updated) { 40 | return Object.assign({}, state, update) 41 | } 42 | } 43 | return state 44 | } 45 | 46 | const rmFromListActionHandler = (state, action) => { 47 | const { success } = action.meta 48 | if (success) { 49 | const update = {current: {}} 50 | let removed = null 51 | update.current.webhooks = state.current.webhooks.reduce((acc, item, index) => { 52 | if (item.id === action.payload.id) { 53 | removed = item 54 | } else { 55 | acc.push(item) 56 | } 57 | return acc 58 | }, []) 59 | if (removed) { 60 | return Object.assign({}, state, update) 61 | } 62 | } 63 | return state 64 | } 65 | 66 | // -------------------------------------- 67 | // Reducer 68 | // -------------------------------------- 69 | export default handleActions({ 70 | [FETCH_WEBHOOKS]: commonActionHandler, 71 | [CREATE_WEBHOOK]: addToListActionHandler, 72 | [UPDATE_WEBHOOK]: updateListActionHandler, 73 | [REMOVE_WEBHOOK]: rmFromListActionHandler 74 | }, { 75 | isProcessing: false, 76 | current: { 77 | webhooks: [] 78 | }, 79 | error: null 80 | }) 81 | 82 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors' 10 | @import './variables/components' 11 | @import './themes/default' 12 | */ 13 | 14 | @import 'scroll'; 15 | @import 'vendor/normalize'; 16 | @import 'readable'; 17 | @import 'bugfix'; 18 | 19 | -------------------------------------------------------------------------------- /src/styles/_bugfix.scss: -------------------------------------------------------------------------------- 1 | // Workaround for the following bug: 2 | // https://github.com/Semantic-Org/Semantic-UI/issues/2851 3 | body.dimmable.undetached.dimmed { 4 | .pushable { 5 | transform: none; 6 | } 7 | .pusher { 8 | position: static; 9 | } 10 | } 11 | 12 | // Workaround for the following bug: 13 | // https://github.com/Semantic-Org/Semantic-UI-React/pull/659 14 | .ui.menu .item.hack { 15 | > i.dropdown.icon { 16 | margin: 0; 17 | font-family: Icons; 18 | } 19 | } 20 | .ui.top.right.dropdown.hack { 21 | > i.dropdown.icon { 22 | display: none; 23 | } 24 | } 25 | 26 | .ui.top.right.dropdown.hack, 27 | .ui.menu .item.hack { 28 | &.plus > i.dropdown.icon { 29 | &:before { 30 | content: "\f067"; 31 | } 32 | } 33 | &.ellipsis-v > i.dropdown.icon { 34 | &:before { 35 | content: "\f142"; 36 | } 37 | } 38 | &.visible .menu.transition { 39 | display: block!important; 40 | visibility: visible!important; 41 | } 42 | } 43 | 44 | .ui.form .fields { 45 | margin: 0 !important; 46 | } 47 | 48 | .ui.label .icons { 49 | margin: 0 .75em 0 0; 50 | } 51 | 52 | @media screen and (max-width: 640px) { 53 | .ui.attached.tabular.menu { 54 | overflow-x: auto; 55 | overflow-y: hidden; 56 | } 57 | } 58 | 59 | // Workaround for modals that are not correctly centered 60 | .ui.page.modals.dimmer.transition.visible.active { 61 | display: flex!important; 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/_readable.scss: -------------------------------------------------------------------------------- 1 | .readable { 2 | font-size: 1em; 3 | font-size-adjust: none; 4 | font-style: normal; 5 | font-variant: normal; 6 | font-weight: normal; 7 | margin: 1em 0; 8 | img, figure { max-width: 100%; margin: 0; }; 9 | p { color: #111; font-weight: 300;} 10 | p img { margin: 0; padding: 0; } 11 | h1,h2, h3,h4,h5,h6 { font-weight: normal; color: #333; } 12 | h1 { font-size: 2.0em; } 13 | h2 { font-size: 1.8em; } 14 | h3 { font-size: 1.6em; } 15 | h4 { font-size: 1.4em; } 16 | h5,h6 { font-size: 1.2em; } 17 | ul { list-style-position: outside; } 18 | li ul, li ol { margin: 0 1em; } 19 | ul, ol { margin: 0 0 1em 0; } 20 | dl { margin: 0 0 1em 0; } 21 | dl dt { font-weight: bold; } 22 | dl dd { margin-left: 1em; } 23 | a { color:#005AF2; text-decoration:none; } 24 | a:hover { text-decoration: underline; } 25 | table { border-collapse: collapse; } 26 | th { font-weight:bold; } 27 | tr,th,td { margin:0; } 28 | tfoot { font-style: italic; } 29 | caption { text-align: center; } 30 | abbr, acronym { border-bottom:1px dotted #000; } 31 | address { margin-top:1.625em; font-style: italic; } 32 | del {color:#000;} 33 | blockquote { padding: 1em; font-style: italic; } 34 | blockquote:before { content: "\201C"; font-size: 3em; color: #aaa; line-height: 0; } /* From Tripoli */ 35 | blockquote > p { padding: 0; margin: 0; } 36 | strong { font-weight: bold; } 37 | em, dfn { font-style: italic; } 38 | dfn { font-weight: bold; } 39 | pre { margin: 0; padding: 10px; white-space: pre-wrap; color: #333; background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px;} 40 | pre, code { margin: 0.5em 0; } 41 | pre, code, tt { font: 1em monospace; line-height: 1.5; } 42 | p > code { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 0.2em 0.5em; } 43 | tt { display: block; margin: 1em 0; } 44 | hr { margin-bottom: 1em; } 45 | } 46 | -------------------------------------------------------------------------------- /src/styles/_scroll.scss: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | -webkit-appearance: none; 3 | background-color: rgba(0,0,0,.15); 4 | width: 8px; 5 | height: 8px; 6 | } 7 | 8 | ::-webkit-scrollbar-thumb { 9 | border-radius: 0; 10 | background-color: rgba(0,0,0,.4); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | // Some best-practice CSS that's useful for most apps 4 | // Just remove them if they're not what you want 5 | html { 6 | box-sizing: border-box; 7 | font-family: 'Roboto', sans-serif; 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | min-width: inherit !important; 15 | } 16 | 17 | *, *:before, *:after { 18 | box-sizing: inherit; 19 | } 20 | 21 | img { 22 | max-width: 100%; 23 | } 24 | 25 | #welcome { 26 | height: inherit; 27 | h2 { 28 | color: #fff; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/views/AboutView/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import AppBar from 'components/AppBar' 4 | 5 | export default class AboutView extends React.Component { 6 | componentDidMount () { 7 | document.title = 'About' 8 | } 9 | 10 | get header () { 11 | const $title = About 12 | return ( 13 | 14 | ) 15 | } 16 | 17 | render () { 18 | return ( 19 |
20 | {this.header} 21 |
22 |
23 |

24 | 25 | Nunux Keeper 26 |
Your personal content curation service
27 |

28 | 42 |
43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/views/BookmarkletView/styles.css: -------------------------------------------------------------------------------- 1 | .bookmarklet .ui.top.menu { 2 | border-radius: 0; 3 | margin: 0; 4 | } 5 | .bookmarklet .dropzone { 6 | height: 160px; 7 | text-align: center; 8 | padding: 2em; 9 | } 10 | .bookmarklet .dropzone.over { 11 | background-color: blue; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/views/DocumentView/styles.css: -------------------------------------------------------------------------------- 1 | .modificationDate, .originLink { 2 | font-size: smaller; 3 | color: #808080; 4 | font-style: italic; 5 | } 6 | 7 | .modificationDate { 8 | float: right; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/views/GraveyardView/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Menu, Icon } from 'semantic-ui-react' 4 | 5 | import { DocumentsView } from 'views/DocumentsView' 6 | import AppSignPanel from 'components/AppSignPanel' 7 | 8 | export class GraveyardView extends DocumentsView { 9 | constructor () { 10 | super() 11 | this.title = 'Trash' 12 | this.headerStyle = {backgroundColor: '#696969'} 13 | this.headerIcon = 'trash' 14 | this.handleEmptyGraveyard = this.handleEmptyGraveyard.bind(this) 15 | } 16 | 17 | get headerAltButton () { 18 | return 19 | } 20 | 21 | get noContent () { 22 | return ( 23 | 24 | 25 | The trash is empty 26 | 27 | ) 28 | } 29 | 30 | get data () { 31 | return this.props.graveyard 32 | } 33 | 34 | fetchFollowingDocuments () { 35 | const { actions, graveyard: { params } } = this.props 36 | params.from += params.size 37 | return actions.graveyard.fetchGhosts(params) 38 | } 39 | 40 | handleEmptyGraveyard () { 41 | const { actions } = this.props 42 | actions.graveyard.emptyGraveyard().then(() => { 43 | actions.notification.showNotification({header: 'The trash is emptied'}) 44 | }).catch((err) => { 45 | actions.notification.showNotification({ 46 | header: 'Unable to empty the trash', 47 | message: err.error, 48 | level: 'error' 49 | }) 50 | }) 51 | } 52 | } 53 | 54 | export default DocumentsView.connect(GraveyardView) 55 | 56 | -------------------------------------------------------------------------------- /src/views/LabelDocumentsView/index.jsx: -------------------------------------------------------------------------------- 1 | import { DocumentsView } from 'views/DocumentsView' 2 | 3 | export class LabelDocumentsView extends DocumentsView { 4 | constructor () { 5 | super() 6 | this.contextMenuItems = 'refresh,order,divider,editLabel,shareLabel,divider,deleteLabel' 7 | this.headerIcon = 'tag' 8 | } 9 | 10 | get label () { 11 | const { label } = this.props 12 | return label.current ? label.current : {label: 'Undefined'} 13 | } 14 | 15 | set title (title) { 16 | this._title = title 17 | } 18 | 19 | get title () { 20 | return this.label.label 21 | } 22 | 23 | set headerStyle (style) { 24 | this._headerStyle = style 25 | } 26 | 27 | get headerStyle () { 28 | return {backgroundColor: this.label.color} 29 | } 30 | 31 | get creatDocumentLink () { 32 | const link = super.creatDocumentLink() 33 | link.query = { 34 | labels: [this.label.id] 35 | } 36 | } 37 | } 38 | 39 | export default DocumentsView.connect(LabelDocumentsView) 40 | -------------------------------------------------------------------------------- /src/views/LabelView/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Form, Button } from 'semantic-ui-react' 4 | 5 | import { bindActions } from 'store/helper' 6 | 7 | import { routerActions as RouterActions } from 'react-router-redux' 8 | import LabelActions from 'store/label/actions' 9 | import { actions as NotificationActions } from 'store/modules/notification' 10 | 11 | import ColorSwatch from 'components/ColorSwatch' 12 | import AppBar from 'components/AppBar' 13 | 14 | export class LabelView extends React.Component { 15 | static propTypes = { 16 | actions: PropTypes.object.isRequired, 17 | label: PropTypes.object, 18 | location: PropTypes.object.isRequired 19 | }; 20 | 21 | constructor (props) { 22 | super(props) 23 | if (this.isCreateForm) { 24 | this.state = { 25 | label: '', 26 | color: '#8E44AD' 27 | } 28 | } else { 29 | this.state = {...props.label.current} 30 | } 31 | this.handleSubmit = this.handleSubmit.bind(this) 32 | this.handleCancel = this.handleCancel.bind(this) 33 | this.handleChange = this.handleChange.bind(this) 34 | this.handleColorChoose = this.handleColorChoose.bind(this) 35 | } 36 | 37 | componentWillReceiveProps (nextProps) { 38 | if ( 39 | !nextProps.label.isProcessing && 40 | nextProps.label.current 41 | ) { 42 | this.setState(nextProps.label.current) 43 | } 44 | } 45 | 46 | componentDidUpdate (prevProps) { 47 | document.title = this.title 48 | } 49 | 50 | get title () { 51 | if (this.isCreateForm) { 52 | return 'New label' 53 | } else if (this.props.label.current) { 54 | const { current: {label} } = this.props.label 55 | return `Edit label: ${label}` 56 | } 57 | } 58 | 59 | get isCreateForm () { 60 | const pathname = this.props.location.pathname 61 | return pathname === '/labels/create' 62 | } 63 | 64 | get isModalDisplayed () { 65 | const routerState = this.props.location.state 66 | return routerState && routerState.modal 67 | } 68 | 69 | get header () { 70 | return ( 71 | 75 | ) 76 | } 77 | 78 | get isValidLabel () { 79 | const { label } = this.state 80 | return label !== '' 81 | } 82 | 83 | get labelForm () { 84 | const { color, label } = this.state 85 | const { isProcessing } = this.props.label 86 | const loading = isProcessing 87 | const disabled = !this.isValidLabel 88 | return ( 89 |
90 | 101 | 102 | 103 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ) 118 | } 119 | 120 | handleSubmit (e) { 121 | e.preventDefault() 122 | if (!this.isValidLabel) { 123 | return false 124 | } 125 | const { actions } = this.props 126 | if (this.isCreateForm) { 127 | actions.label.createLabel(this.state).then((label) => { 128 | actions.router.push(`/labels/${label.id}`) 129 | actions.notification.showNotification({message: 'Label created'}) 130 | }).catch((err) => { 131 | actions.notification.showNotification({ 132 | header: 'Unable to create label', 133 | message: err.error, 134 | level: 'error' 135 | }) 136 | }) 137 | } else { 138 | const { current } = this.props.label 139 | actions.label.updateLabel(current, this.state).then((label) => { 140 | actions.router.push(`/labels/${label.id}`) 141 | actions.notification.showNotification({message: 'Label updated'}) 142 | }).catch((err) => { 143 | actions.notification.showNotification({ 144 | header: 'Unable to update label', 145 | message: err.error, 146 | level: 'error' 147 | }) 148 | }) 149 | } 150 | return false 151 | } 152 | 153 | handleCancel (e) { 154 | e.preventDefault() 155 | const { actions, location: loc } = this.props 156 | const { state } = loc 157 | if (state && state.returnTo) { 158 | actions.router.push(state.returnTo) 159 | } else { 160 | actions.router.push('/documents') 161 | } 162 | return false 163 | } 164 | 165 | handleChange (event) { 166 | this.setState({[event.target.name]: event.target.value}) 167 | } 168 | 169 | handleColorChoose (color) { 170 | this.setState({color}) 171 | } 172 | 173 | render () { 174 | return ( 175 |
176 | {this.header} 177 |
178 | {this.labelForm} 179 |
180 |
181 | ) 182 | } 183 | } 184 | 185 | const mapStateToProps = (state) => ({ 186 | location: state.router.locationBeforeTransitions, 187 | label: state.label 188 | }) 189 | 190 | const mapActionsToProps = (dispatch) => (bindActions({ 191 | notification: NotificationActions, 192 | label: LabelActions, 193 | router: RouterActions 194 | }, dispatch)) 195 | 196 | export default connect(mapStateToProps, mapActionsToProps)(LabelView) 197 | -------------------------------------------------------------------------------- /src/views/PublicDocumentView/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Dimmer, Loader } from 'semantic-ui-react' 4 | 5 | import DocumentContent from 'components/DocumentContent' 6 | 7 | import { bindActions } from 'store/helper' 8 | 9 | import { routerActions as RouterActions } from 'react-router-redux' 10 | 11 | import * as NProgress from 'nprogress' 12 | 13 | export class PublicDocumentView extends React.Component { 14 | static propTypes = { 15 | actions: PropTypes.object.isRequired, 16 | document: PropTypes.object.isRequired, 17 | location: PropTypes.object.isRequired 18 | }; 19 | 20 | constructor (props) { 21 | super(props) 22 | this.redirectBack = this.redirectBack.bind(this) 23 | } 24 | 25 | componentWillReceiveProps (nextProps) { 26 | // if no document found then redirect... 27 | if ( 28 | !nextProps.document.isFetching && 29 | !nextProps.document.isProcessing && 30 | !nextProps.document.current 31 | ) { 32 | console.debug('No more document. Redirecting...') 33 | this.redirectBack() 34 | } 35 | } 36 | 37 | componentDidUpdate (prevProps) { 38 | const {isProcessing} = this.props.document 39 | const {isProcessing: wasProcessing} = prevProps.document 40 | if (!wasProcessing && isProcessing) { 41 | NProgress.start() 42 | } else if (wasProcessing && !isProcessing) { 43 | NProgress.done() 44 | } 45 | document.title = this.title 46 | } 47 | 48 | redirectBack () { 49 | const {actions, location} = this.props 50 | const url = location.pathname 51 | const to = url.substr(0, url.lastIndexOf('/')) 52 | actions.router.push(to) 53 | } 54 | 55 | get originLink () { 56 | const { current: doc } = this.props.document 57 | if (doc.origin) { 58 | return ( 59 | 60 | Origin: {doc.origin} 61 | 62 | ) 63 | } 64 | } 65 | 66 | get modificationDate () { 67 | const { current: doc } = this.props.document 68 | if (doc.date) { 69 | const date = String(doc.date) 70 | return ( 71 | 72 | Last modification: {date} 73 | 74 | ) 75 | } 76 | } 77 | 78 | get title () { 79 | const { isFetching, current: doc } = this.props.document 80 | return isFetching || !doc ? 'Document' : doc.title 81 | } 82 | 83 | get header () { 84 | return ( 85 |

{this.title}

86 | ) 87 | } 88 | 89 | get document () { 90 | const { isFetching, isProcessing, current: doc } = this.props.document 91 | if (doc && !isFetching && !isProcessing) { 92 | return ( 93 |
94 | {this.originLink} 95 | 96 | {this.modificationDate} 97 |
98 | ) 99 | } 100 | } 101 | 102 | render () { 103 | const { isFetching, isProcessing } = this.props.document 104 | return ( 105 |
106 | {this.header} 107 | 108 | 109 | Loading 110 | 111 | {this.document} 112 | 113 |
114 | ) 115 | } 116 | } 117 | 118 | const mapStateToProps = (state) => ({ 119 | document: state.document, 120 | location: state.router.locationBeforeTransitions 121 | }) 122 | 123 | const mapActionsToProps = (dispatch) => (bindActions({ 124 | router: RouterActions 125 | }, dispatch)) 126 | 127 | export default connect(mapStateToProps, mapActionsToProps)(PublicDocumentView) 128 | 129 | -------------------------------------------------------------------------------- /src/views/PublicDocumentsView/index.jsx: -------------------------------------------------------------------------------- 1 | import { DocumentsView } from 'views/DocumentsView' 2 | 3 | export class PublicDocumentsView extends DocumentsView { 4 | constructor () { 5 | super() 6 | this.title = 'Public sharing' 7 | this.pub = true 8 | } 9 | 10 | get header () { 11 | return null 12 | } 13 | } 14 | 15 | export default DocumentsView.connect(PublicDocumentsView) 16 | -------------------------------------------------------------------------------- /src/views/SettingsView/ApiKeyTab/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { bindActions } from 'store/helper' 5 | import ProfileActions from 'store/profile/actions' 6 | import { actions as NotificationActions } from 'store/modules/notification' 7 | 8 | import { Modal, Message, Icon, Input, Button, Header, Divider, Segment } from 'semantic-ui-react' 9 | 10 | const API_ROOT = process.env.REACT_APP_API_ROOT 11 | const _parts = API_ROOT.split('://') 12 | const API_KEY_URL = `${_parts[0]}://api:KEY@${_parts[1]}` 13 | 14 | class ApiKeyTab extends React.Component { 15 | static propTypes = { 16 | actions: PropTypes.object.isRequired, 17 | profile: PropTypes.object 18 | }; 19 | 20 | state = { modalOpen: false }; 21 | 22 | handleOpen = () => this.setState({ modalOpen: true }); 23 | 24 | handleClose = () => this.setState({ modalOpen: false }); 25 | 26 | handleRef = (c) => { 27 | this.inputRef = c 28 | }; 29 | 30 | handleCopy = () => { 31 | this.inputRef.inputRef.select() 32 | document.execCommand('copy') 33 | }; 34 | 35 | handleGenerateApiKey = () => { 36 | this.handleClose() 37 | const { actions } = this.props 38 | actions.profile.updateProfile({resetApiKey: true}).then((profile) => { 39 | }).catch((err) => { 40 | actions.notification.showNotification({ 41 | header: 'Unable to generate API key', 42 | message: err.error, 43 | level: 'error' 44 | }) 45 | }) 46 | }; 47 | 48 | get apiKey () { 49 | const profile = this.props.profile.current 50 | 51 | if (profile && profile.apiKey) { 52 | return ( 53 | 54 | 55 | 56 | API key generated with success 57 | 58 | 63 |

64 | Please save somewhere this API key because we will never be able to show it again. 65 |

66 |
67 |
68 | ) 69 | } 70 | } 71 | 72 | render () { 73 | return ( 74 |
75 |
API key
76 | 77 |

78 | To fully access the API you have to use an OpenID Connect client and claim a valid access token. 79 | It's the standard way to interact with the API.
80 | But if you want something a bit simpler you have the possibility to use an API key. 81 | You only have to use this key as a basic password to acces the API. 82 |

83 |

84 | Ex: curl {API_KEY_URL}/documents 85 |

86 |

87 | An API key is not something secure. It's why you only have a limited acces to the API:
88 | You can only make POST or GET actions onto 89 | the /documents API. 90 |

91 | 92 | Regenerate API key} 94 | open={this.state.modalOpen} 95 | onClose={this.handleClose}> 96 | Regenerate API key 97 | 98 | 99 |

Are you sure you want to generate a new API key?

100 |

Previous key will be revoked.

101 |
102 |
103 | 104 | 107 | 110 | 111 |
112 | {this.apiKey} 113 |
114 |
115 | ) 116 | } 117 | } 118 | 119 | const mapStateToProps = (state) => ({ 120 | profile: state.profile 121 | }) 122 | 123 | const mapActionsToProps = (dispatch) => (bindActions({ 124 | notification: NotificationActions, 125 | profile: ProfileActions 126 | }, dispatch)) 127 | 128 | export default connect(mapStateToProps, mapActionsToProps)(ApiKeyTab) 129 | -------------------------------------------------------------------------------- /src/views/SettingsView/BookmarkletTab/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './styles.css' 4 | 5 | import { Header, Divider, Segment } from 'semantic-ui-react' 6 | 7 | export default class BookmarkletTab extends React.Component { 8 | 9 | handleClick () { 10 | alert('Don\'t click on me! But drag and drop me to your toolbar.') 11 | } 12 | 13 | baseUrl () { 14 | const { origin, pathname } = document.location 15 | const basePath = pathname.replace('/settings/bookmarklet', '') 16 | return origin + basePath 17 | } 18 | 19 | render () { 20 | return ( 21 |
22 |
The bookmarklet
23 | 24 |

25 | A bookmarklet is a small software application stored as a bookmark in a web browser, 26 | which typically allows a user to interact with the currently loaded web page in some way: 27 | In our case save the page as a document. 28 |

29 | 30 | Drag and drop the link bellow in your toolbar:  31 | 37 | Keep This! 38 | 39 | 40 |
41 | ) 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/views/SettingsView/BookmarkletTab/styles.css: -------------------------------------------------------------------------------- 1 | .bookmarklet-link { 2 | background: #E4E4E4; 3 | color: #000; 4 | padding: 10px 15px; 5 | border: 1px dashed; 6 | cursor: move; 7 | } 8 | -------------------------------------------------------------------------------- /src/views/SettingsView/ExportTab/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { bindActions } from 'store/helper' 5 | import ExportsActions from 'store/exports/actions' 6 | import ProfileActions from 'store/profile/actions' 7 | import ExportApi from 'api/export' 8 | 9 | import { Icon, Button, Progress, Statistic, Header, Divider, Message, Loader } from 'semantic-ui-react' 10 | 11 | export class ExportTab extends React.Component { 12 | static propTypes = { 13 | actions: PropTypes.object.isRequired, 14 | exports: PropTypes.object, 15 | profile: PropTypes.object 16 | }; 17 | 18 | componentDidMount () { 19 | const { actions } = this.props 20 | actions.exports.getExportStatus() 21 | actions.profile.fetchProfile(true) 22 | } 23 | 24 | handleScheduleAnExport = () => { 25 | const { actions } = this.props 26 | actions.exports.scheduleExport().then(actions.exports.getExportStatus) 27 | }; 28 | 29 | get exportWidget () { 30 | const {progress} = this.props.exports 31 | if (progress < 0) { 32 | return this.renderNoExportAvailable() 33 | } else if (progress >= 0 && progress < 100) { 34 | return this.renderExportInProgress() 35 | } else { 36 | return this.renderExportAvailable() 37 | } 38 | } 39 | 40 | get statistics () { 41 | const {isProcessing, current, error} = this.props.profile 42 | if (isProcessing) { 43 | return () 44 | } else if (error) { 45 | return ( 46 | 47 | Unable to get statistics 48 |

{error.toString()}

49 |
50 | ) 51 | } else if (current != null) { 52 | const usage = Math.ceil(current.storageUsage / 1048576) 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | } 62 | } 63 | 64 | renderNoExportAvailable () { 65 | const {error} = this.props.exports 66 | const txt = error ? error.toString() : 'No export available' 67 | return ( 68 | 69 | {txt} 70 |

71 | You can schedule an export. Depending the number of document this can take a while. 72 |

73 |

74 | 77 |

78 |
79 | ) 80 | } 81 | 82 | renderExportInProgress () { 83 | const {progress, exported, total, error} = this.props.exports 84 | 85 | const txt = error ? error.toString() : `${exported} / ${total}` 86 | return ( 87 | 88 | 89 | Export in progress 90 | 91 |

92 |

Exporting documents...

93 | 94 | { txt } 95 | 96 |
97 | ) 98 | } 99 | 100 | renderExportAvailable () { 101 | const downloadUrl = ExportApi.getDownloadUrl() 102 | return ( 103 | 104 | 105 | Export available 106 | 107 |

Export ready to download.

108 | 111 | 114 |
115 | ) 116 | } 117 | 118 | render () { 119 | return ( 120 |
121 |
Usage and Export
122 | 123 |

124 | Export all your documents in order to re-import them into another 125 | Nunux Keeper instance or to simply create a backup. 126 |

127 |

Here your current usage:

128 | { this.statistics } 129 | { this.exportWidget } 130 |
131 | ) 132 | } 133 | } 134 | 135 | const mapStateToProps = (state) => ({ 136 | exports: state.exports, 137 | profile: state.profile 138 | }) 139 | 140 | const mapActionsToProps = (dispatch) => (bindActions({ 141 | exports: ExportsActions, 142 | profile: ProfileActions 143 | }, dispatch)) 144 | 145 | export default connect(mapStateToProps, mapActionsToProps)(ExportTab) 146 | 147 | -------------------------------------------------------------------------------- /src/views/SettingsView/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActions } from 'store/helper' 4 | 5 | import AppBar from 'components/AppBar' 6 | 7 | import { Tab } from 'semantic-ui-react' 8 | 9 | import { routerActions as RouterActions } from 'react-router-redux' 10 | 11 | import BookmarkletTab from 'views/SettingsView/BookmarkletTab' 12 | import ApiKeyTab from 'views/SettingsView/ApiKeyTab' 13 | import ApiClientsTab from 'views/SettingsView/ApiClientsTab' 14 | import ExportTab from 'views/SettingsView/ExportTab' 15 | import WebhooksTab from 'views/SettingsView/WebhooksTab' 16 | 17 | const PANES = [ 18 | { 19 | menuItem: { key: 'bookmarklet', icon: 'bookmark', content: 'Bookmarklet' }, 20 | render: () => 21 | }, { 22 | menuItem: { key: 'api-key', icon: 'key', content: 'API key' }, 23 | render: () => 24 | }, { 25 | menuItem: { key: 'api-clients', icon: 'protect', content: 'API clients' }, 26 | render: () => 27 | }, { 28 | menuItem: { key: 'export', icon: 'download', content: 'Export' }, 29 | render: () => 30 | }, { 31 | menuItem: { key: 'webhooks', icon: 'send', content: 'Webhooks' }, 32 | render: () => 33 | } 34 | ] 35 | 36 | class SettingsView extends React.Component { 37 | static propTypes = { 38 | params: PropTypes.object.isRequired, 39 | actions: PropTypes.object.isRequired, 40 | loc: PropTypes.object.isRequired 41 | }; 42 | 43 | componentDidMount () { 44 | const title = this.panes[this.activeIndex].menuItem.content 45 | document.title = `Settings: ${title}` 46 | } 47 | 48 | handleTabChange = (e, data) => { 49 | const { actions } = this.props 50 | const key = this.panes[data.activeIndex].menuItem.key 51 | const pathname = `/settings/${key}` 52 | actions.router.push({pathname}) 53 | }; 54 | 55 | get header () { 56 | const $title = Settings 57 | return ( 58 | 59 | ) 60 | } 61 | 62 | get panes () { 63 | return PANES 64 | } 65 | 66 | get activeIndex () { 67 | const { tab } = this.props.params 68 | const index = this.panes.findIndex(pane => pane.menuItem.key === tab) 69 | return index < 0 ? 0 : index 70 | } 71 | 72 | render () { 73 | return ( 74 |
75 | {this.header} 76 |
77 | 82 |
83 |
84 | ) 85 | } 86 | } 87 | 88 | const mapStateToProps = (state) => ({ 89 | loc: state.router.locationBeforeTransitions 90 | }) 91 | 92 | const mapActionsToProps = (dispatch) => (bindActions({ 93 | router: RouterActions 94 | }, dispatch)) 95 | 96 | export default connect(mapStateToProps, mapActionsToProps)(SettingsView) 97 | -------------------------------------------------------------------------------- /src/views/SharedDocumentsView/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Icon } from 'semantic-ui-react' 4 | 5 | import { DocumentsView } from 'views/DocumentsView' 6 | import AppSignPanel from 'components/AppSignPanel' 7 | 8 | export class SharedDocumentsView extends DocumentsView { 9 | 10 | constructor () { 11 | super() 12 | this.title = 'Sharing' 13 | this.tileContextMenuItems = 'detail' 14 | this.headerStyle = {backgroundColor: '#1678c2'} 15 | this.headerIcon = 'share alternate' 16 | } 17 | 18 | get headerAltButton () { 19 | return null 20 | } 21 | 22 | get noContent () { 23 | return ( 24 | 25 | 26 | No shared documents 27 | 28 | ) 29 | } 30 | } 31 | 32 | export default DocumentsView.connect(SharedDocumentsView) 33 | --------------------------------------------------------------------------------