diff --git a/.babelrc b/.babelrc index b16cc73..6464282 100644 --- a/.babelrc +++ b/.babelrc @@ -11,6 +11,7 @@ "react" ], "plugins": [ + "transform-decorators-legacy", "transform-class-properties", ["transform-object-rest-spread", { "useBuiltIns": true }] ] diff --git a/.eslintrc.json b/.eslintrc.json index bd858cc..632602f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "parser": "babel-eslint", "env": { "browser": true, "commonjs": true, @@ -12,15 +13,38 @@ "sourceType": "module" }, "rules": { + "strict": 0, "no-const-assign": "warn", - "no-this-before-super": "warn", - "no-undef": "warn", + "no-this-before-super": "error", + "no-undef": "error", "no-unreachable": "warn", "no-unused-vars": "warn", "constructor-super": "warn", - "valid-typeof": "warn" + "react/jsx-uses-vars": 1, + "react/jsx-uses-react": 1, + "valid-typeof": "warn", + "babel/new-cap": 1, + "babel/no-invalid-this": 1, + "babel/object-curly-spacing": 0, + "babel/quotes": 0, + "babel/semi": 0, + "babel/no-unused-expressions": 1, + "import/order": [ + 1, + { + "groups": ["external", "internal", "builtin", ["parent", "sibling", "index"]], + "newlines-between": "never" + } + ], + "babel/no-invalid-this": 0, + "no-unused-vars": 0 }, "plugins": [ - "class-property" - ] + "babel", + "react", + "import" + ], + "globals": { + "chrome": true + } } diff --git a/README.md b/README.md index 05f20b5..722a26d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,35 @@ # Gitako -Yet another Chrome extension for GitHub. Inspired by [Octotree](https://github.com/buunguyen/octotree) & [GayHub](https://github.com/jawil/GayHub). Aimed at providing better UX, consistent UI regards to GitHub and more essential features. +Yet another Chrome extension for GitHub. Inspired by [Octotree](https://github.com/buunguyen/octotree) & [GayHub](https://github.com/jawil/GayHub), thanks to them! Gitako is aimed at providing better UX, consistent UI regards to GitHub and more essential features. ### Features - [x] neat file tree - [x] instant file search -- [x] keyboard accessibility - - press 'up' and 'down' to nav between search input and files in the list - - press 'enter' or 'right' to redirect to file or expand folder - - press 'left' to jump to parent folder or collapse current folder +- [x] fast even in gigantic projects +- [x] intuitive keyboard accessibility + - file navigation using arrow keys + - set shortcut to toggle sidebar - [x] redirect smoothly with pjax -- [x] enlargeable when window is wide enough -- [x] copy snippets with one click - - copy code snippets inside markdown preview - - copy all contents of a file -- [x] support access token (for private repo and frequent usage) -- [x] consistent UI with GitHub - - [x] use [octicons](https://octicons.github.com) -- [x] light size (~30kb) & low memory usage +- [x] resize-able when window is wide enough +- [x] copy snippets with single click + - copy code snippets from markdown preview + - copy whole content of a file +- [x] support private repo and frequent usages +- [x] support git submodule +- [x] UI consistent to GitHub's +- [x] light size (~60kb) & low memory usage - [more WIP](https://github.com/EnixCoda/Gitako/projects/1) -### Download +[Change Log](https://github.com/EnixCoda/Gitako/wiki/ChangeLog) -[install from Chrome web store](https://chrome.google.com/webstore/detail/gitako/giljefjcheohhamkjphiebfjnlphnokk) +### Install + +[Please install from Chrome Web Store](https://chrome.google.com/webstore/detail/gitako/giljefjcheohhamkjphiebfjnlphnokk) ### Issues -Any bug report or feature requests are welcomed, feel free to draft a [issue](https://github.com/EnixCoda/Gitako/issues/)! +Any bug report or feature request discussions are welcomed, feel free to draft a [issue](https://github.com/EnixCoda/Gitako/issues/)! ### About diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..b50e39b --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "es2017", + "experimentalDecorators": true, + "baseUrl": "./", + "paths": { + "*" : ["src/*", "packages/*"] + } + }, + "exclude": [ + "node_modules", + ], + "include": [ + "src/**/*", + "packages/**/*", + ] +} diff --git a/package.json b/package.json index 6099958..71d8a6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "0.2.4", + "version": "0.3.16", "description": "yet another extension for GitHub", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda", @@ -8,28 +8,41 @@ "private": true, "scripts": { "start": "webpack --watch", - "dev": "webpack", - "prod": "NODE_ENV=production webpack && cd dist && rm -f ./gitako.zip && zip gitako.zip -r *" + "stamp": "node scripts/version.js", + "postversion": "npm run stamp && git add src/manifest.json && git commit --amend --no-edit", + "prod": "npm run stamp && NODE_ENV=production webpack && cd dist && rm -f ./gitako.zip && zip gitako.zip -r *" }, "dependencies": { + "ini": "^1.3.5", "nprogress": "^0.2.0", "octicons": "^7.1.0", - "preact": "^8.2.6", - "preact-portal": "^1.1.3" + "prop-types": "^15.6.1", + "react": "^16.4.0", + "react-dom": "^16.4.0" }, "devDependencies": { "babel-core": "^6.26.0", + "babel-eslint": "^8.2.3", "babel-loader": "^7.1.2", "babel-plugin-react-require": "^3.0.0", "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators-legacy": "^1.3.5", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "copy-webpack-plugin": "^4.2.0", "css-loader": "^0.28.7", + "eslint": "^4.19.1", + "eslint-plugin-babel": "^5.1.0", + "eslint-plugin-class-property": "^1.1.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-react": "^7.9.1", "file-loader": "^1.1.5", + "fsevents": "^1.2.4", + "json-loader": "^0.5.7", "less": "^2.7.3", "less-loader": "^4.0.5", + "react-life-hook": "^0.2.0", "style-loader": "^0.19.0", "svgr": "^1.6.0", "uglifyjs-webpack-plugin": "^1.1.1", diff --git a/generate-octicon.js b/scripts/generate-octicon.js similarity index 100% rename from generate-octicon.js rename to scripts/generate-octicon.js diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000..3aa7456 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,10 @@ +const fs = require('fs') +const path = require('path') + +const rootPath = path.resolve(__dirname, '../') +const packageJSON = require(path.resolve(rootPath, 'package.json')) +const manifestPath = path.resolve(rootPath, 'src/manifest.json') +const manifest = require(manifestPath) + +manifest.version = packageJSON.version +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, ' '), 'utf-8') diff --git a/src/analytics.js b/src/analytics.js new file mode 100644 index 0000000..9cf2a31 --- /dev/null +++ b/src/analytics.js @@ -0,0 +1,34 @@ +// TODO: set this through ENV or something else +const LOG_ENDPOINT = 'https://enix.one/gitako/log' + +export function raiseError(error) { + return reportError(error) +} + +export function withErrorLog(method, args) { + return [ + function() { + try { + method.apply(this, arguments) + } catch (error) { + raiseError(error) + } + }, + args, + ] +} + +function encodeParams(params) { + return Object.keys(params) + .map(key => `${key}=${encodeURIComponent(JSON.stringify(params[key]))}`) + .join('&') +} + +function reportError(error) { + return fetch( + `${LOG_ENDPOINT}?${encodeParams({ + error: (error && error.message) || error, + path: window.location.href, + })}` + ) +} diff --git a/src/components/FileExplorer.js b/src/components/FileExplorer.js index dc1b32f..3dd07c6 100644 --- a/src/components/FileExplorer.js +++ b/src/components/FileExplorer.js @@ -1,203 +1,100 @@ -import preact from 'preact' -/** @jsx preact.h */ -import SearchBar from './SearchBar' -import Node from './Node' +import React from 'react' +import PropTypes from 'prop-types' +import connect from 'driver/connect' +import { FileExplorer as FileExplorerCore } from 'driver/core' +import SearchBar from 'components/SearchBar' +import Node from 'components/Node' +import LoadingIndicator from 'components/LoadingIndicator' +import cx from 'utils/cx' -import cx from '../utils/cx' -import DOMHelper from '../utils/DOMHelper' -import treeParser from '../utils/treeParser' -import URLHelper from '../utils/URLHelper' -import VisibleNodesGenerator from '../utils/VisibleNodesGenerator' +@connect(FileExplorerCore) +export default class FileExplorer extends React.Component { + static propTypes = { + treeData: PropTypes.object, + metaData: PropTypes.object, + freeze: PropTypes.bool, + visibleNodes: PropTypes.object, -export default class List extends preact.Component { - state = { - // generated by this.visibleNodesGenerator - visibleNodes: null, + init: PropTypes.func.isRequired, + execAfterRender: PropTypes.func.isRequired, + handleKeyDown: PropTypes.func.isRequired, + handleSearchKeyChange: PropTypes.func.isRequired, + setExpand: PropTypes.func.isRequired, + toggleNodeExpansion: PropTypes.func.isRequired, + focusNode: PropTypes.func.isRequired, + onNodeClick: PropTypes.func.isRequired, + updateVisibleNodes: PropTypes.func.isRequired, } - props = { + static defaultProps = { treeData: null, metaData: null, freeze: false, + visibleNodes: null, } - tasksAfterRender = [] - visibleNodesGenerator = new VisibleNodesGenerator() - componentWillMount() { - const { treeData, metaData } = this.props - const { root, nodes } = treeParser.parse(treeData, metaData) - this.visibleNodesGenerator.plantTree(root, nodes) - const currentPath = URLHelper.getCurrentPath(true) - this.tasksAfterRender.push(DOMHelper.focusSearchInput) - if (currentPath.length) { - const nodeExpandedTo = this.visibleNodesGenerator.expandTo(currentPath) - if (nodeExpandedTo) { - this.visibleNodesGenerator.focusNode(nodeExpandedTo) - const { nodes } = this.visibleNodesGenerator.visibleNodes - this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo))) - } - } - this.updateVisibleNodes() + const { init, setUpTree, treeData } = this.props + init() + setUpTree(treeData) } componentDidMount() { - this.execAfterRender() + const { execAfterRender } = this.props + execAfterRender() } - componentDidUpdate(prevProps, prevState) { - this.execAfterRender() - } - - execAfterRender() { - for (const task of this.tasksAfterRender) { - task() - } - this.tasksAfterRender.length = 0 - } - - updateVisibleNodes() { - const { visibleNodes } = this.visibleNodesGenerator - this.setState({ visibleNodes }) - } - - handleKeyDown = event => { - const { key } = event - const { visibleNodes: { nodes, focusedNode, expandedNodes, depths } } = this.state - let shouldStopPropagation = true // prevent body scrolling - if (focusedNode) { - const focusedNodeIndex = nodes.indexOf(focusedNode) - switch (key) { - case 'ArrowUp': - // focus on previous node - if (focusedNodeIndex === 0) { - this.focusNode(null) - this.tasksAfterRender.push(DOMHelper.focusSearchInput) - } else { - this.focusNode(nodes[focusedNodeIndex - 1]) - } - break - - case 'ArrowDown': - // focus on next node - if (focusedNodeIndex + 1 < nodes.length) { - this.focusNode(nodes[focusedNodeIndex + 1]) - } else { - this.focusNode(null) - this.tasksAfterRender.push(DOMHelper.focusSearchInput) - } - break - - case 'ArrowLeft': - // collapse node or go to parent node - if (expandedNodes.has(focusedNode)) { - this.setExpand(focusedNode, false) - } else { - // go forward to the start of the list, find the closest node with lower depth - let indexOfParentNode = focusedNodeIndex - const focusedNodeDepth = depths.get(nodes[focusedNodeIndex]) - while ( - indexOfParentNode !== -1 && - depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth - ) { - --indexOfParentNode - } - if (indexOfParentNode !== -1) { - this.focusNode(nodes[indexOfParentNode]) - } - } - break - - // consider the two keys as 'confirm' key - case 'ArrowRight': - case 'Enter': - // expand node or redirect to file page - if (focusedNode.type === 'tree') { - this.setExpand(focusedNode, true) - } else { - // simulate click to trigger pjax - DOMHelper.clickOnNodeElement(focusedNodeIndex) - } - break - - default: - shouldStopPropagation = false - } - } else { - // now search input is focused - if (nodes.length) { - switch (key) { - case 'ArrowDown': - this.focusNode(nodes[0]) - break - case 'ArrowUp': - this.focusNode(nodes[nodes.length - 1]) - break - default: - shouldStopPropagation = false - } - } else { - shouldStopPropagation = false - } - } - if (shouldStopPropagation) { - event.stopPropagation() - event.preventDefault() + componentWillReceiveProps(nextProps) { + if (nextProps.treeData !== this.props.treeData) { + const { setUpTree } = nextProps + setUpTree() } } - handleSearchKeyChange = async event => { - const searchKey = event.target.value - await this.visibleNodesGenerator.search(searchKey) - this.updateVisibleNodes() + componentDidUpdate() { + const { execAfterRender } = this.props + execAfterRender() } - setExpand = (node, expand) => { - this.visibleNodesGenerator.setExpand(node, expand) - this.focusNode(node) - this.tasksAfterRender.push(DOMHelper.focusSearchInput) - } - - toggleNodeExpand = node => { - this.visibleNodesGenerator.toggleExpand(node) - this.focusNode(node) - this.tasksAfterRender.push(DOMHelper.focusFileExplorer) - } - - focusNode = node => { - this.visibleNodesGenerator.focusNode(node) - if (node) { - // when focus a node not in viewport(by keyboard), scroll to it - const { visibleNodes: { nodes } } = this.state - const indexOfToBeFocusedNode = nodes.indexOf(node) - this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode)) - this.tasksAfterRender.push(DOMHelper.focusSearchInput) + renderFiles(visibleNodes, onNodeClick) { + const { nodes, depths, focusedNode, expandedNodes } = visibleNodes + if (nodes.length === 0) { + return ( + + ) } - this.updateVisibleNodes() + return ( +
-
- Gitako needs access token with proper scopes (recommended: repo) to access this - repository. Please save it in the settings below. + Due to{' '} + + limitation of GitHub + {' '} + or{' '} + + auth needs + + , Gitako needs access token to continue. Please follow the instructions in the settings + panel below.