├── .clang-format ├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── angular-cli-build.js ├── angular-cli.json ├── config ├── environment.dev.ts ├── environment.js ├── environment.prod.ts ├── karma-test-shim.js ├── karma.conf.js └── protractor.conf.js ├── e2e ├── app.e2e.ts ├── app.po.ts ├── tsconfig.json └── typings.d.ts ├── firebase.json ├── package.json ├── public └── .npmignore ├── src ├── app │ ├── +issues │ │ ├── +filter │ │ │ ├── filter.component.css │ │ │ ├── filter.component.html │ │ │ ├── filter.component.spec.ts │ │ │ ├── filter.component.ts │ │ │ ├── index.ts │ │ │ └── shared │ │ │ │ └── index.ts │ │ ├── +list │ │ │ ├── index.ts │ │ │ ├── issue-row │ │ │ │ ├── index.ts │ │ │ │ ├── issue-row.component.css │ │ │ │ ├── issue-row.component.html │ │ │ │ ├── issue-row.component.spec.ts │ │ │ │ └── issue-row.component.ts │ │ │ ├── list.component.css │ │ │ ├── list.component.html │ │ │ ├── list.component.spec.ts │ │ │ ├── list.component.ts │ │ │ ├── not-pending-removal.pipe.spec.ts │ │ │ ├── not-pending-removal.pipe.ts │ │ │ ├── shared │ │ │ │ └── index.ts │ │ │ └── toolbar │ │ │ │ ├── index.ts │ │ │ │ ├── toolbar.component.css │ │ │ │ ├── toolbar.component.html │ │ │ │ ├── toolbar.component.spec.ts │ │ │ │ └── toolbar.component.ts │ │ ├── +triage │ │ │ ├── index.ts │ │ │ ├── is-checked.pipe.spec.ts │ │ │ ├── is-checked.pipe.ts │ │ │ ├── shared │ │ │ │ └── index.ts │ │ │ ├── to-date.pipe.spec.ts │ │ │ ├── to-date.pipe.ts │ │ │ ├── triage.component.css │ │ │ ├── triage.component.html │ │ │ ├── triage.component.spec.ts │ │ │ └── triage.component.ts │ │ ├── index.ts │ │ ├── issues.component.css │ │ ├── issues.component.html │ │ ├── issues.component.spec.ts │ │ ├── issues.component.ts │ │ └── shared │ │ │ └── index.ts │ ├── +login │ │ ├── index.ts │ │ ├── login.component.css │ │ ├── login.component.html │ │ ├── login.component.spec.ts │ │ ├── login.component.ts │ │ └── shared │ │ │ └── index.ts │ ├── +repo-selector │ │ ├── index.ts │ │ ├── repo-selector-row │ │ │ ├── index.ts │ │ │ ├── repo-selector-row.component.css │ │ │ ├── repo-selector-row.component.html │ │ │ ├── repo-selector-row.component.spec.ts │ │ │ └── repo-selector-row.component.ts │ │ ├── repo-selector.component.css │ │ ├── repo-selector.component.html │ │ ├── repo-selector.component.spec.ts │ │ ├── repo-selector.component.ts │ │ └── shared │ │ │ └── index.ts │ ├── environment.ts │ ├── filter-store.service.spec.ts │ ├── filter-store.service.ts │ ├── github.service.spec.ts │ ├── github.service.ts │ ├── index.ts │ ├── issue-zero.component.css │ ├── issue-zero.component.html │ ├── issue-zero.component.spec.ts │ ├── issue-zero.component.ts │ ├── repo-params.service.spec.ts │ ├── repo-params.service.ts │ └── shared │ │ ├── config.ts │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts ├── favicon.ico ├── icons │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── icon.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── index.html ├── main-app-shell.ts ├── main.ts ├── manifest.webapp ├── system-config.ts ├── system-import.js ├── tsconfig.json └── typings.d.ts ├── tslint.json └── typings.json /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | 14 | # misc 15 | /.sass-cache 16 | /connect.lock 17 | /coverage/* 18 | /libpeerconnection.log 19 | npm-debug.log 20 | testem.log 21 | /typings 22 | 23 | # e2e 24 | /e2e/*.js 25 | /e2e/*.map 26 | 27 | #System Files 28 | .DS_Store 29 | Thumbs.db 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/.bin/ng", 9 | "stopOnEntry": false, 10 | "args": ["serve"], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": false, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "ChildProc", 26 | "type": "node", 27 | "request": "launch", 28 | "program": "${workspaceRoot}/node_modules/angular2-broccoli-prerender/dist/child_proc.js", 29 | "stopOnEntry": false, 30 | "args": [ "/Users/crossj/Projects/issue-zero/node_modules/angular2-broccoli-prerender/dist/child_proc.js", 31 | "--sourceHtml=/Users/crossj/Projects/issue-zero/tmp/app_shell_plugin-input_base_path-yrSu8NXb.tmp/0/index.html", 32 | "--optionsPath=/Users/crossj/Projects/issue-zero/tmp/app_shell_plugin-input_base_path-yrSu8NXb.tmp/0/main-app-shell", 33 | "--outputIndexPath=/Users/crossj/Projects/issue-zero/tmp/app_shell_plugin-output_path-nPBVCEAe.tmp/index.html" ], 34 | "cwd": "${workspaceRoot}", 35 | "preLaunchTask": null, 36 | "runtimeExecutable": null, 37 | "runtimeArgs": [ 38 | "--nolazy" 39 | ], 40 | "env": { 41 | "NODE_ENV": "development" 42 | }, 43 | "externalConsole": false, 44 | "sourceMaps": false, 45 | "outDir": null 46 | }, 47 | { 48 | "name": "Attach", 49 | "type": "node", 50 | "request": "attach", 51 | "port": 5858, 52 | "address": "localhost", 53 | "restart": false, 54 | "sourceMaps": false, 55 | "outDir": null, 56 | "localRoot": "${workspaceRoot}", 57 | "remoteRoot": null 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.watcherExclude": { 4 | "**/.git/objects/**": true, 5 | "**/node_modules/**": true, 6 | "tmp/": true, 7 | "dist/": true, 8 | "typings/": true 9 | }, 10 | "search.exclude": { 11 | "**/.git": true, 12 | "**/.DS_Store": true, 13 | "tmp/": true, 14 | "node_modules": true, 15 | "typings/": true 16 | } 17 | } -------------------------------------------------------------------------------- /angular-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | 3 | var Angular2App = require('angular-cli/lib/broccoli/angular2-app'); 4 | 5 | module.exports = function(defaults) { 6 | return new Angular2App(defaults, { 7 | vendorNpmFiles: [ 8 | 'systemjs/dist/system-polyfills.js', 9 | 'systemjs/dist/system.src.js', 10 | 'zone.js/dist/*.js', 11 | 'es6-shim/es6-shim.js', 12 | 'reflect-metadata/*.js', 13 | 'rxjs/**/*.js', 14 | '@angular/**/*.js', 15 | '@angular2-material/**/*.+(js|map|css|svg)', 16 | 'angularfire2/**/*.js', 17 | 'firebase/lib/firebase-web.js', 18 | 'hammerjs/hammer.min.js', 19 | 'material-design-icons/navigation/svg/production/ic_menu_24px.svg', 20 | 'material-design-icons/navigation/svg/production/ic_arrow_back_24px.svg', 21 | 'material-design-icons/content/svg/production/ic_filter_list_24px.svg', 22 | 'material-design-icons/action/svg/production/ic_delete_24px.svg', 23 | '@ngrx/**/*.js' 24 | ] 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.2-mobile.2", 4 | "name": "issue-zero" 5 | }, 6 | "apps": [ 7 | { 8 | "main": "src/main.ts", 9 | "tsconfig": "src/tsconfig.json", 10 | "mobile": true 11 | } 12 | ], 13 | "addons": [], 14 | "packages": [], 15 | "e2e": { 16 | "protractor": { 17 | "config": "config/protractor.conf.js" 18 | } 19 | }, 20 | "test": { 21 | "karma": { 22 | "config": "config/karma.conf.js" 23 | } 24 | }, 25 | "defaults": { 26 | "prefix": "app", 27 | "sourceDir": "src" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | return { 5 | environment: environment, 6 | baseURL: '/', 7 | locationType: 'auto' 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /config/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | __karma__.loaded = function () { 6 | }; 7 | 8 | var distPath = '/base/dist/'; 9 | var appPath = distPath + 'app/'; 10 | 11 | function isJsFile(path) { 12 | return path.slice(-3) == '.js'; 13 | } 14 | 15 | function isSpecFile(path) { 16 | return path.slice(-8) == '.spec.js'; 17 | } 18 | 19 | function isAppFile(path) { 20 | return isJsFile(path) && (path.substr(0, appPath.length) == appPath); 21 | } 22 | 23 | var allSpecFiles = Object.keys(window.__karma__.files) 24 | .filter(isSpecFile) 25 | .filter(isAppFile); 26 | 27 | // Load our SystemJS configuration. 28 | System.config({ 29 | baseURL: distPath 30 | }); 31 | 32 | System.import('system-config.js').then(function() { 33 | // Load and configure the TestComponentBuilder. 34 | return Promise.all([ 35 | System.import('@angular/core/testing'), 36 | System.import('@angular/platform-browser-dynamic/testing') 37 | ]).then(function (providers) { 38 | var testing = providers[0]; 39 | var testingBrowser = providers[1]; 40 | 41 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 42 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 43 | }); 44 | }).then(function() { 45 | // Finally, load all spec files. 46 | // This will run the tests directly. 47 | return Promise.all( 48 | allSpecFiles.map(function (moduleName) { 49 | return System.import(moduleName); 50 | })); 51 | }).then(__karma__.start, __karma__.error); -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '..', 4 | frameworks: ['jasmine'], 5 | plugins: [ 6 | require('karma-jasmine'), 7 | require('karma-chrome-launcher') 8 | ], 9 | customLaunchers: { 10 | // chrome setup for travis CI using chromium 11 | Chrome_travis_ci: { 12 | base: 'Chrome', 13 | flags: ['--no-sandbox'] 14 | } 15 | }, 16 | files: [ 17 | { pattern: 'dist/vendor/es6-shim/es6-shim.js', included: true, watched: false }, 18 | { pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false }, 19 | { pattern: 'dist/vendor/reflect-metadata/Reflect.js', included: true, watched: false }, 20 | { pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false }, 21 | { pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false }, 22 | { pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false }, 23 | 24 | { pattern: 'config/karma-test-shim.js', included: true, watched: true }, 25 | 26 | // Distribution folder. 27 | { pattern: 'dist/**/*', included: false, watched: true } 28 | ], 29 | exclude: [ 30 | // Vendor packages might include spec files. We don't want to use those. 31 | 'dist/vendor/**/*.spec.js' 32 | ], 33 | preprocessors: {}, 34 | reporters: ['progress'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /config/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /*global jasmine */ 2 | var SpecReporter = require('jasmine-spec-reporter'); 3 | 4 | exports.config = { 5 | allScriptsTimeout: 11000, 6 | specs: [ 7 | '../e2e/**/*.e2e.ts' 8 | ], 9 | capabilities: { 10 | 'browserName': 'chrome' 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function() {} 19 | }, 20 | useAllAngular2AppRoots: true, 21 | beforeLaunch: function() { 22 | require('ts-node').register({ 23 | project: 'e2e' 24 | }); 25 | }, 26 | onPrepare: function() { 27 | jasmine.getEnv().addReporter(new SpecReporter()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /e2e/app.e2e.ts: -------------------------------------------------------------------------------- 1 | import { IssueZeroPage } from './app.po'; 2 | 3 | describe('issue-zero App', function() { 4 | let page: IssueZeroPage; 5 | 6 | beforeEach(() => { 7 | page = new IssueZeroPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('issue-zero works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | export class IssueZeroPage { 2 | navigateTo() { 3 | return browser.get('/'); 4 | } 5 | 6 | getParagraphText() { 7 | return element(by.css('issue-zero-app h1')).getText(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "mapRoot": "", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": false, 12 | "rootDir": ".", 13 | "sourceMap": true, 14 | "sourceRoot": "/", 15 | "target": "es5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "issue-zero", 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [{ 10 | "source": "/issues", 11 | "destination": "/index.html" 12 | },{ 13 | "source": "/login", 14 | "destination": "/index.html" 15 | },{ 16 | "source": "/issues/*/**", 17 | "destination": "/index.html" 18 | },{ 19 | "source": "/repo-selector", 20 | "destination": "/index.html" 21 | }] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "issue-zero", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "start": "ng server", 8 | "postinstall": "typings install", 9 | "lint": "tslint \"src/**/*.ts\"", 10 | "format": "clang-format -i -style=file --glob=src/**/*.ts", 11 | "test": "ng test", 12 | "pree2e": "webdriver-manager update", 13 | "e2e": "protractor" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/app-shell": "0.0.0", 18 | "@angular/common": "2.0.0-rc.1", 19 | "@angular/compiler": "2.0.0-rc.1", 20 | "@angular/core": "2.0.0-rc.1", 21 | "@angular/http": "2.0.0-rc.1", 22 | "@angular/platform-browser": "2.0.0-rc.1", 23 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 24 | "@angular/router": "2.0.0-rc.1", 25 | "@angular2-material/button": "^2.0.0-alpha.4", 26 | "@angular2-material/card": "^2.0.0-alpha.4", 27 | "@angular2-material/checkbox": "^2.0.0-alpha.4", 28 | "@angular2-material/core": "^2.0.0-alpha.4", 29 | "@angular2-material/icon": "^2.0.0-alpha.4", 30 | "@angular2-material/input": "^2.0.0-alpha.4", 31 | "@angular2-material/list": "^2.0.0-alpha.4", 32 | "@angular2-material/progress-circle": "^2.0.0-alpha.4", 33 | "@angular2-material/sidenav": "^2.0.0-alpha.4", 34 | "@angular2-material/toolbar": "^2.0.0-alpha.4", 35 | "@ngrx/store": "^1.5.0", 36 | "angular-cli": "^1.0.0-beta.4", 37 | "angularfire2": "^2.0.0-beta.0", 38 | "es6-shim": "^0.35.0", 39 | "firebase": "^2.4.2", 40 | "hammerjs": "^2.0.8", 41 | "material-design-icons": "^2.2.3", 42 | "reflect-metadata": "0.1.3", 43 | "rxjs": "5.0.0-beta.6", 44 | "systemjs": "0.19.26", 45 | "zone.js": "^0.6.12" 46 | }, 47 | "devDependencies": { 48 | "@angular/platform-server": "2.0.0-rc.1", 49 | "@angular/router-deprecated": "2.0.0-rc.1", 50 | "@angular/service-worker": "^0.2.0", 51 | "angular2-broccoli-prerender": "^0.11.0", 52 | "angular2-universal": "^0.100.3", 53 | "angular2-universal-polyfills": "^0.4.1", 54 | "preboot": "^2.0.10", 55 | "angular-cli": "^1.0.0-beta.2-mobile.2", 56 | "clang-format": "^1.0.35", 57 | "codelyzer": "0.0.14", 58 | "ember-cli-inject-live-reload": "^1.4.0", 59 | "jasmine-core": "^2.4.1", 60 | "jasmine-spec-reporter": "^2.4.0", 61 | "karma": "^0.13.15", 62 | "karma-chrome-launcher": "^0.2.3", 63 | "karma-jasmine": "^0.3.8", 64 | "protractor": "^3.3.0", 65 | "ts-node": "^0.5.5", 66 | "tslint": "^3.6.0", 67 | "typescript": "^1.8.10", 68 | "typings": "^0.8.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/public/.npmignore -------------------------------------------------------------------------------- /src/app/+issues/+filter/filter.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+filter/filter.component.css -------------------------------------------------------------------------------- /src/app/+issues/+filter/filter.component.html: -------------------------------------------------------------------------------- 1 |

2 | filter works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+issues/+filter/filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { FilterComponent } from './filter.component'; 13 | 14 | describe('Component: Filter', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [FilterComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([FilterComponent], 23 | (component: FilterComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(FilterComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(FilterComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [FilterComponent] 43 | }) 44 | class FilterComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/+filter/filter.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ROUTER_DIRECTIVES} from '@angular/router-deprecated'; 3 | import {MdToolbar} from '@angular2-material/toolbar'; 4 | import {MD_CARD_DIRECTIVES} from '@angular2-material/card'; 5 | import {MdIcon} from '@angular2-material/icon'; 6 | import { MdAnchor, MdButton } from '@angular2-material/button'; 7 | import {Observable} from 'rxjs/Observable'; 8 | 9 | import { 10 | FilterStoreService, 11 | Filter as ServiceFilter, 12 | UnlabeledCriteria, 13 | LabelCriteria, 14 | Criteria 15 | } from '../../filter-store.service'; 16 | import {GithubService} from '../../github.service'; 17 | import {RepoParamsService} from '../../repo-params.service'; 18 | 19 | @Component({ 20 | template: ` 21 | 22 | 27 | Untriaged Issue Filter 28 | 29 | 30 | 31 |

{{criteria.name}}

32 |
33 |
34 | 44 |
45 |
46 | 47 | 52 |
53 | 54 | 55 | 61 |
62 | `, 63 | styles: [` 64 | .fill-remaining-space { 65 | flex: 1 1 auto; 66 | } 67 | .back-link { 68 | color: rgba(0, 0, 0, 0.870588) 69 | } 70 | `], 71 | directives: [MdIcon, MdToolbar, MD_CARD_DIRECTIVES, ROUTER_DIRECTIVES, MdAnchor, MdButton], 72 | providers: [FilterStoreService, GithubService, RepoParamsService] 73 | }) 74 | export class FilterComponent { 75 | filter: ServiceFilter; 76 | labels: any[]; 77 | org: string; 78 | repo: string; 79 | repoFull: string; 80 | availableCriteria: any[] = [LabelCriteria, UnlabeledCriteria]; 81 | constructor( 82 | public filterStore: FilterStoreService, 83 | public gh: GithubService, 84 | private repoParams: RepoParamsService) { 85 | var {org, repo} = repoParams.getRepo(); 86 | this.org = org; 87 | this.repo = repo; 88 | this.repoFull = `${this.org}/${this.repo}`; 89 | this.filter = this.filterStore.getFilter(this.repoFull); 90 | gh.fetchLabels(this.repoFull) 91 | .take(1) 92 | .subscribe(labels => this.labels = labels); 93 | } 94 | 95 | fetchLabels(): Observable { 96 | return this.gh.fetchLabels(this.repoFull); 97 | } 98 | 99 | updateLabelCriteria(idx: number, evt: any) { 100 | this.filter.updateCriteria(idx, { 101 | type: 'hasLabel', 102 | name: 'Has label', 103 | label: evt.target.value, 104 | query: 'label:%s' 105 | }); 106 | } 107 | 108 | onChange(evt) { 109 | this.filter.addCriteria(this.availableCriteria 110 | .filter((c: Criteria) => c.name === evt.target.value)[0]); 111 | 112 | } 113 | 114 | labelTrack(label: any): string { 115 | return label.type + label.label; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/app/+issues/+filter/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterComponent } from './filter.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/+filter/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+filter/shared/index.ts -------------------------------------------------------------------------------- /src/app/+issues/+list/index.ts: -------------------------------------------------------------------------------- 1 | export { ListComponent } from './list.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/+list/issue-row/index.ts: -------------------------------------------------------------------------------- 1 | export { IssueRowComponent } from './issue-row.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/+list/issue-row/issue-row.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+list/issue-row/issue-row.component.css -------------------------------------------------------------------------------- /src/app/+issues/+list/issue-row/issue-row.component.html: -------------------------------------------------------------------------------- 1 |

2 | issue-row works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+issues/+list/issue-row/issue-row.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { IssueRowComponent } from './issue-row.component'; 13 | 14 | describe('Component: IssueRow', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [IssueRowComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([IssueRowComponent], 23 | (component: IssueRowComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(IssueRowComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(IssueRowComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [IssueRowComponent] 43 | }) 44 | class IssueRowComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/+list/issue-row/issue-row.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | EventEmitter, 6 | Input, 7 | Output 8 | } from '@angular/core'; 9 | import {MD_LIST_DIRECTIVES} from '@angular2-material/list'; 10 | 11 | import {GithubService} from '../../../github.service'; 12 | import {Issue} from '../../../shared'; 13 | 14 | @Component({ 15 | selector: 'issue-row', 16 | template: ` 17 |
18 | 21 | 24 |
25 | 32 | {{issue.user.login}} logo 33 | {{issue.title}} 34 |

35 | @{{issue.user.login}} 36 | - 37 | {{issue.body}} 38 |

39 | 40 | `, 41 | styles: [` 42 | [md-line].secondary { 43 | color: rgba(0,0,0,0.54); 44 | } 45 | 46 | .hidden { 47 | display: flex; 48 | flex-direction: column; 49 | align-items: center; 50 | justify-content: center; 51 | text-align: center; 52 | color: white; 53 | height: 72px; 54 | width: 0; 55 | 56 | } 57 | 58 | .hidden.leave { 59 | transition: width 0.3s; 60 | } 61 | 62 | .hidden span { 63 | height: 16px; 64 | display: inline-block; 65 | vertical-align: middle; 66 | } 67 | 68 | .hidden-triage { 69 | float:left; 70 | background: #090; 71 | } 72 | 73 | .hidden-close { 74 | background: #c00; 75 | float: right; 76 | } 77 | 78 | md-list-item.leave { 79 | transition: left 0.3s; 80 | } 81 | `], 82 | providers: [GithubService], 83 | directives: [MD_LIST_DIRECTIVES], 84 | pipes: [] 85 | }) 86 | export class IssueRowComponent implements AfterViewInit { 87 | @Input('issue') issue: Issue; 88 | @Output('close') close = new EventEmitter(); 89 | @Output('triage') triage = new EventEmitter(); 90 | touchStartCoords: {x: number, y: number}; 91 | listItemNativeEl: HTMLElement; 92 | triageNativeEl: HTMLElement; 93 | closeNativeEl: HTMLElement; 94 | 95 | constructor(public el: ElementRef, public gh: GithubService) {} 96 | 97 | onTouchStart (evt) { 98 | this.closeNativeEl.classList.remove('leave'); 99 | this.triageNativeEl.classList.remove('leave'); 100 | this.listItemNativeEl.classList.remove('leave'); 101 | this.listItemNativeEl.style.position = 'relative'; 102 | this.listItemNativeEl.style.display = 'block'; 103 | this.listItemNativeEl.style.left = '0'; 104 | var coords = evt.targetTouches[0]; 105 | this.touchStartCoords = { 106 | x: coords.clientX, 107 | y: coords.clientY 108 | }; 109 | } 110 | 111 | onTouchMove (evt) { 112 | var {pageX} = evt.targetTouches[0]; 113 | var left = pageX - this.touchStartCoords.x; 114 | 115 | 116 | if (left > 0) { 117 | this.closeNativeEl.style.width = '0'; 118 | this.triageNativeEl.style.width = `${left}px`; 119 | this.listItemNativeEl.style.left = '0'; 120 | } else { 121 | // TODO(jeffbcross): fix the truncating as it's dragged off screen 122 | this.triageNativeEl.style.width = '0'; 123 | this.listItemNativeEl.style.left = `${left}px`; 124 | this.closeNativeEl.style.width = `${Math.abs(left)}px`; 125 | } 126 | } 127 | 128 | onTouchEnd (evt) { 129 | this.closeNativeEl.classList.add('leave'); 130 | this.triageNativeEl.classList.add('leave'); 131 | this.listItemNativeEl.classList.add('leave'); 132 | this.listItemNativeEl.style.left = '0'; 133 | this.closeNativeEl.style.width = '0'; 134 | this.triageNativeEl.style.width = '0'; 135 | } 136 | 137 | ngAfterViewInit () { 138 | this.listItemNativeEl = this.el.nativeElement.querySelector('md-list-item'); 139 | this.triageNativeEl = this.el.nativeElement.querySelector('.hidden-triage'); 140 | this.closeNativeEl = this.el.nativeElement.querySelector('.hidden-close'); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/app/+issues/+list/list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+list/list.component.css -------------------------------------------------------------------------------- /src/app/+issues/+list/list.component.html: -------------------------------------------------------------------------------- 1 |

2 | list works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+issues/+list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { ListComponent } from './list.component'; 13 | 14 | describe('Component: List', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [ListComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([ListComponent], 23 | (component: ListComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(ListComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(ListComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [ListComponent] 43 | }) 44 | class ListComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/+list/list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ChangeDetectionStrategy} from '@angular/core'; 2 | import {ROUTER_DIRECTIVES, Router} from '@angular/router-deprecated'; 3 | import {Observable} from 'rxjs/Observable'; 4 | import {Subscription} from 'rxjs/Subscription'; 5 | import {MD_LIST_DIRECTIVES} from '@angular2-material/list'; 6 | import {Store} from '@ngrx/store'; 7 | 8 | import {ToolbarComponent} from './toolbar/toolbar.component'; 9 | import {IssueRowComponent} from './issue-row/issue-row.component'; 10 | import { AppState, Issue, Repo } from '../../shared'; 11 | import {GithubService} from '../../github.service'; 12 | import { 13 | FilterStoreService, 14 | Filter, 15 | FilterObject, 16 | FilterMap, 17 | generateQuery 18 | } from '../../filter-store.service'; 19 | import {RepoParamsService} from '../../repo-params.service'; 20 | import {NotPendingRemoval} from './not-pending-removal.pipe'; 21 | 22 | @Component({ 23 | styles: [` 24 | md-list-item { 25 | background-color: rgb(245, 245, 245) 26 | } 27 | `], 28 | template: ` 29 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | `, 44 | providers: [GithubService, FilterStoreService, RepoParamsService], 45 | directives: [MD_LIST_DIRECTIVES, ToolbarComponent, IssueRowComponent, ROUTER_DIRECTIVES], 46 | pipes: [NotPendingRemoval], 47 | changeDetection: ChangeDetectionStrategy.OnPush 48 | }) 49 | export class ListComponent implements OnInit { 50 | issues: Observable; 51 | repos: Observable; 52 | repoSelection: Observable; 53 | addIssueSubscription: Subscription; 54 | constructor( 55 | private gh: GithubService, private filterStore: FilterStoreService, 56 | private store: Store, private repoParams: RepoParamsService, 57 | private router: Router) {} 58 | 59 | ngOnInit() { 60 | var {repo, org} = this.repoParams.getRepo(); 61 | 62 | /** 63 | * Get full repo object based on route params. 64 | */ 65 | this.repoSelection = this.store.select('repos') 66 | .filter((r: Repo) => !!r) 67 | .map((repos: Repo[]) => repos.filter((repository: Repo) => { 68 | return repository.name === repo && repository.owner.login === org; 69 | })[0]); 70 | 71 | this.gh.getRepo(`${org}/${repo}`).subscribe((_repo: Repo) => { 72 | this.store.dispatch({type: 'AddRepo', payload: _repo}); 73 | }); 74 | 75 | /** 76 | * Fetch the issues for this repo. 77 | */ 78 | this.addIssueSubscription = 79 | this.store.select('filters') 80 | .map((filters: FilterMap) => filters && filters[`${org}/${repo}`]) 81 | .filter((filter: Filter) => !!filter) 82 | .flatMap((filter: Filter) => filter.changes) 83 | .map((filter: FilterObject) => generateQuery(filter)) 84 | .switchMap((query: string) => this.gh.getIssues(query)) 85 | .subscribe( 86 | (issues: Issue[]) => {this.store.dispatch({type: 'AddIssues', payload: issues});}); 87 | 88 | this.store.dispatch({type: 'SetFilter', payload: this.filterStore.getFilter(`${org}/${repo}`)}); 89 | 90 | this.issues = this.store.select('issues') 91 | .filter((i: Issue[]) => !!i) 92 | .map( 93 | (issues: Issue[]) => issues.filter( 94 | (issue: Issue) => {return issue.org === org && issue.repo === repo;})); 95 | } 96 | 97 | getSmallAvatar(repo: Repo): string { return repo ? `${repo.owner.avatar_url}&s=40` : ''; } 98 | 99 | ngOnDestroy() { 100 | if (this.addIssueSubscription) { 101 | this.addIssueSubscription.unsubscribe(); 102 | } 103 | } 104 | 105 | closeIssue(issue: Issue): void { 106 | // Set the issue as pending removal 107 | this.store.dispatch({type: 'PendingRemoveIssue', payload: issue}); 108 | this.gh.closeIssue(issue).take(1).subscribe( 109 | () => { this.store.dispatch({type: 'RemoveIssue', payload: issue}); }); 110 | } 111 | 112 | triageIssue(issue: Issue) { this.router.navigate(['../Triage', {number: issue.number}]); } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/+issues/+list/not-pending-removal.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | expect, 5 | inject, 6 | beforeEachProviders 7 | } from '@angular/core/testing'; 8 | import { NotPendingRemoval } from './not-pending-removal.pipe'; 9 | 10 | describe('NotPendingRemoval Pipe', () => { 11 | beforeEachProviders(() => [NotPendingRemoval]); 12 | 13 | it('should transform the input', inject([NotPendingRemoval], (pipe: NotPendingRemoval) => { 14 | // expect(pipe.transform(true)).toBe(null); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/+issues/+list/not-pending-removal.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Issue } from '../../shared'; 3 | 4 | @Pipe({ 5 | name: 'notPendingRemoval' 6 | }) 7 | export class NotPendingRemoval implements PipeTransform { 8 | transform (issues:Issue[]): Issue[] { 9 | if (!issues) return issues; 10 | return issues.filter((issue:Issue) => !issue.isPendingRemoval) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/+issues/+list/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+list/shared/index.ts -------------------------------------------------------------------------------- /src/app/+issues/+list/toolbar/index.ts: -------------------------------------------------------------------------------- 1 | export { ToolbarComponent } from './toolbar.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/+list/toolbar/toolbar.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+list/toolbar/toolbar.component.css -------------------------------------------------------------------------------- /src/app/+issues/+list/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 |

2 | toolbar works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+issues/+list/toolbar/toolbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { ToolbarComponent } from './toolbar.component'; 13 | 14 | describe('Component: Toolbar', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [ToolbarComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([ToolbarComponent], 23 | (component: ToolbarComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(ToolbarComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(ToolbarComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [ToolbarComponent] 43 | }) 44 | class ToolbarComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/+list/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output 6 | } from '@angular/core'; 7 | import { ROUTER_DIRECTIVES } from '@angular/router-deprecated'; 8 | import { MdIcon } from '@angular2-material/icon'; 9 | import {MdToolbar} from '@angular2-material/toolbar'; 10 | import {MdButton} from '@angular2-material/button'; 11 | 12 | @Component({ 13 | selector: 'issue-list-toolbar', 14 | template: ` 15 |
16 | 17 | logo for {{repo.owner.login}} 18 | {{repo.full_name}} 19 | 22 | 23 | 24 | 25 | filter 26 | 27 | 28 |
29 | `, 30 | styles: [` 31 | md-toolbar img { 32 | margin-right: 16px; 33 | } 34 | .fill-remaining-space { 35 | flex: 1 1 auto; 36 | } 37 | .change-repo { 38 | margin-left: 16px; 39 | } 40 | `], 41 | directives: [MdButton, MdIcon, MdToolbar, ROUTER_DIRECTIVES] 42 | }) 43 | export class ToolbarComponent { 44 | @Input('repo') repo:any; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/index.ts: -------------------------------------------------------------------------------- 1 | export { TriageComponent } from './triage.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/is-checked.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | expect, 5 | inject, 6 | beforeEachProviders 7 | } from '@angular/core/testing'; 8 | import { IsChecked } from './is-checked.pipe'; 9 | 10 | describe('IsChecked Pipe', () => { 11 | beforeEachProviders(() => [IsChecked]); 12 | 13 | it('should transform the input', inject([IsChecked], (pipe: IsChecked) => { 14 | // expect(pipe.transform(true)).toBe(null); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/is-checked.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Label, Issue } from '../../shared'; 3 | 4 | @Pipe({ 5 | name: 'isChecked' 6 | }) 7 | export class IsChecked { 8 | transform (label: Label, [issue]: [Issue]): boolean { 9 | return issue ? issue.labels.filter(l => l.name === label.name).length === 1 : false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+triage/shared/index.ts -------------------------------------------------------------------------------- /src/app/+issues/+triage/to-date.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | expect, 5 | inject, 6 | beforeEachProviders 7 | } from '@angular/core/testing'; 8 | import { ToDate } from './to-date.pipe'; 9 | 10 | describe('ToDate Pipe', () => { 11 | beforeEachProviders(() => [ToDate]); 12 | 13 | it('should transform the input', inject([ToDate], (pipe: ToDate) => { 14 | // expect(pipe.transform(true)).toBe(null); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/to-date.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'toDate' 5 | }) 6 | export class ToDate { 7 | transform(date:string): Date { 8 | return new Date(date); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/triage.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/+triage/triage.component.css -------------------------------------------------------------------------------- /src/app/+issues/+triage/triage.component.html: -------------------------------------------------------------------------------- 1 |

2 | triage works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/triage.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { TriageComponent } from './triage.component'; 13 | 14 | describe('Component: Triage', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [TriageComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([TriageComponent], 23 | (component: TriageComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(TriageComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(TriageComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [TriageComponent] 43 | }) 44 | class TriageComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/+triage/triage.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {RouteParams, ROUTER_DIRECTIVES, Router} from '@angular/router-deprecated'; 3 | import {MdButton} from '@angular2-material/button'; 4 | import {MD_LIST_DIRECTIVES} from '@angular2-material/list'; 5 | import {MdToolbar} from '@angular2-material/toolbar'; 6 | import {MdCheckbox} from '@angular2-material/checkbox'; 7 | import {MdInput} from '@angular2-material/input'; 8 | import {MdCard} from '@angular2-material/card'; 9 | import {Observable} from 'rxjs/Observable'; 10 | import {Store} from '@ngrx/store'; 11 | 12 | import {GithubService} from '../../github.service'; 13 | import {RepoParamsService} from '../../repo-params.service'; 14 | import { AppState, Issue, Label } from '../../shared'; 15 | import { ToDate } from './to-date.pipe'; 16 | import { IsChecked } from './is-checked.pipe'; 17 | 18 | @Component({ 19 | template: ` 20 | 21 | 22 | {{issue?.title}} 23 | 24 | #{{issue?.number}} 25 | 26 | 27 | 28 | @{{issue.user.login}} on {{issue.created_at | toDate | date}}: 29 | 30 | 31 | {{issue?.body}} 32 | 33 | 34 | 35 | 36 | Triage 37 | 38 | 39 |
40 | 41 | 42 | 43 |

Labels

44 | 45 | 46 | 47 | {{label.name}} 48 | 49 | 50 | 51 | 52 | 55 | 58 | 59 |
60 |
61 |
62 | `, 63 | styles: [` 64 | .description { 65 | font-size: 1.2em; 66 | } 67 | md-card { 68 | margin: 16px 16px 0; 69 | } 70 | .issue-number { 71 | color: rgba(0,0,0,0.54); 72 | } 73 | .user-and-date { 74 | color: rgba(0,0,0,0.54); 75 | } 76 | `], 77 | providers: [RepoParamsService], 78 | directives: [MD_LIST_DIRECTIVES, MdButton, MdCheckbox, ROUTER_DIRECTIVES, MdToolbar, MdInput, MdCard], 79 | pipes: [IsChecked, ToDate] 80 | }) 81 | export class TriageComponent { 82 | comment: string; 83 | org: string; 84 | repo: string; 85 | labels: Observable; 86 | issue: Issue; 87 | labelsToApply: {[key:string]: boolean} = {}; 88 | constructor( 89 | private repoParams: RepoParamsService, 90 | private gh: GithubService, 91 | private store: Store, 92 | routeParams:RouteParams, 93 | private router:Router) { 94 | var {org, repo} = repoParams.getRepo(); 95 | this.org = org; 96 | this.repo = repo; 97 | this.labels = gh.fetchLabels(`${org}/${repo}`); 98 | this.store.select('issues') 99 | .filter((i:Issue[]) => !!i) 100 | .map((issues:Issue[]) => issues 101 | .filter((issue:Issue) => { 102 | return issue.org === org && issue.repo === repo && issue.number === parseInt(routeParams.get('number'), 10) 103 | })[0] 104 | ) 105 | .subscribe((issue:Issue) => { 106 | this.issue = issue; 107 | }); 108 | this.gh.getIssue(org, repo, routeParams.get('number')) 109 | .subscribe((issue:Issue) => { 110 | this.store.dispatch({ 111 | type: 'AddIssues', 112 | payload: [issue] 113 | }); 114 | }); 115 | } 116 | 117 | updateComment(val) { 118 | this.comment = val.target.value; 119 | } 120 | 121 | updateIssue() { 122 | var patch = { 123 | labels: Object.keys(this.labelsToApply) 124 | }; 125 | 126 | var patchIssue = this.gh.patchIssue(this.org, this.repo, this.issue.number, patch); 127 | 128 | 129 | if (this.comment) { 130 | patchIssue = patchIssue.merge(this.gh.addComment(this.org, this.repo, this.issue.number, this.comment)); 131 | } 132 | 133 | patchIssue.take(2).subscribe(null, null, () => { 134 | this.store.dispatch({ 135 | type: 'RemoveIssue', 136 | payload: this.issue 137 | }); 138 | this.router.navigate(['/Issues', {org: this.org, repo: this.repo}, 'List']) 139 | }); 140 | } 141 | 142 | labelChanged (label: Label, value: boolean) { 143 | if (value) { 144 | this.labelsToApply[label.name] = value; 145 | } else { 146 | delete this.labelsToApply[label.name]; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/app/+issues/index.ts: -------------------------------------------------------------------------------- 1 | export { IssuesComponent } from './issues.component'; 2 | -------------------------------------------------------------------------------- /src/app/+issues/issues.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/issues.component.css -------------------------------------------------------------------------------- /src/app/+issues/issues.component.html: -------------------------------------------------------------------------------- 1 |

2 | issues works! 3 |

4 | 5 | -------------------------------------------------------------------------------- /src/app/+issues/issues.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { IssuesComponent } from './issues.component'; 13 | 14 | describe('Component: Issues', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [IssuesComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([IssuesComponent], 23 | (component: IssuesComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(IssuesComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(IssuesComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [IssuesComponent] 43 | }) 44 | class IssuesComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+issues/issues.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ListComponent } from './+list'; 3 | import { RouteConfig , ROUTER_DIRECTIVES} from '@angular/router-deprecated'; 4 | import { FilterComponent } from './+filter'; 5 | import { TriageComponent } from './+triage'; 6 | 7 | @Component({ 8 | template: ``, 9 | directives: [ROUTER_DIRECTIVES] 10 | }) 11 | @RouteConfig([ 12 | {path: '/list', name: 'List', component: ListComponent, useAsDefault: true}, 13 | {path: '/filter', name: 'Filter', component: FilterComponent}, 14 | {path: '/triage/:number', name: 'Triage', component: TriageComponent} 15 | ]) 16 | export class IssuesComponent {} 17 | -------------------------------------------------------------------------------- /src/app/+issues/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+issues/shared/index.ts -------------------------------------------------------------------------------- /src/app/+login/index.ts: -------------------------------------------------------------------------------- 1 | export { LoginComponent } from './login.component'; 2 | -------------------------------------------------------------------------------- /src/app/+login/login.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+login/login.component.css -------------------------------------------------------------------------------- /src/app/+login/login.component.html: -------------------------------------------------------------------------------- 1 |

2 | login works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { LoginComponent } from './login.component'; 13 | 14 | describe('Component: Login', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [LoginComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([LoginComponent], 23 | (component: LoginComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(LoginComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(LoginComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [LoginComponent] 43 | }) 44 | class LoginComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MdButton } from '@angular2-material/button'; 3 | import { CanActivate } from '@angular/router-deprecated'; 4 | import { AngularFire } from 'angularfire2'; 5 | 6 | import { FB_URL } from '../shared'; 7 | 8 | @Component({ 9 | moduleId: module.id, 10 | styles: [` 11 | button[md-raised-button] { 12 | margin: 8px; 13 | } 14 | h3.headline { 15 | margin: 8px; 16 | font-size: 24px; 17 | line-height: 32px; 18 | } 19 | 20 | [md-raised-button][color=primary] { 21 | background-color: rgb(33, 150, 243); 22 | } 23 | `], 24 | template: ` 25 |
26 |

27 | Keep your Github issues tidy,
28 | and your users happy. 29 |

30 | 33 |
34 | `, 35 | directives: [MdButton] 36 | }) 37 | // If not a login redirect, and no existing auth state 38 | @CanActivate(() => !(window).__IS_POST_LOGIN && !(new Firebase(FB_URL).getAuth())) 39 | export class LoginComponent { 40 | constructor(public af: AngularFire) {} 41 | } 42 | -------------------------------------------------------------------------------- /src/app/+login/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+login/shared/index.ts -------------------------------------------------------------------------------- /src/app/+repo-selector/index.ts: -------------------------------------------------------------------------------- 1 | export { RepoSelectorComponent } from './repo-selector.component'; 2 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector-row/index.ts: -------------------------------------------------------------------------------- 1 | export { RepoSelectorRowComponent } from './repo-selector-row.component'; 2 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector-row/repo-selector-row.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+repo-selector/repo-selector-row/repo-selector-row.component.css -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector-row/repo-selector-row.component.html: -------------------------------------------------------------------------------- 1 |

2 | repo-selector-row works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector-row/repo-selector-row.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { RepoSelectorRowComponent } from './repo-selector-row.component'; 13 | 14 | describe('Component: RepoSelectorRow', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [RepoSelectorRowComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([RepoSelectorRowComponent], 23 | (component: RepoSelectorRowComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(RepoSelectorRowComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(RepoSelectorRowComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [RepoSelectorRowComponent] 43 | }) 44 | class RepoSelectorRowComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector-row/repo-selector-row.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | import {MD_LIST_DIRECTIVES} from '@angular2-material/list'; 3 | 4 | @Component({ 5 | selector: 'repo-selector-row', 6 | styles: [` 7 | .md-list-item { 8 | background-color: rgb(245, 245, 245) 9 | } 10 | `], 11 | template: ` 12 | 13 | {{repo.owner.login}} logo 14 | {{repo.full_name}} 15 | 16 | `, 17 | directives: [MD_LIST_DIRECTIVES] 18 | }) 19 | export class RepoSelectorRowComponent { 20 | @Input('repo') repo:any; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+repo-selector/repo-selector.component.css -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector.component.html: -------------------------------------------------------------------------------- 1 |

2 | repo-selector works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEach, 3 | beforeEachProviders, 4 | describe, 5 | expect, 6 | it, 7 | inject, 8 | } from '@angular/core/testing'; 9 | import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing'; 10 | import { Component } from '@angular/core'; 11 | import { By } from '@angular/platform-browser'; 12 | import { RepoSelectorComponent } from './repo-selector.component'; 13 | 14 | describe('Component: RepoSelector', () => { 15 | let builder: TestComponentBuilder; 16 | 17 | beforeEachProviders(() => [RepoSelectorComponent]); 18 | beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) { 19 | builder = tcb; 20 | })); 21 | 22 | it('should inject the component', inject([RepoSelectorComponent], 23 | (component: RepoSelectorComponent) => { 24 | expect(component).toBeTruthy(); 25 | })); 26 | 27 | it('should create the component', inject([], () => { 28 | return builder.createAsync(RepoSelectorComponentTestController) 29 | .then((fixture: ComponentFixture) => { 30 | let query = fixture.debugElement.query(By.directive(RepoSelectorComponent)); 31 | expect(query).toBeTruthy(); 32 | expect(query.componentInstance).toBeTruthy(); 33 | }); 34 | })); 35 | }); 36 | 37 | @Component({ 38 | selector: 'test', 39 | template: ` 40 | 41 | `, 42 | directives: [RepoSelectorComponent] 43 | }) 44 | class RepoSelectorComponentTestController { 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/app/+repo-selector/repo-selector.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | import { ROUTER_DIRECTIVES, RouteParams } from '@angular/router-deprecated'; 4 | import { MdButton } from '@angular2-material/button'; 5 | import { MdIcon } from '@angular2-material/icon'; 6 | import {MD_LIST_DIRECTIVES} from '@angular2-material/list'; 7 | import {MdToolbar} from '@angular2-material/toolbar'; 8 | import {Observable} from 'rxjs/Observable'; 9 | 10 | import {RepoSelectorRowComponent} from './repo-selector-row/repo-selector-row.component'; 11 | import {GithubService} from '../github.service'; 12 | import {Repo} from '../shared'; 13 | 14 | @Component({ 15 | selector: 'repo-selector', 16 | template: ` 17 | 18 | 21 | 22 | Select Repository 23 | 24 | 25 | 26 | 30 | 31 | 32 | `, 33 | styles: [` 34 | md-toolbar[color=accent] { 35 | background-color: #ff5252; 36 | } 37 | `], 38 | directives: [ 39 | MD_LIST_DIRECTIVES, 40 | ROUTER_DIRECTIVES, 41 | MdToolbar, 42 | RepoSelectorRowComponent, 43 | MdIcon, 44 | MdButton 45 | ], 46 | providers: [GithubService] 47 | }) 48 | export class RepoSelectorComponent implements OnInit { 49 | repos:Observable; 50 | constructor(private gh:GithubService, private routeParams:RouteParams) {} 51 | 52 | ngOnInit() { 53 | this.repos = this.gh.fetch(`/user/repos`, 'affiliation=owner,collaborator&sort=updated'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/+repo-selector/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/+repo-selector/shared/index.ts -------------------------------------------------------------------------------- /src/app/environment.ts: -------------------------------------------------------------------------------- 1 | // The file for the current environment will overwrite this one during build 2 | // Different environments can be found in config/environment.{dev|prod}.ts 3 | // The build system defaults to the dev environment 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/filter-store.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEachProviders, 3 | it, 4 | describe, 5 | expect, 6 | inject 7 | } from '@angular/core/testing'; 8 | import { FilterStoreService } from './filter-store.service'; 9 | 10 | describe('FilterStore Service', () => { 11 | beforeEachProviders(() => [FilterStoreService]); 12 | 13 | it('should ...', 14 | inject([FilterStoreService], (service: FilterStoreService) => { 15 | expect(service).toBeTruthy(); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/filter-store.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@angular/core'; 2 | import {Observable} from 'rxjs/Observable'; 3 | import {BehaviorSubject} from 'rxjs/BehaviorSubject'; 4 | import {Operator} from 'rxjs/Operator'; 5 | 6 | import {LOCAL_STORAGE} from './shared'; 7 | 8 | export const LOCAL_STORAGE_KEY = 'FilterStore.filters'; 9 | 10 | export interface FilterMap { 11 | [key:string]: Filter; 12 | } 13 | 14 | 15 | @Injectable() 16 | export class FilterStoreService { 17 | private _filters = new Map(); 18 | constructor(@Inject(LOCAL_STORAGE) private localStorage:any) {} 19 | 20 | getFilter (repository:string): Filter { 21 | var filter = this._filters.get(repository); 22 | if (filter) { 23 | return filter; 24 | } else { 25 | var newFilter = retrieveFromCache(repository, this.localStorage); 26 | if (!newFilter) { 27 | newFilter = new Filter(this.localStorage, repository); 28 | updateCache(repository, newFilter.changes.value, this.localStorage); 29 | } 30 | this._filters.set(repository, newFilter); 31 | return newFilter; 32 | } 33 | } 34 | } 35 | 36 | export function updateCache(repo:string, filter:FilterObject, storage:any): void { 37 | storage.setItem(`${LOCAL_STORAGE_KEY}:${repo}`, JSON.stringify(filter)); 38 | } 39 | 40 | export function retrieveFromCache(repo:string, storage:any): Filter { 41 | var filterCached = storage.getItem(`${LOCAL_STORAGE_KEY}:${repo}`); 42 | return filterCached ? Filter.fromJson(storage, JSON.parse(filterCached)) : null; 43 | } 44 | 45 | export class Filter { 46 | org: string; 47 | repo: string; 48 | changes: BehaviorSubject; 49 | constructor(private localStorage:any, repo?:string, criteria: Criteria[] = [UnlabeledCriteria]) { 50 | var split = repo.split('/'); 51 | this.org = split[0]; 52 | this.repo = split[1]; 53 | this.changes = new BehaviorSubject({ 54 | repo, 55 | criteria 56 | }); 57 | } 58 | 59 | updateCriteria(index:number, newCriteria:Criteria | LabelCriteria): void { 60 | var initialValue = this.changes.value; 61 | var newValue = { 62 | repo: initialValue.repo, 63 | criteria: initialValue.criteria.map((oldCriteria:Criteria, i:number) => { 64 | return i === index ? newCriteria : oldCriteria; 65 | }) 66 | }; 67 | this._cacheAndEmit(newValue); 68 | } 69 | 70 | addCriteria(c:Criteria): void { 71 | var initialValue = this.changes.value; 72 | var newValue = { 73 | repo: initialValue.repo, 74 | criteria: initialValue.criteria.concat([c]) 75 | }; 76 | switch(c.type) { 77 | case 'unlabeled': 78 | newValue.criteria = removeHasLabelCriteria(newValue.criteria); 79 | break; 80 | case 'hasLabel': 81 | newValue.criteria = removeNoLabelIfHasLabel(newValue.criteria); 82 | break; 83 | } 84 | this._cacheAndEmit(newValue); 85 | } 86 | 87 | removeCriteria(index:number): void { 88 | var initialValue = this.changes.value; 89 | var newValue = { 90 | repo: initialValue.repo, 91 | criteria: initialValue.criteria.reduce((prev:Criteria[], curr:Criteria, i) => { 92 | if (i === index) return prev; 93 | return prev.concat([curr]); 94 | }, []) 95 | }; 96 | this._cacheAndEmit(newValue); 97 | } 98 | 99 | _cacheAndEmit(newValue:FilterObject): void { 100 | updateCache(newValue.repo, newValue, this.localStorage); 101 | this.changes.next(newValue); 102 | } 103 | 104 | setStorage(localStorage: any): void { 105 | this.localStorage = localStorage; 106 | } 107 | 108 | static fromJson (storage:any, json:FilterObject):Filter { 109 | return new Filter(storage, json.repo, json.criteria); 110 | } 111 | } 112 | 113 | /** 114 | * Prevents the presence of a hasLabel and unlabeled type of criteria, 115 | * which are incompatible. 116 | */ 117 | export function removeNoLabelIfHasLabel(criteria:Criteria[]): Criteria[] { 118 | var hasLabelInList = !!criteria.filter((c:Criteria) => c.type === 'hasLabel').length; 119 | // No hasLabel type in list, return as-is. 120 | if (!hasLabelInList) return criteria; 121 | if (hasLabelInList) { 122 | return criteria.reduce((prev, curr:Criteria) => { 123 | if (curr.type === 'unlabeled') return prev; 124 | prev.push(curr) 125 | return prev; 126 | }, []); 127 | } 128 | } 129 | 130 | export function removeHasLabelCriteria(criteria: Criteria[]): Criteria[] { 131 | var hasUnlabeledInList = !!criteria.filter((c:Criteria) => c.type === 'unlabeled').length; 132 | // No hasLabel type in list, return as-is. 133 | if (!hasUnlabeledInList) return criteria; 134 | if (hasUnlabeledInList) { 135 | return criteria.reduce((prev, curr:Criteria) => { 136 | if (curr.type === 'hasLabel') return prev; 137 | prev.push(curr) 138 | return prev; 139 | }, []); 140 | } 141 | } 142 | 143 | export function generateQuery (filter:FilterObject): string { 144 | var generated = `${filter.criteria 145 | .map(c => { 146 | var interpolated = c.query; 147 | if ((c).label) { 148 | let label = (c).label; 149 | // Replace spaces with pluses to make Github API happy 150 | label = label.replace(/ /g, '+') 151 | // Wrap in quotes for more complex labels 152 | label = `"${label}"` 153 | interpolated = c.query.replace('%s', label) 154 | } 155 | 156 | // URI encode it 157 | interpolated = encodeURI(interpolated); 158 | // Unescape quotes 159 | return interpolated.replace(/%22/g, '"'); 160 | }) 161 | .join('+')}+repo:${filter.repo}+state:open` 162 | return generated; 163 | } 164 | 165 | export interface FilterObject { 166 | criteria: Criteria[]; 167 | repo: string; 168 | } 169 | 170 | export interface Criteria { 171 | type: string; 172 | name: string; 173 | query: string; 174 | } 175 | 176 | export interface LabelCriteria extends Criteria { 177 | label?: string; 178 | } 179 | 180 | export const UnlabeledCriteria:Criteria = { 181 | type: 'unlabeled', 182 | name: 'Has NO label', 183 | query: 'no:label' 184 | }; 185 | 186 | export const LabelCriteria = { 187 | type: 'hasLabel', 188 | name: 'Has label', 189 | query: 'label:%s' 190 | } 191 | -------------------------------------------------------------------------------- /src/app/github.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEachProviders, 3 | it, 4 | describe, 5 | expect, 6 | inject 7 | } from '@angular/core/testing'; 8 | import { GithubService } from './github.service'; 9 | 10 | describe('Github Service', () => { 11 | beforeEachProviders(() => [GithubService]); 12 | 13 | it('should ...', 14 | inject([GithubService], (service: GithubService) => { 15 | expect(service).toBeTruthy(); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/github.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import {Http} from '@angular/http'; 3 | 4 | import {AngularFire, FirebaseAuthState} from 'angularfire2'; 5 | 6 | 7 | import {Observable} from 'rxjs/Observable'; 8 | import {ScalarObservable} from 'rxjs/observable/ScalarObservable'; 9 | import {ErrorObservable} from 'rxjs/observable/ErrorObservable'; 10 | 11 | import {Issue, Label, LOCAL_STORAGE, Repo, User} from './shared'; 12 | 13 | const GITHUB_API = 'https://api.github.com'; 14 | 15 | interface LocalStorage { 16 | getItem(key:string): string; 17 | setItem(key:string, value:string): void; 18 | } 19 | 20 | @Injectable() 21 | export class GithubService { 22 | constructor( 23 | private _http:Http, 24 | @Inject(LOCAL_STORAGE) private _localStorage:LocalStorage, 25 | private _af:AngularFire 26 | ) {} 27 | 28 | // TODO(jeffbcross): don't use error paths here 29 | fetch(path:string, params?: string): any {//Observable { 30 | return this._getCache(path) 31 | .catch(() => this._af.auth 32 | .filter(auth => auth !== null && auth.github) 33 | .map((auth:any) => auth.github.accessToken) 34 | .mergeMap((tokenValue) => this._httpRequest(path, tokenValue, params))); 35 | } 36 | 37 | getRepo(repoFullName:string): Observable { 38 | // TODO(jeffbcross): check cache first 39 | return this._af.auth 40 | .filter(auth => auth !== null && auth.github) 41 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${repoFullName}?access_token=${auth.github.accessToken}`) 42 | .switchMap((url:string) => this._http.get(url).map((res) => res.json())); 43 | } 44 | 45 | getIssues(query:string):Observable { 46 | return this._af.auth 47 | .filter(auth => auth !== null && auth.github) 48 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/search/issues?q=${query}&access_token=${auth.github.accessToken}`) 49 | .switchMap((url:string) => this._http.get(url).map(res => res.json().items)) 50 | .map((issues:Issue[]) => issues 51 | .map((issue:Issue) => { 52 | var [url, org, repo, num] = /\/([a-z0-9\-]*)\/([a-z0-9\-]*)\/issues\/([0-9]*)$/i.exec(issue.url); 53 | return Object.assign({}, issue, {org, repo}); 54 | })) 55 | } 56 | 57 | getIssue(org:string, repo:string, number:number | string): Observable { 58 | return this._af.auth 59 | .filter(auth => auth !== null && auth.github) 60 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${org}/${repo}/issues/${number}?access_token=${auth.github.accessToken}`) 61 | .switchMap((url:string) => this._http.get(url).map(res => res.json())) 62 | .map((issue:Issue) => { 63 | return Object.assign({}, issue, {org, repo}); 64 | }); 65 | } 66 | 67 | closeIssue(issue:Issue): Observable { 68 | var [url, org, repo, number] = /\/([a-z0-9\-]*)\/([a-z0-9\-]*)\/issues\/([0-9]*)$/.exec(issue.url); 69 | return this._af.auth 70 | .filter(auth => auth !== null && auth.github) 71 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${org}/${repo}/issues/${number}?access_token=${auth.github.accessToken}`) 72 | .switchMap((url:string) => this._http.patch(url, JSON.stringify({ 73 | state: 'closed' 74 | })) 75 | .map(res => res.json())); 76 | } 77 | 78 | addComment(org: string, repo: string, number: number, comment: string): Observable { 79 | return this._af.auth 80 | .filter(auth => auth !== null && auth.github) 81 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${org}/${repo}/issues/${number}/comments?access_token=${auth.github.accessToken}`) 82 | .switchMap((url:string) => this._http.post(url, JSON.stringify({ 83 | body: comment})) 84 | .map(res => res.json())); 85 | } 86 | 87 | patchIssue (org: string, repo: string, number: number, patch:Object) { 88 | return this._af.auth 89 | .filter(auth => auth !== null && auth.github) 90 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${org}/${repo}/issues/${number}?access_token=${auth.github.accessToken}`) 91 | .switchMap((url:string) => this._http.patch(url, JSON.stringify(patch)) 92 | .map(res => res.json())); 93 | } 94 | 95 | fetchLabels(repo:string): Observable { 96 | return this._af.auth 97 | .filter(auth => auth !== null && auth.github) 98 | .map((auth:FirebaseAuthState) => `${GITHUB_API}/repos/${repo}/labels?per_page=100&access_token=${auth.github.accessToken}`) 99 | .switchMap((url:string) => this._http.get(url) 100 | .map(res => res.json())); 101 | } 102 | 103 | _httpRequest (path:string, accessToken:string, params?:string) { 104 | var url = `${GITHUB_API}${path}?${params ? params + '&' : ''}access_token=${accessToken}`; 105 | return this._http.get(url) 106 | // Set the http response to cache 107 | // TODO(jeffbcross): issues should be cached in more structured and queryable format 108 | // Get the JSON object from the response 109 | .map(res => { 110 | // TODO: should be in do() 111 | this._setCache(path, res.text()); 112 | return res.json(); 113 | }); 114 | 115 | } 116 | 117 | /** 118 | * TODO(jeffbcross): get rid of this for a more sophisticated, queryable cache 119 | */ 120 | _getCache (path:string): Observable { 121 | var cacheKey = `izCache${path}`; 122 | var cache = this._localStorage.getItem(cacheKey); 123 | if (cache) { 124 | return ScalarObservable.create(JSON.parse(cache)); 125 | } else { 126 | return ErrorObservable.create(null); 127 | } 128 | } 129 | 130 | /** 131 | * TODO(jeffbcross): get rid of this for a more sophisticated, queryable cache 132 | */ 133 | _setCache(path:string, value:string): void { 134 | var cacheKey = `izCache${path}`; 135 | this._localStorage.setItem(cacheKey, value); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export {environment} from './environment'; 2 | export {IssueZeroAppComponent} from './issue-zero.component'; 3 | -------------------------------------------------------------------------------- /src/app/issue-zero.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/app/issue-zero.component.css -------------------------------------------------------------------------------- /src/app/issue-zero.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 |

4 | 5 | -------------------------------------------------------------------------------- /src/app/issue-zero.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEachProviders, 3 | describe, 4 | expect, 5 | it, 6 | inject 7 | } from '@angular/core/testing'; 8 | import { IssueZeroAppComponent } from '../app/issue-zero.component'; 9 | 10 | beforeEachProviders(() => [IssueZeroAppComponent]); 11 | 12 | describe('App: IssueZero', () => { 13 | it('should create the app', 14 | inject([IssueZeroAppComponent], (app: IssueZeroAppComponent) => { 15 | expect(app).toBeTruthy(); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/issue-zero.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from '@angular/core'; 2 | import {RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, Router} from '@angular/router-deprecated'; 3 | import {Location} from '@angular/common'; 4 | import {AngularFire, FirebaseAuthState} from 'angularfire2'; 5 | import { LoginComponent } from './+login'; 6 | import { IssuesComponent } from './+issues'; 7 | import { RepoSelectorComponent } from './+repo-selector'; 8 | 9 | import {MdButton} from '@angular2-material/button'; 10 | import {MD_CARD_DIRECTIVES} from '@angular2-material/card'; 11 | import {MdIcon} from '@angular2-material/icon'; 12 | import {MdProgressCircle} from '@angular2-material/progress-circle'; 13 | import {MD_SIDENAV_DIRECTIVES} from '@angular2-material/sidenav'; 14 | import {MdToolbar} from '@angular2-material/toolbar'; 15 | import {Observable} from 'rxjs/Observable'; 16 | import {ArrayObservable} from 'rxjs/observable/ArrayObservable'; 17 | // import {Issues} from './issues/issues'; 18 | import {GithubService} from './github.service'; 19 | // import {RepoSelectorComponent} from './+repo-selector/index'; 20 | import { APP_SHELL_DIRECTIVES, IS_PRERENDER } from '@angular/app-shell'; 21 | import { IS_POST_LOGIN, Repo } from './shared'; 22 | import { MdIconRegistry } from '@angular2-material/icon'; 23 | 24 | @Component({ 25 | moduleId: module.id, 26 | selector: 'issue-zero-app', 27 | styles: [` 28 | md-toolbar[color=primary] { 29 | background: rgb(33, 150, 243) 30 | } 31 | 32 | md-sidenav { 33 | width: 200px; 34 | padding: 8px; 35 | } 36 | 37 | md-toolbar md-progress-circle[mode="indeterminate"] { 38 | width: 24px; 39 | height: 24px; 40 | margin: 0 8px; 41 | } 42 | 43 | md-toolbar md-progress-circle[mode="indeterminate"] /deep/ circle { 44 | stroke: white !important; 45 | } 46 | 47 | .indicator-container { 48 | height: 0; 49 | margin-top: 50%; 50 | } 51 | 52 | .indicator-container md-progress-circle { 53 | margin: -50px auto 0; 54 | } 55 | 56 | .indicator-container md-progress-circle[mode="indeterminate"] /deep/ circle { 57 | stroke: rgb(33, 150, 243) !important; 58 | } 59 | `], 60 | template: ` 61 | 62 | 63 |
64 | 65 | 66 | 67 | 68 | {{ (af.auth | async).github.displayName }} 69 | 70 | 71 | @{{ (af.auth | async).github.username }} 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | Not Logged In 84 | 85 | 86 | 87 | 90 | 91 | 92 |
93 |
94 | 95 | 96 | 99 | 100 | Issue Zero 101 | 102 |
103 | 104 |
105 | 106 | 107 |
108 | `, 109 | directives: [ 110 | ROUTER_DIRECTIVES, 111 | MdToolbar, MD_CARD_DIRECTIVES, MD_SIDENAV_DIRECTIVES, MdButton, 112 | MdProgressCircle, APP_SHELL_DIRECTIVES, MdIcon 113 | ], 114 | pipes: [], 115 | providers: [MdIconRegistry] 116 | }) 117 | @RouteConfig([ 118 | { path: '/login', name: 'Login', component: LoginComponent }, 119 | { path: '/issues/:org/:repo/...', name: 'Issues', component: IssuesComponent }, 120 | { path: '/repo-selector', name: 'RepoSelector', component: RepoSelectorComponent } 121 | ]) 122 | export class IssueZeroAppComponent { 123 | constructor( 124 | @Inject(IS_PRERENDER) isPrerender: boolean, 125 | public af: AngularFire, 126 | public router: Router, 127 | @Inject(IS_POST_LOGIN) isPostLogin:boolean, 128 | location:Location, 129 | public gh:GithubService, 130 | mdIconRegistry:MdIconRegistry) { 131 | // Add navigation icons 132 | [['navigation', 'menu'], ['content', 'filter_list'], ['navigation', 'arrow_back'], ['action', 'delete']].forEach(([section,icon]) => { 133 | mdIconRegistry.addSvgIcon(icon, `/vendor/material-design-icons/${section}/svg/production/ic_${icon}_24px.svg`) 134 | }); 135 | 136 | /** 137 | * Check login state and redirect to appropriate 138 | * page: Login or Issues route. 139 | */ 140 | if (!isPrerender) { 141 | // If the page was part of the Firebase OAuth flow (the successful login redirect), 142 | // then short-circuit the auth observable. 143 | ArrayObservable.of(isPostLogin) 144 | .filter(v => v === true) 145 | .concat(af.auth) 146 | // Cast nulls to booleans 147 | .map(v => !!v) 148 | .distinctUntilChanged() 149 | .do((loggedIn: boolean) => { 150 | // If state is null (user not logged in) navigate to log in 151 | if (loggedIn && (!location.path() || location.path() === '/login')) { 152 | gh.fetch(`/user/repos`, 'affiliation=owner,collaborator&sort=updated') 153 | .map((repos:Repo[]) => ({ 154 | org: repos[0].owner.login, 155 | repo: repos[0].name 156 | })) 157 | .take(1) 158 | .subscribe(config => router.navigate(['./Issues', config])); 159 | } else if (!isPostLogin) { 160 | router.navigate(['./Login']); 161 | } 162 | }) 163 | // Only emit if user is logged in (state is non-null) 164 | .filter(state => !!state) 165 | // Complete this Observable after successful login 166 | .take(1) 167 | // onLogoutObervable takes over once user is logged in. 168 | // User will be redirected to login page whenever they log out. 169 | .concat(af.auth 170 | // Keep this Observable alive for the duration of the app. 171 | .do((state: FirebaseAuthState) => { 172 | if (!state) { 173 | router.navigate(['./Login']) 174 | } 175 | })) 176 | .subscribe(); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/app/repo-params.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEachProviders, 3 | it, 4 | describe, 5 | expect, 6 | inject 7 | } from '@angular/core/testing'; 8 | import { RepoParamsService } from './repo-params.service'; 9 | 10 | describe('RepoParams Service', () => { 11 | beforeEachProviders(() => [RepoParamsService]); 12 | 13 | it('should ...', 14 | inject([RepoParamsService], (service: RepoParamsService) => { 15 | expect(service).toBeTruthy(); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/repo-params.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Location} from '@angular/common'; 3 | 4 | @Injectable() 5 | export class RepoParamsService { 6 | constructor(private location:Location) {} 7 | 8 | getRepo ():{org: string, repo: string} { 9 | var [path, org, repo] = /issues\/([a-zA-Z\+\-0-9]+)\/([a-zA-Z\+\-0-9]+)/.exec(this.location.path()); 10 | return {org, repo}; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/config.ts: -------------------------------------------------------------------------------- 1 | import {OpaqueToken} from '@angular/core'; 2 | export const FB_URL = 'https://issue-zero.firebaseIO.com'; 3 | export const IS_POST_LOGIN = new OpaqueToken('IsPostLogin'); 4 | export const LOCAL_STORAGE = new OpaqueToken('LocalStorage'); 5 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './types'; 3 | export * from './store'; -------------------------------------------------------------------------------- /src/app/shared/store.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Issue, Repo, Label, User } from './types'; 6 | import { FilterMap } from '../filter-store.service'; 7 | 8 | export function issues (state: Issue[] = [], action:Action): Issue[] { 9 | switch (action.type) { 10 | case 'AddIssues': 11 | state = addIssues(action, state); 12 | break; 13 | case 'RemoveIssue': 14 | state = state.filter((issue:Issue) => { 15 | return issue.id !== action.payload.id; 16 | }); 17 | break; 18 | case 'PendingRemoveIssue': 19 | state = state 20 | .map((issue:Issue) => { 21 | if (issue.id === action.payload.id) { 22 | return Object.assign({}, issue, { 23 | isPendingRemoval: true 24 | }) 25 | } 26 | return issue; 27 | }) 28 | break; 29 | } 30 | return state; 31 | } 32 | 33 | // Creates keys of org:repo:number to quickly filter against. 34 | function getIssueUnique(issue:Issue): string { 35 | return `${issue.org}:${issue.repo}:${issue.number}`; 36 | } 37 | 38 | function addIssues(action:Action, state:Issue[]) { 39 | /** 40 | * Make sure no duplicate issues, newest issues win. 41 | **/ 42 | var existingKeys = action.payload.reduce((prev, curr) => { 43 | prev[curr.id] = true; 44 | return prev; 45 | }, {}); 46 | state = action.payload.concat(state.filter((issue:Issue) => { 47 | // Only return issues that aren't in the new payload. 48 | return !existingKeys[issue.id]; 49 | })); 50 | return state; 51 | } 52 | 53 | export function repos(state: Repo[] = [], action:Action): Repo[] { 54 | switch(action.type) { 55 | case 'AddRepo': 56 | state = state.concat(action.payload); 57 | break; 58 | } 59 | return state; 60 | } 61 | 62 | export function users(state: User[] = [], action:Action): User[] { 63 | return state; 64 | } 65 | 66 | export function labels(state: Label[] = [], action:Action): Label[] { 67 | return state; 68 | } 69 | 70 | export function filters(state: FilterMap = {}, action:Action): FilterMap { 71 | switch(action.type) { 72 | case 'SetFilter': 73 | state = Object.assign({}, state, { 74 | [`${action.payload.org}/${action.payload.repo}`]: action.payload 75 | }); 76 | break; 77 | } 78 | return state; 79 | } 80 | 81 | export interface AppState { 82 | issues: Issue[]; 83 | labels: Label[]; 84 | users: User[]; 85 | repos: Repo[]; 86 | filters: FilterMap; 87 | } 88 | -------------------------------------------------------------------------------- /src/app/shared/types.ts: -------------------------------------------------------------------------------- 1 | export type Repo = { 2 | name: string; 3 | full_name: string; 4 | owner: User; 5 | } 6 | 7 | export type User = { 8 | avatar_url: string; 9 | login: string; 10 | } 11 | 12 | export enum GithubObjects { 13 | User, 14 | Repo, 15 | Issue 16 | } 17 | 18 | export type Issue = { 19 | id: number; 20 | url: string; 21 | user: User; 22 | body: string; 23 | title: string; 24 | number: number; 25 | labels: Label[] 26 | // Additional properties 27 | org: string; 28 | repo: string; 29 | isPendingRemoval?: boolean; 30 | } 31 | 32 | export type Label = { 33 | url: string; 34 | name: string; 35 | color: string; 36 | } 37 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/favicon.ico -------------------------------------------------------------------------------- /src/icons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-144x144.png -------------------------------------------------------------------------------- /src/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/icons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-36x36.png -------------------------------------------------------------------------------- /src/icons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-48x48.png -------------------------------------------------------------------------------- /src/icons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-72x72.png -------------------------------------------------------------------------------- /src/icons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/android-chrome-96x96.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/favicon-96x96.png -------------------------------------------------------------------------------- /src/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/icon.png -------------------------------------------------------------------------------- /src/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/mstile-144x144.png -------------------------------------------------------------------------------- /src/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/mstile-310x150.png -------------------------------------------------------------------------------- /src/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/mstile-310x310.png -------------------------------------------------------------------------------- /src/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/issue-zero/b5628226a783157630a2a2ed76137c9f3bf09d36/src/icons/mstile-70x70.png -------------------------------------------------------------------------------- /src/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IssueZero 6 | 7 | 8 | {{#unless environment.production}} 9 | 10 | {{/unless}} 11 | 12 | 13 | 14 | 15 | 16 | {{#each mobile.icons}} 17 | 18 | {{/each}} 19 | 20 | {{#if environment.production}} 21 | 28 | {{/if}} 29 | 30 | 53 | 54 | 55 | 56 | Loading... 57 | 58 | 59 | 60 | {{#if environment.production}} 61 | 62 | {{else}} 63 | {{#each scripts.polyfills}}{{/each}} 64 | 69 | {{/if}} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main-app-shell.ts: -------------------------------------------------------------------------------- 1 | import {provide} from '@angular/core'; 2 | import {APP_BASE_HREF} from '@angular/common'; 3 | import { 4 | REQUEST_URL, 5 | ORIGIN_URL, 6 | NODE_ROUTER_PROVIDERS, 7 | NODE_LOCATION_PROVIDERS, 8 | NODE_HTTP_PROVIDERS 9 | } from 'angular2-universal'; 10 | import { APP_SHELL_BUILD_PROVIDERS } from '@angular/app-shell'; 11 | import { AngularFire, FIREBASE_PROVIDERS, defaultFirebase } from 'angularfire2'; 12 | import {Observable} from 'rxjs/Observable'; 13 | import 'rxjs/add/observable/empty'; 14 | 15 | import {FB_URL, IS_POST_LOGIN, LOCAL_STORAGE} from './app/shared'; 16 | import {IssueZeroAppComponent} from './app/'; 17 | import { GithubService } from './app/github.service'; 18 | 19 | export const options = { 20 | directives: [ 21 | // The component that will become the main App Shell 22 | IssueZeroAppComponent 23 | ], 24 | platformProviders: [ 25 | NODE_ROUTER_PROVIDERS, 26 | NODE_LOCATION_PROVIDERS, 27 | provide(ORIGIN_URL, { 28 | useValue: '' 29 | }) 30 | ], 31 | providers: [ 32 | APP_SHELL_BUILD_PROVIDERS, 33 | FIREBASE_PROVIDERS, 34 | defaultFirebase(FB_URL), 35 | provide(AngularFire, { 36 | useValue: { 37 | auth: Observable.empty() 38 | } 39 | }), 40 | provide(IS_POST_LOGIN, { 41 | useValue: false 42 | }), 43 | // What URL should Angular be treating the app as if navigating 44 | provide(APP_BASE_HREF, {useValue: '/'}), 45 | provide(REQUEST_URL, {useValue: '/'}), 46 | NODE_HTTP_PROVIDERS, 47 | GithubService, 48 | provide(LOCAL_STORAGE, { 49 | useValue: { 50 | getItem: () => null, 51 | setItem: () => null 52 | } 53 | }) 54 | ] 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode, provide } from '@angular/core'; 3 | import { APP_BASE_HREF } from '@angular/common'; 4 | import { HTTP_PROVIDERS } from '@angular/http'; 5 | import { IssueZeroAppComponent, environment } from './app/'; 6 | import { APP_SHELL_RUNTIME_PROVIDERS } from '@angular/app-shell'; 7 | import {FIREBASE_PROVIDERS, defaultFirebase, AuthMethods, AuthProviders, firebaseAuthConfig} from 'angularfire2'; 8 | import { ROUTER_PROVIDERS } from '@angular/router-deprecated'; 9 | import { GithubService } from './app/github.service'; 10 | import {provideStore} from '@ngrx/store'; 11 | 12 | // Import auto-patching RxJS operators 13 | import 'rxjs/add/operator/do'; 14 | import 'rxjs/add/operator/filter'; 15 | import 'rxjs/add/operator/take'; 16 | import 'rxjs/add/operator/concat'; 17 | import 'rxjs/add/operator/switchMap'; 18 | import 'rxjs/add/operator/map'; 19 | import 'rxjs/add/operator/distinctUntilChanged'; 20 | import 'rxjs/add/operator/combineLatest'; 21 | import 'rxjs/add/operator/catch'; 22 | import 'rxjs/add/operator/mergeMap'; 23 | import 'rxjs/add/operator/merge'; 24 | 25 | // Just required so that `Hammer` gets added to global namespace 26 | require('hammerjs'); 27 | 28 | import { 29 | filters, 30 | FB_URL, 31 | issues, 32 | Issue, 33 | IS_POST_LOGIN, 34 | labels, 35 | LOCAL_STORAGE, 36 | repos, 37 | users 38 | } from './app/shared'; 39 | 40 | if (environment.production) { 41 | enableProdMode(); 42 | } 43 | 44 | // Checks if this is the OAuth redirect callback from Firebase 45 | // Has to be global so can be used in CanActivate 46 | (window).__IS_POST_LOGIN = /\&__firebase_request_key/.test(window.location.hash); 47 | 48 | bootstrap(IssueZeroAppComponent, [ 49 | APP_SHELL_RUNTIME_PROVIDERS, FIREBASE_PROVIDERS, ROUTER_PROVIDERS, HTTP_PROVIDERS, 50 | defaultFirebase(FB_URL), 51 | provide(IS_POST_LOGIN, { 52 | useValue: (window).__IS_POST_LOGIN 53 | }), 54 | GithubService, 55 | provideStore({repos:repos, issues:issues, labels:labels, users:users, filters:filters}), 56 | provide(LOCAL_STORAGE, { 57 | useValue: (window.localStorage) 58 | }), 59 | firebaseAuthConfig( 60 | {provider: AuthProviders.Github, method: AuthMethods.Redirect, scope: ['repo']}) 61 | ]); 62 | -------------------------------------------------------------------------------- /src/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Issue Zero", 3 | "short_name": "Issue Zero", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image/png", 9 | "density": 0.75 10 | }, 11 | { 12 | "src": "/android-chrome-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image/png", 15 | "density": 1 16 | }, 17 | { 18 | "src": "/android-chrome-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image/png", 21 | "density": 1.5 22 | }, 23 | { 24 | "src": "/android-chrome-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image/png", 27 | "density": 2 28 | }, 29 | { 30 | "src": "/android-chrome-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image/png", 33 | "density": 3 34 | }, 35 | { 36 | "src": "/android-chrome-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png", 39 | "density": 4 40 | } 41 | ], 42 | "theme_color": "#000000", 43 | "background_color": "#e0e0e0", 44 | "start_url": "/index.html", 45 | "display": "standalone", 46 | "orientation": "portrait" 47 | } 48 | -------------------------------------------------------------------------------- /src/system-config.ts: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | * User Configuration. 3 | **********************************************************************************************/ 4 | /** Map relative paths to URLs. */ 5 | const map: any = { 6 | '@angular2-material': 'vendor/@angular2-material', 7 | 'angularfire2': 'vendor/angularfire2', 8 | 'firebase': 'vendor/firebase/lib/firebase-web.js', 9 | 'hammerjs': 'vendor/hammerjs/hammer.min.js', 10 | '@ngrx': 'vendor/@ngrx' 11 | }; 12 | 13 | /** User packages configuration. */ 14 | var packages: any = { 15 | '@ngrx/db': { 16 | format: 'cjs', 17 | main: 'index.js', 18 | defaultExtension: 'js' 19 | }, 20 | '@ngrx/store': { 21 | format: 'cjs', 22 | main: 'index.js', 23 | defaultExtension: 'js' 24 | }, 25 | 'angularfire2': { 26 | defaultExtension: 'js', 27 | main: 'angularfire2' 28 | } 29 | }; 30 | 31 | // Add Angular Material packages 32 | packages = [ 33 | 'button', 34 | 'card', 35 | 'checkbox', 36 | 'core', 37 | 'icon', 38 | 'input', 39 | 'list', 40 | 'progress-circle', 41 | 'sidenav', 42 | 'toolbar' 43 | ].reduce((prev, main) => { 44 | return Object.assign({}, prev, { 45 | [`@angular2-material/${main}`]: { 46 | defaultExtension: 'js', 47 | main 48 | } 49 | }) 50 | }, packages); 51 | 52 | //////////////////////////////////////////////////////////////////////////////////////////////// 53 | /*********************************************************************************************** 54 | * Everything underneath this line is managed by the CLI. 55 | **********************************************************************************************/ 56 | const barrels: string[] = [ 57 | // Angular specific barrels. 58 | '@angular/core', 59 | '@angular/common', 60 | '@angular/compiler', 61 | '@angular/http', 62 | '@angular/router', 63 | '@angular/platform-browser', 64 | '@angular/platform-browser-dynamic', 65 | '@angular/router-deprecated', 66 | '@angular/app-shell', 67 | '@angular2-material/toolbar', 68 | '@angular2-material/card', 69 | '@angular2-material/core', 70 | '@angular2-material/sidenav', 71 | '@angular2-material/button', 72 | '@angular2-material/progress-circle', 73 | 'angularfire2', 74 | 75 | // Thirdparty barrels. 76 | 'rxjs', 77 | 78 | // App specific barrels. 79 | 'app', 80 | 'app/shared', 81 | 'app/+login', 82 | 'app/+issues', 83 | 'app/+issues/+list', 84 | 'app/+issues/+filter', 85 | 'app/+issues/+triage', 86 | 'app/+issues/+list/toolbar', 87 | 'app/+issues/+list/issue-row', 88 | 'app/+issues/+list/repo-selector-row', 89 | 'app/+repo-selector', 90 | 'app/+repo-selector/repo-selector-row', 91 | /** @cli-barrel */ 92 | ]; 93 | 94 | const cliSystemConfigPackages: any = {}; 95 | barrels.forEach((barrelName: string) => { 96 | cliSystemConfigPackages[barrelName] = { main: 'index' }; 97 | }); 98 | 99 | /** Type declaration for ambient System. */ 100 | declare var System: any; 101 | 102 | // Apply the CLI SystemJS configuration. 103 | System.config({ 104 | map: { 105 | '@angular': 'vendor/@angular', 106 | 'rxjs': 'vendor/rxjs', 107 | 'main': 'main.js' 108 | }, 109 | packages: cliSystemConfigPackages 110 | }); 111 | 112 | // Apply the user's configuration. 113 | System.config({ map, packages }); 114 | -------------------------------------------------------------------------------- /src/system-import.js: -------------------------------------------------------------------------------- 1 | System.import('main') 2 | .catch(console.error.bind(console)); 3 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "mapRoot": "", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": false, 12 | "outDir": "../dist/", 13 | "rootDir": ".", 14 | "sourceMap": true, 15 | "target": "es5", 16 | "inlineSources": true 17 | }, 18 | 19 | "files": [ 20 | "main.ts", 21 | "main-app-shell.ts", 22 | "typings.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "max-line-length": [true, 100], 5 | "no-inferrable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "eofline": true, 16 | "no-duplicate-variable": true, 17 | "no-eval": true, 18 | "no-arg": true, 19 | "no-internal-module": true, 20 | "no-trailing-whitespace": true, 21 | "no-bitwise": true, 22 | "no-shadowed-variable": true, 23 | "no-unused-expression": true, 24 | "no-unused-variable": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-else", 29 | "check-open-brace", 30 | "check-whitespace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [true, "always"], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "curly": true, 49 | "variable-name": [ 50 | true, 51 | "ban-keywords", 52 | "check-format", 53 | "allow-trailing-underscore" 54 | ], 55 | "whitespace": [ 56 | true, 57 | "check-branch", 58 | "check-decl", 59 | "check-operator", 60 | "check-separator", 61 | "check-type" 62 | ], 63 | "component-selector-name": [true, "kebab-case"], 64 | "component-selector-type": [true, "element"], 65 | "host-parameter-decorator": true, 66 | "input-parameter-decorator": true, 67 | "output-parameter-decorator": true, 68 | "attribute-parameter-decorator": true, 69 | "input-property-directive": true, 70 | "output-property-directive": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDevDependencies": { 3 | "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", 4 | "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", 5 | "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654" 6 | }, 7 | "ambientDependencies": { 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", 9 | "firebase": "registry:dt/firebase#2.4.1+20160412125105", 10 | "node": "registry:dt/node#4.0.0+20160509154515" 11 | } 12 | } 13 | --------------------------------------------------------------------------------