├── .nvmrc
├── src
├── features
│ ├── settings
│ │ ├── store
│ │ │ ├── settings.actions.ts
│ │ │ ├── settings.facade.ts
│ │ │ ├── settings.reducer.ts
│ │ │ ├── settings.actions.spec.ts
│ │ │ ├── settings.facade.spec.ts
│ │ │ └── settings.reducer.spec.ts
│ │ ├── service
│ │ │ └── settings.service.ts
│ │ ├── pages
│ │ │ └── settings
│ │ │ │ ├── settings.module.ts
│ │ │ │ ├── settings.html
│ │ │ │ ├── settings.ts
│ │ │ │ └── settings.scss
│ │ └── settings.module.ts
│ ├── connections
│ │ ├── components
│ │ │ ├── task-list
│ │ │ │ ├── task-list.component.scss
│ │ │ │ ├── task-list.component.html
│ │ │ │ ├── task-list.component.ts
│ │ │ │ └── task-list.component.spec.ts
│ │ │ ├── instructions
│ │ │ │ ├── instructions.component.scss
│ │ │ │ ├── instructions.component.html
│ │ │ │ ├── instructions.component.ts
│ │ │ │ └── instructions.component.spec.ts
│ │ │ ├── project-item
│ │ │ │ ├── project-item.component.scss
│ │ │ │ ├── project-item.component.html
│ │ │ │ ├── project-item.component.ts
│ │ │ │ └── project-item.component.spec.ts
│ │ │ ├── project-list
│ │ │ │ ├── project-list.component.scss
│ │ │ │ ├── project-list.component.html
│ │ │ │ ├── project-list.component.spec.ts
│ │ │ │ └── project-list.component.ts
│ │ │ ├── connection-list
│ │ │ │ ├── connection-list.component.ts
│ │ │ │ ├── connection-list.component.spec.ts
│ │ │ │ ├── connection-list.component.html
│ │ │ │ └── connection-list.component.scss
│ │ │ ├── verify-modal
│ │ │ │ ├── verify-modal.component.ts
│ │ │ │ ├── verify-modal.component.html
│ │ │ │ └── verify-modal.component.scss
│ │ │ └── share-scope-link-modal
│ │ │ │ ├── share-scope-link-modal.component.html
│ │ │ │ ├── share-scope-link-modal.component.ts
│ │ │ │ └── share-scope-link-modal.component.scss
│ │ ├── pages
│ │ │ ├── select-project
│ │ │ │ ├── select-project.component.scss
│ │ │ │ ├── select-project.component.module.ts
│ │ │ │ ├── select-project.component.html
│ │ │ │ ├── select-project.component.spec.ts
│ │ │ │ └── select-project.component.ts
│ │ │ ├── add-connection
│ │ │ │ ├── add-connection.component.scss
│ │ │ │ ├── add-connection.component.module.ts
│ │ │ │ ├── add-connection.component.spec.ts
│ │ │ │ └── add-connection.component.html
│ │ │ ├── connection-details
│ │ │ │ ├── connection-details.component.module.ts
│ │ │ │ ├── connection-details.component.scss
│ │ │ │ ├── connection-details.component.spec.ts
│ │ │ │ ├── connection-details.component.html
│ │ │ │ └── connection-details.component.ts
│ │ │ └── select-task-list
│ │ │ │ ├── select-task-list.component.module.ts
│ │ │ │ ├── select-task-list.component.scss
│ │ │ │ ├── select-task-list.component.spec.ts
│ │ │ │ ├── select-task-list.component.html
│ │ │ │ └── select-task-list.component.ts
│ │ ├── services
│ │ │ └── connection.service.spec.ts
│ │ └── connections.module.ts
│ ├── scoping
│ │ ├── components
│ │ │ ├── session-header
│ │ │ │ ├── session-header.component.html
│ │ │ │ ├── session-header.component.scss
│ │ │ │ ├── session-header.component.ts
│ │ │ │ └── session-header.component.spec.ts
│ │ │ ├── task-card
│ │ │ │ ├── task-card.component.scss
│ │ │ │ ├── task-card.component.html
│ │ │ │ ├── task-card.component.ts
│ │ │ │ └── task-card.component.spec.ts
│ │ │ ├── result-estimate
│ │ │ │ ├── result-estimate.component.ts
│ │ │ │ ├── result-estimate.component.html
│ │ │ │ ├── result-estimate.component.spec.ts
│ │ │ │ └── result-estimate.component.scss
│ │ │ ├── vote
│ │ │ │ ├── vote.component.html
│ │ │ │ ├── vote.component.spec.ts
│ │ │ │ ├── vote.component.scss
│ │ │ │ └── vote.component.ts
│ │ │ ├── counted-votes
│ │ │ │ ├── counted-votes.component.html
│ │ │ │ ├── counted-votes.component.ts
│ │ │ │ ├── counted-votes.component.scss
│ │ │ │ └── counted-votes.component.spec.ts
│ │ │ ├── session-access
│ │ │ │ ├── session-access.component.html
│ │ │ │ ├── session-access.component.scss
│ │ │ │ ├── session-access.component.spec.ts
│ │ │ │ └── session-access.component.ts
│ │ │ └── select-result
│ │ │ │ ├── select-result.component.spec.ts
│ │ │ │ ├── select-result.component.html
│ │ │ │ ├── select-result.component.scss
│ │ │ │ └── select-result.component.ts
│ │ ├── services
│ │ │ └── scoping.service.spec.ts
│ │ ├── pages
│ │ │ ├── session-results
│ │ │ │ ├── session-results.component.module.ts
│ │ │ │ ├── session-results.component.scss
│ │ │ │ ├── session-results.component.spec.ts
│ │ │ │ ├── session-results.component.ts
│ │ │ │ └── session-results.component.html
│ │ │ └── session-scoping
│ │ │ │ ├── session-scoping.component.module.ts
│ │ │ │ ├── session-scoping.component.spec.ts
│ │ │ │ └── session-scoping.component.scss
│ │ └── scoping.module.ts
│ ├── dashboard
│ │ ├── components
│ │ │ ├── session-history-list
│ │ │ │ ├── session-history-list.component.scss
│ │ │ │ ├── session-history-list.component.html
│ │ │ │ ├── session-history-list.component.spec.ts
│ │ │ │ └── session-history-list.component.ts
│ │ │ ├── join-session
│ │ │ │ ├── join-session.component.scss
│ │ │ │ ├── join-session.component.html
│ │ │ │ ├── join-session.component.ts
│ │ │ │ └── join-session.component.spec.ts
│ │ │ ├── session-history-item
│ │ │ │ ├── session-history-item.component.scss
│ │ │ │ ├── session-history-item.component.spec.ts
│ │ │ │ ├── session-history-item.component.html
│ │ │ │ └── session-history-item.component.ts
│ │ │ └── session-detail-modal
│ │ │ │ ├── session-detail-modal.component.html
│ │ │ │ ├── session-detail-modal.component.scss
│ │ │ │ └── session-detail-modal.component.ts
│ │ ├── pages
│ │ │ └── dashboard
│ │ │ │ ├── dashboard.component.scss
│ │ │ │ ├── dashboard.component.module.ts
│ │ │ │ └── dashboard.component.html
│ │ ├── dashboard-routing.module.ts
│ │ ├── services
│ │ │ └── history.service.spec.ts
│ │ ├── dashboard.module.ts
│ │ └── store
│ │ │ └── dashboard.reducer.ts
│ └── authentication
│ │ ├── pages
│ │ └── login
│ │ │ ├── login.component.module.ts
│ │ │ ├── login.component.scss
│ │ │ └── login.component.html
│ │ ├── authentication-routing.module.ts
│ │ ├── authentication.module.ts
│ │ └── store
│ │ ├── auth.actions.ts
│ │ ├── auth.actions.spec.ts
│ │ └── auth.reducer.ts
├── shared
│ ├── components
│ │ └── info-modal
│ │ │ ├── info-modal.scss
│ │ │ ├── info-modal.html
│ │ │ └── info-modal.ts
│ ├── pipes
│ │ ├── object-iterators.pipe.spec.ts
│ │ └── object-iterators.pipe.ts
│ ├── loading.service.ts
│ ├── api.interceptor.ts
│ └── shared.module.ts
├── app
│ ├── app.html
│ ├── main.ts
│ ├── not-found.component.ts
│ ├── app.constants.ts
│ ├── app-routing.module.ts
│ ├── auth.guard.ts
│ ├── un-auth.guard.ts
│ └── app.component.ts
├── test
│ ├── jest-test.ts
│ ├── jest-test.helper.ts
│ └── jest-global-mocks.ts
├── models
│ ├── task-list.ts
│ ├── faq.ts
│ ├── project.ts
│ ├── user.ts
│ ├── task.ts
│ ├── connection.ts
│ ├── scoping-session.ts
│ └── history-item.ts
├── assets
│ ├── imgs
│ │ └── logo.png
│ ├── icon
│ │ └── favicon.ico
│ └── fonts
│ │ └── Avenir-Next
│ │ ├── bold.eot
│ │ ├── bold.ttf
│ │ ├── bold.woff
│ │ ├── demi.eot
│ │ ├── demi.ttf
│ │ ├── demi.woff
│ │ ├── thin.eot
│ │ ├── thin.ttf
│ │ ├── thin.woff
│ │ ├── bold.woff2
│ │ ├── demi.woff2
│ │ ├── medium.eot
│ │ ├── medium.ttf
│ │ ├── medium.woff
│ │ ├── regular.eot
│ │ ├── regular.ttf
│ │ ├── regular.woff
│ │ ├── thin.woff2
│ │ ├── ultralight.eot
│ │ ├── ultralight.ttf
│ │ └── ultralight.woff
├── store
│ ├── app.actions.ts
│ ├── router.actions.ts
│ ├── app.facade.ts
│ ├── router.reducer.ts
│ ├── router.actions.spec.ts
│ ├── router.facade.ts
│ ├── app.reducer.ts
│ └── router.reducer.spec.ts
├── manifest.json
├── tsconfig.spec.json
├── environments
│ ├── environment.ts
│ └── environment.prod.ts
├── service-worker.js
├── test.ts
└── index.html
├── firestore.indexes.json
├── resources
├── icon.png.md5
├── splash.png.md5
├── icon.png
├── splash.png
├── ios
│ ├── icon
│ │ ├── icon.png
│ │ ├── icon-40.png
│ │ ├── icon-50.png
│ │ ├── icon-60.png
│ │ ├── icon-72.png
│ │ ├── icon-76.png
│ │ ├── icon@2x.png
│ │ ├── icon-1024.png
│ │ ├── icon-40@2x.png
│ │ ├── icon-40@3x.png
│ │ ├── icon-50@2x.png
│ │ ├── icon-60@2x.png
│ │ ├── icon-60@3x.png
│ │ ├── icon-72@2x.png
│ │ ├── icon-76@2x.png
│ │ ├── icon-small.png
│ │ ├── icon-83.5@2x.png
│ │ ├── icon-small@2x.png
│ │ └── icon-small@3x.png
│ └── splash
│ │ ├── Default-667h.png
│ │ ├── Default-736h.png
│ │ ├── Default~iphone.png
│ │ ├── Default@2x~iphone.png
│ │ ├── Default-Portrait~ipad.png
│ │ ├── Default-568h@2x~iphone.png
│ │ ├── Default-Landscape-736h.png
│ │ ├── Default-Landscape~ipad.png
│ │ ├── Default-Landscape@2x~ipad.png
│ │ ├── Default-Landscape@~ipadpro.png
│ │ ├── Default-Portrait@2x~ipad.png
│ │ ├── Default-Portrait@~ipadpro.png
│ │ ├── Default@2x~universal~anyany.png
│ │ ├── Default@2x~universal~anyany iphonex-landscape.png
│ │ └── Default@2x~universal~anyany iphonex-portrait.png
├── android
│ ├── icon
│ │ ├── drawable-hdpi-icon.png
│ │ ├── drawable-ldpi-icon.png
│ │ ├── drawable-mdpi-icon.png
│ │ ├── drawable-xhdpi-icon.png
│ │ ├── drawable-xxhdpi-icon.png
│ │ └── drawable-xxxhdpi-icon.png
│ └── splash
│ │ ├── drawable-land-hdpi-screen.png
│ │ ├── drawable-land-ldpi-screen.png
│ │ ├── drawable-land-mdpi-screen.png
│ │ ├── drawable-land-xhdpi-screen.png
│ │ ├── drawable-land-xxhdpi-screen.png
│ │ ├── drawable-port-hdpi-screen.png
│ │ ├── drawable-port-ldpi-screen.png
│ │ ├── drawable-port-mdpi-screen.png
│ │ ├── drawable-port-xhdpi-screen.png
│ │ ├── drawable-port-xxhdpi-screen.png
│ │ ├── drawable-land-xxxhdpi-screen.png
│ │ └── drawable-port-xxxhdpi-screen.png
└── README.md
├── .npmrc
├── functions
├── src
│ ├── middleware
│ │ ├── index.ts
│ │ └── id-token-auth.ts
│ ├── api
│ │ ├── connections
│ │ │ ├── projects
│ │ │ │ ├── sessions
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── add-session.ts
│ │ │ │ ├── tasklists
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── get-tasks.ts
│ │ │ │ │ └── get-tasklists.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── get-projects.ts
│ │ │ ├── tasks
│ │ │ │ ├── index.ts
│ │ │ │ ├── get-task.ts
│ │ │ │ └── put-estimate.ts
│ │ │ ├── delete-connection.ts
│ │ │ ├── index.ts
│ │ │ └── add-connection.ts
│ │ ├── estimate
│ │ │ ├── index.ts
│ │ │ └── set-estimate.ts
│ │ ├── index.ts
│ │ └── session-links
│ │ │ ├── index.ts
│ │ │ ├── delete-session.ts
│ │ │ ├── refresh-access-code.ts
│ │ │ └── decode-session-link.ts
│ ├── fw.html
│ ├── services
│ │ ├── index.ts
│ │ └── encryption.service.ts
│ └── index.ts
├── tsconfig.json
└── package.json
├── juntoscope.jks
├── .runtimeconfig.json
├── .firebaserc
├── ionic.config.json
├── tslint.json
├── apple-app-site-association
├── .editorconfig
├── firebase.json
├── .gitignore
├── tsconfig.json
├── .travis.yml
└── firestore.rules
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
2 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.actions.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.facade.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.reducer.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/components/info-modal/info-modal.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.actions.spec.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.facade.spec.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/settings/store/settings.reducer.spec.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": []
3 | }
4 |
--------------------------------------------------------------------------------
/resources/icon.png.md5:
--------------------------------------------------------------------------------
1 | 0036184da03326d2165c8c5255980d96
--------------------------------------------------------------------------------
/resources/splash.png.md5:
--------------------------------------------------------------------------------
1 | 5dba1401df8cd17293ee687404148024
--------------------------------------------------------------------------------
/src/app/app.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
2 | message="chore(release): %s"
3 |
--------------------------------------------------------------------------------
/functions/src/middleware/index.ts:
--------------------------------------------------------------------------------
1 | export * from './id-token-auth';
2 |
--------------------------------------------------------------------------------
/src/features/connections/components/task-list/task-list.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/connections/components/instructions/instructions.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-project/select-project.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/juntoscope.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/juntoscope.jks
--------------------------------------------------------------------------------
/src/test/jest-test.ts:
--------------------------------------------------------------------------------
1 | import "jest-preset-angular";
2 | import "jest-global-mocks";
3 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/icon.png
--------------------------------------------------------------------------------
/resources/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/splash.png
--------------------------------------------------------------------------------
/src/models/task-list.ts:
--------------------------------------------------------------------------------
1 | export interface TaskList {
2 | id: string;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/imgs/logo.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon.png
--------------------------------------------------------------------------------
/src/assets/icon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/icon/favicon.ico
--------------------------------------------------------------------------------
/.runtimeconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "encryption": {
3 | "secret": "J*+48Iz{J%m9xr08]Z1DLNTj5f66pE7G"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/resources/ios/icon/icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-50.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-72.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-76.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-1024.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40@3x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-50@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60@3x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-72@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-76@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small.png
--------------------------------------------------------------------------------
/src/features/connections/components/instructions/instructions.component.html:
--------------------------------------------------------------------------------
1 |
2 | instructions works!
3 |
4 |
--------------------------------------------------------------------------------
/resources/ios/icon/icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-83.5@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small@2x.png
--------------------------------------------------------------------------------
/resources/ios/icon/icon-small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small@3x.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-667h.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-667h.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-736h.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-736h.png
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/demi.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/demi.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/demi.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.woff
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "juntoscope-dev-963e3",
4 | "production": "juntoscope-prod-61b4d"
5 | }
6 | }
--------------------------------------------------------------------------------
/ionic.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "juntoscope",
3 | "integrations": {
4 | "cordova": {}
5 | },
6 | "type": "ionic-angular"
7 | }
--------------------------------------------------------------------------------
/resources/ios/splash/Default~iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default~iphone.png
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/demi.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.woff2
--------------------------------------------------------------------------------
/resources/ios/splash/Default@2x~iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~iphone.png
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/ultralight.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/ultralight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.ttf
--------------------------------------------------------------------------------
/src/models/faq.ts:
--------------------------------------------------------------------------------
1 | export interface Faq {
2 | id?: string;
3 | category: string;
4 | question: string;
5 | answer: string;
6 | }
7 |
--------------------------------------------------------------------------------
/resources/android/icon/drawable-hdpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-hdpi-icon.png
--------------------------------------------------------------------------------
/resources/android/icon/drawable-ldpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-ldpi-icon.png
--------------------------------------------------------------------------------
/resources/android/icon/drawable-mdpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-mdpi-icon.png
--------------------------------------------------------------------------------
/resources/android/icon/drawable-xhdpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xhdpi-icon.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Portrait~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait~ipad.png
--------------------------------------------------------------------------------
/src/assets/fonts/Avenir-Next/ultralight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.woff
--------------------------------------------------------------------------------
/resources/android/icon/drawable-xxhdpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xxhdpi-icon.png
--------------------------------------------------------------------------------
/resources/android/icon/drawable-xxxhdpi-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xxxhdpi-icon.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-568h@2x~iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-568h@2x~iphone.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Landscape-736h.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape-736h.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Landscape~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape~ipad.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Landscape@2x~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape@2x~ipad.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Landscape@~ipadpro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape@~ipadpro.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Portrait@2x~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait@2x~ipad.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default-Portrait@~ipadpro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait@~ipadpro.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default@2x~universal~anyany.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany.png
--------------------------------------------------------------------------------
/src/store/app.actions.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "@ngrx/store";
2 |
3 | export class NoopAction implements Action {
4 | readonly type = "[App] NO-OP";
5 | }
6 |
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-hdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-hdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-ldpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-ldpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-mdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-mdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-xhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xhdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-xxhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xxhdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-hdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-hdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-ldpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-ldpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-mdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-mdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-xhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xhdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-xxhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xxhdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-land-xxxhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xxxhdpi-screen.png
--------------------------------------------------------------------------------
/resources/android/splash/drawable-port-xxxhdpi-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xxxhdpi-screen.png
--------------------------------------------------------------------------------
/src/models/project.ts:
--------------------------------------------------------------------------------
1 | import { TaskList } from "./task-list";
2 |
3 | export interface Project {
4 | id?: string;
5 | name: string;
6 | taskLists: { [id: string]: TaskList };
7 | }
8 |
--------------------------------------------------------------------------------
/resources/ios/splash/Default@2x~universal~anyany iphonex-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany iphonex-landscape.png
--------------------------------------------------------------------------------
/resources/ios/splash/Default@2x~universal~anyany iphonex-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany iphonex-portrait.png
--------------------------------------------------------------------------------
/src/app/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2 |
3 | import { AppModule } from './app.module';
4 |
5 | platformBrowserDynamic().bootstrapModule(AppModule);
6 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-header/session-header.component.html:
--------------------------------------------------------------------------------
1 | {{ session.projectName }}
2 | Project Task List {{ session.numScopedTasks + 1 }}/{{ session.numTasks }}
--------------------------------------------------------------------------------
/src/features/connections/components/project-item/project-item.component.scss:
--------------------------------------------------------------------------------
1 | app-project-item {
2 | ion-item.item.item-block .item-inner {
3 | border-bottom: 2px solid $primary-orange;
4 | }
5 | }
--------------------------------------------------------------------------------
/src/features/scoping/components/session-header/session-header.component.scss:
--------------------------------------------------------------------------------
1 | app-session-header {
2 | p {
3 | text-align: center;
4 | border-bottom: 2px solid $primary-orange;
5 | padding-bottom: 5px;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/features/scoping/components/task-card/task-card.component.scss:
--------------------------------------------------------------------------------
1 | app-task-card {
2 | .task-card-container {
3 | p:first-child {
4 | border-bottom: 2px solid;
5 | padding-bottom: 5px;
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/models/user.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | uid: string;
3 | displayName: string;
4 | }
5 |
6 | export enum SessionUserType {
7 | MODERATOR = "Session Moderator",
8 | PARTICIPANT = "Session Participant"
9 | }
10 |
--------------------------------------------------------------------------------
/src/features/connections/components/task-list/task-list.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ taskList.name }}
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/features/scoping/components/task-card/task-card.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Task: {{task.name}}
4 |
5 |
6 | Description: {{task.description}}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/models/task.ts:
--------------------------------------------------------------------------------
1 | export interface Task {
2 | name: string;
3 | description: string;
4 | votes?: Votes;
5 | estimate?: number;
6 | id?: number;
7 | }
8 |
9 | export interface Votes {
10 | [userId: string]: number;
11 | }
12 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-duplicate-variable": true,
4 | "no-unused-variable": [
5 | true
6 | ]
7 | },
8 | "rulesDirectory": [
9 | "node_modules/tslint-eslint-rules/dist/rules"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/models/connection.ts:
--------------------------------------------------------------------------------
1 | import { Project } from "./project";
2 |
3 | export interface Connection {
4 | id?: string;
5 | type: string;
6 | token: string;
7 | externalData?: any;
8 | projects?: { [projectId: string]: Project };
9 | }
10 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/sessions/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { addSession } from './add-session';
4 |
5 | export const sessionsRouter = express.Router({ mergeParams: true });
6 |
7 | sessionsRouter.post('/', addSession);
8 |
--------------------------------------------------------------------------------
/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "apps": [],
4 | "details": [
5 | {
6 | "appID": "FA5H7L6B2W.com.openforge.juntoscope",
7 | "paths": [ "*" ]
8 | }
9 | ]
10 | }
11 | }
--------------------------------------------------------------------------------
/src/shared/pipes/object-iterators.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { ObjectIteratorsPipe } from "./object-iterators.pipe";
2 |
3 | describe("ObjectIteratorsPipe", () => {
4 | it("create an instance", () => {
5 | const pipe = new ObjectIteratorsPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-item/project-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ project.name }}
4 |
5 |
8 |
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es6"],
4 | "module": "commonjs",
5 | "noImplicitReturns": false,
6 | "outDir": "lib",
7 | "sourceMap": true,
8 | "target": "es6"
9 | },
10 | "compileOnSave": true,
11 | "include": [
12 | "src"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/functions/src/api/estimate/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { setEstimate } from "./set-estimate";
4 |
5 | export const estimateRouter = express.Router({ mergeParams: true });
6 |
7 | estimateRouter.put(
8 | "/:ownerId/connections/:connectionId/sessions/:sessionId/tasks/:taskId",
9 | setEstimate
10 | );
11 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "JuntoScope",
3 | "short_name": "JuntoScope",
4 | "start_url": "index.html",
5 | "display": "standalone",
6 | "icons": [{
7 | "src": "assets/imgs/logo.png",
8 | "sizes": "512x512",
9 | "type": "image/png"
10 | }],
11 | "background_color": "#4e8ef7",
12 | "theme_color": "#4e8ef7"
13 | }
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.ng-cli.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "baseUrl": "",
8 | "types": ["jasmine", "node"]
9 | },
10 | "files": ["test.ts"],
11 | "include": ["**/*.spec.ts", "**/*.d.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/functions/src/fw.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
11 |
12 |
--------------------------------------------------------------------------------
/functions/src/api/connections/tasks/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { getTask } from './get-task';
4 |
5 | import { putEstimate } from './put-estimate';
6 |
7 | export const tasksRouter = express.Router({ mergeParams: true });
8 |
9 | tasksRouter.get('/:taskId', getTask);
10 |
11 | tasksRouter.put('/:taskId', putEstimate);
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FIREBASE_DEV_CONFIG,
3 | FIREBASE_DEV_FUNCTIONS,
4 | GOOGLE_WEB_CLIENT_ID_DEV
5 | } from "../config/config";
6 |
7 | export const environment = {
8 | production: false,
9 | apiBaseUrl: FIREBASE_DEV_FUNCTIONS,
10 | firebase: FIREBASE_DEV_CONFIG,
11 | webClientId: GOOGLE_WEB_CLIENT_ID_DEV
12 | };
13 |
--------------------------------------------------------------------------------
/src/features/connections/components/instructions/instructions.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 |
3 | @Component({
4 | selector: "app-instructions",
5 | templateUrl: "./instructions.component.html"
6 | })
7 | export class InstructionsComponent implements OnInit {
8 | constructor() {}
9 |
10 | ngOnInit() {}
11 | }
12 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FIREBASE_PROD_FUNCTIONS,
3 | FIREBASE_PROD_CONFIG,
4 | GOOGLE_WEB_CLIENT_ID_PROD
5 | } from "../config/config";
6 |
7 | export const environment = {
8 | production: true,
9 | apiBaseUrl: FIREBASE_PROD_FUNCTIONS,
10 | firebase: FIREBASE_PROD_CONFIG,
11 | webClientId: GOOGLE_WEB_CLIENT_ID_PROD
12 | };
13 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-list/session-history-list.component.scss:
--------------------------------------------------------------------------------
1 | app-session-history-list {
2 | .list-headers {
3 | margin: 2rem 1rem;
4 |
5 | ion-list-header.list-header {
6 | border-bottom: 2px solid $primary-orange;
7 | }
8 | }
9 |
10 | ion-list.list {
11 | border: 2px solid $primary-orange;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/features/settings/service/settings.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import { AngularFirestore } from "angularfire2/firestore";
3 |
4 | @Injectable()
5 | export class SettingsService {
6 | constructor(private afs: AngularFirestore) {}
7 |
8 | getFaqs() {
9 | return this.afs.collection(`/faqs`).valueChanges();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "@angular/core";
2 |
3 | @Component({
4 | selector: "app-not-found",
5 | template: `
6 |
7 | Uh Oh ... '{{ url$ | async }}' Not Found!
8 |
9 | `,
10 | styles: []
11 | })
12 | export class NotFoundComponent {
13 | // url$ = this.routerFacade.url$;
14 |
15 | constructor() {}
16 | }
17 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/tasklists/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { getTaskLists } from './get-tasklists';
4 |
5 | import { getTasks } from './get-tasks';
6 |
7 | export const tasklistsRouter = express.Router({ mergeParams: true });
8 |
9 | tasklistsRouter.get('/', getTaskLists);
10 |
11 | tasklistsRouter.get('/:tasklistId', getTasks);
12 |
--------------------------------------------------------------------------------
/src/features/settings/pages/settings/settings.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SettingsPage } from "./settings";
4 |
5 | @NgModule({
6 | declarations: [SettingsPage],
7 | imports: [IonicPageModule.forChild(SettingsPage)],
8 | exports: [SettingsPage]
9 | })
10 | export class SettingsPageModule {}
11 |
--------------------------------------------------------------------------------
/src/features/dashboard/pages/dashboard/dashboard.component.scss:
--------------------------------------------------------------------------------
1 | app-dashboard {
2 | ion-infinite-scroll ion-infinite-scroll-content.dashboard-spinner {
3 | .infinite-loading .infinite-loading-spinner {
4 | ion-spinner.spinner {
5 | position: relative;
6 | display: inline-block;
7 | width: 28px;
8 | height: 28px;
9 | }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/join-session/join-session.component.scss:
--------------------------------------------------------------------------------
1 | app-join-session {
2 | .container {
3 | margin-top: 2rem;
4 |
5 | .list-headers {
6 | margin: 2rem 1rem;
7 |
8 | ion-list-header.list-header {
9 | border-bottom: 2px solid $primary-orange;
10 | }
11 | }
12 |
13 | .join {
14 | height: 44px;
15 | padding: 0 2rem;
16 | margin: 0;
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | },
6 | "functions": {
7 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build"
8 | },
9 | "hosting": {
10 | "public": "www",
11 | "rewrites": [
12 | {
13 | "source": "**",
14 | "destination": "/index.html"
15 | }
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/features/connections/pages/add-connection/add-connection.component.scss:
--------------------------------------------------------------------------------
1 | app-add-connection {
2 | .container {
3 | align-items: center;
4 | display: flex;
5 | flex-direction: column;
6 | height: 80%;
7 | justify-content: center;
8 | width: 100%;
9 |
10 | p {
11 | padding: 0 2rem;
12 | }
13 |
14 | button.button {
15 | width: 60%;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/shared/components/info-modal/info-modal.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ title }}
10 | {{ text }}
11 |
12 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-list/project-list.component.scss:
--------------------------------------------------------------------------------
1 | app-project-list {
2 | ion-list.list {
3 | margin: 0;
4 | width: 100%;
5 |
6 | ion-list-header.list-header {
7 |
8 | .icon, h2 {
9 | display: inline-block;
10 | font-weight: bolder;
11 | text-transform: none;
12 | vertical-align: middle;
13 | }
14 |
15 | ion-label.label {
16 | margin-top: 3rem;
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/functions/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { connectionsRouter } from "./connections";
4 | import { sessionLinksRouter } from "./session-links";
5 | import { estimateRouter } from "./estimate";
6 |
7 | export const apiRouter = express.Router();
8 |
9 | apiRouter.use("/connections", connectionsRouter);
10 | apiRouter.use("/session-links", sessionLinksRouter);
11 | apiRouter.use("/estimate", estimateRouter);
12 |
--------------------------------------------------------------------------------
/src/features/scoping/components/result-estimate/result-estimate.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from "@angular/core";
2 |
3 | @Component({
4 | selector: "app-result-estimate",
5 | templateUrl: "./result-estimate.component.html"
6 | })
7 | export class ResultEstimateComponent implements OnInit {
8 | @Input() estimate: number;
9 | @Input() finalResults: boolean;
10 |
11 | constructor() {}
12 |
13 | ngOnInit() {}
14 | }
15 |
--------------------------------------------------------------------------------
/resources/README.md:
--------------------------------------------------------------------------------
1 | These are Cordova resources. You can replace icon.png and splash.png and run
2 | `ionic cordova resources` to generate custom icons and splash screens for your
3 | app. See `ionic cordova resources --help` for details.
4 |
5 | Cordova reference documentation:
6 |
7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html
8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/
9 |
--------------------------------------------------------------------------------
/src/features/authentication/pages/login/login.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { LoginPage } from "./login.component";
4 | import { SharedModule } from "../../../../shared/shared.module";
5 |
6 | @NgModule({
7 | declarations: [LoginPage],
8 | imports: [IonicPageModule.forChild(LoginPage), SharedModule],
9 | exports: [LoginPage]
10 | })
11 | export class LoginPageModule {}
12 |
--------------------------------------------------------------------------------
/src/features/scoping/components/task-card/task-card.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from "@angular/core";
2 | import { Task } from "../../../../models/task";
3 |
4 | @Component({
5 | selector: "app-task-card",
6 | templateUrl: "./task-card.component.html"
7 | })
8 | export class TaskCardComponent implements OnInit {
9 | @Input() task: Task;
10 |
11 | @Input() onlyTitle: boolean;
12 |
13 | constructor() {}
14 |
15 | ngOnInit() {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-header/session-header.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from "@angular/core";
2 | import { ScopingSession } from "../../../../models/scoping-session";
3 |
4 | @Component({
5 | selector: "app-session-header",
6 | templateUrl: "./session-header.component.html"
7 | })
8 | export class SessionHeaderComponent implements OnInit {
9 | @Input() session: ScopingSession;
10 |
11 | constructor() {}
12 |
13 | ngOnInit() {}
14 | }
15 |
--------------------------------------------------------------------------------
/src/features/dashboard/dashboard-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { Routes, RouterModule } from "@angular/router";
3 | import { DashboardPage } from "./pages/dashboard/dashboard.component";
4 |
5 | const routes: Routes = [
6 | {
7 | path: "",
8 | component: DashboardPage
9 | }
10 | ];
11 |
12 | @NgModule({
13 | imports: [RouterModule.forChild(routes)],
14 | exports: [RouterModule]
15 | })
16 | export class DashboardRoutingModule {}
17 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-list/project-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ connection.externalData.company }} - {{connection.type | titlecase }}
6 |
7 |
8 |
9 | project
10 |
11 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { getProjects } from './get-projects';
4 | import { sessionsRouter } from './sessions';
5 | import { tasklistsRouter } from './tasklists';
6 |
7 | export const projectsRouter = express.Router({ mergeParams: true });
8 |
9 | projectsRouter.get('/', getProjects);
10 |
11 | projectsRouter.use('/:projectId/tasklists', tasklistsRouter);
12 |
13 | projectsRouter.use('/:projectId/sessions', sessionsRouter);
14 |
--------------------------------------------------------------------------------
/src/features/scoping/components/vote/vote.component.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/src/features/scoping/services/scoping.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from "@angular/core/testing";
2 | import { ScopingService } from "./scoping.service";
3 |
4 | describe("ScopingService", () => {
5 | beforeEach(() => {
6 | TestBed.configureTestingModule({
7 | providers: [ScopingService]
8 | });
9 | });
10 |
11 | it("should be created", inject(
12 | [ScopingService],
13 | (service: ScopingService) => {
14 | expect(service).toBeTruthy();
15 | }
16 | ));
17 | });
18 |
--------------------------------------------------------------------------------
/src/features/dashboard/services/history.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from "@angular/core/testing";
2 |
3 | import { HistoryService } from "./history.service";
4 |
5 | describe("HistoryService", () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [HistoryService]
9 | });
10 | });
11 |
12 | it("should be created", inject(
13 | [HistoryService],
14 | (service: HistoryService) => {
15 | expect(service).toBeTruthy();
16 | }
17 | ));
18 | });
19 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/join-session/join-session.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/features/connections/pages/add-connection/add-connection.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SharedModule } from "../../../../shared/shared.module";
4 | import { AddConnectionPage } from "./add-connection.component";
5 |
6 | @NgModule({
7 | declarations: [AddConnectionPage],
8 | imports: [IonicPageModule.forChild(AddConnectionPage), SharedModule],
9 | exports: [AddConnectionPage]
10 | })
11 | export class AddConnectionPageModule {}
12 |
--------------------------------------------------------------------------------
/src/features/connections/components/connection-list/connection-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from "@angular/core";
2 |
3 | import { Connection } from "../../../../models/connection";
4 |
5 | @Component({
6 | selector: "app-connection-list",
7 | templateUrl: "./connection-list.component.html"
8 | })
9 | export class ConnectionListComponent {
10 | @Input() connections: Connection[];
11 | @Output() select = new EventEmitter();
12 | @Output() add = new EventEmitter();
13 | }
14 |
--------------------------------------------------------------------------------
/functions/src/api/session-links/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { decodeSessionLink } from './decode-session-link';
4 | import { refreshAccessCode } from './refresh-access-code';
5 | import { deleteSession } from './delete-session';
6 |
7 | export const sessionLinksRouter = express.Router({ mergeParams: true });
8 |
9 | sessionLinksRouter.get('/:sessionLink', decodeSessionLink);
10 | sessionLinksRouter.get('/:sessionLink/refresh', refreshAccessCode);
11 | sessionLinksRouter.delete('/:sessionLink', deleteSession);
12 |
--------------------------------------------------------------------------------
/src/features/connections/services/connection.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from "@angular/core/testing";
2 |
3 | import { ConnectionService } from "./connection.service";
4 |
5 | describe("ConnectionService", () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [ConnectionService]
9 | });
10 | });
11 |
12 | it("should be created", inject(
13 | [ConnectionService],
14 | (service: ConnectionService) => {
15 | expect(service).toBeTruthy();
16 | }
17 | ));
18 | });
19 |
--------------------------------------------------------------------------------
/src/features/scoping/components/counted-votes/counted-votes.component.html:
--------------------------------------------------------------------------------
1 |
2 |
COUNTED VOTES
3 |
4 | {{ vote === 0 ? '?' : (vote === -1 ? 'N/A' : vote) }}
5 |
6 |
7 |
8 |
9 |
COUNTED VOTES
10 |
11 | -
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/features/connections/components/task-list/task-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | Input,
4 | Output,
5 | ChangeDetectionStrategy,
6 | EventEmitter
7 | } from "@angular/core";
8 | import { TaskList } from "../../../../models/task-list";
9 |
10 | @Component({
11 | selector: "app-task-list",
12 | templateUrl: "./task-list.component.html",
13 | changeDetection: ChangeDetectionStrategy.OnPush
14 | })
15 | export class TaskListComponent {
16 | @Input() taskList: TaskList;
17 | @Output() toggle = new EventEmitter();
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/app.constants.ts:
--------------------------------------------------------------------------------
1 | import { InAppBrowserOptions } from "@ionic-native/in-app-browser";
2 |
3 | export const MORE_INFO_NEEDED = 0;
4 | export const NOT_APPLICABLE = -1;
5 |
6 | // Time for seconds to show task result view before auto navigating to next task
7 | export const TIMER_FOR_NEXT_TASK = 5000;
8 |
9 | export const IAB_OPTIONS: InAppBrowserOptions = {
10 | location: "no",
11 |
12 | // Android Only
13 | zoom: "no",
14 | hardwareback: "no",
15 | shouldPauseOnSuspend: "yes",
16 |
17 | // iOS Only
18 | closebuttoncaption: "Close"
19 | };
20 |
--------------------------------------------------------------------------------
/src/features/connections/pages/connection-details/connection-details.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SharedModule } from "../../../../shared/shared.module";
4 | import { ConnectionDetailsPage } from "./connection-details.component";
5 |
6 | @NgModule({
7 | declarations: [ConnectionDetailsPage],
8 | imports: [IonicPageModule.forChild(ConnectionDetailsPage), SharedModule],
9 | exports: [ConnectionDetailsPage]
10 | })
11 | export class ConnectionDetailsPageModule {}
12 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-item/project-item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | Input,
4 | Output,
5 | EventEmitter,
6 | ChangeDetectionStrategy
7 | } from "@angular/core";
8 | import { Project } from "../../../../models/project";
9 |
10 | @Component({
11 | selector: "app-project-item",
12 | templateUrl: "./project-item.component.html",
13 | changeDetection: ChangeDetectionStrategy.OnPush
14 | })
15 | export class ProjectItemComponent {
16 | @Input() project: Project;
17 | @Output() select = new EventEmitter();
18 | }
19 |
--------------------------------------------------------------------------------
/src/features/settings/settings.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 | import { SharedModule } from "../../shared/shared.module";
3 | import { SettingsService } from "./service/settings.service";
4 |
5 | @NgModule({
6 | imports: [SharedModule],
7 | declarations: [],
8 | exports: [],
9 | entryComponents: [],
10 | providers: []
11 | })
12 | export class SettingsModule {
13 | static forRoot(): ModuleWithProviders {
14 | return {
15 | ngModule: SettingsModule,
16 | providers: [SettingsService]
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/features/scoping/components/counted-votes/counted-votes.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from "@angular/core";
2 | import { Votes } from "../../../../models/task";
3 | import * as _ from "lodash";
4 |
5 | @Component({
6 | selector: "app-counted-votes",
7 | templateUrl: "./counted-votes.component.html"
8 | })
9 | export class CountedVotesComponent implements OnInit {
10 | @Input() votes: Array;
11 |
12 | @Input() participantCount: number;
13 |
14 | constructor() {}
15 |
16 | ngOnInit() {}
17 |
18 | getRangeForEmptyVotes(num) {
19 | return _.range(num);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/features/scoping/components/result-estimate/result-estimate.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Total Estimate
3 |
4 | {{ estimate }}
5 |
6 |
Hours
7 |
8 |
9 |
10 |
11 |
12 |
Total Estimate
13 |
14 | {{ estimate }}
15 |
16 |
Hours
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Specifies intentionally untracked files to ignore when using Git
2 | # http://git-scm.com/docs/gitignore
3 |
4 | *~
5 | *.sw[mnpcod]
6 | *.log
7 | *.tmp
8 | *.tmp.*
9 | log.txt
10 | *.sublime-project
11 | *.sublime-workspace
12 | .vscode/
13 | npm-debug.log*
14 |
15 | .idea/
16 | .sourcemaps/
17 | .sass-cache/
18 | .tmp/
19 | .versions/
20 | coverage/
21 | dist/
22 | node_modules/
23 | tmp/
24 | temp/
25 | hooks/
26 | src/config/
27 | platforms/
28 | plugins/
29 | plugins/android.json
30 | plugins/ios.json
31 | www/
32 | functions/lib
33 | $RECYCLE.BIN/
34 |
35 | .DS_Store
36 | Thumbs.db
37 | UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/functions/src/api/connections/delete-connection.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 | import { firestore } from "../../services";
3 |
4 | export async function deleteConnection(
5 | req: express.Request,
6 | res: express.Response
7 | ) {
8 | const uid = res.locals.user.uid;
9 | const connectionId = req.params.connectionId;
10 | await firestore
11 | .doc(`users/${uid}/connections/${connectionId}`)
12 | .delete()
13 | .then(
14 | () => {
15 | return res.status(201).send();
16 | },
17 | err => {
18 | return res.status(400).json({ message: err });
19 | }
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/functions/src/api/connections/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { addConnection } from "./add-connection";
4 | import { projectsRouter } from "./projects";
5 | import { tasksRouter } from "./tasks";
6 | import { deleteConnection } from "./delete-connection";
7 |
8 | export const connectionsRouter = express.Router({ mergeParams: true });
9 |
10 | connectionsRouter.post("/", addConnection);
11 |
12 | connectionsRouter.delete("/:connectionId", deleteConnection);
13 |
14 | connectionsRouter.use("/:connectionId/projects", projectsRouter);
15 |
16 | connectionsRouter.use("/:connectionId/tasks", tasksRouter);
17 |
--------------------------------------------------------------------------------
/src/features/connections/components/verify-modal/verify-modal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { NavParams, ViewController } from "ionic-angular";
3 |
4 | @Component({
5 | selector: "app-verify-modal",
6 | templateUrl: "./verify-modal.component.html"
7 | })
8 | export class VerifyModalComponent implements OnInit {
9 | connectionData;
10 |
11 | constructor(private params: NavParams, private viewCtrl: ViewController) {}
12 |
13 | ngOnInit() {
14 | this.connectionData = this.params.data.connectionData;
15 | }
16 |
17 | dismiss() {
18 | this.viewCtrl.dismiss();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/models/scoping-session.ts:
--------------------------------------------------------------------------------
1 | import { Task } from "./task";
2 |
3 | export interface ScopingSession {
4 | id: string;
5 | sessionId: string;
6 | projectName: string;
7 | ownerId: string;
8 | connectionId: string;
9 | currentTaskId: string;
10 | numTasks: number;
11 | numScopedTasks: number;
12 | tasks: { [taskId: string]: Task };
13 | participants: { [userId: string]: number };
14 | }
15 |
16 | export enum SessionStatus {
17 | COMPLETE = "Session Completed",
18 | INCOMPLETE = "Session Incomplete"
19 | }
20 |
21 | export interface SessionValidation {
22 | sessionLink: string;
23 | accessCode: string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/models/history-item.ts:
--------------------------------------------------------------------------------
1 | import { ScopingSession, SessionStatus } from "./scoping-session";
2 | import { SessionUserType } from "./user";
3 |
4 | export interface HistoryItem extends Partial {
5 | id?: string;
6 | ownerId: string;
7 | connectionId: string;
8 | sessionId: string;
9 | sessionCode: string;
10 | projectId?: string;
11 | participants: { [uid: string]: number };
12 | }
13 |
14 | export interface HistoryItemOptionEvent {
15 | userType: SessionUserType;
16 | item: HistoryItem;
17 | }
18 |
19 | export interface HistoryItemDetailEvent {
20 | status: SessionStatus;
21 | item: HistoryItem;
22 | }
23 |
--------------------------------------------------------------------------------
/src/features/dashboard/pages/dashboard/dashboard.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 |
4 | import { DashboardPage } from "./dashboard.component";
5 | import { DashboardModule } from "../../dashboard.module";
6 | import { ConnectionsModule } from "../../../connections/connections.module";
7 |
8 | @NgModule({
9 | declarations: [DashboardPage],
10 | imports: [
11 | IonicPageModule.forChild(DashboardPage),
12 | DashboardModule,
13 | ConnectionsModule
14 | ],
15 | exports: [],
16 | entryComponents: []
17 | })
18 | export class DashboardPageModule {}
19 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-project/select-project.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SharedModule } from "../../../../shared/shared.module";
4 | import { SelectProjectPage } from "./select-project.component";
5 | import { ConnectionsModule } from "../../connections.module";
6 |
7 | @NgModule({
8 | declarations: [SelectProjectPage],
9 | imports: [
10 | IonicPageModule.forChild(SelectProjectPage),
11 | SharedModule,
12 | ConnectionsModule
13 | ],
14 | exports: [SelectProjectPage]
15 | })
16 | export class SelectProjectPageModule {}
17 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-results/session-results.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SessionResultsPage } from "./session-results.component";
4 | import { ScopingModule } from "../../scoping.module";
5 | import { SharedModule } from "../../../../shared/shared.module";
6 |
7 | @NgModule({
8 | declarations: [SessionResultsPage],
9 | imports: [
10 | IonicPageModule.forChild(SessionResultsPage),
11 | ScopingModule,
12 | SharedModule
13 | ],
14 | exports: [],
15 | entryComponents: []
16 | })
17 | export class SessionResultsPageModule {}
18 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-scoping/session-scoping.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SessionScopingPage } from "./session-scoping.component";
4 | import { ScopingModule } from "../../scoping.module";
5 | import { SharedModule } from "../../../../shared/shared.module";
6 |
7 | @NgModule({
8 | declarations: [SessionScopingPage],
9 | imports: [
10 | IonicPageModule.forChild(SessionScopingPage),
11 | ScopingModule,
12 | SharedModule
13 | ],
14 | exports: [],
15 | entryComponents: []
16 | })
17 | export class SessionScopingPageModule {}
18 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-task-list/select-task-list.component.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { IonicPageModule } from "ionic-angular";
3 | import { SharedModule } from "../../../../shared/shared.module";
4 | import { SelectTaskListPage } from "./select-task-list.component";
5 | import { ConnectionsModule } from "../../connections.module";
6 |
7 | @NgModule({
8 | declarations: [SelectTaskListPage],
9 | imports: [
10 | IonicPageModule.forChild(SelectTaskListPage),
11 | SharedModule,
12 | ConnectionsModule
13 | ],
14 | exports: [SelectTaskListPage]
15 | })
16 | export class SelectTaskListPageModule {}
17 |
--------------------------------------------------------------------------------
/functions/src/api/session-links/delete-session.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { sessionService } from "../../services";
4 |
5 | export async function deleteSession(
6 | req: express.Request,
7 | res: express.Response
8 | ) {
9 | const uid = res.locals.user.uid;
10 | const sessionLink = req.params.sessionLink;
11 |
12 | if (!sessionLink) {
13 | return res.status(400).json({ message: "Session Link is required." });
14 | }
15 |
16 | try {
17 | await sessionService.deleteSession(uid, sessionLink);
18 | } catch (error) {
19 | return res.status(400).json({ message: error });
20 | }
21 |
22 | return res.sendStatus(204);
23 | }
24 |
--------------------------------------------------------------------------------
/src/features/connections/components/verify-modal/verify-modal.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TOKEN SUCCESS!
6 |
7 |
8 | {{ connectionData.externalData.company }}
9 | Connection Type: {{ connectionData.type }}
10 | Account Admin Name: {{ connectionData.externalData.name }}
11 |
12 |
13 |
14 | CONFIRM
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-task-list/select-task-list.component.scss:
--------------------------------------------------------------------------------
1 | app-select-task-list {
2 |
3 |
4 | ion-list.list {
5 | margin: 0;
6 | width: 100%;
7 |
8 | ion-list-header.list-header {
9 |
10 | .icon, h2 {
11 | display: inline-block;
12 | font-weight: bolder;
13 | text-transform: none;
14 | vertical-align: middle;
15 | }
16 |
17 | ion-label.label {
18 | margin-top: 3rem;
19 | }
20 | }
21 |
22 | ion-item.item.item-block .item-inner {
23 | border-bottom: 2px solid $primary-orange;
24 | }
25 | }
26 |
27 | .start-btn {
28 | margin-top: 2rem;
29 |
30 | button {
31 | width: 60%;
32 | }
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/functions/src/services/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from 'firebase-functions';
2 | import * as admin from 'firebase-admin';
3 |
4 | import { EncryptionService } from './encryption.service';
5 | import { TeamworkService } from './teamwork.service';
6 | import { SessionService } from './session.service';
7 |
8 | export const config = functions.config();
9 |
10 | admin.initializeApp(config.firebase);
11 |
12 | export const auth = admin.auth();
13 | export const firestore = admin.firestore();
14 |
15 | export const encryptionService = new EncryptionService();
16 | export const teamworkService = new TeamworkService();
17 | export const sessionService = new SessionService(firestore);
18 |
--------------------------------------------------------------------------------
/functions/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import * as express from "express";
3 | import * as cors from "cors";
4 |
5 | import { idTokenAuth } from "./middleware";
6 | import { apiRouter } from "./api";
7 |
8 | const app = express();
9 | app.disable("X-Powered-By");
10 |
11 | app.use(cors({ origin: true }));
12 |
13 | app.use(idTokenAuth);
14 |
15 | app.use(apiRouter);
16 | app.get("/*", (req: express.Request, res: express.Response) => {
17 | res.status(404).send();
18 | });
19 |
20 | app.get("/fw", (req: express.Request, res: express.Response) => {
21 | res.sendfile("/fw.html");
22 | });
23 |
24 | exports.api = functions.https.onRequest(app);
25 |
--------------------------------------------------------------------------------
/functions/src/api/session-links/refresh-access-code.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { sessionService } from './../../services';
4 |
5 | export async function refreshAccessCode(
6 | req: express.Request,
7 | res: express.Response
8 | ) {
9 | const uid = res.locals.user.uid;
10 | const sessionLink = req.params.sessionLink;
11 |
12 | if (!sessionLink) {
13 | return res.status(400).json({ message: 'Session Link is required.' });
14 | }
15 |
16 | try {
17 | await sessionService.refreshAccessCode(sessionLink, uid);
18 | } catch (error) {
19 | return res.status(400).json({ message: error });
20 | }
21 |
22 | return res.sendStatus(204);
23 | }
24 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-access/session-access.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-project/select-project.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Select Project
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | You have no projects for this specific connection. Please add a project for this connection in Teamwork in order to continue.
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-item/session-history-item.component.scss:
--------------------------------------------------------------------------------
1 | app-session-history-item {
2 | ion-card-header.card-header {
3 | padding: 0;
4 |
5 | h2 {
6 | font-weight: bold;
7 | color: $primary-orange;
8 | margin-left: 0.5rem;
9 | padding: 1.5rem 0;
10 | }
11 |
12 | button.fab {
13 | float: right;
14 | }
15 | }
16 |
17 | ion-card.card {
18 | border-bottom: 2px solid $primary-orange;
19 | box-shadow: none;
20 |
21 | p {
22 | color: $primary-orange;
23 | }
24 | }
25 |
26 | ion-item {
27 | p.cta-text {
28 | text-decoration: underline;
29 | font-weight: bold;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/authentication/authentication-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { Routes, RouterModule } from "@angular/router";
3 |
4 | import { UnAuthGuard } from "../../app/un-auth.guard";
5 | import { LoginPage } from "./pages/login/login.component";
6 |
7 | const routes: Routes = [
8 | {
9 | path: "",
10 | pathMatch: "full",
11 | redirectTo: "/login"
12 | },
13 | {
14 | path: "login",
15 | component: LoginPage,
16 | canActivate: [UnAuthGuard]
17 | }
18 | ];
19 |
20 | @NgModule({
21 | imports: [RouterModule.forChild(routes)],
22 | exports: [RouterModule],
23 | providers: [UnAuthGuard]
24 | })
25 | export class AuthenticationRoutingModule {}
26 |
--------------------------------------------------------------------------------
/src/features/connections/pages/connection-details/connection-details.component.scss:
--------------------------------------------------------------------------------
1 | app-connection-details {
2 |
3 | ion-list.list {
4 | margin: 0;
5 | width: 100%;
6 |
7 | ion-list-header.list-header {
8 |
9 | .icon, h2 {
10 | display: inline-block;
11 | font-weight: bolder;
12 | text-transform: none;
13 | vertical-align: middle;
14 | }
15 |
16 | ion-label.label {
17 | margin-top: 3rem;
18 | }
19 | }
20 |
21 | ion-item.item.item-block .item-inner {
22 | border-bottom: 2px solid $primary-orange;
23 | margin-right: 3rem;
24 | }
25 | }
26 |
27 | .delete-btn {
28 | margin-top: 2rem;
29 |
30 | button {
31 | width: 60%;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/features/scoping/components/counted-votes/counted-votes.component.scss:
--------------------------------------------------------------------------------
1 | app-counted-votes {
2 | .vote-group {
3 | align-items: center;
4 | display: flex;
5 | flex-wrap: wrap;
6 | justify-content: center;
7 | margin: 25px;
8 | text-align: center;
9 |
10 | h2 {
11 | flex-direction: column;
12 | flex-wrap: nowrap;
13 | padding: 0;
14 | }
15 |
16 | .vote {
17 | background: white;
18 | border: 1px solid $primary-orange ;
19 | border-radius: 4px;
20 | flex-direction: row;
21 | font-size: 34px;
22 | font-weight: 500;
23 | margin: 20px 5px 5px 5px;
24 | padding: 10px 25px;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/jest-test.helper.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from "@angular/core/testing";
2 |
3 | type CompilerOptions = Partial<{
4 | providers: any[];
5 | useJit: boolean;
6 | preserveWhitespaces: boolean;
7 | }>;
8 | export type ConfigureFn = (testBed: typeof TestBed) => void;
9 |
10 | export const configureTests = (
11 | configure: ConfigureFn,
12 | compilerOptions: CompilerOptions = {}
13 | ) => {
14 | const compilerConfig: CompilerOptions = {
15 | preserveWhitespaces: false,
16 | ...compilerOptions
17 | };
18 |
19 | const configuredTestBed = TestBed.configureCompiler(compilerConfig);
20 |
21 | configure(configuredTestBed);
22 |
23 | return configuredTestBed.compileComponents().then(() => configuredTestBed);
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 | import { RouterModule } from "@angular/router";
3 | import { RouterStateSerializer } from "@ngrx/router-store";
4 | import { CustomSerializer } from "../store/router.reducer";
5 | import { RouterFacade } from "../store/router.facade";
6 |
7 | @NgModule({
8 | imports: [],
9 | exports: [RouterModule]
10 | })
11 | export class AppRoutingModule {
12 | static forRoot(): ModuleWithProviders {
13 | return {
14 | ngModule: AppRoutingModule,
15 | providers: [
16 | {
17 | provide: RouterStateSerializer,
18 | useClass: CustomSerializer
19 | },
20 | RouterFacade
21 | ]
22 | };
23 | }
24 | }
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "scripts": {
4 | "build": "tsc",
5 | "serve": "npm run build && firebase serve --only functions",
6 | "shell": "npm run build && firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "main": "lib/index.js",
12 | "dependencies": {
13 | "cors": "^2.8.4",
14 | "express": "^4.16.3",
15 | "firebase-admin": "~5.12.1",
16 | "firebase-functions": "^1.1.0",
17 | "lodash": "^4.17.10",
18 | "request": "^2.85.0",
19 | "request-promise-native": "^1.0.5"
20 | },
21 | "devDependencies": {
22 | "typescript": "^2.5.3"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/src/features/scoping/components/vote/vote.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { VoteComponent } from "./vote.component";
4 |
5 | describe("VoteComponent", () => {
6 | let component: VoteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [VoteComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(VoteComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/join-session/join-session.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ChangeDetectionStrategy,
4 | Output,
5 | EventEmitter
6 | } from "@angular/core";
7 |
8 | @Component({
9 | selector: "app-join-session",
10 | templateUrl: "./join-session.component.html",
11 | changeDetection: ChangeDetectionStrategy.OnPush
12 | })
13 | export class JoinSessionComponent {
14 |
15 | @Output() join = new EventEmitter();
16 |
17 | onJoin(sessionCode: string) {
18 | if (sessionCode.includes("juntoscope.com")) {
19 | sessionCode = sessionCode.substring(sessionCode.lastIndexOf("/") + 1);
20 | }
21 | if (sessionCode && sessionCode.length > 3) {
22 | this.join.emit(sessionCode);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-access/session-access.component.scss:
--------------------------------------------------------------------------------
1 | app-session-access {
2 | form {
3 | align-items: center;
4 | display: flex;
5 | flex-direction: column;
6 | height: 100%;
7 | justify-content: center;
8 | width: 100%;
9 |
10 | ion-item.item-block.item[no-lines] {
11 | border: 2px solid $primary-orange;
12 | border-radius: 10px;
13 | height: 8%;
14 | margin-bottom: 1rem;
15 | width: 90%;
16 |
17 | ion-input.input {
18 | color: #000;
19 | font-weight: 400;
20 | }
21 | }
22 |
23 | p {
24 | font-style: italic;
25 | width: 80%;
26 | text-align: center;
27 | }
28 | }
29 |
30 | button.button {
31 | align-items: center;
32 | width: 60%;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/scoping/components/task-card/task-card.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { TaskCardComponent } from "./task-card.component";
4 |
5 | describe("TaskCardComponent", () => {
6 | let component: TaskCardComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [TaskCardComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(TaskCardComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/connections/components/task-list/task-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { TaskListComponent } from "./task-list.component";
4 |
5 | describe("TaskListComponent", () => {
6 | let component: TaskListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [TaskListComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(TaskListComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-list/session-history-list.component.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | You don't have any history yet. Join or start a session to add it to your history
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-item/project-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { ProjectItemComponent } from "./project-item.component";
4 |
5 | describe("ProjectItemComponent", () => {
6 | let component: ProjectItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ProjectItemComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ProjectItemComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-list/project-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { ProjectListComponent } from "./project-list.component";
4 |
5 | describe("ProjectListComponent", () => {
6 | let component: ProjectListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ProjectListComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ProjectListComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/join-session/join-session.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { JoinSessionComponent } from "./join-session.component";
4 |
5 | describe("JoinSessionComponent", () => {
6 | let component: JoinSessionComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [JoinSessionComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(JoinSessionComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-results/session-results.component.scss:
--------------------------------------------------------------------------------
1 | app-session-results {
2 | .session-results-top {
3 | text-align: center;
4 | h1 {
5 | text-transform: uppercase;
6 | }
7 | }
8 |
9 | .tasks-header {
10 | display: grid;
11 | color: white;
12 | grid-template-columns: 0.25fr 1fr;
13 | margin: 10px;
14 | background: #f1652d;
15 | border-radius: 5px;
16 | ion-label.label {
17 | margin: 0;
18 | padding: 4px;
19 | }
20 |
21 | .first {
22 | text-align: center;
23 | }
24 | }
25 |
26 | .tasks-item {
27 | display: grid;
28 | color: #c94313;
29 | grid-template-columns: 0.25fr 1fr;
30 | ion-label.label {
31 | margin: 0;
32 | }
33 |
34 | .first {
35 | text-align: center;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/features/connections/components/instructions/instructions.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { InstructionsComponent } from "./instructions.component";
4 |
5 | describe("InstructionsComponent", () => {
6 | let component: InstructionsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [InstructionsComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(InstructionsComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/counted-votes/counted-votes.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { CountedVotesComponent } from "./counted-votes.component";
4 |
5 | describe("CountedVotesComponent", () => {
6 | let component: CountedVotesComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [CountedVotesComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(CountedVotesComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/select-result/select-result.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SelectResultComponent } from "./select-result.component";
4 |
5 | describe("SelectResultComponent", () => {
6 | let component: SelectResultComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SelectResultComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SelectResultComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": false,
5 | "emitDecoratorMetadata": true,
6 | "experimentalDecorators": true,
7 | "lib": [
8 | "dom",
9 | "es2015"
10 | ],
11 | "module": "es2015",
12 | "moduleResolution": "node",
13 | "sourceMap": true,
14 | "target": "es5"
15 | },
16 | "include": [
17 | "src/**/*.ts"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "src/**/*.spec.ts",
22 | "src/**/__tests__/*.ts"
23 | ],
24 | "baseUrl": "./src/",
25 | "paths": {
26 | "@app/*": ["app/*"],
27 | "@env/*": ["environments/*"],
28 | "@models/*": ["models/*"],
29 | "@test/*": ["test/*"]
30 | },
31 | "compileOnSave": false,
32 | "atom": {
33 | "rewriteTsconfig": false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/connections/pages/add-connection/add-connection.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { AddConnectionComponent } from "./add-connection.component";
4 |
5 | describe("AddConnectionComponent", () => {
6 | let component: AddConnectionComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [AddConnectionComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(AddConnectionComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-project/select-project.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SelectProjectComponent } from "./select-project.component";
4 |
5 | describe("SelectProjectComponent", () => {
6 | let component: SelectProjectComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SelectProjectComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SelectProjectComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-access/session-access.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionAccessComponent } from "./session-access.component";
4 |
5 | describe("SessionAccessComponent", () => {
6 | let component: SessionAccessComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionAccessComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionAccessComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-header/session-header.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionHeaderComponent } from "./session-header.component";
4 |
5 | describe("SessionHeaderComponent", () => {
6 | let component: SessionHeaderComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionHeaderComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionHeaderComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/shared/components/info-modal/info-modal.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { NavParams, ViewController } from "ionic-angular";
3 |
4 | @Component({
5 | selector: "app-info-modal",
6 | templateUrl: "./info-modal.html"
7 | })
8 | export class InfoModalComponent implements OnInit {
9 | title;
10 | text;
11 | label;
12 | callback;
13 |
14 | constructor(private viewCtrl: ViewController, private params: NavParams) {}
15 |
16 | ngOnInit() {
17 | this.title = this.params.data.title;
18 | this.text = this.params.data.text;
19 | this.label = this.params.data.label;
20 | this.callback = this.params.data.callback;
21 | }
22 |
23 | closeModal() {
24 | this.viewCtrl.dismiss();
25 | }
26 |
27 | dismiss() {
28 | this.viewCtrl.dismiss();
29 | this.callback();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-results/session-results.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionResultsComponent } from "./session-results.component";
4 |
5 | describe("SessionResultsComponent", () => {
6 | let component: SessionResultsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionResultsComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionResultsComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-scoping/session-scoping.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionScopingComponent } from "./session-scoping.component";
4 |
5 | describe("SessionScopingComponent", () => {
6 | let component: SessionScopingComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionScopingComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionScopingComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-task-list/select-task-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SelectTaskListComponent } from "./select-task-list.component";
4 |
5 | describe("SelectTaskListComponent", () => {
6 | let component: SelectTaskListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SelectTaskListComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SelectTaskListComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/result-estimate/result-estimate.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { ResultEstimateComponent } from "./result-estimate.component";
4 |
5 | describe("ResultEstimateComponent", () => {
6 | let component: ResultEstimateComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ResultEstimateComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ResultEstimateComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/store/router.actions.ts:
--------------------------------------------------------------------------------
1 | import { NavigationExtras, Params } from "@angular/router";
2 |
3 | import { Action } from "@ngrx/store";
4 |
5 | export interface NavigationOptions {
6 | path: any[];
7 | query?: Params;
8 | extras?: NavigationExtras;
9 | }
10 |
11 | export enum RouterActionTypes {
12 | GO = "[Router] Go",
13 | BACK = "[Router] Back",
14 | FORWARD = "[Router] Forward"
15 | }
16 |
17 | export class GoAction implements Action {
18 | readonly type = RouterActionTypes.GO;
19 | constructor(public payload: NavigationOptions) {}
20 | }
21 |
22 | export class BackAction implements Action {
23 | readonly type = RouterActionTypes.BACK;
24 | }
25 |
26 | export class ForwardAction implements Action {
27 | readonly type = RouterActionTypes.FORWARD;
28 | }
29 |
30 | export type RouterActions = GoAction | BackAction | ForwardAction;
31 |
--------------------------------------------------------------------------------
/functions/src/api/session-links/decode-session-link.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { sessionService } from "../../services";
4 |
5 | export async function decodeSessionLink(
6 | req: express.Request,
7 | res: express.Response
8 | ) {
9 | const uid = res.locals.user.uid;
10 | const sessionLink = req.params.sessionLink;
11 | const accessCode = req.query.accessCode;
12 |
13 | if (!sessionLink) {
14 | return res.status(400).json({ message: "Session Link is required." });
15 | }
16 |
17 | if (!accessCode) {
18 | return res.status(400).json({ message: "Access Code is required." });
19 | }
20 |
21 | try {
22 | await sessionService.validateSession(sessionLink, accessCode, uid);
23 | } catch (error) {
24 | return res.status(400).json({ message: error });
25 | }
26 |
27 | return res.send(204);
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/connections/components/connection-list/connection-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { ConnectionListComponent } from "./connection-list.component";
4 |
5 | describe("ConnectionListComponent", () => {
6 | let component: ConnectionListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ConnectionListComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ConnectionListComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check out https://googlechromelabs.github.io/sw-toolbox/ for
3 | * more info on how to use sw-toolbox to custom configure your service worker.
4 | */
5 |
6 | 'use strict';
7 | importScripts('./build/sw-toolbox.js');
8 |
9 | self.toolbox.options.cache = {
10 | name: 'ionic-cache',
11 | };
12 |
13 | // pre-cache our key assets
14 | self.toolbox.precache([
15 | './build/main.js',
16 | './build/vendor.js',
17 | './build/main.css',
18 | './build/polyfills.js',
19 | 'index.html',
20 | 'manifest.json',
21 | ]);
22 |
23 | // dynamically cache any other local assets
24 | self.toolbox.router.any('/*', self.toolbox.fastest);
25 |
26 | // for any other requests go to the network, cache,
27 | // and then only use that cached resource if your user goes offline
28 | self.toolbox.router.default = self.toolbox.networkFirst;
29 |
--------------------------------------------------------------------------------
/src/features/connections/pages/connection-details/connection-details.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { ConnectionDetailsComponent } from "./connection-details.component";
4 |
5 | describe("ConnectionDetailsComponent", () => {
6 | let component: ConnectionDetailsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ConnectionDetailsComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ConnectionDetailsComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/scoping/components/vote/vote.component.scss:
--------------------------------------------------------------------------------
1 | app-vote {
2 | .vote-form-container {
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: center;
6 | margin: 15px;
7 |
8 | form {
9 | display: flex;
10 | flex-direction: row;
11 |
12 | ion-input {
13 | border-radius: 6px;
14 | -webkit-box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75);
15 | -moz-box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75);
16 | box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75);
17 | background: #f2f2f2;
18 | margin-right: 10px;
19 |
20 | input {
21 | font-size: 16px;
22 | font-weight: 500;
23 | margin-top: 17px;
24 | }
25 |
26 | input::placeholder {
27 | color: #999999;
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-item/session-history-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionHistoryItemComponent } from "./session-history-item.component";
4 |
5 | describe("SessionHistoryItemComponent", () => {
6 | let component: SessionHistoryItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionHistoryItemComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionHistoryItemComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-list/session-history-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2 |
3 | import { SessionHistoryListComponent } from "./session-history-list.component";
4 |
5 | describe("SessionHistoryListComponent", () => {
6 | let component: SessionHistoryListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [SessionHistoryListComponent]
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(SessionHistoryListComponent);
17 | component = fixture.componentInstance;
18 | fixture.detectChanges();
19 | });
20 |
21 | it("should create", () => {
22 | expect(component).toBeTruthy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/authentication/authentication.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 |
3 | import { StoreModule } from "@ngrx/store";
4 | import { EffectsModule } from "@ngrx/effects";
5 |
6 | import { SharedModule } from "../../shared/shared.module";
7 | import { authReducer } from "./store/auth.reducer";
8 | import { AuthService } from "./services/auth.service";
9 | import { AuthFacade } from "./store/auth.facade";
10 |
11 | @NgModule({
12 | imports: [
13 | SharedModule,
14 | StoreModule.forFeature("auth", authReducer),
15 | EffectsModule.forFeature([AuthFacade])
16 | ],
17 | declarations: [],
18 | entryComponents: []
19 | })
20 | export class AuthenticationModule {
21 | static forRoot(): ModuleWithProviders {
22 | return {
23 | ngModule: AuthenticationModule,
24 | providers: [AuthService, AuthFacade]
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/features/connections/components/project-list/project-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
2 | import { Project } from "../../../../models/project";
3 | import { Connection } from "../../../../models/connection";
4 | import { ConnectionFacade } from "../../store/connection.facade";
5 |
6 | @Component({
7 | selector: "app-project-list",
8 | templateUrl: "./project-list.component.html"
9 | })
10 | export class ProjectListComponent implements OnInit {
11 | @Input() projects: Project[];
12 | @Output() select = new EventEmitter();
13 |
14 | connection: Connection;
15 |
16 | constructor(private connectionFacade: ConnectionFacade) {}
17 |
18 | ngOnInit() {
19 | this.connectionFacade.selectedConnection$
20 | .subscribe(con => {
21 | this.connection = con;
22 | })
23 | .unsubscribe();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-item/session-history-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ item.projectName }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | - {{ item.tasks && item.tasks[item.currentTaskId].name }}
17 |
18 |
19 |
20 |
21 |
{{ time | date: 'MM/dd/yyyy' }}
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/features/authentication/pages/login/login.component.scss:
--------------------------------------------------------------------------------
1 | app-login {
2 | .container {
3 | align-items: center;
4 | display: flex;
5 | flex-direction: column;
6 | height: 100%;
7 | justify-content: center;
8 | width: 100%;
9 |
10 | p {
11 | width: 70%;
12 | font-weight: 600;
13 | }
14 |
15 | button.button {
16 | font-weight: 600;
17 | margin: 0.75rem;
18 | width: 60%;
19 |
20 | &:disabled {
21 | background-color: white;
22 | border: 2px solid $primary-orange;
23 | color: $primary-orange;
24 | }
25 | }
26 |
27 | ion-item.item.item-block .item-inner {
28 | border: none;
29 | }
30 |
31 | .item.item-md .checkbox-md {
32 | margin: 0px 10px 0px 0px;
33 | }
34 |
35 | }
36 |
37 | .terms {
38 | flex-direction: row;
39 |
40 | a {
41 | font-weight: 600;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/store/app.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 |
3 | import { Store, select } from "@ngrx/store";
4 |
5 | import { AppState, AppQuery } from "./app.reducer";
6 | import { filter } from "rxjs/operators";
7 |
8 | @Injectable()
9 | export class AppFacade {
10 | /*
11 | * Observable Store Queries
12 | */
13 |
14 | authRedirect$ = this.store.pipe(select(AppQuery.selectAuthRedirect));
15 |
16 | uid$ = this.store.pipe(
17 | select(AppQuery.selectUid),
18 | filter(exists => !!exists)
19 | );
20 |
21 | uidDocPath$ = this.store.pipe(
22 | select(AppQuery.selectUidDocPath),
23 | filter(exists => !!exists)
24 | );
25 |
26 | connectionsClnPath$ = this.store.pipe(
27 | select(AppQuery.selectConnectionsClnPath),
28 | filter(exists => !!exists)
29 | );
30 |
31 | /*
32 | * Module-level Effects
33 | */
34 |
35 | constructor(private store: Store) {}
36 |
37 | /*
38 | * Action Creators
39 | */
40 | }
41 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-detail-modal/session-detail-modal.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Session Url
4 |
5 |
https://juntoscope.com/{{accountData.item.sessionCode}}
6 |
7 |
8 |
Access Code
9 |
10 |
11 | {{ letter }}
12 |
13 |
14 |
* Code Expired
15 |
16 |
Contact moderator to receive new code
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/features/scoping/components/select-result/select-result.component.html:
--------------------------------------------------------------------------------
1 |
2 | Results: JuntoScope has determined the average and maximum scope per all the
3 | votes submitted in this session. Please select one of the following values,
4 | or enter in a custom value to be recorded as the final scope for this task.
5 |
6 |
7 |
8 |
9 |
10 |
Average
11 |
12 |
13 |
14 |
Max
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/src/features/scoping/components/select-result/select-result.component.scss:
--------------------------------------------------------------------------------
1 | app-select-result {
2 | .container {
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: center;
6 | text-align: center;
7 | border-bottom: 2px solid;
8 | .result {
9 | margin: 25px 25px 15px 25px;
10 |
11 | button {
12 | height: 60px;
13 | width: 70px;
14 | margin: 0px;
15 | border-radius: 4px;
16 | .button-inner {
17 | font-size: 35px;
18 | font-weight: bolder;
19 | }
20 | }
21 |
22 | ion-input.input {
23 | border: 1px solid;
24 | border-radius: 4px;
25 | height: 60px;
26 | width: 70px;
27 | input {
28 | height: calc(100% - 8px - 16px);
29 | font-size: 35px;
30 | font-weight: bolder;
31 | }
32 | }
33 |
34 | .selected {
35 | background-color: $primary-orange;
36 | color: white;
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/features/scoping/components/vote/vote.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Output, EventEmitter } from "@angular/core";
2 | import {
3 | FormGroup,
4 | FormBuilder,
5 | Validators,
6 | FormControl
7 | } from "@angular/forms";
8 |
9 | @Component({
10 | selector: "app-vote",
11 | templateUrl: "./vote.component.html"
12 | })
13 | export class VoteComponent implements OnInit {
14 | @Output() vote = new EventEmitter();
15 |
16 | voteForm: FormGroup;
17 | error: string;
18 |
19 | constructor(private fb: FormBuilder) {}
20 |
21 | ngOnInit() {
22 | this.createForm();
23 | }
24 |
25 | createForm() {
26 | this.voteForm = this.fb.group({
27 | estimate: ["", [Validators.required, this.isValid]]
28 | });
29 | }
30 |
31 | sendVote() {
32 | this.vote.emit(this.voteForm.value.estimate);
33 | }
34 |
35 | isValid(control: FormControl): any {
36 | if (control.value < 0.5) {
37 | return { invalid_quantity: true };
38 | }
39 |
40 | return null;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/store/router.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Params, RouterStateSnapshot } from "@angular/router";
2 | import { RouterStateSerializer, RouterReducerState } from "@ngrx/router-store";
3 |
4 | interface SerializedRouterState {
5 | url: string;
6 | params: Params;
7 | queryParams: Params;
8 | }
9 |
10 | export class CustomSerializer
11 | implements RouterStateSerializer {
12 | serialize(snapshot: RouterStateSnapshot): SerializedRouterState {
13 | const { url, root: { queryParams } } = snapshot;
14 | let { root: route } = snapshot;
15 |
16 | while (route.firstChild) {
17 | route = route.firstChild;
18 | }
19 |
20 | const { params } = route;
21 |
22 | return { url, params, queryParams };
23 | }
24 | }
25 |
26 | export type RouterState = RouterReducerState;
27 |
28 | export const initialRouterState: RouterState = {
29 | state: { url: null, params: null, queryParams: null },
30 | navigationId: 0
31 | };
32 |
33 | export { routerReducer } from "@ngrx/router-store";
34 |
--------------------------------------------------------------------------------
/src/features/dashboard/pages/dashboard/dashboard.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | JuntoScope
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/store/router.actions.spec.ts:
--------------------------------------------------------------------------------
1 | import * as RouterActions from "./router.actions";
2 |
3 | describe("Router Actions", () => {
4 | describe("Go Action", () => {
5 | it("should create action", () => {
6 | const payload: RouterActions.NavigationOptions = { path: ["/login"] };
7 | const action = new RouterActions.GoAction(payload);
8 |
9 | expect(action).toEqual({
10 | type: RouterActions.RouterActionTypes.GO,
11 | payload
12 | });
13 | });
14 | });
15 |
16 | describe("Back Action", () => {
17 | it("should create action", () => {
18 | const action = new RouterActions.BackAction();
19 |
20 | expect(action).toEqual({
21 | type: RouterActions.RouterActionTypes.BACK
22 | });
23 | });
24 | });
25 |
26 | describe("Forward Action", () => {
27 | it("should create action", () => {
28 | const action = new RouterActions.ForwardAction();
29 |
30 | expect(action).toEqual({
31 | type: RouterActions.RouterActionTypes.FORWARD
32 | });
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 | Share Scope Link
9 |
10 |
11 |
12 |
13 | {{connectionName}}
14 | {{projectName}}
15 |
16 |
17 |
Session Url
18 |
19 |
https://juntoscope.com/{{sessionUrl}}
20 |
21 |
22 |
Access Code
23 |
24 |
25 | {{ letter }}
26 |
27 |
28 |
29 |
30 |
33 |
34 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-scoping/session-scoping.component.scss:
--------------------------------------------------------------------------------
1 | app-session-scoping {
2 |
3 | .loading {
4 | display: flex;
5 | justify-content: center;
6 | top: 400px;
7 | }
8 |
9 | .session-info-container {
10 | margin: 30px;
11 | }
12 |
13 | .session-votes-counted {
14 | display: flex;
15 | justify-content: center;
16 | background: $primary-orange;
17 | p {
18 | color: white;
19 | }
20 | }
21 |
22 | ion-list.list {
23 | ion-item.item {
24 | border-bottom: 1px solid $primary-orange;
25 | .item-inner {
26 | border-bottom: none;
27 | }
28 | &:last-child {
29 | border-bottom: none;
30 | }
31 | }
32 | }
33 |
34 | ion-footer.footer {
35 | position: initial;
36 | ion-toolbar.toolbar {
37 | .toolbar-background {
38 | background: white;
39 | }
40 | .toolbar-content {
41 | display: flex;
42 | justify-content: center;
43 | }
44 | button.button {
45 | width: 50%;
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/features/authentication/pages/login/login.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | First, we need a way of identifying your participation.
5 | Please sign in with:
6 |
7 |
8 |
11 |
12 |
15 |
16 |
19 |
20 |
26 |
27 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/functions/src/middleware/id-token-auth.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { auth } from './../services';
4 |
5 | /**
6 | * Middleware function that will intercept the id token
7 | * from Authorization Headers of all incoming requests.
8 | * And will validate the token assigning the user object
9 | * to the locals response object.
10 | */
11 | export function idTokenAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
12 | const authHeader = req.headers && req.headers.authorization;
13 | const headerValue = authHeader
14 | ? Array.isArray(authHeader) ? authHeader[0] : authHeader
15 | : '';
16 |
17 | if (!headerValue || !headerValue.startsWith('Bearer ')) {
18 | res.status(403).send('Unauthorized');
19 | return;
20 | }
21 |
22 | const idToken = headerValue.split('Bearer ')[1];
23 |
24 | auth.verifyIdToken(idToken)
25 | .then(decodedIdToken => {
26 | res.locals.user = decodedIdToken;
27 | return next();
28 | })
29 | .catch(error => {
30 | res.status(403).send('Unauthorized');
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/jest-global-mocks.ts:
--------------------------------------------------------------------------------
1 | global["CSS"] = null;
2 |
3 | const mock = () => {
4 | let storage = {};
5 | return {
6 | getItem: key => (key in storage ? storage[key] : null),
7 | setItem: (key, value) => (storage[key] = value || ""),
8 | removeItem: key => delete storage[key],
9 | clear: () => (storage = {})
10 | };
11 | };
12 |
13 | Object.defineProperty(window, "localStorage", { value: mock() });
14 | Object.defineProperty(window, "sessionStorage", { value: mock() });
15 | Object.defineProperty(document, "doctype", {
16 | value: ""
17 | });
18 | Object.defineProperty(window, "getComputedStyle", {
19 | value: () => {
20 | return {
21 | display: "none",
22 | appearance: ["-webkit-appearance"]
23 | };
24 | }
25 | });
26 | /**
27 | * ISSUE: https://github.com/angular/material2/issues/7101
28 | * Workaround for JSDOM missing transform property
29 | */
30 | Object.defineProperty(document.body.style, "transform", {
31 | value: () => {
32 | return {
33 | enumerable: true,
34 | configurable: true
35 | };
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/src/features/connections/pages/add-connection/add-connection.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | New Connection
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Connect to Teamwork
11 |
12 |
13 | NOTICE
14 |
15 | This is made specifically for Teamwork.
16 |
17 | If you are not using Teamwork, we recommend trying it!
18 |
19 |
20 |
21 | Connections should only be made by Teamwork users with Administrator privileges in order to successfully deploy
22 | scoping sessions and assign scoped times to tasks.
23 |
24 |
25 |
26 | Register here and use the code "juntoscope" for a 10% discount!
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/features/scoping/components/result-estimate/result-estimate.component.scss:
--------------------------------------------------------------------------------
1 | app-result-estimate {
2 | .result-estimate-container {
3 | align-items: center;
4 | display: flex;
5 | flex-direction: column;
6 |
7 | h2,
8 | h3 {
9 | text-transform: uppercase;
10 | margin-bottom: 25px;
11 | }
12 |
13 | p {
14 | font-size: 16px;
15 | text-transform: capitalize;
16 | margin: 10px 0 20px 0;
17 | }
18 |
19 |
20 | .box-estimate {
21 | background: white;
22 | border: 1px solid $primary-orange;
23 | border-radius: 4px;
24 | font-size: 34px;
25 | font-weight: bold;
26 | margin: 0;
27 | padding: 10px 25px;
28 | position: initial;
29 | }
30 |
31 |
32 | .final-estimate-container {
33 | margin: 10px;
34 | display: flex;
35 | flex-direction: column;
36 | align-items: center;
37 | text-align: center;
38 | background: rgba(255, 200, 148, 0.5);
39 | width: 85%;
40 | border-radius: 6px;
41 | }
42 | }
43 |
44 | .border {
45 | border-bottom: 2px solid $primary-orange;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-task-list/select-task-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Select Task List
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ projectName }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | You have no task lists for this specific project!
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import {
3 | CanActivate,
4 | ActivatedRouteSnapshot,
5 | RouterStateSnapshot
6 | } from "@angular/router";
7 |
8 | import { map, tap, filter } from "rxjs/operators";
9 |
10 | import { RouterFacade } from "../store/router.facade";
11 | import { AuthFacade } from "../features/authentication/store/auth.facade";
12 | import { AuthUiState } from "../features/authentication/store/auth.reducer";
13 |
14 | @Injectable()
15 | export class AuthGuard implements CanActivate {
16 | constructor(
17 | private routerFacade: RouterFacade,
18 | private authFacade: AuthFacade
19 | ) {}
20 |
21 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
22 | return this.authFacade.uiState$.pipe(
23 | filter(uiState => uiState !== AuthUiState.LOADING),
24 | map(uiState => uiState === AuthUiState.AUTHENTICATED),
25 | tap(isAuth => {
26 | if (!isAuth) {
27 | this.routerFacade.navigate({
28 | path: ["/login"],
29 | query: { returnUrl: state.url }
30 | });
31 | }
32 | })
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/shared/loading.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import { Loading, LoadingController } from "ionic-angular";
3 |
4 | @Injectable()
5 | export class LoadingService {
6 | loading: Loading;
7 | isLoading = false;
8 |
9 | constructor(private loadingCtrl: LoadingController) {}
10 |
11 | // hide loading object and create to show again in view
12 | hide() {
13 | if (this.loading) {
14 | this.loading.dismiss().then(() => {
15 | this.loading = this.loadingCtrl.create({
16 | spinner: "crescent",
17 | cssClass: "custom-loading"
18 | });
19 | });
20 | }
21 | }
22 |
23 | // dismiss loading object completely from view
24 | dismiss() {
25 | if (this.isLoading) {
26 | this.loading.dismiss().catch(() => {});
27 | this.isLoading = false;
28 | }
29 | }
30 |
31 | // show loading object in view
32 | present() {
33 | this.isLoading = true;
34 |
35 | this.loading = this.loadingCtrl.create({
36 | spinner: "crescent",
37 | cssClass: "custom-loading"
38 | });
39 |
40 | if (!this.isLoading) {
41 | this.loading.present();
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-list/session-history-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ChangeDetectionStrategy,
4 | Input,
5 | Output,
6 | EventEmitter
7 | } from "@angular/core";
8 | import {
9 | HistoryItem,
10 | HistoryItemOptionEvent,
11 | HistoryItemDetailEvent
12 | } from "../../../../models/history-item";
13 | import { SessionUserType } from "../../../../models/user";
14 | import { SessionStatus } from "../../../../models/scoping-session";
15 |
16 | @Component({
17 | selector: "app-session-history-list",
18 | templateUrl: "./session-history-list.component.html",
19 | changeDetection: ChangeDetectionStrategy.OnPush
20 | })
21 | export class SessionHistoryListComponent {
22 | @Input() items: HistoryItem[];
23 | @Input() uid: string;
24 |
25 | @Output() options = new EventEmitter();
26 | @Output() detail = new EventEmitter();
27 |
28 | handleOptionClick(userType: SessionUserType, item: HistoryItem) {
29 | this.options.emit({ userType, item });
30 | }
31 |
32 | handleDetailClick(status: SessionStatus, item: HistoryItem) {
33 | this.detail.emit({ status, item });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/connections/pages/connection-details/connection-details.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Connection Detail
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ connection.externalData.company }} - {{ connection.type | titlecase }}
16 |
17 |
18 |
19 |
20 | Account: {{ connection.type | titlecase }}
21 |
22 |
23 | Name: {{ connection.externalData.name }}
24 |
25 |
26 | Company: {{ connection.externalData.company }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/shared/pipes/object-iterators.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from "@angular/core";
2 |
3 | @Pipe({ name: "objKeys" })
4 | export class ObjectKeysPipe implements PipeTransform {
5 | transform(obj: Object): Array {
6 | const transformed = [];
7 |
8 | for (const key in obj) {
9 | if (obj.hasOwnProperty(key)) {
10 | transformed.push(key);
11 | }
12 | }
13 |
14 | return transformed;
15 | }
16 | }
17 |
18 | @Pipe({ name: "objValues" })
19 | export class ObjectValuesPipe implements PipeTransform {
20 | transform(obj: Object): Array {
21 | const transformed = [];
22 |
23 | for (const key in obj) {
24 | if (obj.hasOwnProperty(key)) {
25 | transformed.push(obj[key]);
26 | }
27 | }
28 |
29 | return transformed;
30 | }
31 | }
32 |
33 | @Pipe({ name: "objKeyValue" })
34 | export class ObjectKeyValuePipe implements PipeTransform {
35 | transform(obj: Object): Array<{ key: string; value: any }> {
36 | const transformed = [];
37 |
38 | for (const key in obj) {
39 | if (obj.hasOwnProperty(key)) {
40 | transformed.push({ key, value: obj[key] });
41 | }
42 | }
43 |
44 | return transformed;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/get-projects.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { firestore, teamworkService, encryptionService } from './../../../services';
3 |
4 | export async function getProjects(req: express.Request, res: express.Response) {
5 | const uid = res.locals.user.uid;
6 | const connectionId = req.params.connectionId;
7 |
8 | let connectionRef;
9 | try {
10 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get();
11 | } catch(error) {
12 | return res.status(400).json({ message: 'Error getting connection' });
13 | }
14 |
15 | const connection = connectionRef.data();
16 |
17 | switch (connection.type.toLowerCase()) {
18 | case 'teamwork': {
19 |
20 | let teamworkResponse;
21 | try {
22 | teamworkResponse = await teamworkService.getProjects(encryptionService.decrypt(connection.token), connection.externalData.baseUrl);
23 | } catch(error) {
24 | return res.status(400).json({ message: error.message });
25 | }
26 |
27 | return res.json(teamworkResponse);
28 | }
29 |
30 | default: {
31 | return res.status(400).json({ message: 'Unknown Connection Type' });
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import "zone.js/dist/long-stack-trace-zone";
4 | import "zone.js/dist/proxy.js";
5 | import "zone.js/dist/sync-test";
6 | import "zone.js/dist/jasmine-patch";
7 | import "zone.js/dist/async-test";
8 | import "zone.js/dist/fake-async-test";
9 |
10 | import { getTestBed } from "@angular/core/testing";
11 | import {
12 | BrowserDynamicTestingModule,
13 | platformBrowserDynamicTesting
14 | } from "@angular/platform-browser-dynamic/testing";
15 |
16 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
17 | declare var __karma__: any;
18 | declare var require: any;
19 |
20 | // Prevent Karma from running prematurely.
21 | __karma__.loaded = function(): void {
22 | // noop
23 | };
24 |
25 | // First, initialize the Angular testing environment.
26 | getTestBed().initTestEnvironment(
27 | BrowserDynamicTestingModule,
28 | platformBrowserDynamicTesting()
29 | );
30 | // Then we find all the tests.
31 | const context: any = require.context("./", true, /\.spec\.ts$/);
32 | // And load the modules.
33 | context.keys().map(context);
34 | // Finally, start Karma to run the tests.
35 | __karma__.start();
36 |
--------------------------------------------------------------------------------
/functions/src/api/connections/tasks/get-task.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { firestore, teamworkService, encryptionService } from './../../../services';
3 |
4 | export async function getTask(req: express.Request, res: express.Response) {
5 | const uid = res.locals.user.uid;
6 | const connectionId = req.params.connectionId;
7 | const taskId = req.params.taskId;
8 |
9 | let connectionRef;
10 | try {
11 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get();
12 | } catch(error) {
13 | return res.status(400).json({ message: 'Error getting connection' });
14 | }
15 |
16 | const connection = connectionRef.data();
17 |
18 | switch (connection.type.toLowerCase()) {
19 | case 'teamwork': {
20 |
21 | let teamworkResponse;
22 | try {
23 | teamworkResponse = await teamworkService.getTask(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, taskId);
24 | } catch(error) {
25 | return res.status(400).json({ message: error.message });
26 | }
27 |
28 | return res.json(teamworkResponse);
29 | }
30 |
31 | default: {
32 | return res.status(400).json({ message: 'Unknown Connection Type' });
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/connections/components/verify-modal/verify-modal.component.scss:
--------------------------------------------------------------------------------
1 | app-verify-modal {
2 | background: rgba(0, 0, 0, 0.5);
3 | width: 100%;
4 | height: 50%;
5 | ion-content.content {
6 | transform: translateY(50%);
7 | border-radius: 2rem;
8 | height: 230px;
9 | width: calc(100% - 7rem);
10 | margin: 3.5rem;
11 |
12 | display: flex;
13 | justify-content: center;
14 |
15 | ion-list.list {
16 | flex-direction: column;
17 | margin-top: 8px;
18 | margin-bottom: 8px;
19 |
20 | ion-list-header.list-header {
21 | margin: 0;
22 | border-radius: 2rem;
23 | h2 {
24 | font-weight: bolder;
25 | }
26 | }
27 |
28 | ion-label.label {
29 | margin: 10px 0 10px 0;
30 | &.title {
31 | font-weight: bold;
32 | font-size: 2rem;
33 | }
34 | }
35 |
36 | ion-item.item {
37 | border-bottom-left-radius: 2rem;
38 | border-bottom-right-radius: 2rem;
39 | border-top: 2px solid $primary-orange;
40 | text-align: center;
41 | margin-top: 3rem;
42 | ion-label.label {
43 | font-weight: bold;
44 | font-size: 2rem;
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/functions/src/services/encryption.service.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from "crypto";
2 | const functions = require("firebase-functions");
3 |
4 | export class EncryptionService {
5 | private algorithm = "aes-256-cbc";
6 | // Must be 32 characters long
7 |
8 | private SECRET_KEY = functions.config().encryption.secret;
9 |
10 | encrypt(text: string) {
11 | const iv = crypto.randomBytes(16); // Must be 16 bytes long for AES encryption
12 | const cipher = crypto.createCipheriv(
13 | this.algorithm,
14 | new Buffer(this.SECRET_KEY),
15 | iv
16 | );
17 |
18 | let encrypted = cipher.update(text, "utf8");
19 | encrypted = Buffer.concat([encrypted, cipher.final()]);
20 |
21 | return `${iv.toString("hex")}:${encrypted.toString("hex")}`;
22 | }
23 |
24 | decrypt(text: string) {
25 | const parts = text.split(":");
26 | const iv = new Buffer(parts.shift(), "hex");
27 | const encryptedText = new Buffer(parts.join(":"), "hex");
28 | const decipher = crypto.createDecipheriv(
29 | this.algorithm,
30 | new Buffer(this.SECRET_KEY),
31 | iv
32 | );
33 |
34 | let decrypted = decipher.update(encryptedText);
35 | decrypted = Buffer.concat([decrypted, decipher.final()]);
36 |
37 | return decrypted.toString();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/tasklists/get-tasks.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { firestore, teamworkService, encryptionService } from './../../../../services';
3 |
4 | export async function getTasks(req: express.Request, res: express.Response) {
5 | const uid = res.locals.user.uid;
6 | const connectionId = req.params.connectionId;
7 | const tasklistId = req.params.tasklistId;
8 |
9 | let connectionRef;
10 | try {
11 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get();
12 | } catch(error) {
13 | return res.status(400).json({ message: 'Error getting connection' });
14 | }
15 |
16 | const connection = connectionRef.data();
17 |
18 | switch (connection.type.toLowerCase()) {
19 | case 'teamwork': {
20 |
21 | let teamworkResponse;
22 | try {
23 | teamworkResponse = await teamworkService.getTasks(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, tasklistId);
24 | } catch(error) {
25 | return res.status(400).json({ message: error.message });
26 | }
27 |
28 | return res.json(teamworkResponse);
29 | }
30 |
31 | default: {
32 | return res.status(400).json({ message: 'Unknown Connection Type' });
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/un-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import {
3 | CanActivate,
4 | ActivatedRouteSnapshot,
5 | RouterStateSnapshot
6 | } from "@angular/router";
7 |
8 | import { map, tap, filter, switchMap } from "rxjs/operators";
9 |
10 | import { RouterFacade } from "../store/router.facade";
11 | import { AuthFacade } from "../features/authentication/store/auth.facade";
12 | import { AuthUiState } from "../features/authentication/store/auth.reducer";
13 | import { AppFacade } from "../store/app.facade";
14 |
15 | @Injectable()
16 | export class UnAuthGuard implements CanActivate {
17 | constructor(
18 | private appFacade: AppFacade,
19 | private routerFacade: RouterFacade,
20 | private authFacade: AuthFacade
21 | ) {}
22 |
23 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
24 | return this.appFacade.authRedirect$.pipe(
25 | filter(navOptions => !!navOptions),
26 | switchMap(navOptions =>
27 | this.authFacade.uiState$.pipe(
28 | filter(uiState => uiState !== AuthUiState.LOADING),
29 | map(uiState => uiState === AuthUiState.NOT_AUTHENTICATED),
30 | tap(unAuth => {
31 | if (!unAuth) {
32 | this.routerFacade.navigate(navOptions);
33 | }
34 | })
35 | )
36 | )
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/tasklists/get-tasklists.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { firestore, teamworkService, encryptionService } from './../../../../services';
3 |
4 | export async function getTaskLists(req: express.Request, res: express.Response) {
5 | const uid = res.locals.user.uid;
6 | const connectionId = req.params.connectionId;
7 | const projectId = req.params.projectId;
8 | let page = 1;
9 | if (req.query.page) {
10 | page = req.query.page;
11 | }
12 |
13 | let connectionRef;
14 | try {
15 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get();
16 | } catch(error) {
17 | return res.status(400).json({ message: 'Error getting connection' });
18 | }
19 |
20 | const connection = connectionRef.data();
21 |
22 | switch (connection.type.toLowerCase()) {
23 | case 'teamwork': {
24 |
25 | let teamworkResponse;
26 | try {
27 | teamworkResponse = await teamworkService.getTaskLists(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, projectId, page);
28 | } catch(error) {
29 | return res.status(400).json({ message: error.message });
30 | }
31 |
32 | return res.json(teamworkResponse);
33 | }
34 |
35 | default: {
36 | return res.status(400).json({ message: 'Unknown Connection Type' });
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/features/connections/components/connection-list/connection-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | My Connections
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 | {{ connection.externalData.company }}
20 | {{ connection.type | titlecase }}
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Add New Connection
34 |
35 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { NavParams, NavController, ViewController } from "ionic-angular";
3 |
4 | import { LoadingService } from "../../../../shared/loading.service";
5 |
6 | @Component({
7 | selector: "app-share-scope-link",
8 | templateUrl: "./share-scope-link-modal.component.html"
9 | })
10 | export class ShareScopeLinkModalComponent implements OnInit {
11 | connectionName;
12 | projectName;
13 | sessionUrl;
14 | accessCode;
15 | accessCodeLetters;
16 |
17 | constructor(
18 | private params: NavParams,
19 | private viewCtrl: ViewController,
20 | private navCtrl: NavController,
21 | private loadingSrv: LoadingService
22 | ) {}
23 |
24 | ngOnInit() {
25 | this.connectionName = this.params.data.connectionName;
26 | this.projectName = this.params.data.projectName;
27 | this.sessionUrl = this.params.data.sessionUrl;
28 | this.accessCode = this.params.data.accessCode;
29 | this.accessCodeLetters = this.accessCode.split("");
30 | }
31 |
32 | startScoping() {
33 | this.viewCtrl.dismiss();
34 | this.loadingSrv.dismiss();
35 | this.navCtrl.push("SessionScopingPage", { sessionUrl: this.sessionUrl });
36 | }
37 |
38 | goDashboard() {
39 | this.viewCtrl.dismiss();
40 | this.navCtrl.setRoot("DashboardPage");
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/shared/api.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import {
3 | HttpRequest,
4 | HttpHandler,
5 | HttpEvent,
6 | HttpInterceptor,
7 | HttpErrorResponse
8 | } from "@angular/common/http";
9 |
10 | import { Observable, throwError } from "rxjs";
11 | import { switchMap, take, catchError } from "rxjs/operators";
12 |
13 | import { AngularFireAuth } from "angularfire2/auth";
14 |
15 | import { environment } from "../environments/environment";
16 |
17 | @Injectable()
18 | export class ApiInterceptor implements HttpInterceptor {
19 | constructor(private afAuth: AngularFireAuth) {}
20 |
21 | intercept(
22 | req: HttpRequest,
23 | next: HttpHandler
24 | ): Observable> {
25 | if (!req.url.includes(environment.apiBaseUrl)) {
26 | return next.handle(req);
27 | }
28 |
29 | return this.afAuth.authState.pipe(
30 | take(1),
31 | switchMap(user => user.getIdToken()),
32 | switchMap(idToken => {
33 | const headers = req.headers.set("Authorization", `Bearer ${idToken}`);
34 | const authReq = req.clone({ headers });
35 |
36 | return next.handle(authReq).pipe(
37 | catchError(response => {
38 | if (response instanceof HttpErrorResponse) {
39 | return throwError(response.error);
40 | }
41 |
42 | return throwError(response);
43 | })
44 | );
45 | })
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/scoping/components/select-result/select-result.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";
2 | import { FormGroup, FormBuilder, Validators } from "@angular/forms";
3 |
4 | enum SELECTED_RESULT {
5 | AVG_RESULT = "avg",
6 | MAX_RESULT = "max",
7 | CUSTOM_RESULT = "custom"
8 | }
9 |
10 | @Component({
11 | selector: "app-select-result",
12 | templateUrl: "./select-result.component.html"
13 | })
14 | export class SelectResultComponent implements OnInit {
15 | SELECTED_RESULT = SELECTED_RESULT;
16 | selectionForm: FormGroup;
17 |
18 | @Input() avg: number;
19 |
20 | @Input() max: number;
21 |
22 | selectedResult: SELECTED_RESULT;
23 |
24 | @Output() estimate = new EventEmitter();
25 |
26 | constructor(private fb: FormBuilder) {}
27 |
28 | ngOnInit() {
29 | this.createForm();
30 | }
31 |
32 | createForm() {
33 | this.selectionForm = this.fb.group({
34 | custom: ["", Validators.required]
35 | });
36 | }
37 |
38 | selectAvg() {
39 | this.selectedResult = SELECTED_RESULT.AVG_RESULT;
40 | this.estimate.emit(this.avg);
41 | }
42 |
43 | selectMax() {
44 | this.selectedResult = SELECTED_RESULT.MAX_RESULT;
45 | this.estimate.emit(this.max);
46 | }
47 |
48 | onChangeValue(ev) {
49 | if (parseInt(ev.value) < 99) {
50 | this.selectedResult = SELECTED_RESULT.CUSTOM_RESULT;
51 | this.estimate.emit(parseInt(ev.value, 10));
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-results/session-results.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "@angular/core";
2 | import { ScopingFacade } from "../../store/scoping.facade";
3 | import { NavController, IonicPage } from "ionic-angular";
4 | import { ScopingSession } from "../../../../models/scoping-session";
5 | import { Subscription } from "rxjs";
6 |
7 | @IonicPage({
8 | segment: "SessionResultsPage",
9 | priority: "high"
10 | })
11 | @Component({
12 | selector: "app-session-results",
13 | templateUrl: "./session-results.component.html"
14 | })
15 | export class SessionResultsPage {
16 | session: ScopingSession;
17 | sessionCode: string;
18 | sessionSub: Subscription;
19 |
20 | constructor(
21 | private scopingFacade: ScopingFacade,
22 | private navCtrl: NavController
23 | ) {
24 | this.sessionSub = this.scopingFacade.session$.subscribe(session => {
25 | this.session = session;
26 | });
27 | }
28 |
29 | ionViewWillLeave() {
30 | if (this.sessionSub) {
31 | this.sessionSub.unsubscribe();
32 | }
33 | this.scopingFacade.clearSession();
34 | }
35 |
36 | goDashboard() {
37 | this.navCtrl.push("DashboardPage");
38 | }
39 |
40 | goSettings() {
41 | this.navCtrl.push("SettingsPage");
42 | }
43 |
44 | getTotalEstimate(tasks) {
45 | let totalEstimate = 0;
46 | Object.keys(tasks).map(function(key, index) {
47 | totalEstimate += tasks[key].estimate;
48 | });
49 | return totalEstimate;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/functions/src/api/connections/tasks/put-estimate.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 | import {
3 | firestore,
4 | teamworkService,
5 | encryptionService
6 | } from "./../../../services";
7 |
8 | export async function putEstimate(req: express.Request, res: express.Response) {
9 | const uid = res.locals.user.uid;
10 | const connectionId = req.params.connectionId;
11 | const taskId = req.params.taskId;
12 |
13 | const { hours } = req.body as { hours: number };
14 |
15 | if (!hours) {
16 | return res.status(400).json({ message: "Estimated hours are required." });
17 | }
18 |
19 | let connectionRef;
20 | try {
21 | connectionRef = await firestore
22 | .collection(`/users/${uid}/connections`)
23 | .doc(connectionId)
24 | .get();
25 | } catch (error) {
26 | return res.status(400).json({ message: "Error getting connection" });
27 | }
28 |
29 | const connection = connectionRef.data();
30 |
31 | switch (connection.type.toLowerCase()) {
32 | case "teamwork": {
33 | let teamworkResponse;
34 | try {
35 | teamworkResponse = await teamworkService.putEstimate(
36 | encryptionService.decrypt(connection.token),
37 | connection.externalData.baseUrl,
38 | taskId,
39 | hours
40 | );
41 | } catch (error) {
42 | return res.status(200).json({ message: error.message });
43 | }
44 |
45 | return res.json(teamworkResponse);
46 | }
47 |
48 | default: {
49 | return res.status(400).json({ message: "Unknown Connection Type" });
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 | import { IonicModule } from "ionic-angular";
3 | import { ReactiveFormsModule, FormsModule } from "@angular/forms";
4 | import { HTTP_INTERCEPTORS } from "@angular/common/http";
5 | import { CommonModule } from "@angular/common";
6 |
7 | import { ApiInterceptor } from "./api.interceptor";
8 | import { PopupService } from "./popup.service";
9 | import { LoadingService } from "./loading.service";
10 | import {
11 | ObjectKeysPipe,
12 | ObjectValuesPipe,
13 | ObjectKeyValuePipe
14 | } from "./pipes/object-iterators.pipe";
15 | import { InfoModalComponent } from "./components/info-modal/info-modal";
16 |
17 | @NgModule({
18 | imports: [IonicModule, FormsModule, ReactiveFormsModule, CommonModule],
19 | declarations: [
20 | ObjectKeysPipe,
21 | ObjectValuesPipe,
22 | ObjectKeyValuePipe,
23 | InfoModalComponent
24 | ],
25 | entryComponents: [InfoModalComponent],
26 | exports: [
27 | ObjectKeysPipe,
28 | ObjectValuesPipe,
29 | ObjectKeyValuePipe,
30 | InfoModalComponent,
31 | IonicModule,
32 | FormsModule,
33 | ReactiveFormsModule,
34 | CommonModule
35 | ]
36 | })
37 | export class SharedModule {
38 | static forRoot(): ModuleWithProviders {
39 | return {
40 | ngModule: SharedModule,
41 | providers: [
42 | {
43 | provide: HTTP_INTERCEPTORS,
44 | useClass: ApiInterceptor,
45 | multi: true
46 | },
47 | PopupService,
48 | LoadingService
49 | ]
50 | };
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/features/dashboard/dashboard.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 |
3 | import { StoreModule } from "@ngrx/store";
4 | import { EffectsModule } from "@ngrx/effects";
5 |
6 | import { HistoryService } from "./services/history.service";
7 | import { dashboardReducer } from "./store/dashboard.reducer";
8 | import { DashboardFacade } from "./store/dashboard.facade";
9 | import { SharedModule } from "../../shared/shared.module";
10 | import { SessionDetailModalComponent } from "./components/session-detail-modal/session-detail-modal.component";
11 | import { JoinSessionComponent } from "./components/join-session/join-session.component";
12 | import { SessionHistoryItemComponent } from "./components/session-history-item/session-history-item.component";
13 | import { SessionHistoryListComponent } from "./components/session-history-list/session-history-list.component";
14 |
15 | @NgModule({
16 | imports: [
17 | SharedModule,
18 | StoreModule.forFeature("dashboard", dashboardReducer),
19 | EffectsModule.forFeature([DashboardFacade])
20 | ],
21 | declarations: [
22 | JoinSessionComponent,
23 | SessionDetailModalComponent,
24 | SessionHistoryItemComponent,
25 | SessionHistoryListComponent
26 | ],
27 | entryComponents: [SessionDetailModalComponent],
28 | exports: [JoinSessionComponent, SessionHistoryListComponent]
29 | })
30 | export class DashboardModule {
31 | static forRoot(): ModuleWithProviders {
32 | return {
33 | ngModule: DashboardModule,
34 | providers: [DashboardFacade, HistoryService]
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8.11'
4 | branches:
5 | only:
6 | - develop
7 | script:
8 | - npm run build
9 | - npm run firebase:deploy
10 | notifications:
11 | slack:
12 | rooms:
13 | secure: lxH1zn6wXA4+WuqDRUFTePBCBIzkFAS99EgPYeaJKJ6ignwwHoGqn3ciKAbDD5VqmQU+N8I8f2aAO/Ti3zMcL9Gh0zJaVYK3bXBAmUk+/OKOypWNP/oyOh7FJFmaaLcmJawPRvnJ8iGK5VyVB4o/n8tFg4AqxTphy0eDeDLTIAFoI3v5r3zViAbKCeiw1M5JGUhaCzj79icPM1zcoNoP8CuLzGRdvEqQuIXxopta1tmggMKcjpKyGqZgsVdCNUVfxpOE481ZO6GUfw33cIgJDDyvCo17ijuUB06JTqFpEQ566VadKTz22VdzhvqQIE6A5BnyNNFMT4mMISN22pikOnqHN6UWPIW0p1loZOKRT7bZ81n2889LVRWlQxETO8ooAkvBRjv0Uk3mutMGyAeLCZK3tgyF3zMNWt5pXRQjCNcz10emzLT1NYA+jVnrEPGM6jLgUu33UicEfXD78/PpIh3M70JaP+qFjhB3VSSD+jvdquaXqlWVBnzqzvZDxEbXnbDqYqFQOkPo9tHUw/xzIdYVa30daxVmF+pzqHGCyolILByjrj+r/oaPUQLuI/QQxaUnYLNh5y3EIaXYucUXPm4x/gAH8cl9tzqF5kBCwXYO/2m2z93afLI26RkDnQqEYfyZRBCUKc7zC86ZjRwVAKjMN5WWf8GByoyHpTCL9bg=
14 | env:
15 | global:
16 | - secure: AYkLA5NELZRYFHJiEfxtumdAFme2/a+LlyBWG+92WFBl9+J24L3sTBRTI7P68959iUhdkfMGfO1U48ldvFRd8JD2qMqBV4xnQ95Oai1Dr+uJlNICuTFIqO87dlv7GeO/vyEtg8BeCLEpjn992mqDwDa1k3jfK+76IHLV2FX7uQb7oVnjVzg7bJpKXM6u9cEeeLcdXdItwcl4OgcEYBgSXvpjlc1HgwUhTq1M3MymH3JaOodp5S2EFuw5H6LHzPA6Hnd0uAUQgpPEOBJbbKrvi6O8KCPl5l8Hlqb2EuPLGlOgqselyW3bHzE/waPy/sBXarm3K8yUshwMr2hntcrgxrJFiJDT+MjSg3/cHymtyZCeX2jpQvOIfClo8kfUafqVr+5J8UVBx0mfh7hGwG47yh5qLorZ9BO/Qn3HIfVgxIHNstuQRXhqcdndLuU9mzSUsKR3K8PcTd06w/6BoZ2BubY8hEvhYsQjes5jogAvq10eDEt4lAwngADB+WFBSIiOQKPkR8pLPZxqS/XhP+H7w7UIOFWsxHL4wbKHSJxBQYIoxSZOW48GqdOs+acPbPtjXqo1jZSSWEN5vlUH+XWNmQ57eiFowm8jAK10B6h6zAqDidSS1vn37dkZ6Es3C9JxDd8VsAjB3TXxkspLQX6b3nZIt0LRXn/dU264c+U/dBQ=
17 |
--------------------------------------------------------------------------------
/functions/src/api/estimate/set-estimate.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 |
3 | import { sessionService } from "../../services";
4 |
5 | export async function setEstimate(req: express.Request, res: express.Response) {
6 | const uid = res.locals.user.uid;
7 | const ownerId = req.params.ownerId;
8 | const connectionId = req.params.connectionId;
9 | const sessionId = req.params.sessionId;
10 | const taskId = req.params.taskId;
11 | const estimate = req.body.estimate;
12 |
13 | if (!uid) {
14 | return res.status(401).json({ message: "Unauthorized." });
15 | }
16 | if (!ownerId) {
17 | return res.status(400).json({ message: "Owner ID is required." });
18 | }
19 | // The user must be the moderator of this session
20 | if (ownerId !== uid) {
21 | return res.status(401).json({ message: "Permission denied." });
22 | }
23 | if (!connectionId) {
24 | return res.status(400).json({ message: "Connection ID is required." });
25 | }
26 | if (!sessionId) {
27 | return res.status(400).json({ message: "Session ID is required." });
28 | }
29 | if (!taskId) {
30 | return res.status(400).json({ message: "Task ID is required." });
31 | }
32 | if (!estimate) {
33 | return res.status(400).json({ message: "Estimate is required." });
34 | }
35 |
36 | try {
37 | await sessionService.setTaskEstimate(
38 | uid,
39 | ownerId,
40 | connectionId,
41 | sessionId,
42 | taskId,
43 | estimate
44 | );
45 | } catch (error) {
46 | return res.status(400).json({ message: error });
47 | }
48 |
49 | return res.sendStatus(204);
50 | }
51 |
--------------------------------------------------------------------------------
/src/features/authentication/store/auth.actions.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "@ngrx/store";
2 |
3 | import { User } from "../../../models/user";
4 |
5 | export enum AuthActionTypes {
6 | GET_USER = "[Auth] Get User",
7 | AUTHENTICATED = "[Auth] User Authenticated",
8 | NOT_AUTHENTICATED = "[Auth] User not Authenticated",
9 | LOGIN = "[Auth] Login Attempt",
10 | AUTH_ERROR = "[Auth] Auth Error",
11 | LOGOUT = "[Auth] Logout",
12 | CLEAR_ERROR = "[Auth] Clear Error"
13 | }
14 |
15 | export class GetUserAction implements Action {
16 | readonly type = AuthActionTypes.GET_USER;
17 | }
18 |
19 | export class AuthenticatedAction implements Action {
20 | readonly type = AuthActionTypes.AUTHENTICATED;
21 | constructor(public payload: User) {}
22 | }
23 |
24 | export class NotAuthenticatedAction implements Action {
25 | readonly type = AuthActionTypes.NOT_AUTHENTICATED;
26 | }
27 |
28 | export class LoginAction implements Action {
29 | readonly type = AuthActionTypes.LOGIN;
30 | constructor(public payload: { provider: string }) {}
31 | }
32 |
33 | export class AuthErrorAction implements Action {
34 | readonly type = AuthActionTypes.AUTH_ERROR;
35 | constructor(public payload: { message: string }) {}
36 | }
37 |
38 | export class LogoutAction implements Action {
39 | readonly type = AuthActionTypes.LOGOUT;
40 | }
41 |
42 | export class ClearErrorAction implements Action {
43 | readonly type = AuthActionTypes.CLEAR_ERROR;
44 | }
45 |
46 | export type AuthActions =
47 | | GetUserAction
48 | | AuthenticatedAction
49 | | NotAuthenticatedAction
50 | | LoginAction
51 | | AuthErrorAction
52 | | LogoutAction
53 | | ClearErrorAction;
54 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-detail-modal/session-detail-modal.component.scss:
--------------------------------------------------------------------------------
1 | app-session-detail-modal {
2 | background: rgba(0, 0, 0, 0.5);
3 | width: 100%;
4 | height: 50%;
5 |
6 | ion-content.content {
7 | border-radius: 2rem;
8 | height: calc(100% - 28rem);
9 | width: calc(100% - 7rem);
10 | margin: 3.5rem;
11 | display: flex;
12 | justify-content: center;
13 | }
14 |
15 | .share-box {
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | padding: 30px;
20 | h4 {
21 | text-transform: uppercase;
22 | }
23 | .share-url-section {
24 | display: flex;
25 | margin-bottom: 20px;
26 | .session-url {
27 | border-bottom: 1px solid;
28 | margin-top: 0;
29 | margin-bottom: 0;
30 | margin-right: 15px;
31 | }
32 | }
33 | .access-code-letters {
34 | display: flex;
35 | justify-content: center;
36 |
37 | .single-letter {
38 | color: black;
39 | border: 1px solid $primary-orange;
40 | padding: 5px;
41 | border-radius: 5px;
42 | margin-right: 10px;
43 | margin-left: 10px;
44 | }
45 | }
46 | button.button {
47 | width: 50%;
48 | text-transform: capitalize;
49 | }
50 | }
51 |
52 | .delete-box {
53 | border-top: 2px solid $primary-orange;
54 | padding-top: 20px;
55 | display: flex;
56 | flex-direction: row;
57 | justify-content: center;
58 | button.button {
59 | margin-left: 10px;
60 | width: 35%;
61 | margin-right: 10px;
62 | text-transform: capitalize;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/features/scoping/pages/session-results/session-results.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 | Session Results
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
RESULTS
24 |
{{ session.projectName }}
25 |
{{ session.numScopedTasks }} Tasks Scoped
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {{ (task.estimate == -1) ? "N/A" : task.estimate }}
41 |
42 |
43 | {{ task.name }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/functions/src/api/connections/add-connection.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 | import { firestore, teamworkService } from "./../../services";
3 |
4 | export async function addConnection(
5 | req: express.Request,
6 | res: express.Response
7 | ) {
8 | const { type, token } = req.body as { type: string; token: string };
9 | const uid = res.locals.user.uid;
10 |
11 | if (!type) {
12 | return res.status(400).json({ message: "Connection Type is required." });
13 | }
14 |
15 | if (!token) {
16 | return res.status(400).json({ message: "Connection Token is required." });
17 | }
18 |
19 | switch (type.toLowerCase()) {
20 | case "teamwork": {
21 | let teamworkResponse;
22 | try {
23 | teamworkResponse = await teamworkService.validateToken(token);
24 | } catch (error) {
25 | return res.status(400).json({ message: error.message });
26 | }
27 |
28 | const snapshot = await firestore
29 | .collection(`/users/${uid}/connections`)
30 | .where("type", "==", type.toLowerCase())
31 | .where("externalData.id", "==", teamworkResponse.id)
32 | .get();
33 |
34 | if (snapshot.size > 0) {
35 | return res.status(422).json({ message: "Connection already exists!" });
36 | }
37 |
38 | await firestore.collection(`/users/${uid}/connections`).add({
39 | type: "teamwork",
40 | token: teamworkResponse.accessToken,
41 | externalData: teamworkResponse
42 | });
43 |
44 | return res.status(201).send({
45 | type: "teamwork",
46 | externalData: teamworkResponse
47 | });
48 | }
49 |
50 | default: {
51 | return res.status(400).json({ message: "Unknown Connection Type" });
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-history-item/session-history-item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ChangeDetectionStrategy,
4 | Input,
5 | Output,
6 | EventEmitter,
7 | OnInit
8 | } from "@angular/core";
9 |
10 | import { HistoryItem } from "../../../../models/history-item";
11 | import { SessionUserType } from "../../../../models/user";
12 | import { SessionStatus } from "../../../../models/scoping-session";
13 |
14 | @Component({
15 | selector: "app-session-history-item",
16 | templateUrl: "./session-history-item.component.html",
17 | changeDetection: ChangeDetectionStrategy.OnPush
18 | })
19 | export class SessionHistoryItemComponent implements OnInit {
20 | @Input() item: HistoryItem;
21 | @Input() uid: string;
22 | @Input() time: number;
23 |
24 | @Output() options = new EventEmitter();
25 | @Output() detail = new EventEmitter();
26 |
27 | abbreviation;
28 |
29 | ngOnInit() {
30 | this.abbreviation = this.item.ownerId === this.uid ? "M" : "P";
31 | }
32 |
33 | get sessionStatus(): string {
34 | return this.isComplete() ? "Complete" : "Incomplete";
35 | }
36 |
37 | get ctaText(): string {
38 | return this.isComplete() ? "View Results" : "Continue Scoping";
39 | }
40 |
41 | handleOptionsClick() {
42 | const userType =
43 | this.item.ownerId === this.uid
44 | ? SessionUserType.MODERATOR
45 | : SessionUserType.PARTICIPANT;
46 |
47 | this.options.emit(userType);
48 | }
49 |
50 | handleDetailClick() {
51 | this.detail.emit(
52 | this.isComplete() ? SessionStatus.COMPLETE : SessionStatus.INCOMPLETE
53 | );
54 | }
55 |
56 | private isComplete() {
57 | return this.item.numScopedTasks === this.item.numTasks;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | JuntoScope
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/store/router.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import { Location } from "@angular/common";
3 | import { ViewChild } from "@angular/core";
4 | import { Nav } from "ionic-angular";
5 | import { Store } from "@ngrx/store";
6 | import { Effect, Actions, ofType } from "@ngrx/effects";
7 | import { map, tap } from "rxjs/operators";
8 | import { AppState } from "./app.reducer";
9 | import {
10 | GoAction,
11 | BackAction,
12 | ForwardAction,
13 | RouterActionTypes,
14 | NavigationOptions
15 | } from "./router.actions";
16 |
17 | @Injectable()
18 | export class RouterFacade {
19 | /*
20 | * Observable Store Queries
21 | */
22 |
23 | @ViewChild(Nav) nav: Nav;
24 |
25 | /*
26 | * Module-level Effects
27 | */
28 |
29 | @Effect({ dispatch: false })
30 | go$ = this.actions$.pipe(
31 | ofType(RouterActionTypes.GO),
32 | map(action => action.payload),
33 | tap(({ path, query: queryParams, extras }) => {
34 | this.nav.push(path[0]);
35 | })
36 | );
37 |
38 | @Effect({ dispatch: false })
39 | back$ = this.actions$.pipe(
40 | ofType(RouterActionTypes.BACK),
41 | tap(() => this.location.back())
42 | );
43 |
44 | @Effect({ dispatch: false })
45 | forward$ = this.actions$.pipe(
46 | ofType(RouterActionTypes.FORWARD),
47 | tap(() => this.location.forward())
48 | );
49 |
50 | constructor(
51 | private store: Store,
52 | private actions$: Actions,
53 | private location: Location
54 | ) {}
55 |
56 | /*
57 | * Action Creators
58 | */
59 |
60 | navigate(options: NavigationOptions) {
61 | this.store.dispatch(new GoAction(options));
62 | }
63 |
64 | back() {
65 | this.store.dispatch(new BackAction());
66 | }
67 |
68 | forward() {
69 | this.store.dispatch(new ForwardAction());
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-project/select-project.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { map, filter, find } from "rxjs/operators";
3 | import { ConnectionFacade } from "../../store/connection.facade";
4 | import { Project } from "../../../../models/project";
5 | import { Connection } from "../../../../models/connection";
6 | import { NavController, NavParams, IonicPage } from "ionic-angular";
7 |
8 | @IonicPage({
9 | segment: "SelectProjectPage",
10 | priority: "high"
11 | })
12 | @Component({
13 | selector: "app-select-project",
14 | templateUrl: "./select-project.component.html"
15 | })
16 | export class SelectProjectPage implements OnInit {
17 | connectionId: string;
18 | loaded: boolean = false;
19 | projects$ = this.connectionFacade.selectedConnection$.pipe(
20 | filter((connection: Connection) => !!connection && !!connection.projects),
21 | map((connection: Connection) => {
22 | this.loaded = true;
23 |
24 | return Object.keys(connection.projects).map(
25 | keys => connection.projects[keys]
26 | );
27 | })
28 | );
29 |
30 | connection$ = this.connectionFacade.selectConnection$.pipe(
31 | find(
32 | (connection: any) => connection.id == this.navParams.get("connectionId")
33 | )
34 | );
35 |
36 | constructor(
37 | private connectionFacade: ConnectionFacade,
38 | private navCtrl: NavController,
39 | private navParams: NavParams
40 | ) {}
41 |
42 | ngOnInit(): void {
43 | this.connectionId = this.navParams.get("connectionId");
44 | this.connectionFacade.selectConnection(this.connectionId);
45 | }
46 |
47 | handleProjectSelect(project: Project) {
48 | this.navCtrl.push("SelectTaskListPage", {
49 | connectionId: this.connectionId,
50 | projectId: project.id,
51 | projectName: project.name
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/scoping/components/session-access/session-access.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | OnInit,
4 | Output,
5 | Input,
6 | EventEmitter,
7 | OnDestroy
8 | } from "@angular/core";
9 | import { FormGroup, FormBuilder, Validators } from "@angular/forms";
10 | import { ScopingFacade } from "../../store/scoping.facade";
11 | import { SessionValidation } from "../../../../models/scoping-session";
12 | import { PopupService } from "../../../../shared/popup.service";
13 | import { Subscription } from "rxjs";
14 |
15 | @Component({
16 | selector: "app-session-access",
17 | templateUrl: "./session-access.component.html"
18 | })
19 | export class SessionAccessComponent implements OnInit, OnDestroy {
20 | accessForm: FormGroup;
21 | error$ = this.scopingFacade.error$;
22 | errorSubscription: Subscription;
23 |
24 | @Input() sessionLink: string;
25 | @Output() access = new EventEmitter();
26 |
27 | constructor(
28 | private fb: FormBuilder,
29 | private scopingFacade: ScopingFacade,
30 | private popupSvc: PopupService
31 | ) {}
32 |
33 | ngOnInit() {
34 | this.createForm();
35 | this.errorSubscription = this.error$.subscribe(error => {
36 | if (error) {
37 | this.popupSvc.simpleAlert("Uh Oh!", error, "OK");
38 | this.scopingFacade.clearError();
39 | }
40 | });
41 | }
42 |
43 | ngOnDestroy() {
44 | this.errorSubscription.unsubscribe();
45 | }
46 |
47 | continue() {
48 | if (this.accessForm.valid) {
49 | const sessionValidation = {
50 | sessionLink: this.sessionLink,
51 | accessCode: this.accessForm.get("code").value
52 | };
53 | this.access.emit(sessionValidation);
54 | } else {
55 | this.accessForm.get("code").markAsDirty();
56 | }
57 | }
58 |
59 | createForm() {
60 | this.accessForm = this.fb.group({
61 | code: ["", Validators.required]
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/features/connections/connections.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 | import { StoreModule } from "@ngrx/store";
3 | import { EffectsModule } from "@ngrx/effects";
4 | import { SharedModule } from "../../shared/shared.module";
5 | import { connectionReducer } from "./store/connection.reducer";
6 | import { ConnectionFacade } from "./store/connection.facade";
7 | import { ConnectionService } from "./services/connection.service";
8 | import { VerifyModalComponent } from "./components/verify-modal/verify-modal.component";
9 | import { ProjectListComponent } from "./components/project-list/project-list.component";
10 | import { TaskListComponent } from "./components/task-list/task-list.component";
11 | import { ProjectItemComponent } from "./components/project-item/project-item.component";
12 | import { InstructionsComponent } from "./components/instructions/instructions.component";
13 | import { ConnectionListComponent } from "./components/connection-list/connection-list.component";
14 | import { ShareScopeLinkModalComponent } from "./components/share-scope-link-modal/share-scope-link-modal.component";
15 |
16 | @NgModule({
17 | imports: [
18 | SharedModule,
19 | StoreModule.forFeature("connection", connectionReducer),
20 | EffectsModule.forFeature([ConnectionFacade])
21 | ],
22 | declarations: [
23 | ConnectionListComponent,
24 | InstructionsComponent,
25 | ProjectItemComponent,
26 | ProjectListComponent,
27 | TaskListComponent,
28 | VerifyModalComponent,
29 | ShareScopeLinkModalComponent
30 | ],
31 | entryComponents: [VerifyModalComponent, ShareScopeLinkModalComponent],
32 | exports: [TaskListComponent, ProjectListComponent, ConnectionListComponent],
33 | providers: []
34 | })
35 | export class ConnectionsModule {
36 | static forRoot(): ModuleWithProviders {
37 | return {
38 | ngModule: ConnectionsModule,
39 | providers: [ConnectionFacade, ConnectionService]
40 | };
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/features/scoping/scoping.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders } from "@angular/core";
2 |
3 | import { EffectsModule } from "@ngrx/effects";
4 | import { SharedModule } from "../../shared/shared.module";
5 | import { SessionAccessComponent } from "./components/session-access/session-access.component";
6 | import { CountedVotesComponent } from "./components/counted-votes/counted-votes.component";
7 | import { TaskCardComponent } from "./components/task-card/task-card.component";
8 | import { SessionHeaderComponent } from "./components/session-header/session-header.component";
9 | import { VoteComponent } from "./components/vote/vote.component";
10 | import { ResultEstimateComponent } from "./components/result-estimate/result-estimate.component";
11 | import { SelectResultComponent } from "./components/select-result/select-result.component";
12 | import { ScopingFacade } from "./store/scoping.facade";
13 | import { StoreModule } from "@ngrx/store";
14 | import { scopingReducer } from "./store/scoping.reducer";
15 | import { ScopingService } from "./services/scoping.service";
16 |
17 | @NgModule({
18 | imports: [
19 | SharedModule,
20 | StoreModule.forFeature("scoping", scopingReducer),
21 | EffectsModule.forFeature([ScopingFacade])
22 | ],
23 | declarations: [
24 | CountedVotesComponent,
25 | ResultEstimateComponent,
26 | SelectResultComponent,
27 | SessionAccessComponent,
28 | SessionHeaderComponent,
29 | TaskCardComponent,
30 | VoteComponent
31 | ],
32 | entryComponents: [],
33 | exports: [
34 | ResultEstimateComponent,
35 | SessionHeaderComponent,
36 | SessionAccessComponent,
37 | TaskCardComponent,
38 | VoteComponent,
39 | SelectResultComponent,
40 | CountedVotesComponent
41 | ]
42 | })
43 | export class ScopingModule {
44 | static forRoot(): ModuleWithProviders {
45 | return {
46 | ngModule: ScopingModule,
47 | providers: [ScopingFacade, ScopingService]
48 | };
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/features/connections/components/connection-list/connection-list.component.scss:
--------------------------------------------------------------------------------
1 | app-connection-list {
2 | ion-list.list {
3 | background-color: $primary-orange;
4 | border-bottom: 1px solid white !important;
5 | margin: 0;
6 | width: 100%;
7 |
8 | ion-list-header.list-header {
9 | background-color: $primary-orange;
10 | padding: 1rem 1rem 0rem 1rem;
11 |
12 | .icon,
13 | h2 {
14 | color: white;
15 | display: inline-block;
16 | font-size: 24px;
17 | font-weight: 600;
18 | text-transform: none;
19 | vertical-align: middle;
20 | }
21 |
22 | .icon {
23 | font-size: 36px;
24 | }
25 |
26 | ion-label.label {
27 | padding: 1rem;
28 | margin: 1rem;
29 | }
30 | }
31 |
32 | // existing projects
33 | ion-item.item-block {
34 | background-color: $primary-orange;
35 | border-bottom: 1px solid $primary-orange;
36 | color: white;
37 | }
38 | }
39 |
40 | ion-item.item.item-block.new-connection {
41 | margin-top: 20px;
42 | border: 2px solid #c94313;
43 | }
44 |
45 | .connections-header {
46 | display: grid;
47 | color: white;
48 | grid-template-columns: 0.75fr 1fr;
49 | margin: 10px;
50 | background: rgba(241, 101, 45, 0.65);
51 | border-radius: 5px;
52 |
53 | ion-label.label {
54 | margin: 0;
55 | padding: 4px;
56 | }
57 | }
58 |
59 | .connection-item {
60 | display: grid;
61 | color: white;
62 | grid-template-columns: 0.75fr 0.75fr;
63 |
64 | ion-label.label {
65 | margin: 0;
66 | }
67 | }
68 |
69 | // custom buttons for android and windows
70 | .button-md,
71 | .button-wp {
72 | border-radius: 11px;
73 | &.existing-button span {
74 | padding-left: 8px;
75 | padding-right: 6px;
76 | }
77 | span {
78 | padding-left: 3px;
79 | padding-right: 2px;
80 | }
81 | }
82 |
83 | // custom buttons for ios
84 | .button-ios {
85 | border: none;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.scss:
--------------------------------------------------------------------------------
1 | app-share-scope-link {
2 | ion-header .toolbar {
3 | // Background color
4 | .toolbar-background {
5 | background: $primary-orange;
6 | border: none;
7 | } // Title
8 | .toolbar-title {
9 | // overflow: visible;
10 | color: white;
11 | font-size: 16px;
12 | text-transform: capitalize;
13 | } // Buttons
14 | .bar-button-default {
15 | color: white;
16 | }
17 | ion-buttons.bar-buttons {
18 | order: 1;
19 | }
20 | }
21 | ion-content.container {
22 | .scroll-content {
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | .share-box {
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | border: 1px solid;
31 | border-radius: 5px;
32 | padding: 30px;
33 | -webkit-box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75);
34 | -moz-box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75);
35 | box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75);
36 | h4 {
37 | text-transform: uppercase;
38 | }
39 | .share-url-section {
40 | display: flex;
41 | margin-bottom: 20px;
42 | .session-url {
43 | border-bottom: 1px solid;
44 | margin-top: 0;
45 | margin-bottom: 0;
46 | margin-right: 15px;
47 | }
48 | }
49 | .access-code-letters {
50 | display: flex;
51 | justify-content: center;
52 | .single-letter {
53 | color: black;
54 | border: 1px solid $primary-orange;
55 | padding: 5px;
56 | border-radius: 5px;
57 | margin-right: 10px;
58 | margin-left: 10px;
59 | }
60 | }
61 | }
62 | button.button {
63 | width: 50%;
64 | margin-top: 20px;
65 | text-transform: capitalize;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/features/settings/pages/settings/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Settings
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | My Connections
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 | {{ connection.externalData.company }}
28 | {{ connection.type | titlecase }}
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Add New Connection
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 | FAQ
51 |
52 |
53 |
54 |
55 | {{ faq.question}}
56 | {{ faq.answer}}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/store/app.reducer.ts:
--------------------------------------------------------------------------------
1 | import { MetaReducer, ActionReducerMap, createSelector } from "@ngrx/store";
2 | import { storeFreeze } from "ngrx-store-freeze";
3 | import { environment } from "../environments/environment";
4 | import { NavigationOptions } from "./router.actions";
5 | import {
6 | AuthState,
7 | AuthQuery,
8 | AuthUiState
9 | } from "../features/authentication/store/auth.reducer";
10 | import {
11 | ConnectionState,
12 | connectionReducer,
13 | initialConnectionState
14 | } from "../features/connections/store/connection.reducer";
15 | import {
16 | DashboardState,
17 | dashboardReducer,
18 | initialDashboardState
19 | } from "../features/dashboard/store/dashboard.reducer";
20 |
21 | interface FullAppState {
22 | auth: AuthState;
23 | connection: ConnectionState;
24 | dashboard: DashboardState;
25 | }
26 |
27 | export type AppState = Partial;
28 |
29 | export const reducers: ActionReducerMap = {
30 | connection: connectionReducer,
31 | dashboard: dashboardReducer
32 | };
33 |
34 | export const metaReducers: MetaReducer[] = environment.production
35 | ? []
36 | : [storeFreeze];
37 |
38 | export const initialState: AppState = {
39 | connection: initialConnectionState,
40 | dashboard: initialDashboardState
41 | };
42 |
43 | export namespace AppQuery {
44 | export const selectUid = createSelector(
45 | AuthQuery.selectUser,
46 | user => (user ? user.uid : null)
47 | );
48 |
49 | export const selectAuthRedirect = createSelector(
50 | AuthQuery.selectUiState,
51 | (authUiState): NavigationOptions => {
52 | if (authUiState === AuthUiState.NOT_AUTHENTICATED) {
53 | return { path: ["LoginPage"] };
54 | }
55 |
56 | return { path: ["DashboardPage"] };
57 | }
58 | );
59 |
60 | export const selectUidDocPath = createSelector(
61 | AuthQuery.selectUser,
62 | user => user && `users/${user.uid}`
63 | );
64 | export const selectConnectionsClnPath = createSelector(
65 | selectUidDocPath,
66 | uidPath => uidPath && `${uidPath}/connections`
67 | );
68 | export const selectPublicSessionsClnPath = "public/data/sessions";
69 | }
70 |
--------------------------------------------------------------------------------
/src/features/authentication/store/auth.actions.spec.ts:
--------------------------------------------------------------------------------
1 | import * as AuthActions from "./auth.actions";
2 | import { User } from "../../../models/user";
3 |
4 | describe("Authentication Actions", () => {
5 | describe("GetUser Action", () => {
6 | it("should create action", () => {
7 | const action = new AuthActions.GetUserAction();
8 |
9 | expect(action).toEqual({ type: AuthActions.AuthActionTypes.GET_USER });
10 | });
11 | });
12 |
13 | describe("AuthenticatedAction Action", () => {
14 | it("should create action", () => {
15 | const payload: User = { uid: "testuid", displayName: "testUser" };
16 | const action = new AuthActions.AuthenticatedAction(payload);
17 |
18 | expect(action).toEqual({
19 | type: AuthActions.AuthActionTypes.AUTHENTICATED,
20 | payload
21 | });
22 | });
23 | });
24 |
25 | describe("NotAuthenticatedAction Action", () => {
26 | it("should create action", () => {
27 | const action = new AuthActions.NotAuthenticatedAction();
28 |
29 | expect(action).toEqual({
30 | type: AuthActions.AuthActionTypes.NOT_AUTHENTICATED
31 | });
32 | });
33 | });
34 |
35 | describe("LoginAction Action", () => {
36 | it("should create action", () => {
37 | const payload = { provider: "testProvider" };
38 | const action = new AuthActions.LoginAction(payload);
39 |
40 | expect(action).toEqual({
41 | type: AuthActions.AuthActionTypes.LOGIN,
42 | payload
43 | });
44 | });
45 | });
46 |
47 | describe("AuthErrorAction Action", () => {
48 | it("should create action", () => {
49 | const payload = { message: "test error message" };
50 | const action = new AuthActions.AuthErrorAction(payload);
51 |
52 | expect(action).toEqual({
53 | type: AuthActions.AuthActionTypes.AUTH_ERROR,
54 | payload
55 | });
56 | });
57 | });
58 |
59 | describe("LogoutAction Action", () => {
60 | it("should create action", () => {
61 | const action = new AuthActions.LogoutAction();
62 |
63 | expect(action).toEqual({ type: AuthActions.AuthActionTypes.LOGOUT });
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/features/connections/pages/connection-details/connection-details.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from "@angular/core";
2 | import { NavParams, IonicPage, NavController } from "ionic-angular";
3 |
4 | import { Subscription } from "rxjs";
5 | import { TakeUntilDestroy } from "ngx-take-until-destroy";
6 |
7 | import { Connection } from "../../../../models/connection";
8 | import { ConnectionFacade } from "../../store/connection.facade";
9 | import { PopupService } from "../../../../shared/popup.service";
10 |
11 | @TakeUntilDestroy()
12 | @IonicPage({
13 | segment: "ConnectionDetailsPage",
14 | priority: "high"
15 | })
16 | @Component({
17 | selector: "app-connection-details",
18 | templateUrl: "./connection-details.component.html"
19 | })
20 | export class ConnectionDetailsPage implements OnInit, OnDestroy {
21 | connectionId = this.navParams.get("connectionId");
22 | connection: Connection;
23 | connectionSub: Subscription;
24 |
25 | constructor(
26 | private connectionFacade: ConnectionFacade,
27 | private navParams: NavParams,
28 | private navCtrl: NavController,
29 | private popupSvc: PopupService
30 | ) {}
31 |
32 | ngOnInit() {
33 | this.connectionFacade.getConnections();
34 |
35 | this.connectionSub = this.connectionFacade.connections$.subscribe(
36 | (connections: Connection[]) => {
37 | if (connections) {
38 | this.connection = connections.filter(
39 | c => c.id === this.connectionId
40 | )[0];
41 | }
42 | }
43 | );
44 | }
45 |
46 | ngOnDestroy() {
47 | this.connectionSub.unsubscribe();
48 | }
49 |
50 | deleteConnection(connectionId) {
51 | this.popupSvc.customButtonsAlert(
52 | "Delete Connection",
53 | "Are you sure you would like to delete this connection? Please confirm below.",
54 | [
55 | {
56 | text: "Cancel",
57 | role: "cancel"
58 | },
59 | {
60 | text: "Confirm",
61 | handler: () => {
62 | this.connectionFacade.removeConnection(connectionId);
63 | this.navCtrl.pop();
64 | }
65 | }
66 | ]
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/store/router.reducer.spec.ts:
--------------------------------------------------------------------------------
1 | import { CustomSerializer, RouterQuery } from "./router.reducer";
2 | import { AppState } from "./app.reducer";
3 |
4 | const testParams = { one: "1", two: "2" };
5 |
6 | describe("CustomSerializer", () => {
7 | it("should serialize router snapshot", () => {
8 | const serializer = new CustomSerializer();
9 | const testSnapshot = {
10 | url: "/test-url",
11 | root: {
12 | queryParams: testParams,
13 | firstChild: {
14 | firstChild: {
15 | firstChild: { params: testParams }
16 | }
17 | }
18 | }
19 | };
20 | const expected = {
21 | url: "/test-url",
22 | params: testParams,
23 | queryParams: testParams
24 | };
25 |
26 | const serialized = serializer.serialize(testSnapshot);
27 |
28 | expect(serialized).toEqual(expected);
29 | });
30 | });
31 |
32 | describe("Router Queries", () => {
33 | const testState: AppState = {
34 | router: {
35 | state: { url: "/login", params: testParams, queryParams: testParams },
36 | navigationId: 1
37 | }
38 | };
39 |
40 | describe("getState", () => {
41 | it("should select router state from slice", () => {
42 | const expected = testState.router.state;
43 |
44 | const actual = RouterQuery.getState(testState);
45 |
46 | expect(actual).toEqual(expected);
47 | });
48 | });
49 |
50 | describe("getUrl", () => {
51 | it("should select route url", () => {
52 | const expected = testState.router.state.url;
53 |
54 | const actual = RouterQuery.getUrl(testState);
55 |
56 | expect(actual).toEqual(expected);
57 | });
58 | });
59 |
60 | describe("getParams", () => {
61 | it("should select route params", () => {
62 | const expected = testState.router.state.params;
63 |
64 | const actual = RouterQuery.getParams(testState);
65 |
66 | expect(actual).toEqual(expected);
67 | });
68 | });
69 |
70 | describe("getQueryParams", () => {
71 | it("should select route queryParams", () => {
72 | const expected = testState.router.state.queryParams;
73 |
74 | const actual = RouterQuery.getQueryParams(testState);
75 |
76 | expect(actual).toEqual(expected);
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/features/dashboard/components/session-detail-modal/session-detail-modal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { NavParams, ViewController } from "ionic-angular";
3 | import { PopupService } from "../../../../shared/popup.service";
4 | import { InfoModalComponent } from "../../../../shared/components/info-modal/info-modal";
5 | import { Store } from "@ngrx/store";
6 | import { AppState } from "../../../../store/app.reducer";
7 | import {
8 | DeleteSessionAction,
9 | RefreshAccessCodeAction
10 | } from "../../store/dashboard.actions";
11 | import * as moment from "moment";
12 |
13 | @Component({
14 | selector: "app-session-detail-modal",
15 | templateUrl: "./session-detail-modal.component.html"
16 | })
17 | export class SessionDetailModalComponent implements OnInit {
18 | accountData;
19 | isModerator: boolean;
20 | isExpired: boolean;
21 | expirationDate;
22 | accessCodeLetters;
23 |
24 | constructor(
25 | private popupSvc: PopupService,
26 | private viewCtrl: ViewController,
27 | private params: NavParams,
28 | private store: Store
29 | ) {}
30 |
31 | ngOnInit() {
32 | this.accountData = this.params.data.accountData;
33 | this.isModerator =
34 | this.params.data.accountData.userType === "Session Moderator";
35 | const now = moment();
36 | this.isExpired = now.isAfter(this.accountData.item.expirationDate);
37 | this.expirationDate = now.to(this.accountData.item.expirationDate);
38 | this.accessCodeLetters = this.accountData.item.accessCode.split("");
39 | }
40 |
41 | closeModal() {
42 | this.viewCtrl.dismiss();
43 | }
44 |
45 | refreshCode() {
46 | this.viewCtrl.dismiss();
47 | this.store.dispatch(
48 | new RefreshAccessCodeAction(this.accountData.item.sessionCode)
49 | );
50 | }
51 |
52 | deleteSession() {
53 | this.viewCtrl.dismiss();
54 | this.popupSvc.openModal({
55 | component: InfoModalComponent,
56 | componentProps: {
57 | title: "Are you sure?",
58 | text: "If you delete the session there is no come back.",
59 | label: "Delete",
60 | callback: () =>
61 | this.store.dispatch(
62 | new DeleteSessionAction(this.accountData.item.sessionCode)
63 | )
64 | }
65 | });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/features/authentication/store/auth.reducer.ts:
--------------------------------------------------------------------------------
1 | import { createSelector, createFeatureSelector } from "@ngrx/store";
2 |
3 | import { AuthActions, AuthActionTypes } from "./auth.actions";
4 | import { User } from "../../../models/user";
5 |
6 | export enum AuthUiState {
7 | UNKNOWN = "Unknown",
8 | LOADING = "Loading",
9 | AUTHENTICATED = "Authenticated",
10 | NOT_AUTHENTICATED = "Not Authenticated"
11 | }
12 |
13 | export interface AuthState {
14 | user: User;
15 | uiState: AuthUiState;
16 | error: any;
17 | }
18 |
19 | export const initialAuthState: AuthState = {
20 | user: null,
21 | uiState: AuthUiState.UNKNOWN,
22 | error: null
23 | };
24 |
25 | export function authReducer(
26 | state = initialAuthState,
27 | action: AuthActions
28 | ): AuthState {
29 | switch (action.type) {
30 | case AuthActionTypes.GET_USER:
31 | case AuthActionTypes.LOGIN:
32 | case AuthActionTypes.LOGOUT:
33 | return { ...state, uiState: AuthUiState.LOADING };
34 |
35 | case AuthActionTypes.AUTHENTICATED:
36 | return {
37 | user: action.payload,
38 | uiState: AuthUiState.AUTHENTICATED,
39 | error: null
40 | };
41 |
42 | case AuthActionTypes.LOGOUT:
43 | return {
44 | user: null,
45 | uiState: AuthUiState.NOT_AUTHENTICATED,
46 | error: null
47 | };
48 |
49 | case AuthActionTypes.NOT_AUTHENTICATED:
50 | return {
51 | user: null,
52 | uiState: AuthUiState.NOT_AUTHENTICATED,
53 | error: null
54 | };
55 |
56 | case AuthActionTypes.AUTH_ERROR:
57 | return {
58 | user: null,
59 | uiState: AuthUiState.NOT_AUTHENTICATED,
60 | error: action.payload.message
61 | };
62 |
63 | case AuthActionTypes.CLEAR_ERROR:
64 | return {
65 | ...state,
66 | error: null
67 | };
68 |
69 | default:
70 | return state;
71 | }
72 | }
73 |
74 | export namespace AuthQuery {
75 | const selectSlice = createFeatureSelector("auth");
76 | export const selectUser = createSelector(
77 | selectSlice,
78 | state => (state.user ? state.user : null)
79 | );
80 | export const selectUiState = createSelector(
81 | selectSlice,
82 | state => state.uiState
83 | );
84 | export const selectError = createSelector(selectSlice, state => state.error);
85 | }
86 |
--------------------------------------------------------------------------------
/src/features/connections/pages/select-task-list/select-task-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { map, filter } from "rxjs/operators";
3 | import * as _ from "lodash";
4 | import { ConnectionFacade } from "../../store/connection.facade";
5 | import { TaskList } from "../../../../models/task-list";
6 | import { Connection } from "../../../../models/connection";
7 | import { NavParams, IonicPage } from "ionic-angular";
8 | import { LoadingService } from "../../../../shared/loading.service";
9 |
10 | @IonicPage({
11 | segment: "SelectTaskListPage",
12 | priority: "high"
13 | })
14 | @Component({
15 | selector: "app-select-task-list",
16 | templateUrl: "./select-task-list.component.html"
17 | })
18 | export class SelectTaskListPage implements OnInit {
19 | connectionId: string;
20 | projectId: string;
21 | projectName: string;
22 | loaded: boolean = false;
23 |
24 | taskLists$ = this.connectionFacade.selectedConnection$.pipe(
25 | filter(connection => {
26 | return _.has(connection, `projects.${this.projectId}.taskLists`);
27 | }),
28 | map((connection: Connection) => {
29 | this.loaded = true;
30 |
31 | return Object.keys(connection.projects[this.projectId].taskLists).map(
32 | keys => connection.projects[this.projectId].taskLists[keys]
33 | );
34 | })
35 | );
36 |
37 | selectedLists: { [taskListId: string]: boolean } = {};
38 |
39 | get taskListIds() {
40 | return _.invertBy(this.selectedLists)["true"];
41 | }
42 |
43 | constructor(
44 | private connectionFacade: ConnectionFacade,
45 | private loadingSrv: LoadingService,
46 | private navParams: NavParams
47 | ) {}
48 |
49 | ngOnInit(): void {
50 | this.connectionId = this.navParams.get("connectionId");
51 | this.projectId = this.navParams.get("projectId");
52 | this.projectName = this.navParams.get("projectName");
53 | this.connectionFacade.selectProject(this.connectionId, this.projectId);
54 | }
55 |
56 | handleToggle(checked: boolean, taskList: TaskList) {
57 | this.selectedLists[taskList.id] = checked;
58 | }
59 |
60 | startSession() {
61 | this.loadingSrv.present();
62 | this.connectionFacade.createSession(
63 | this.connectionId,
64 | this.projectId,
65 | this.taskListIds,
66 | this.projectName
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "@angular/core";
2 | import { Platform } from "ionic-angular";
3 | import { StatusBar } from "@ionic-native/status-bar";
4 | import { ScreenOrientation } from "@ionic-native/screen-orientation";
5 | import { SplashScreen } from "@ionic-native/splash-screen";
6 | import { AuthFacade } from "../features/authentication/store/auth.facade";
7 | import { Deeplinks } from "@ionic-native/deeplinks";
8 | import { DashboardPage } from "../features/dashboard/pages/dashboard/dashboard.component";
9 | import { ConnectionFacade } from "../features/connections/store/connection.facade";
10 | // import { LoadingService } from "../shared/loading.service";
11 |
12 | @Component({
13 | templateUrl: "app.html"
14 | })
15 | export class JuntoScopeComponent {
16 | rootPage: any = "LoginPage";
17 |
18 | constructor(
19 | platform: Platform,
20 | statusBar: StatusBar,
21 | splashScreen: SplashScreen,
22 | screenOrientation: ScreenOrientation,
23 | authFacade: AuthFacade,
24 | deeplinks: Deeplinks,
25 | connectionFacade: ConnectionFacade
26 | ) {
27 | platform.ready().then(() => {
28 | // Okay, so the platform is ready and our plugins are available.
29 | // Here you can do any higher level native things you might need.
30 | statusBar.styleDefault();
31 | splashScreen.hide();
32 | screenOrientation
33 | .lock(screenOrientation.ORIENTATIONS.PORTRAIT)
34 | .catch(error => console.log(error));
35 |
36 | authFacade.checkAuth();
37 |
38 | deeplinks
39 | .route({
40 | "/": DashboardPage
41 | })
42 | .subscribe(
43 | match => {
44 | // match.$route - the route we matched, which is the matched entry from the arguments to route()
45 | // match.$args - the args passed in the link
46 | // match.$link - the full link data
47 | console.log("Successfully matched route", match);
48 | const code = match.$args.code;
49 |
50 | const connection = {
51 | token: code,
52 | type: "teamwork"
53 | };
54 |
55 | connectionFacade.addConnection(connection);
56 | },
57 | nomatch => {
58 | // nomatch.$link - the full link data
59 | console.error("Got a deeplink that didn't match", nomatch);
60 | }
61 | );
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/features/settings/pages/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from "@angular/core";
2 | import { IonicPage, NavController } from "ionic-angular";
3 | import { TakeUntilDestroy, untilDestroyed } from "ngx-take-until-destroy";
4 | import { filter, take } from "rxjs/operators";
5 | import { AuthFacade } from "../../../authentication/store/auth.facade";
6 | import { AuthUiState } from "../../../authentication/store/auth.reducer";
7 | import { ConnectionFacade } from "../../../connections/store/connection.facade";
8 | import { LoginPage } from "../../../authentication/pages/login/login.component";
9 | import { Subscription } from "rxjs";
10 | import { SettingsService } from "../../service/settings.service";
11 |
12 | @TakeUntilDestroy()
13 | @IonicPage({
14 | segment: "SettingsPage",
15 | priority: "high"
16 | })
17 | @Component({
18 | selector: "app-settings",
19 | templateUrl: "./settings.html"
20 | })
21 | export class SettingsPage implements OnInit, OnDestroy {
22 | connections$ = this.connectionFacade.connections$;
23 | logOutSub: Subscription;
24 | faqsSub: Subscription;
25 |
26 | private logoutRedirect$ = this.authFacade.uiState$.pipe(
27 | untilDestroyed(this),
28 | filter(authState => authState === AuthUiState.NOT_AUTHENTICATED)
29 | );
30 | faqs;
31 |
32 | constructor(
33 | private navCtrl: NavController,
34 | private connectionFacade: ConnectionFacade,
35 | private authFacade: AuthFacade,
36 | private settingsSvc: SettingsService
37 | ) {}
38 |
39 | ngOnDestroy() {
40 | this.unsub();
41 | }
42 |
43 | ngOnInit() {
44 | this.connectionFacade.getConnections();
45 | this.faqsSub = this.settingsSvc.getFaqs().subscribe(faqs => {
46 | if (faqs) {
47 | this.faqs = faqs;
48 | }
49 | });
50 | }
51 |
52 | viewConnectionDetails(connectionId) {
53 | this.navCtrl.push("ConnectionDetailsPage", {
54 | connectionId: connectionId
55 | });
56 | }
57 |
58 | addConnection() {
59 | this.navCtrl.push("AddConnectionPage");
60 | }
61 |
62 | logout() {
63 | this.unsub();
64 | this.authFacade.logout();
65 |
66 | this.logOutSub = this.logoutRedirect$.pipe(take(1)).subscribe(() => {
67 | this.navCtrl.push(LoginPage);
68 | });
69 | }
70 |
71 | unsub() {
72 | if (this.logOutSub) {
73 | this.logOutSub.unsubscribe();
74 | }
75 | if (this.faqsSub) {
76 | this.faqsSub.unsubscribe();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/functions/src/api/connections/projects/sessions/add-session.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express";
2 | import {
3 | firestore,
4 | encryptionService,
5 | sessionService,
6 | teamworkService
7 | } from "./../../../../services";
8 |
9 | export async function addSession(req: express.Request, res: express.Response) {
10 | const uid = res.locals.user.uid;
11 | const { connectionId, projectId } = req.params;
12 | const { taskListIds, projectName } = req.body;
13 |
14 | if (!connectionId) {
15 | return res
16 | .status(400)
17 | .json({ message: "Connection id is a required parameter." });
18 | }
19 |
20 | if (!projectId) {
21 | return res
22 | .status(400)
23 | .json({ message: "Project id is a required parameter." });
24 | }
25 |
26 | if (!Array.isArray(taskListIds) || taskListIds.length === 0) {
27 | return res.status(400).json({ message: "Task List Ids is required" });
28 | }
29 |
30 | const connection = await firestore
31 | .doc(`/users/${uid}/connections/${connectionId}`)
32 | .get()
33 | .then(snapshot => snapshot.data());
34 |
35 | if (!connection) {
36 | return res.status(400).json({ message: "Invalid connection id." });
37 | }
38 |
39 | let sessionTasks;
40 | let newSession;
41 | try {
42 | switch (connection.type) {
43 | case "teamwork": {
44 | const token = encryptionService.decrypt(connection.token);
45 | const baseUrl = connection.externalData.baseUrl;
46 |
47 | sessionTasks = await Promise.all(
48 | taskListIds.map(id => teamworkService.getTasks(token, baseUrl, id))
49 | ).then(responses => {
50 | return responses
51 | .map(response => response.tasks)
52 | .reduce((allTasks, taskListTasks) => {
53 | for (const taskId in taskListTasks) {
54 | if (taskListTasks.hasOwnProperty(taskId)) {
55 | allTasks[taskId] = taskListTasks[taskId];
56 | }
57 | }
58 | return allTasks;
59 | }, {});
60 | });
61 |
62 | break;
63 | }
64 |
65 | default: {
66 | return res.status(400).json({ message: "Unknown Connection Type" });
67 | }
68 | }
69 |
70 | newSession = await sessionService.createSession(
71 | uid,
72 | connectionId,
73 | projectId,
74 | sessionTasks,
75 | projectName
76 | );
77 | } catch (error) {
78 | return res.status(400).json({ message: error });
79 | }
80 |
81 | return res.status(201).json(newSession);
82 | }
83 |
--------------------------------------------------------------------------------
/src/features/settings/pages/settings/settings.scss:
--------------------------------------------------------------------------------
1 | app-settings {
2 | .connections {
3 | ion-list.list {
4 | background-color: $primary-orange;
5 | border-bottom: 1px solid white !important;
6 | margin: 0;
7 | width: 100%;
8 |
9 | ion-list-header.list-header {
10 | background-color: $primary-orange;
11 | padding: 1rem 1rem 0rem 1rem;
12 |
13 | .icon,
14 | h2 {
15 | color: white;
16 | display: inline-block;
17 | font-size: 24px;
18 | font-weight: 600;
19 | text-transform: none;
20 | vertical-align: middle;
21 | }
22 |
23 | .icon {
24 | font-size: 36px;
25 | }
26 |
27 | ion-label.label {
28 | padding: 1rem;
29 | margin: 1rem;
30 | }
31 | }
32 |
33 | // existing projects
34 | ion-item.item-block {
35 | background-color: $primary-orange;
36 | border-bottom: 1px solid $primary-orange;
37 | color: white;
38 | }
39 | }
40 | }
41 |
42 | ion-item.item.item-block.new-connection {
43 | margin-top: 20px;
44 | border: 2px solid #c94313;
45 | }
46 |
47 | .connections-header {
48 | display: grid;
49 | color: white;
50 | grid-template-columns: 0.75fr 1fr;
51 | margin: 10px;
52 | background: rgba(241, 101, 45, 0.65);
53 | border-radius: 5px;
54 |
55 | ion-label.label {
56 | margin: 0;
57 | padding: 4px;
58 | }
59 | }
60 |
61 | .connection-item {
62 | display: grid;
63 | color: white;
64 | grid-template-columns: 0.75fr 0.75fr;
65 |
66 | ion-label.label {
67 | margin: 0;
68 | }
69 | }
70 |
71 | // custom buttons for android and windows
72 | .button-md,
73 | .button-wp {
74 | border-radius: 11px;
75 | &.existing-button span {
76 | padding-left: 8px;
77 | padding-right: 6px;
78 | }
79 | span {
80 | padding-left: 3px;
81 | padding-right: 2px;
82 | }
83 | }
84 |
85 | // custom buttons for ios
86 | .button-ios {
87 | border: none;
88 | }
89 |
90 | .faq {
91 | margin: 2rem 1rem;
92 |
93 | ion-list-header.list-header {
94 | border-bottom: 2px solid $primary-orange;
95 | }
96 |
97 | p,
98 | h2 {
99 | color: $primary-orange;
100 | }
101 |
102 | ion-item.item {
103 | h2 {
104 | font-weight: bold;
105 | }
106 | p {
107 | color: $primary-orange;
108 | white-space: initial;
109 | font-size: 13px;
110 | }
111 | }
112 | }
113 |
114 | .logout {
115 | margin: 2rem 0;
116 |
117 | button {
118 | width: 60%;
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/features/dashboard/store/dashboard.reducer.ts:
--------------------------------------------------------------------------------
1 | import { createSelector, createFeatureSelector } from "@ngrx/store";
2 | import { EntityState, EntityAdapter, createEntityAdapter } from "@ngrx/entity";
3 |
4 | import { DashboardActions, DashboardActionTypes } from "./dashboard.actions";
5 | import { HistoryItem } from "../../../models/history-item";
6 |
7 | export enum DashboardUiState {
8 | LOADING = "Loading",
9 | LOADED = "Loaded",
10 | NOT_LOADED = "Not Loaded"
11 | }
12 |
13 | export interface DashboardState extends EntityState {
14 | uiState: DashboardUiState;
15 | error: string;
16 | }
17 |
18 | export const adapter: EntityAdapter = createEntityAdapter<
19 | HistoryItem
20 | >();
21 |
22 | export const initialDashboardState: DashboardState = adapter.getInitialState({
23 | uiState: DashboardUiState.NOT_LOADED,
24 | error: null
25 | });
26 |
27 | export function dashboardReducer(
28 | state = initialDashboardState,
29 | action: DashboardActions
30 | ): DashboardState {
31 | switch (action.type) {
32 | case DashboardActionTypes.LOAD_HISTORY: {
33 | return adapter.removeAll({ ...state, uiState: DashboardUiState.LOADING });
34 | }
35 |
36 | case DashboardActionTypes.LOAD_MORE_HISTORY: {
37 | return {
38 | ...state,
39 | uiState: DashboardUiState.LOADING
40 | };
41 | }
42 |
43 | case DashboardActionTypes.NO_HISTORY: {
44 | return {
45 | ...state,
46 | uiState: DashboardUiState.LOADED
47 | };
48 | }
49 |
50 | case DashboardActionTypes.ADDED: {
51 | return adapter.upsertOne(action.payload.historyItem, {
52 | ...state,
53 | uiState: DashboardUiState.LOADED
54 | });
55 | }
56 |
57 | case DashboardActionTypes.MODIFIED: {
58 | return adapter.updateOne(action.payload.update, state);
59 | }
60 |
61 | case DashboardActionTypes.REMOVED: {
62 | return adapter.removeOne(action.payload.historyItemId, {
63 | ...state,
64 | uiState: DashboardUiState.LOADED
65 | });
66 | }
67 |
68 | case DashboardActionTypes.DELETE_SESSION: {
69 | return {
70 | ...state,
71 | uiState: DashboardUiState.LOADING
72 | };
73 | }
74 |
75 | case DashboardActionTypes.CLEAR_ERROR: {
76 | return {
77 | ...state,
78 | error: null
79 | };
80 | }
81 |
82 | default: {
83 | return state;
84 | }
85 | }
86 | }
87 |
88 | export namespace DashboardQuery {
89 | const selectSlice = createFeatureSelector("dashboard");
90 | export const { selectIds, selectEntities, selectAll } = adapter.getSelectors(
91 | selectSlice
92 | );
93 | export const selectUiState = createSelector(
94 | selectSlice,
95 | state => state.uiState
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 |
4 | match /public/data/sessions/{sessionUrl} {
5 | function isParticipant() {
6 | return request.auth.uid in resource.data.participants;
7 | }
8 |
9 | // Allow authorized users to fetch session links
10 | allow read: if isParticipant();
11 | allow list: if isParticipant() && request.query.limit <= 10;
12 |
13 | // Writing happens through the Cloud API when creating sessions
14 | // for Access Code & Session URL generation
15 | // allow write: if never --;
16 | }
17 |
18 | match /users/{userId} {
19 | // Allow users to read and write to their own profile
20 | allow read: if request.auth.uid == userId;
21 |
22 | // TODO: Should any user profile data be written from the client side?
23 | // allow write: if ??;
24 |
25 | match /connections/{connectionId} {
26 | // Allow users to read their own connections
27 | allow read: if request.auth.uid == userId;
28 |
29 | // Writing happens through the Cloud API for token validation purposes
30 | // allow write: if never --;
31 |
32 | match /sessions/{sessionId} {
33 | // Helper function to access session in child rules
34 | function getSessionData() {
35 | return get(/databases/$(database)/documents/users/$(userId)/connections/$(connectionId)/sessions/$(sessionId)).data;
36 | }
37 |
38 | // Helper function to access session participants
39 | function getSessionParticipants() {
40 | return get(/databases/$(database)/documents/public/data/sessions/$(getSessionData().sessionCode)).data.participants;
41 | }
42 |
43 | // Allow session participants to view session data
44 | allow read: if request.auth.uid in getSessionParticipants();
45 |
46 | // TODO: Should any session data be written from the client side?
47 | // allow write: if ??
48 |
49 | match /tasks/{taskId} {
50 | // Helper function to validate vote hours
51 | function isVoteValid() {
52 | // -1 = 'not applicable', 0 = 'More info needed', 'value > 0' is estimate
53 | return !math.isNaN(request.resource.data.votes[request.auth.uid]);
54 | }
55 |
56 | // Allow session participants to view session tasks
57 | allow read: if request.auth.uid in getSessionParticipants();
58 |
59 | // Allow users to register ONLY their OWN votes on tasks
60 | allow update: if request.writeFields.size() == 1 && isVoteValid();
61 | }
62 |
63 | }
64 |
65 | }
66 |
67 | }
68 |
69 | match /faqs/{faqId} {
70 | allow read: if request.auth.uid != null;
71 | }
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------