├── .bowerrc ├── .dockerignore ├── .editorconfig ├── .ember-cli ├── .eslintrc.yml ├── .gitignore ├── .ignore ├── .npmrc ├── .tool-versions ├── .travis.yml ├── .watchmanconfig ├── Dockerfile ├── LICENSE ├── Procfile.dev ├── ProcfileElectron.dev ├── ProcfileElectronBuild.dev ├── README.md ├── app.json ├── app ├── adapters │ ├── account.js │ ├── application.js │ ├── block.js │ ├── canvas.js │ ├── comment.js │ ├── session.js │ ├── slack-channel.js │ ├── team.js │ ├── thread-subscription.js │ ├── unfurl.js │ ├── upload-signature.js │ └── user.js ├── app.js ├── components │ ├── .gitkeep │ ├── add-to-slack │ │ ├── component.js │ │ └── template.hbs │ ├── canvas-block-filter │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-channel-list │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-link │ │ ├── component.js │ │ └── template.hbs │ ├── canvas-list-empty-state │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-list-filter │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-list-item │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-list │ │ ├── component.js │ │ └── template.hbs │ ├── canvas-pulse-item │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-pulse │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-title-actions │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── canvas-title-byline │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── channel-list-add-to-slack │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── channel-topic-canvas │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── comment-thread │ │ ├── component.js │ │ ├── form │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── item │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── styles.css │ │ └── template.hbs │ ├── drop-zone │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── github-oauth │ │ ├── component.js │ │ └── template.hbs │ ├── intercom-launcher │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── new-canvas-button │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── personal-team-form │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── pulse-settings │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── routes │ │ ├── canvas-history │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── canvas-pulse │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── canvas-settings │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── canvas-show │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── login-x │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── setup-x │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── team-index │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ └── team-settings │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ ├── sign-in-with-slack │ │ ├── component.js │ │ └── template.hbs │ ├── slack-oauth │ │ ├── component.js │ │ └── template.hbs │ ├── team-export │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── team-list-item │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── team-list-wrapper │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── team-list │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── team-nav │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── ui-billboard │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── ui-flash-message │ │ ├── component.js │ │ ├── styles.css │ │ └── template.hbs │ ├── ui-list │ │ ├── component.js │ │ ├── item │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ └── template.hbs │ ├── ui-menu │ │ ├── component.js │ │ ├── item │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ ├── styles.css │ │ └── template.hbs │ ├── ui-settings-list │ │ ├── component.js │ │ ├── item │ │ │ ├── component.js │ │ │ ├── styles.css │ │ │ └── template.hbs │ │ └── template.hbs │ └── yielding-link-to │ │ ├── component.js │ │ └── template.hbs ├── controllers │ ├── .gitkeep │ ├── not-found.js │ └── team │ │ ├── canvas │ │ ├── history.js │ │ └── show.js │ │ └── index.js ├── helpers │ ├── .gitkeep │ ├── diff.js │ ├── is-dismissed.js │ └── symmetric-diff.js ├── index.html ├── initializers │ ├── error-handling.js │ └── sharedb-fuzz.js ├── lib │ ├── copy-text.js │ ├── key.js │ ├── ns-events.js │ ├── op-application.js │ ├── op-manager.js │ ├── phoenix.js │ ├── preload.js │ ├── sharedb-path.js │ └── undo-manager.js ├── mixins │ ├── channel-ids.js │ ├── oauth.js │ ├── ui-dropdown.js │ └── with-dropzone.js ├── models │ ├── .gitkeep │ ├── account.js │ ├── block.js │ ├── canvas-watch.js │ ├── canvas.js │ ├── comment.js │ ├── custom-inflector-rules.js │ ├── op.js │ ├── pulse-event.js │ ├── slack-channel.js │ ├── team.js │ ├── thread-subscription.js │ ├── token.js │ ├── ui-dismissal.js │ ├── unfurl.js │ ├── upload-signature.js │ └── user.js ├── resolver.js ├── router.js ├── routes │ ├── .gitkeep │ ├── application.js │ ├── index.js │ ├── logout.js │ ├── post-auth.js │ ├── setup.js │ ├── team.js │ └── team │ │ ├── canvas.js │ │ ├── canvas │ │ ├── history.js │ │ ├── pulse.js │ │ ├── settings.js │ │ └── show.js │ │ ├── index.js │ │ └── settings.js ├── serializers │ ├── application.js │ ├── canvas.js │ ├── comment.js │ └── thread-subscription.js ├── services │ ├── csrf-token.js │ ├── current-account.js │ ├── desktop-menus.js │ ├── dexie.js │ ├── phoenix-socket.js │ ├── team-query.js │ ├── teams-list.js │ ├── ui-dismissals.js │ └── unfurler.js ├── styles │ ├── app.css │ ├── application.css │ ├── ember-basic-dropdown.css │ ├── team │ │ └── canvas.css │ └── utilities.css └── templates │ ├── application.hbs │ ├── beta.hbs │ ├── components │ └── .gitkeep │ ├── login.hbs │ ├── logout.hbs │ ├── not-found.hbs │ ├── post-auth.hbs │ ├── server-error.hbs │ ├── setup.hbs │ └── team │ ├── canvas.hbs │ ├── canvas │ ├── history.hbs │ ├── pulse.hbs │ ├── settings.hbs │ └── show.hbs │ ├── index.hbs │ ├── settings.hbs │ └── slack.hbs ├── assets └── icons │ ├── Canvas-macOS.png │ ├── Canvas.icns │ └── make-iconset.sh ├── bower.json ├── circle.yml ├── config └── environment.js ├── electron.js ├── ember-cli-build.js ├── mirage ├── .eslintrc.yml ├── config.js ├── factories │ ├── account.js │ ├── team.js │ └── user.js ├── models │ ├── account.js │ ├── canvas-watch.js │ ├── comment.js │ ├── team.js │ ├── thread-subscription.js │ ├── ui-dismissal.js │ └── user.js ├── scenarios │ └── default.js └── serializers │ └── application.js ├── package.json ├── public ├── canvas │ └── slack-mark.svg ├── crossdomain.xml ├── images │ └── personal-notes-avatar.png ├── primaries │ ├── Activity.svg │ ├── Add.svg │ ├── AlignLeft.svg │ ├── ArrowLeft.svg │ ├── Bookmark.svg │ ├── Branch.svg │ ├── Close.svg │ ├── Delete.svg │ ├── Document.svg │ ├── Error.svg │ ├── Gear.svg │ ├── Globe.svg │ ├── Help.svg │ ├── Home.svg │ ├── More.svg │ ├── Notification.svg │ ├── Receipt.svg │ └── SlidersVertical.svg └── robots.txt ├── static.json ├── testem.js ├── tests ├── .eslintrc.yml ├── acceptance │ ├── .eslintrc.yml │ ├── logged-out-test.js │ └── needs-slack-id-test.js ├── electron.js ├── helpers │ ├── destroy-app.js │ ├── flash-message.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ ├── add-to-slack │ │ └── component-test.js │ │ ├── canvas-block-actions │ │ └── component-test.js │ │ ├── canvas-block-filter │ │ └── component-test.js │ │ ├── canvas-channel-list │ │ └── component-test.js │ │ ├── canvas-list-filter │ │ └── component-test.js │ │ ├── canvas-pulse-item │ │ └── component-test.js │ │ ├── canvas-pulse │ │ └── component-test.js │ │ ├── channel-list-add-to-slack │ │ └── component-test.js │ │ ├── channel-topic-canvas │ │ └── component-test.js │ │ ├── comment-thread │ │ ├── component-test.js │ │ └── item │ │ │ └── component-test.js │ │ ├── drop-zone │ │ └── component-test.js │ │ ├── github-oauth │ │ └── component-test.js │ │ ├── intercom-launcher │ │ └── component-test.js │ │ ├── personal-team-form │ │ └── component-test.js │ │ ├── pulse-settings │ │ └── component-test.js │ │ ├── routes │ │ ├── canvas-history │ │ │ └── component-test.js │ │ ├── canvas-pulse │ │ │ └── component-test.js │ │ ├── canvas-settings │ │ │ └── component-test.js │ │ ├── canvas-show │ │ │ └── component-test.js │ │ └── setup-x │ │ │ └── component-test.js │ │ ├── sign-in-with-slack │ │ └── component-test.js │ │ ├── team-list-wrapper │ │ └── component-test.js │ │ ├── ui-list │ │ ├── component-test.js │ │ └── item │ │ │ └── component-test.js │ │ └── ui-menu │ │ ├── component-test.js │ │ └── item │ │ └── component-test.js ├── package.json ├── test-helper.js └── unit │ ├── .gitkeep │ ├── adapters │ ├── account-test.js │ ├── canvas-test.js │ └── user-test.js │ ├── controllers │ └── team │ │ └── index-test.js │ ├── initializers │ ├── error-handling-test.js │ └── sharedb-fuzz-test.js │ ├── mixins │ ├── oauth-test.js │ └── with-dropzone-test.js │ ├── models │ ├── account-test.js │ ├── canvas-test.js │ ├── op-test.js │ ├── pulse-event-test.js │ ├── team-test.js │ ├── unfurl-test.js │ └── user-test.js │ ├── routes │ ├── application-test.js │ ├── index-test.js │ ├── logout-test.js │ └── post-auth-test.js │ └── services │ ├── csrf-token-test.js │ ├── current-account-test.js │ ├── desktop-menus-test.js │ ├── team-query-test.js │ ├── teams-list-test.js │ └── unfurler-test.js └── vendor ├── .gitkeep ├── diff-match-patch.js ├── normalize.css ├── sharedb.js └── shims ├── dexie.js ├── diff-match-patch.js ├── js-cookie.js ├── qs.js ├── raven.js ├── reconnecting-websocket.js └── sharedb.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /electron-builds 6 | /tmp 7 | 8 | # dependencies 9 | /node_modules 10 | /bower_components 11 | 12 | # misc 13 | /.env 14 | /.env.* 15 | /.prod.env 16 | /.sass-cache 17 | /connect.lock 18 | /coverage/* 19 | /libpeerconnection.log 20 | npm-debug.log 21 | testem.log 22 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | public/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 7.5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | before_install: 13 | - npm config set spin false 14 | - npm install -g bower 15 | - bower --version 16 | - npm install phantomjs-prebuilt 17 | - node_modules/phantomjs-prebuilt/bin/phantomjs --version 18 | 19 | install: 20 | - npm install 21 | - bower install 22 | 23 | script: 24 | - npm test 25 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM buildpack-deps:xenial 2 | 3 | RUN locale-gen en_US.UTF-8 4 | ENV LANG=en_US.UTF-8 5 | 6 | # Install Node.js 7 | WORKDIR /tmp 8 | RUN wget https://nodejs.org/dist/v7.3.0/node-v7.3.0-linux-x64.tar.xz && \ 9 | tar -xJf node-v7.3.0-linux-x64.tar.xz -C /usr/local --strip-components=1 10 | RUN rm -r /tmp/node-v7.3.0-linux-x64.tar.xz 11 | 12 | # Install Ruby & foreman 13 | RUN apt-get update 14 | RUN apt-get install -y ruby-full 15 | RUN gem install foreman 16 | 17 | ARG NPM_TOKEN 18 | ENV NPM_TOKEN=$NPM_TOKEN 19 | 20 | ADD . /app 21 | WORKDIR /app 22 | 23 | # Install app dependencies 24 | RUN npm install 25 | RUN npm install -g bower 26 | RUN bower install --allow-root 27 | 28 | CMD foreman start -f Procfile.dev 29 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/ember server --proxy http://${PROXY_API_HOST:-localhost}:4000 --live-reload-port 49152 2 | -------------------------------------------------------------------------------- /ProcfileElectron.dev: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/ember electron 2 | -------------------------------------------------------------------------------- /ProcfileElectronBuild.dev: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/ember electron:package --platform=darwin --name=Canvas --app-bundle-id=com.usecanvas.canvas-electron --icon=./assets/icons/Canvas.icns --overwrite 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Canvas Web [![CircleCI][circle_ci_badge]][circle_ci_url] [![Deploy][heroku_button_svg]][heroku_deploy] 2 | 3 | This is the Canvas web app. It communicates both with the 4 | [Canvas API][canvas_api] and the [Canvas Realtime][canvas_realtime] apps. 5 | 6 | ## Running on Heroku 7 | 8 | After deploying the [API][canvas_api] and [Realtime][canvas_realtime] apps, 9 | deploy this app to Heroku using the Heroku button in this README. 10 | 11 | [canvas_api]: https://github.com/usecanvas/pro-api 12 | [canvas_realtime]: https://github.com/usecanvas/pro-realtime 13 | [circle_ci_badge]: https://circleci.com/gh/usecanvas/pro-web.svg?style=svg&circle-token=1179a600a98c51819d50e3aa6c9124323da53593 14 | [circle_ci_url]: https://circleci.com/gh/usecanvas/pro-web 15 | [heroku_button_svg]: https://www.herokucdn.com/deploy/button.svg 16 | [heroku_deploy]: https://heroku.com/deploy?template=https://github.com/usecanvas/web-v2 17 | -------------------------------------------------------------------------------- /app/adapters/account.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForQueryRecord() { 5 | return `${this.urlPrefix()}/account`; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/adapters/block.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | import Ember from 'ember'; 3 | 4 | export default ApplicationAdapter.extend({ 5 | /** 6 | * Return a payload client-side so block finds will not fail when 7 | * the comment model requests the block through its relationship 8 | */ 9 | findRecord(_store, _type, id) { 10 | return Ember.RSVP.resolve({ data: { id, type: 'block' } }); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /app/adapters/canvas.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | import Ember from 'ember'; 3 | 4 | const { get } = Ember; 5 | 6 | export default ApplicationAdapter.extend({ 7 | urlForCreateRecord(modelName, snapshot) { 8 | const teamID = snapshot.record.get('team.id'); 9 | return `${this.urlPrefix()}/teams/${teamID}/canvases`; 10 | }, 11 | 12 | urlForDeleteRecord() { 13 | return this.urlForFindRecord(...arguments); 14 | }, 15 | 16 | urlForFindRecord(id, modelName, snapshot) { 17 | const teamID = get(snapshot, 'record.team.id') || 18 | get(snapshot, 'adapterOptions.team.id'); 19 | return `${this.urlPrefix()}/teams/${teamID}/canvases/${id}`; 20 | }, 21 | 22 | urlForUpdateRecord(id, modelName, snapshot) { 23 | snapshot.adapterOptions = { team: snapshot.record.get('team') }; 24 | return this.urlForFindRecord(...arguments); 25 | }, 26 | 27 | /** 28 | * Update the template of a canvas. 29 | * 30 | * @method 31 | * @param {CanvasWeb.Canvas} canvas The canvas to update the template of 32 | * @param {string} templateID The ID of the template 33 | * @returns {Promise} A promise resolved when the update finishes or fails 34 | */ 35 | updateTemplate(canvas, templateID) { 36 | const url = this.urlForFindRecord( 37 | canvas.get('id'), 38 | canvas.constructor.modelName, 39 | { adapterOptions: { team: canvas.get('team') } }); 40 | 41 | const data = { 42 | data: { 43 | attributes: {}, 44 | relationships: { 45 | template: { data: { id: templateID, type: 'canvas' } } 46 | } 47 | } 48 | }; 49 | 50 | return this.ajax(url, 'PATCH', { data }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /app/adapters/comment.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForQuery({ filter: { canvas, block } }) { 5 | const canvasId = canvas.get('id'); 6 | let url = `${this.urlPrefix()}/comments?filter[canvas.id]=${canvasId}`; 7 | if (block) { 8 | const blockId = block.get('id'); 9 | url += `&filter[block.id]=${blockId}`; 10 | } 11 | return url; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/adapters/session.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | export default ApplicationAdapter.extend({ 3 | logout() { 4 | return this.ajax(`${this.urlPrefix()}/session`, 'DELETE'); 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /app/adapters/slack-channel.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForFindAll(modelName, snapshot) { 5 | const teamID = snapshot.adapterOptions.team.get('id'); 6 | return `${this.urlPrefix()}/teams/${teamID}/slack/channels`; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /app/adapters/team.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForQueryRecord({ filter: { domain } }) { 5 | return `${this.urlPrefix()}/teams/${domain}`; 6 | }, 7 | 8 | fetchTemplates(team) { 9 | const url = `${this.urlPrefix()}/teams/${team.get('id')}/templates`; 10 | return this.ajax(url, 'GET', {}); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /app/adapters/thread-subscription.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | createRecord() { 5 | return this.updateRecord(...arguments); 6 | }, 7 | 8 | // Use PUT, the API for thread subs is create-or-replace. 9 | updateRecord(store, type, snapshot) { 10 | const data = {}; 11 | const serializer = store.serializerFor(type.modelName); 12 | const id = snapshot.id; 13 | const url = this.buildURL(type.modelName, id, snapshot, 'updateRecord'); 14 | 15 | serializer.serializeIntoHash(data, type, snapshot); 16 | return this.ajax(url, 'PUT', { data }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/adapters/unfurl.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | import Ember from 'ember'; 3 | import RSVP from 'rsvp'; 4 | 5 | const { run } = Ember; 6 | 7 | export default ApplicationAdapter.extend({ 8 | // coalesceFindRequests: true, 9 | 10 | /** 11 | * Find multiple models by using the bulk endpoint. 12 | * 13 | * @method 14 | * @override 15 | */ 16 | findMany(_store, _type, ids) { 17 | return new RSVP.Promise((resolve, reject) => { 18 | Ember.$.ajax({ 19 | type: 'POST', 20 | url: `${this.urlPrefix()}/bulk`, 21 | contentType: 'application/json', 22 | headers: this.get('headers'), 23 | data: this.prepareBulkData(ids) 24 | }).then( 25 | ({ data }) => this.handleBulkSuccess(resolve, data), 26 | jqXHR => this.handleBulkFailure(reject, jqXHR) 27 | ); 28 | }); 29 | }, 30 | 31 | prepareBulkData(ids) { 32 | return JSON.stringify({ 33 | data: ids.map(id => { 34 | return { method: 'GET', path: this.urlForFindRecord(id) }; 35 | }) 36 | }); 37 | }, 38 | 39 | handleBulkSuccess(resolve, data) { 40 | run(null, resolve, { data: data.map(datum => datum.body.data) }); 41 | }, 42 | 43 | handleBulkFailure(reject, jqXHR) { 44 | jqXHR.then = null; 45 | run(null, reject, jqXHR); 46 | }, 47 | 48 | urlForFindRecord(url) { 49 | return `${this.urlPrefix()}/unfurls?url=${encodeURIComponent(url)}`; 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /app/adapters/upload-signature.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForFindRecord() { 5 | return `${this.urlPrefix()}/upload-signature`; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/adapters/user.js: -------------------------------------------------------------------------------- 1 | import ApplicationAdapter from './application'; 2 | 3 | export default ApplicationAdapter.extend({ 4 | urlForQueryRecord(teamID) { 5 | return `${this.urlPrefix()}/teams/${teamID}/user`; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | import 'canvas-web/models/custom-inflector-rules'; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | const App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/add-to-slack/component.js: -------------------------------------------------------------------------------- 1 | import SlackOAuth from 'canvas-web/components/slack-oauth/component'; 2 | 3 | export default SlackOAuth.extend({}); 4 | -------------------------------------------------------------------------------- /app/components/add-to-slack/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if hasBlock}} 3 | {{yield}} 4 | {{else}} 5 | Add to Slack 6 | {{/if}} 7 | 8 | -------------------------------------------------------------------------------- /app/components/canvas-block-filter/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Key from 'canvas-web/lib/key'; 3 | 4 | const { on } = Ember; 5 | 6 | export default Ember.Component.extend({ 7 | filterTerm: '', 8 | inputSelector: '.js-input', 9 | localClassNames: ['canvas-block-filter'], 10 | 11 | closeFilter() { 12 | this.get('onCloseFilter')(); 13 | this.set('filterTerm', ''); 14 | }, 15 | 16 | focusInput: on('didInsertElement', function() { 17 | this.$(`${this.get('inputSelector')}`).focus(); 18 | }), 19 | 20 | keyDown(evt) { 21 | const key = new Key(evt); 22 | if (key.is('esc')) { 23 | this.closeFilter(); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/canvas-block-filter/styles.css: -------------------------------------------------------------------------------- 1 | .canvas-block-filter { 2 | align-items: center; 3 | background: rgba(0, 0, 0, 0.8); 4 | border-radius: 4px; 5 | color: white; 6 | display: flex; 7 | padding: 0.75rem 1rem; 8 | } 9 | 10 | .input { 11 | background: transparent; 12 | border: none; 13 | color: inherit; 14 | flex: 1; 15 | font-size: 1.125rem; 16 | outline: none; 17 | padding: 0; 18 | } 19 | 20 | .close { 21 | composes: button-reset from 'canvas-web/styles/utilities'; 22 | height: 1.5rem; 23 | width: 1.5rem; 24 | } 25 | 26 | .close-icon { 27 | fill: white; 28 | } 29 | -------------------------------------------------------------------------------- /app/components/canvas-block-filter/template.hbs: -------------------------------------------------------------------------------- 1 | {{input class='js-input' 2 | local-class='input' 3 | placeholder='Filter canvas...' 4 | value=filterTerm 5 | type='text'}} 6 | 7 | -------------------------------------------------------------------------------- /app/components/canvas-channel-list/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | channelIds: computed(_ => []), 7 | localClassNames: ['canvas-channel-list'], 8 | isEditingChannels: false, 9 | 10 | canvasChannels: computed('channelIds.[]', 'channels.[]', function() { 11 | return this.get('channelIds') 12 | .map(id => this.get('channels').findBy('id', id)) 13 | .compact(); 14 | }), 15 | 16 | selectedChannels: computed(function() { 17 | return this.get('canvasChannels'); 18 | }), 19 | 20 | actions: { 21 | persistChannels() { 22 | this.set('isEditingChannels', false); 23 | this.get('updateCanvasChannels')(this.get('selectedChannels')); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/canvas-channel-list/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --dark-gray from 'canvas-colors/styles/colors'; 3 | @value --gray from 'canvas-colors/styles/colors'; 4 | @value --black from 'canvas-colors/styles/colors'; 5 | 6 | .channel-summary-list, 7 | .edit-button, 8 | .done-button { 9 | margin-left: 0.5rem; 10 | } 11 | 12 | .edit-button, 13 | .done-button { 14 | composes: button-reset from 'canvas-web/styles/utilities'; 15 | color: --blue; 16 | cursor: pointer; 17 | } 18 | 19 | .edit-button { 20 | transition: opacity 0.2s ease-in-out; 21 | } 22 | 23 | .edit-button--hidden { 24 | opacity: 0; 25 | } 26 | 27 | .channel-editor { 28 | display: flex; 29 | } 30 | 31 | .channel-editor-select { 32 | flex: 1; 33 | } 34 | 35 | .icon { 36 | fill: --gray; 37 | } 38 | 39 | .channel-summary { 40 | align-items: center; 41 | display: inline-flex; 42 | } 43 | 44 | .channel-summary-list-item:link, 45 | .channel-summary-list-item:visited { 46 | color: --dark-gray; 47 | font-weight: 600; 48 | } 49 | 50 | .channel-summary-list-item:hover { 51 | color: --black; 52 | } 53 | 54 | .channel-summary-list-item::after { 55 | color: --gray; 56 | content: ', '; 57 | font-weight: normal; 58 | } 59 | 60 | .channel-summary-list-item:last-child::after { 61 | content: ''; 62 | } 63 | 64 | .channel-summary:hover .edit-button { 65 | opacity: 1; 66 | } 67 | 68 | .button-label--muted { 69 | color: --gray; 70 | } 71 | -------------------------------------------------------------------------------- /app/components/canvas-channel-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if isEditingChannels}} 2 |
3 |
4 | {{#power-select-multiple 5 | options=channels 6 | placeholder='Add channels...' 7 | searchField='name' 8 | selected=selectedChannels 9 | onchange=(action (mut selectedChannels)) 10 | as |channel|}} 11 | {{channel.name}} 12 | {{/power-select-multiple}} 13 |
14 | 15 | 16 |
17 | {{else}} 18 |
19 | {{svg-jar 'slack-mark' height='16' width='16' local-class='icon'}} 20 | 21 | {{#if canvasChannels}} 22 |
23 | {{#each canvasChannels as |channel|}} 24 | {{link-to channel.name 'team' (query-params channel=channel.name) 25 | local-class='channel-summary-list-item'}} 26 | {{/each}} 27 |
28 | {{/if}} 29 | 30 | 39 |
40 | {{/if}} 41 | -------------------------------------------------------------------------------- /app/components/canvas-link/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Qs from 'qs'; 3 | 4 | const { computed } = Ember; 5 | 6 | export default Ember.Component.extend({ 7 | blockID: computed.oneWay('query.block'), 8 | filter: computed.oneWay('query.filter'), 9 | host: window.location.host, 10 | protocol: window.location.protocol, 11 | tagName: '', 12 | 13 | canvasID: computed('parsedURL', function() { 14 | return this.get('parsedURL.pathname') 15 | .split('/') 16 | .reject(part => !part) 17 | .get('lastObject'); 18 | }), 19 | 20 | parsedURL: computed('url', function() { 21 | const link = document.createElement('a'); 22 | link.href = this.get('url'); 23 | return link; 24 | }), 25 | 26 | query: computed('parsedURL', function() { 27 | return Qs.parse(this.get('parsedURL.search').slice(1)); 28 | }) 29 | }); 30 | -------------------------------------------------------------------------------- /app/components/canvas-link/template.hbs: -------------------------------------------------------------------------------- 1 | {{#yielding-link-to 'team.canvas.show' canvasID 2 | (if (and blockID filter) 3 | (query-params block=blockID filter=filter) 4 | (if blockID 5 | (query-params block=blockID) 6 | (if filter 7 | (query-params filter=filter) 8 | (query-params)))) 9 | class=class as |link|}} 10 | {{#if hasBlock}} 11 | {{yield}} 12 | {{else}} 13 | {{protocol}}//{{host}}{{link.href}} 14 | {{/if}} 15 | {{/yielding-link-to}} 16 | -------------------------------------------------------------------------------- /app/components/canvas-list-empty-state/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-list-empty-state'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/canvas-list-empty-state/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --dark-gray from 'canvas-colors/styles/colors'; 3 | 4 | .canvas-list-empty-state { 5 | color: --dark-gray; 6 | padding: 2rem 0; 7 | } 8 | 9 | .icon { 10 | background: --blue; 11 | border-radius: 50%; 12 | fill: white; 13 | vertical-align: middle; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/canvas-list-empty-state/template.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='👋' title='Welcome to Canvas'}} 2 |

3 | Click on the 4 | {{new-canvas-button 5 | channel=channel 6 | inline=true 7 | team=team 8 | didCreateCanvas=didCreateCanvas}} 9 | icon to create your team's first canvas.
10 | Try typing Getting Started in the title... 11 |

12 | {{/ui-billboard}} 13 | -------------------------------------------------------------------------------- /app/components/canvas-list-filter/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-list-filter'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/canvas-list-filter/styles.css: -------------------------------------------------------------------------------- 1 | @value --black from 'canvas-colors/styles/colors'; 2 | @value --blue from 'canvas-colors/styles/colors'; 3 | @value --dark-gray from 'canvas-colors/styles/colors'; 4 | @value --gray from 'canvas-colors/styles/colors'; 5 | 6 | .canvas-list-filter { 7 | font-size: 0.875rem; 8 | } 9 | 10 | .section { 11 | margin: 0 0 1.5rem; 12 | } 13 | 14 | .heading { 15 | color: --gray; 16 | font-size: 0.7142857143em; 17 | font-weight: normal; 18 | margin: 0 0 0.5rem; 19 | text-transform: uppercase; 20 | } 21 | 22 | .item { 23 | border-radius: 4px; 24 | color: --dark-gray; 25 | display: block; 26 | font-weight: 500; 27 | padding: 0.25rem 0; 28 | } 29 | 30 | .item:hover { 31 | color: --black; 32 | } 33 | 34 | .item--large { 35 | font-size: 1rem; 36 | margin-top: -0.25rem; 37 | } 38 | 39 | .item--selected { 40 | color: --black; 41 | font-weight: 600; 42 | position: relative; 43 | } 44 | 45 | .item--selected::before { 46 | background: --blue; 47 | border-bottom-right-radius: 2px; 48 | border-top-right-radius: 2px; 49 | bottom: 0; 50 | content: ""; 51 | left: -1rem; 52 | position: absolute; 53 | top: 0; 54 | width: 3px; 55 | } 56 | 57 | .channel-prefix { 58 | color: --gray; 59 | } 60 | -------------------------------------------------------------------------------- /app/components/canvas-list-filter/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{link-to 'All Canvases' 'team' (query-params channel=null) 3 | local-class=(concat 'item item--large ' (unless channel 'item--selected'))}} 4 |
5 | 6 |
7 |
Channels
8 | 9 | {{#each channels as |channelItem|}} 10 | {{#link-to 'team' (query-params channel=channelItem.name) 11 | local-class=(concat 'item ' (if (eq channelItem.name channel) 'item--selected'))}} 12 | # {{channelItem.name}} 13 | {{/link-to}} 14 | {{/each}} 15 |
16 | -------------------------------------------------------------------------------- /app/components/canvas-list-item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-list-item'], 5 | localClassNameBindings: ['canvas.isTemplate:is-template'], 6 | 7 | actions: { 8 | onClickDelete(evt) { 9 | evt.stopPropagation(); 10 | evt.preventDefault(); 11 | this.get('onDeleteCanvas')(this.get('canvas')); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/components/canvas-list-item/template.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to 'team.canvas.show' canvas local-class='link'}} 2 |
3 | {{svg-jar 'Document' local-class='icon'}} 4 | 5 |
6 |
{{canvas.title}} {{#if canvas.isTemplate}}Template{{/if}}
7 |
{{canvas.summary}}
8 |
9 | 10 |
11 | 14 |
15 |
16 | {{/link-to}} -------------------------------------------------------------------------------- /app/components/canvas-list/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | sortedCanvases: computed.sort('canvases', function(canvasA, canvasB) { 7 | const timeA = canvasA.get('editedAt') || canvasA.get('updatedAt'); 8 | const timeB = canvasB.get('editedAt') || canvasB.get('updatedAt'); 9 | 10 | return timeB - timeA; 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /app/components/canvas-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each sortedCanvases as |canvas|}} 2 |
3 | {{canvas-list-item 4 | canvas=canvas 5 | onDeleteCanvas=(route-action 'onDeleteCanvas')}} 6 |
7 | {{else}} 8 |
9 | {{canvas-list-empty-state 10 | channel=channel 11 | team=team 12 | didCreateCanvas=didCreateCanvas}} 13 |
14 | {{/each}} 15 | -------------------------------------------------------------------------------- /app/components/canvas-pulse-item/styles.css: -------------------------------------------------------------------------------- 1 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | @value --light-gray from 'canvas-colors/styles/colors'; 4 | 5 | .container { 6 | display: flex; 7 | } 8 | 9 | .icon { 10 | background: white; 11 | outline: 2px solid white; 12 | display: block; 13 | fill: --gray; 14 | margin-right: 0.25rem; 15 | } 16 | 17 | .main { 18 | flex: 1; 19 | } 20 | 21 | .summary { 22 | line-height: 1.5; 23 | } 24 | 25 | .attachment { 26 | margin-top: 0.5rem; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/canvas-pulse-item/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{svg-jar iconName height=24 width=24 local-class='icon'}} 3 | 4 |
5 |
6 | {{#if pulseEvent.referencer.name}} 7 | {{#if pulseEvent.referencer.url}} 8 | {{pulseEvent.referencer.name}} 9 | {{else}} 10 | {{pulseEvent.referencer.name}} 11 | {{/if}} 12 | {{/if}} 13 | 14 | {{actionString}}{{#unless hideProvider}} on {{pulseEvent.providerName}}{{/unless}}. 15 |
16 | 17 | {{#unless failedCanvasUnfurl}} 18 | {{#if (and pulseEvent.url (not hideUnfurl))}} 19 |
20 | {{canvas-block-url 21 | block=pulseEvent 22 | canvasLinkComponent=(component 'canvas-link') 23 | githubAuthComponent=(component 'github-oauth') 24 | unfurl=(action 'unfurlEvent')}} 25 |
26 | {{/if}} 27 | {{/unless}} 28 |
29 |
30 | -------------------------------------------------------------------------------- /app/components/canvas-pulse/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-pulse'], 5 | pulseEvents: Ember.computed(_ => []) 6 | }); 7 | -------------------------------------------------------------------------------- /app/components/canvas-pulse/styles.css: -------------------------------------------------------------------------------- 1 | @value --black from 'canvas-colors/styles/colors'; 2 | @value --dark-gray from 'canvas-colors/styles/colors'; 3 | @value --light-gray from 'canvas-colors/styles/colors'; 4 | 5 | .canvas-pulse { 6 | color: --dark-gray; 7 | position: relative; 8 | } 9 | 10 | .canvas-pulse::before { 11 | background: --light-gray; 12 | bottom: 0; 13 | content: ''; 14 | left: 0.6875rem; 15 | position: absolute; 16 | top: 0; 17 | width: 0.125rem; 18 | } 19 | 20 | .canvas-pulse a { 21 | color: --black; 22 | font-weight: 600; 23 | } 24 | 25 | .item { 26 | margin-bottom: 2rem; 27 | position: relative; 28 | } 29 | 30 | .item:last-child { 31 | margin-bottom: 0; 32 | } 33 | -------------------------------------------------------------------------------- /app/components/canvas-pulse/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each pulseEvents as |pulseEvent|}} 2 |
3 | {{canvas-pulse-item pulseEvent=pulseEvent}} 4 |
5 | {{/each}} 6 | -------------------------------------------------------------------------------- /app/components/canvas-title-actions/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-title-actions'], 5 | menuIsShowing: false, 6 | router: Ember.inject.service(), 7 | 8 | actions: { 9 | deleteCanvas() { 10 | this.get('onDeleteCanvas')(this.get('canvas'), { 11 | transitionTo: ['team'] 12 | }); 13 | }, 14 | 15 | /** 16 | * Called when the user wishes to navigate to the canvas history route. 17 | * 18 | * @method 19 | */ 20 | navigateHistory() { 21 | this.get('onNavigateHistory')(); 22 | }, 23 | 24 | onUseTemplate() { 25 | this.get('onUseTemplate')(); 26 | }, 27 | 28 | toggleMenu() { 29 | this.toggleProperty('menuIsShowing'); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /app/components/canvas-title-actions/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | 3 | .canvas-title-actions { 4 | align-items: center; 5 | display: flex; 6 | } 7 | 8 | .template-badge { 9 | background: #E7E8FF; 10 | border: none; 11 | border-radius: 3px; 12 | color: --blue; 13 | cursor: pointer; 14 | font-size: 0.875rem; 15 | margin-right: 1rem; 16 | outline: 0; 17 | padding: 0.125rem 0.5rem; 18 | } 19 | 20 | .trigger { 21 | align-items: center; 22 | background: --blue; 23 | border-radius: 50%; 24 | display: flex; 25 | height: 2rem; 26 | justify-content: center; 27 | width: 2rem; 28 | } 29 | 30 | .trigger svg { 31 | fill: white; 32 | } 33 | -------------------------------------------------------------------------------- /app/components/canvas-title-actions/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if isTemplate}} 2 | 3 | {{/if}} 4 | 5 |
6 | {{#basic-dropdown horizontalPosition='right' as |dropdown|}} 7 | {{#dropdown.trigger}} 8 |
9 | {{svg-jar "More" height="24" width="24"}} 10 |
11 | {{/dropdown.trigger}} 12 | 13 | {{#dropdown.content}} 14 | {{#ui-menu as |menu|}} 15 | {{#menu.item action=(queue (action dropdown.actions.close) (action toggleShowFilter))}} 16 | Filter 17 | {{/menu.item}} 18 | 19 | {{#menu.item action=(action 'navigateHistory')}} 20 | Time Machine 21 | {{/menu.item}} 22 | 23 | {{#menu.item link=(concat canvas.apiURL '.md')}} 24 | View Markdown 25 | {{/menu.item}} 26 | 27 | {{#menu.item link=(concat canvas.apiURL '.canvas') download=(concat canvas.title '.canvas')}} 28 | Download Canvas 29 | {{/menu.item}} 30 | 31 | {{#if showWatchCanvas}} 32 | {{#menu.item action=(action toggleWatchCanvas)}} 33 | {{#if watchedCanvas}} 34 | Unwatch Canvas 35 | {{else}} 36 | Watch Canvas 37 | {{/if}} 38 | {{/menu.item}} 39 | {{/if}} 40 | 41 | {{#if showDeleteCanvas}} 42 | {{#menu.item action=(action 'deleteCanvas')}} 43 | Delete 44 | {{/menu.item}} 45 | {{/if}} 46 | {{/ui-menu}} 47 | {{/dropdown.content}} 48 | {{/basic-dropdown}} 49 |
-------------------------------------------------------------------------------- /app/components/canvas-title-byline/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['canvas-title-byline'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/canvas-title-byline/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | 3 | .canvas-title-byline { 4 | color: --dark-gray; 5 | font-size: 0.875rem; 6 | line-height: 1.75; 7 | } 8 | 9 | .byline-toggle { 10 | cursor: pointer; 11 | user-select: none; 12 | } 13 | 14 | /** 15 | * Set min-height to make sure the channel-list sits on a baseline. Ideally we 16 | * would style ember-power-select instead. 17 | */ 18 | 19 | .channel-list { 20 | min-height: 2rem; 21 | } 22 | -------------------------------------------------------------------------------- /app/components/canvas-title-byline/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{#if showInsertedAt}} 4 | Created {{moment-from-now canvas.insertedAt}} 5 | by {{canvas.creator.name}} 6 | {{else}} 7 | Last edited {{moment-from-now canvas.editedAt}} 8 | {{/if}} 9 | 10 |
11 | 12 | {{#if showChannelSelector}} 13 |
14 | {{canvas-channel-list 15 | channelIds=canvas.slackChannelIds 16 | channels=canvas.team.channels 17 | static=static 18 | updateCanvasChannels=updateCanvasChannels}} 19 |
20 | {{/if}} 21 | -------------------------------------------------------------------------------- /app/components/channel-list-add-to-slack/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['channel-list-add-to-slack'], 5 | dismiss: Ember.K 6 | }); 7 | -------------------------------------------------------------------------------- /app/components/channel-list-add-to-slack/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 3 | @value --gray from 'canvas-colors/styles/colors'; 4 | 5 | .channel-list-add-to-slack { 6 | align-items: center; 7 | color: --dark-gray; 8 | display: flex; 9 | flex-direction: column; 10 | font-size: 0.75rem; 11 | } 12 | 13 | .faux-data { 14 | align-self: flex-start; 15 | margin-bottom: 1rem; 16 | } 17 | 18 | .faux-data-item { 19 | background: --extra-light-gray; 20 | border-radius: 1000px; 21 | height: 10px; 22 | margin-bottom: 10px; 23 | width: 138px; 24 | 25 | &:nth-child(2n) { 26 | width: 108px; 27 | } 28 | } 29 | 30 | .reasons { 31 | list-style: none; 32 | padding: 0; 33 | } 34 | 35 | .reasons-item { 36 | margin-bottom: 0.125rem; 37 | position: relative; 38 | 39 | &:before { 40 | background: --gray; 41 | border-radius: 50%; 42 | content: ''; 43 | display: block; 44 | height: 6px; 45 | left: -0.625rem; 46 | position: absolute; 47 | top: 50%; 48 | transform: translateY(-50%); 49 | width: 6px; 50 | } 51 | } 52 | 53 | .later { 54 | margin-top: 0.5rem; 55 | } 56 | 57 | .later-action { 58 | color: inherit; 59 | font-weight: 600; 60 | } 61 | -------------------------------------------------------------------------------- /app/components/channel-list-add-to-slack/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 | Set up the Slack integration: 9 |
10 | 11 | 17 | 18 |
19 | {{add-to-slack currentScopes=currentScopes}} 20 |
21 | 22 |
23 | or do this later. 24 |
-------------------------------------------------------------------------------- /app/components/channel-topic-canvas/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { get } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | localClassNames: ['channel-topic-canvas'], 7 | store: Ember.inject.service(), 8 | 9 | actions: { 10 | unfurlBlock(block) { 11 | return this.get('store').findRecord('unfurl', get(block, 'meta.url')); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/components/channel-topic-canvas/styles.css: -------------------------------------------------------------------------------- 1 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | 4 | @value --cell-padding from 'canvas-web/components/routes/team-index/styles'; 5 | 6 | .channel-topic-canvas { 7 | background: --extra-light-gray; 8 | font-size: 0.875rem; 9 | padding: --cell-padding; 10 | } 11 | 12 | .content { 13 | background: white; 14 | border-radius: 4px; 15 | box-shadow: 0 1px 6px --gray; 16 | padding: 1.5rem; 17 | position: relative; 18 | } 19 | 20 | .canvas { 21 | max-height: 20rem; 22 | overflow: hidden; 23 | } 24 | 25 | .footer { 26 | background: rgba(255, 255, 255, 0.9); 27 | border-bottom-left-radius: 4px; 28 | border-bottom-right-radius: 4px; 29 | bottom: 0; 30 | left: 0; 31 | padding: --cell-padding 1.5rem; 32 | position: absolute; 33 | right: 0; 34 | } 35 | -------------------------------------------------------------------------------- /app/components/channel-topic-canvas/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{canvas-editor-next 4 | editingEnabled=false 5 | canvas=canvas 6 | unfurlBlock=(action 'unfurlBlock') 7 | githubAuthComponent=(component 'github-oauth') 8 | titleBylineComponent=(component 'canvas-title-byline' 9 | static=true 10 | canvas=canvas)}} 11 |
12 | 13 |
14 | {{link-to 'Show more' 'team.canvas.show' canvas}} 15 |
16 |
17 | -------------------------------------------------------------------------------- /app/components/comment-thread/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Paragraph from 'canvas-editor/lib/realtime-canvas/paragraph'; 3 | import { task } from 'ember-concurrency'; 4 | 5 | export default Ember.Component.extend({ 6 | store: Ember.inject.service(), 7 | blockId: 0, //default block id 8 | 9 | subscribeProps: Ember.computed(_ => ({ subscriptions: [] })), 10 | 11 | subscription: Ember.computed('subscribeProps.subscriptions.[]', function() { 12 | const subscriptions = this.get('subscribeProps.subscriptions'); 13 | return subscriptions.findBy('id', this.get('blockId')); 14 | }), 15 | 16 | createComment: task(function *(content) { 17 | const store = this.get('store'); 18 | const canvas = this.get('canvas'); 19 | const block = yield store.findRecord('block', this.get('blockId')); 20 | const contentBlock = Paragraph.create({ content }); 21 | const blocks = [contentBlock.toJSON({ serializeId: true })]; 22 | 23 | yield store.createRecord('comment', 24 | { blocks, canvas, block }).save(); 25 | }).drop(), 26 | 27 | editComment: task(function *(comment, content) { 28 | comment.set('blocks', [{ type: 'paragraph', content }]); 29 | yield comment.save(); 30 | }).drop(), 31 | 32 | removeComment: task(function *(comment) { 33 | yield comment.destroyRecord(); 34 | }).drop(), 35 | 36 | setSubscription: task(function *(subscribed) { 37 | const canvas = this.get('canvas'); 38 | const subscription = this.get('store').createRecord('thread-subscription', { 39 | id: this.get('blockId'), canvas, subscribed 40 | }); 41 | yield subscription.save(); 42 | }).drop(), 43 | 44 | toggleSubscription: task(function *(subscription) { 45 | subscription.toggleProperty('subscribed'); 46 | yield subscription.save(); 47 | }).drop(), 48 | }); 49 | -------------------------------------------------------------------------------- /app/components/comment-thread/form/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Key from 'canvas-web/lib/key'; 3 | import { task } from 'ember-concurrency'; 4 | 5 | export default Ember.Component.extend({ 6 | content: '', 7 | 8 | submit: task(function *(content) { 9 | yield this.get('submitComment')(content); 10 | this.set('content', ''); 11 | }).drop(), 12 | 13 | actions: { 14 | commentKeyDown(_, evt) { 15 | evt.stopPropagation(); 16 | const key = new Key(evt.originalEvent); 17 | if (key.is('meta', 'return')) { 18 | this.get('submit').perform(this.get('content')); 19 | } 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/comment-thread/form/styles.css: -------------------------------------------------------------------------------- 1 | @value --light-blue from 'canvas-colors/styles/colors'; 2 | 3 | .comment-field { 4 | background: none; 5 | border: none; 6 | color: inherit; 7 | font: inherit; 8 | display: block; 9 | padding: 0; 10 | outline: none; 11 | resize: none; 12 | width: 100%; 13 | } 14 | 15 | .submit-button { 16 | background: none; 17 | border: none; 18 | color: --light-blue; 19 | padding: 0; 20 | } 21 | -------------------------------------------------------------------------------- /app/components/comment-thread/form/template.hbs: -------------------------------------------------------------------------------- 1 | {{textarea 2 | autofocus=true 3 | key-down=(action 'commentKeyDown') 4 | local-class='comment-field' 5 | placeholder=placeholder 6 | value=content}} 7 | 8 | 14 | -------------------------------------------------------------------------------- /app/components/comment-thread/item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | isEditing: false, 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/comment-thread/item/styles.css: -------------------------------------------------------------------------------- 1 | @value --comment-thread-item-muted-text-color from 'canvas-web/components/comment-thread/styles'; 2 | @value --red from 'canvas-colors/styles/colors'; 3 | 4 | .root { 5 | display: flex; 6 | } 7 | 8 | .avatar { 9 | border-radius: 50%; 10 | display: block; 11 | flex: none; 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .main { 16 | flex: 1; 17 | } 18 | 19 | .meta { 20 | display: flex; 21 | justify-content: space-between; 22 | } 23 | 24 | .action { 25 | margin: 0 0.25rem; 26 | } 27 | 28 | .username { 29 | font-weight: bold; 30 | } 31 | 32 | .timestamp, 33 | .edit, 34 | .delete { 35 | color: --comment-thread-item-muted-text-color; 36 | } 37 | 38 | .edit, 39 | .delete { 40 | cursor: pointer; 41 | } 42 | 43 | .delete:hover { 44 | color: --red; 45 | } 46 | -------------------------------------------------------------------------------- /app/components/comment-thread/item/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 |
10 |
11 | {{comment.creator.name}} 12 | {{moment-from-now comment.updatedAt}} 13 |
14 | 15 |
16 | {{#if (eq currentUser.id comment.creator.id)}}Edit{{/if}} 17 | Delete 18 |
19 |
20 | 21 | {{#if isEditing}} 22 | {{comment-thread/form 23 | content=(join '' (map-by 'content' comment.blocks)) 24 | submitComment=(pipe-action (action (optional editComment)) (action (mut isEditing) false))}} 25 | {{else}} 26 |
27 | {{#each comment.blocks as |block|}} 28 |
29 | {{block.content}} 30 |
31 | {{/each}} 32 |
33 | {{/if}} 34 |
35 |
-------------------------------------------------------------------------------- /app/components/comment-thread/styles.css: -------------------------------------------------------------------------------- 1 | @value --comment-thread-background-color: #1C202C; 2 | @value --comment-thread-border-color: #383A43; 3 | @value --comment-thread-text-color: #D7DAED; 4 | @value --comment-thread-item-muted-text-color: #5B5E6C; 5 | 6 | .root { 7 | background: --comment-thread-background-color; 8 | border-radius: 5px; 9 | color: --comment-thread-text-color; 10 | font-size: 0.75rem; 11 | padding: 1rem; 12 | } 13 | 14 | .actions { 15 | text-align: right; 16 | } 17 | 18 | .item { 19 | border-bottom: 1px solid --comment-thread-border-color; 20 | margin-bottom: 1rem; 21 | padding-bottom: 1rem; 22 | 23 | &:last-child { 24 | border: none; 25 | margin-bottom: 0; 26 | padding-bottom: 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/components/comment-thread/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if subscription}} 4 | 5 | {{if subscription.subscribed 'Unsubscribe' 'Subscribe'}} 6 | 7 | {{else}} 8 | 9 | {{if subscribeProps.defaultSubscribeState 'Subscribe' 'Unsubscribe'}} 10 | 11 | {{/if}} 12 |
13 | 14 | {{#each (sort-by 'insertedAt' blockComments) as |comment|}} 15 |
16 | {{comment-thread/item 17 | comment=comment 18 | currentUser=currentUser 19 | editComment=(perform editComment comment) 20 | removeComment=(perform removeComment comment)}} 21 |
22 | {{/each}} 23 | 24 |
25 | {{comment-thread/form 26 | placeholder='Leave a reply...' 27 | submitComment=(perform createComment)}} 28 |
29 |
30 | -------------------------------------------------------------------------------- /app/components/drop-zone/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import styles from './styles'; 3 | import layout from './template'; 4 | 5 | export default Ember.Component.extend({ 6 | layout, 7 | localClassNames: ['dropzone'], 8 | localClassNameBindings: ['showDropzone'], 9 | styles, 10 | 11 | dragLeave() { 12 | this.set('draggingOver', false); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/components/drop-zone/styles.css: -------------------------------------------------------------------------------- 1 | .dropzone { 2 | align-items: center; 3 | background: rgba(0, 0, 0, 0.9); 4 | bottom: 0; 5 | color: white; 6 | display: none; 7 | justify-content: center; 8 | left: 0; 9 | position: absolute; 10 | right: 0; 11 | top: 0; 12 | z-index: 10; 13 | } 14 | 15 | .dropzone.show-dropzone { 16 | display: flex; 17 | } 18 | 19 | .dropzone * { 20 | pointer-events: none; 21 | } 22 | -------------------------------------------------------------------------------- /app/components/drop-zone/template.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 | -------------------------------------------------------------------------------- /app/components/github-oauth/component.js: -------------------------------------------------------------------------------- 1 | import ENV from 'canvas-web/config/environment'; 2 | import Ember from 'ember'; 3 | import OAuth from 'canvas-web/mixins/oauth'; 4 | 5 | /** 6 | * Provides a link which a user can click to sign in with GitHub. 7 | * 8 | * @class CanvasWeb.GitHubOAuthComponent 9 | * @extends Ember.Component 10 | */ 11 | export default Ember.Component.extend(OAuth, { 12 | clientID: ENV.gitHubClientID, 13 | endpoint: 'https://github.com/login/oauth/authorize', 14 | redirectURL: ENV.gitHubRedirectURL, 15 | scope: 'repo'.w(), 16 | 17 | actions: { 18 | openAuthorizeURL() { 19 | const authWindow = window.open( 20 | this.get('authorizeURL'), 21 | 'github-oauth', 22 | 'height=800,width=1000'); 23 | 24 | const isClosedInterval = setInterval(_ => { 25 | if (!authWindow.closed) return; 26 | window.location.reload(); 27 | clearInterval(isClosedInterval); 28 | }, 1000); 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /app/components/github-oauth/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | Authorize with GitHub 3 | 4 | -------------------------------------------------------------------------------- /app/components/intercom-launcher/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'canvas-web/config/environment'; 3 | 4 | export default Ember.Component.extend({ 5 | intercomAppID: ENV.intercomAppID, 6 | localClassNames: ['intercom-launcher'], 7 | localClassNameBindings: ['intercomAppID::is-hidden'] 8 | }); 9 | -------------------------------------------------------------------------------- /app/components/intercom-launcher/styles.css: -------------------------------------------------------------------------------- 1 | .intercom-launcher { 2 | display: block; 3 | } 4 | 5 | .is-hidden { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /app/components/intercom-launcher/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if intercomAppID}} 2 | 3 | {{yield}} 4 | 5 | {{/if}} 6 | -------------------------------------------------------------------------------- /app/components/new-canvas-button/component.js: -------------------------------------------------------------------------------- 1 | import ChannelIDs from 'canvas-web/mixins/channel-ids'; 2 | import Ember from 'ember'; 3 | 4 | export default Ember.Component.extend(ChannelIDs, { 5 | localClassNames: ['new-canvas-button'], 6 | localClassNameBindings: ['inline:new-canvas-button--inline'], 7 | store: Ember.inject.service(), 8 | 9 | didCreateCanvas: Ember.K, 10 | 11 | click() { 12 | const team = this.get('team'); 13 | 14 | this.get('store').createRecord('canvas', { 15 | slackChannelIds: this.get('channelIDs'), 16 | team 17 | }).save().then(canvas => { 18 | this.sendAction('didCreateCanvas', canvas); 19 | this.get('didCreateCanvas')(canvas); 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/new-canvas-button/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | 3 | .new-canvas-button { 4 | align-items: center; 5 | background: --blue; 6 | border-radius: 50%; 7 | cursor: pointer; 8 | display: flex; 9 | height: 2rem; 10 | justify-content: center; 11 | width: 2rem; 12 | } 13 | 14 | .new-canvas-button--inline { 15 | display: inline-block; 16 | height: 1rem; 17 | width: 1rem; 18 | } 19 | 20 | .icon { 21 | display: block; 22 | fill: white; 23 | height: 100%; 24 | width: 100%; 25 | } 26 | -------------------------------------------------------------------------------- /app/components/new-canvas-button/template.hbs: -------------------------------------------------------------------------------- 1 | {{svg-jar "Add" local-class="icon"}} 2 | -------------------------------------------------------------------------------- /app/components/personal-team-form/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | @value --light-gray from 'canvas-colors/styles/colors'; 4 | @value --red from 'canvas-colors/styles/colors'; 5 | 6 | .personal-team-form { 7 | text-align: left; 8 | } 9 | 10 | .error { 11 | color: --red; 12 | } 13 | 14 | .label { 15 | font-size: 0.75rem; 16 | } 17 | 18 | .label.intro { 19 | margin-bottom: 0.25rem; 20 | } 21 | 22 | .label.outro { 23 | margin-top: 0.25rem; 24 | } 25 | 26 | .input-wrapper { 27 | position: relative; 28 | } 29 | 30 | .input-wrapper:before { 31 | color: --gray; 32 | content: '~'; 33 | font-size: 2rem; 34 | left: -2rem; 35 | position: absolute; 36 | top: 0.5rem; 37 | } 38 | 39 | .input { 40 | border-radius: 4px; 41 | border: 1px solid --light-gray; 42 | display: block; 43 | font: inherit; 44 | font-size: 1.5rem; 45 | padding: 0.5em; 46 | width: 100%; 47 | } 48 | 49 | .input::placeholder { 50 | color: --gray; 51 | } 52 | 53 | .input:focus { 54 | border-color: --blue; 55 | box-shadow: 0 0 0 1px --blue; 56 | outline: none; 57 | } 58 | 59 | .submit-wrapper { 60 | margin-top: 1rem; 61 | text-align: right; 62 | } 63 | 64 | .submit { 65 | composes: button-reset from 'canvas-web/styles/utilities'; 66 | background: --blue; 67 | border-radius: 1000px; 68 | border: none; 69 | color: white; 70 | font-size: 0.875rem; 71 | min-width: 6rem; 72 | padding: 0.5rem 1rem; 73 | } 74 | -------------------------------------------------------------------------------- /app/components/personal-team-form/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if domainError}} 3 |
4 |

{{domainError}}

5 |
6 | {{/if}} 7 | 8 |
9 | Where should your notes live? Dashes and numbers are A-OK! 10 |
11 | 12 |
13 | {{input value=teamDomain local-class='input' placeholder=placeholder}} 14 |
15 | 16 |
17 | https://pro.usecanvas.com/~{{or teamDomain placeholder}} 18 |
19 | 20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /app/components/pulse-settings/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['pulse-settings'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/pulse-settings/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | 3 | .pulse-settings { 4 | font-size: 0.875rem; 5 | padding: 0 0.5em; 6 | } 7 | 8 | .item { 9 | display: flex; 10 | } 11 | 12 | .item.disabled { 13 | opacity: 0.25; 14 | } 15 | 16 | .item-description { 17 | color: --dark-gray; 18 | } 19 | 20 | .item-action { 21 | flex: none; 22 | } 23 | -------------------------------------------------------------------------------- /app/components/pulse-settings/template.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-list as |list|}} 2 | {{#list.item}} 3 |
4 |
5 | Slack
6 | Monitors Slack channels for Canvas references 7 |
8 | 9 |
10 | {{#if slackIsSetUp}} 11 | Enabled 12 | {{else}} 13 | {{#component addToSlack}} 14 | Set Up 15 | {{/component}} 16 | {{/if}} 17 |
18 |
19 | {{/list.item}} 20 | 21 | {{#list.item}} 22 |
23 |
24 | GitHub
25 | Monitors GitHub issues and pull requests for canvas references 26 |
27 | 28 |
29 | 30 |
31 |
32 | {{/list.item}} 33 | {{/ui-list}} 34 | -------------------------------------------------------------------------------- /app/components/routes/canvas-history/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --dark-gray from 'canvas-colors/styles/colors'; 3 | 4 | .route-canvas-history { 5 | position: relative; 6 | } 7 | 8 | .history-manager { 9 | align-items: center; 10 | background: rgba(0, 0, 0, 0.8); 11 | border-radius: 4px; 12 | color: white; 13 | display: flex; 14 | left: 0; 15 | padding: 0.75rem 1rem; 16 | position: absolute; 17 | right: 0; 18 | top: 1.0625rem; 19 | } 20 | 21 | .slider { 22 | width: 100%; 23 | } 24 | 25 | .clone { 26 | composes: button-reset from 'canvas-web/styles/utilities'; 27 | margin-left: 1rem; 28 | } 29 | 30 | .clone-icon { 31 | fill: white; 32 | } 33 | 34 | .clone:hover { 35 | .clone-icon { 36 | fill: --blue; 37 | } 38 | } 39 | 40 | .editor { 41 | margin: 0 auto; 42 | max-width: 47rem; 43 | padding: 4rem 0; 44 | } 45 | -------------------------------------------------------------------------------- /app/components/routes/canvas-history/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{ui-slider 3 | local-class='slider' 4 | max=canvas.version 5 | min=0 6 | onChange=(action 'onVersionChange') 7 | value=initialVersion}} 8 | 9 | 12 |
13 | 14 |
15 | {{canvas-editor-next 16 | editingEnabled=false 17 | unfurlBlock=(action 'unfurlBlock') 18 | canvas=canvas 19 | canvasLinkComponent=(component 'canvas-link') 20 | githubAuthComponent=(component 'github-oauth')}} 21 |
22 | -------------------------------------------------------------------------------- /app/components/routes/canvas-pulse/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['route-canvas-pulse'], 5 | pulseEvents: Ember.computed(_ => []), 6 | slackScopes: ['channels:history'] 7 | }); 8 | -------------------------------------------------------------------------------- /app/components/routes/canvas-pulse/styles.css: -------------------------------------------------------------------------------- 1 | @value --gray from 'canvas-colors/styles/colors'; 2 | 3 | .container { 4 | margin: 4rem auto; 5 | max-width: 42rem; 6 | padding: 1rem; 7 | } 8 | 9 | .settings { 10 | position: absolute; 11 | right: 1rem; 12 | top: 1rem; 13 | } 14 | 15 | .icon { 16 | fill: --gray; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/routes/canvas-pulse/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#basic-dropdown as |dropdown|}} 3 | {{#dropdown.trigger}} 4 | {{svg-jar 'SlidersVertical' height=32 width=32 local-class='icon'}} 5 | {{/dropdown.trigger}} 6 | 7 | {{#dropdown.content}} 8 | {{pulse-settings 9 | addToSlack=(component 'add-to-slack' currentScopes=canvas.team.slackScopes extendedScopes=slackScopes) 10 | closeDropdown=(action dropdown.actions.close) 11 | slackIsSetUp=canvas.team.hasChannelsHistory}} 12 | {{/dropdown.content}} 13 | {{/basic-dropdown}} 14 |
15 | 16 |
17 |
18 | {{canvas-pulse pulseEvents=pulseEvents}} 19 |
20 |
-------------------------------------------------------------------------------- /app/components/routes/canvas-settings/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { task } from 'ember-concurrency'; 3 | 4 | const { computed, on } = Ember; 5 | const { w } = Ember.String; 6 | 7 | export default Ember.Component.extend({ 8 | allowLinkAccess: false, 9 | permissions: w('view edit'), 10 | permission: 'view', 11 | 12 | setInitialPermissionState: on('init', function() { 13 | const state = this.get('canvas.linkAccess'); 14 | if (state === 'none') return; 15 | this.set('allowLinkAccess', true); 16 | this.set('permission', 17 | state === 'edit' ? 'edit' : 'view'); 18 | }), 19 | 20 | currentPermissionState: computed('allowLinkAccess', 21 | 'permission', function() { 22 | const { allowLinkAccess, permission } = 23 | this.getProperties('allowLinkAccess', 'permission'); 24 | if (!allowLinkAccess) { 25 | return 'none'; 26 | } else if (permission === 'view') { 27 | return 'read'; 28 | } 29 | return 'edit'; 30 | }), 31 | 32 | persistPermissionState: task(function *() { 33 | const newState = this.get('currentPermissionState'); 34 | const canvas = this.get('canvas'); 35 | if (canvas.get('linkAccess') !== newState) { 36 | canvas.set('linkAccess', newState); 37 | yield canvas.save(); 38 | } 39 | }).keepLatest(), 40 | 41 | actions: { 42 | toggleIsTemplate() { 43 | this.toggleProperty('canvas.isTemplate'); 44 | this.get('canvas').save(); 45 | }, 46 | 47 | toggleLinkAccess() { 48 | this.toggleProperty('allowLinkAccess'); 49 | this.get('persistPermissionState').perform(); 50 | }, 51 | 52 | updatePermission({ target: { value } }) { 53 | this.set('permission', value); 54 | this.get('persistPermissionState').perform(); 55 | } 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /app/components/routes/canvas-settings/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | 3 | .container { 4 | margin: 4rem auto; 5 | max-width: 42rem; 6 | padding: 1rem; 7 | } 8 | 9 | .public-access, 10 | .template-settings { 11 | display: flex; 12 | } 13 | 14 | .checkbox { 15 | margin-right: 0.25rem; 16 | } 17 | 18 | .select { 19 | margin-left: 0.25rem; 20 | } 21 | 22 | .muted { 23 | color: --dark-gray; 24 | } 25 | -------------------------------------------------------------------------------- /app/components/routes/canvas-settings/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Public Access

3 | 4 |
5 | {{ui-checkbox 6 | checked=allowLinkAccess 7 | local-class="checkbox" 8 | onToggle=(action 'toggleLinkAccess')}} 9 | 10 | 11 | Anyone with the link to this canvas can 12 | 13 | 14 | 21 |
22 | 23 |

Template

24 | 25 |
26 | {{ui-checkbox 27 | checked=canvas.isTemplate 28 | local-class="checkbox" 29 | onToggle=(action 'toggleIsTemplate')}} 30 | 31 | 32 | This canvas is a template. Read more about templates here. 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /app/components/routes/canvas-show/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | 3 | .route-canvas-show { 4 | position: relative; 5 | } 6 | 7 | .message { 8 | color: --dark-gray; 9 | font-size: 0.75rem; 10 | font-style: italic; 11 | left: 0; 12 | padding: 1rem 0; 13 | position: absolute; 14 | right: 0; 15 | top: 0; 16 | text-align: center; 17 | } 18 | 19 | .filter { 20 | left: 0; 21 | position: absolute; 22 | right: 0; 23 | top: 1.0625rem; 24 | } 25 | 26 | .editor { 27 | margin: 0 auto; 28 | max-width: 47rem; 29 | padding: 4rem 0; 30 | } 31 | -------------------------------------------------------------------------------- /app/components/routes/login-x/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['route-login'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/routes/login-x/template.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='🙌' fullScreen=true title='Sign in to Canvas'}} 2 |

3 | Canvas uses Slack to make logging in a breeze.
4 | Right now only invited Slack teams have access. 5 |

6 | 7 | {{sign-in-with-slack}} 8 | {{/ui-billboard}} 9 | -------------------------------------------------------------------------------- /app/components/routes/setup-x/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import styles from './styles'; 3 | 4 | /** 5 | * The route the user can visit to set up an un-set-up personal team 6 | * 7 | * @class CanvasWeb.SetupRouteComponent 8 | * @extends Ember.Component 9 | */ 10 | export default Ember.Component.extend({ 11 | styles 12 | }); 13 | -------------------------------------------------------------------------------- /app/components/routes/setup-x/styles.css: -------------------------------------------------------------------------------- 1 | .content { 2 | composes: content from 'canvas-web/components/team-list-wrapper/styles'; 3 | } 4 | 5 | .container { 6 | padding: 4rem 0; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /app/components/routes/setup-x/template.hbs: -------------------------------------------------------------------------------- 1 | {{#team-list-wrapper teams=teams}} 2 |
3 |
4 | {{#ui-billboard emoji='📓' title='Set up your space for personal notes'}} 5 |

6 | Personal notes are great for shopping lists, home improvement 7 | projects, and your dissertation on the best LaCroix flavors. 8 |

9 | 10 |
11 | {{personal-team-form 12 | team=personalTeam 13 | teamUpdated=(route-action 'teamUpdated')}} 14 |
15 | {{/ui-billboard}} 16 |
17 |
18 | {{/team-list-wrapper}} 19 | -------------------------------------------------------------------------------- /app/components/routes/team-index/styles.css: -------------------------------------------------------------------------------- 1 | @value --light-gray from 'canvas-colors/styles/colors'; 2 | 3 | @value --cell-padding: 1rem; 4 | 5 | .nav { 6 | composes: nav from 'canvas-web/components/team-list-wrapper/styles'; 7 | } 8 | 9 | .content { 10 | composes: content from 'canvas-web/components/team-list-wrapper/styles'; 11 | } 12 | 13 | .sidebar { 14 | border-right: 1px solid --light-gray; 15 | min-width: 14rem; 16 | position: relative; 17 | } 18 | 19 | .sidebar-scroll { 20 | bottom: 0; 21 | left: 0; 22 | overflow-y: auto; 23 | -webkit-overflow-scrolling: touch; 24 | padding: --cell-padding; 25 | position: absolute; 26 | right: 0; 27 | top: 0; 28 | } 29 | 30 | .canvas-list { 31 | flex: 1; 32 | position: relative; 33 | } 34 | 35 | .canvas-list-scroll { 36 | bottom: 0; 37 | left: 0; 38 | overflow-y: auto; 39 | -webkit-overflow-scrolling: touch; 40 | position: absolute; 41 | right: 0; 42 | top: 0; 43 | } 44 | -------------------------------------------------------------------------------- /app/components/routes/team-index/template.hbs: -------------------------------------------------------------------------------- 1 | {{drop-zone 2 | message="Drop to upload to Canvas!" 3 | draggingOver=(mut draggingOver) 4 | showDropzone=showDropzone}} 5 | 6 | {{#team-list-wrapper teams=teamsList.teams}} 7 |
8 | {{team-nav 9 | channel=channel 10 | team=team 11 | didCreateCanvas=(action 'didCreateCanvas')}} 12 |
13 | 14 |
15 | {{#if (and team.slackId team.hasChannelsRead)}} 16 |
17 |
18 | {{canvas-list-filter 19 | channel=channel 20 | channels=team.channels}} 21 |
22 |
23 | {{else if (and team.slackId (not (is-dismissed team.id 'channel-list-add-to-slack')))}} 24 |
25 |
26 | {{channel-list-add-to-slack 27 | currentScopes=team.slackScopes 28 | dismiss=(action 'dismiss' 29 | (join '.' (w '*' team.id 'channel-list-add-to-slack')))}} 30 |
31 |
32 | {{/if}} 33 | 34 |
35 |
36 | {{#if (and team.hasChannelsRead channelModel.topicCanvas.isFulfilled)}} 37 | {{channel-topic-canvas canvas=channelModel.topicCanvas.content}} 38 | {{/if}} 39 | {{canvas-list 40 | canvases=(if channel filteredCanvases team.canvases) 41 | channel=channel 42 | team=team 43 | didCreateCanvas=(action 'didCreateCanvas')}} 44 |
45 |
46 |
47 | {{/team-list-wrapper}} -------------------------------------------------------------------------------- /app/components/routes/team-settings/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { inject } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | teamsList: inject.service() 7 | }); 8 | -------------------------------------------------------------------------------- /app/components/routes/team-settings/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 4rem auto; 3 | max-width: 42rem; 4 | padding: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/routes/team-settings/template.hbs: -------------------------------------------------------------------------------- 1 | {{#team-list-wrapper teams=teamsList.teams}} 2 |
3 |

{{team.name}} Team Settings

4 | 5 | {{#ui-settings-list as |settings|}} 6 | {{#settings.item title="Manage your team"}} 7 | Canvas teams are mapped to Slack teams. To change the team name, team 8 | avatar, or manage team members 9 | head over to Slack. 10 | {{/settings.item}} 11 | 12 | {{#settings.item title="Export"}} 13 | {{team-export team=team}} 14 | {{/settings.item}} 15 | {{/ui-settings-list}} 16 |
17 | {{/team-list-wrapper}} -------------------------------------------------------------------------------- /app/components/sign-in-with-slack/component.js: -------------------------------------------------------------------------------- 1 | import ENV from 'canvas-web/config/environment'; 2 | import Ember from 'ember'; 3 | import OAuth from 'canvas-web/mixins/oauth'; 4 | 5 | /** 6 | * Provides a button which a user can click to sign in with Slack. 7 | * 8 | * @class CanvasWeb.SignInWithSlackComponent 9 | * @extends Ember.Component 10 | */ 11 | export default Ember.Component.extend(OAuth, { 12 | clientID: ENV.slackClientID, 13 | endpoint: 'https://slack.com/oauth/authorize', 14 | redirectURL: ENV.slackRedirectURI, 15 | scope: 'identity.avatar identity.basic identity.team identity.email'.w(), 16 | state: 'identity' 17 | }); 18 | -------------------------------------------------------------------------------- /app/components/sign-in-with-slack/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | Sign in with Slack 3 | 4 | -------------------------------------------------------------------------------- /app/components/slack-oauth/component.js: -------------------------------------------------------------------------------- 1 | import ENV from 'canvas-web/config/environment'; 2 | import Ember from 'ember'; 3 | import OAuth from 'canvas-web/mixins/oauth'; 4 | 5 | const { computed } = Ember; 6 | 7 | /** 8 | * A component that can be used to initiate a Slack OAuth flow. 9 | * 10 | * @class CanvasWeb.SlackOAuthComponent 11 | * @extends Ember.Component 12 | */ 13 | export default Ember.Component.extend(OAuth, { 14 | /** 15 | * @member {Array} A base set of non-invasive scopes that we request. 16 | */ 17 | baseScopes: `bot 18 | channels:read 19 | chat:write:bot 20 | commands 21 | groups:read 22 | team:read 23 | users:read`.w(), 24 | 25 | /** 26 | * @member {?string} The Slack OAuth client ID 27 | */ 28 | clientID: ENV.slackClientID, 29 | 30 | /** 31 | * @member {string} The Slack OAuth authorization endpoint URL 32 | */ 33 | endpoint: 'https://slack.com/oauth/authorize', 34 | 35 | /** 36 | * @member {Array} The scopes that the team currently has. 37 | */ 38 | currentScopes: [], 39 | 40 | /** 41 | * @member {Array} A set of extended invasive scopes to be added on 42 | * to the base OAuth scope set 43 | */ 44 | extendedScopes: [], 45 | 46 | /** 47 | * @member {?string} The redirect URI for the Slack OAuth client 48 | */ 49 | redirectURL: ENV.slackAddRedirectURI, 50 | 51 | /** 52 | * @member {Array} A list of scopes that will be requested unless 53 | * already granted. 54 | */ 55 | unionScopes: computed.union('baseScopes', 'extendedScopes'), 56 | 57 | /** 58 | * @member {Array} A list of scopes that will be requested. 59 | */ 60 | scope: computed.setDiff('unionScopes', 'currentScopes'), 61 | 62 | /** 63 | * @member {string} A Slack OAuth state nonce—we are currently not making 64 | * good use of this. 65 | */ 66 | state: 'add' 67 | }); 68 | -------------------------------------------------------------------------------- /app/components/slack-oauth/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | Authorize with Slack 3 | -------------------------------------------------------------------------------- /app/components/team-export/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'canvas-web/config/environment'; 3 | import { task } from 'ember-concurrency'; 4 | 5 | const { Component, RSVP, computed, inject, run, $ } = Ember; 6 | 7 | export default Component.extend({ 8 | csrfToken: inject.service(), 9 | url: computed('team.domain', function() { 10 | return `/v1/export-tokens/${this.get('team.domain')}`; 11 | }), 12 | 13 | getToken(url) { 14 | return new RSVP.Promise((res, rej) => { 15 | return $.ajax({ 16 | headers: { 17 | Accept: 'application/json', 18 | 'x-csrf-token': this.get('csrfToken.token') 19 | }, 20 | type: 'GET', 21 | url 22 | }).then(payload => run(null, res, JSON.parse(payload)), 23 | jqXHR => run(null, rej, jqXHR)); 24 | }); 25 | }, 26 | 27 | downloadFile(url) { 28 | const el = document.createElement('a'); 29 | document.body.appendChild(el); 30 | el.setAttribute('href', url); 31 | el.style.display = 'none'; 32 | el.click(); 33 | el.remove(); 34 | }, 35 | 36 | exportCanvases: task(function *() { 37 | try { 38 | const { token } = yield this.getToken(this.get('url')); 39 | const url = `${ENV.apiURL}exports/${token}`; 40 | this.downloadFile(url); 41 | } catch (_e) { 42 | this.set('exportFailed', true); 43 | } 44 | }) 45 | }); 46 | -------------------------------------------------------------------------------- /app/components/team-export/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --red from 'canvas-colors/styles/colors'; 3 | @value --dark-gray from 'canvas-colors/styles/colors'; 4 | @value --gray from 'canvas-colors/styles/colors'; 5 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 6 | 7 | .export-button { 8 | background: #E7E8FF; 9 | border: none; 10 | border-radius: 3px; 11 | color: --blue; 12 | cursor: pointer; 13 | font-size: 0.875rem; 14 | margin-right: 0.5rem; 15 | outline: 0; 16 | padding: 0.125rem 0.5rem; 17 | } 18 | 19 | .export-button.disabled { 20 | background: --extra-light-gray; 21 | color: --gray; 22 | } 23 | 24 | .confirmation { 25 | color: --dark-gray; 26 | font-size: 0.75rem; 27 | } 28 | 29 | .error { 30 | color: --red; 31 | font-size: 0.75rem; 32 | } 33 | -------------------------------------------------------------------------------- /app/components/team-export/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Your team's canvases are available for export in the form of a series of 3 | Markdown files (one per canvas). 4 |

5 | 6 |
7 | {{#unless exportCanvases.lastSuccessful}} 8 | 15 | {{/unless}} 16 | 17 | {{#if exportFailed}} 18 | 19 | Export failed. 20 | 21 | {{else if exportCanvases.isRunning}} 22 | 23 | Export started! Once finished, a download will automatically start. 24 | 25 | {{else if exportCanvases.lastSuccessful}} 26 | 27 | Export succeeded! 28 | 29 | {{/if}} 30 |
-------------------------------------------------------------------------------- /app/components/team-list-item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'canvas-web/config/environment'; 3 | 4 | const { computed } = Ember; 5 | 6 | export default Ember.Component.extend({ 7 | localClassNames: ['team-list-item'], 8 | 9 | imageURL: computed('team.image88', function() { 10 | return this.getWithDefault('team.image88', 11 | `${ENV.rootURL}images/personal-notes-avatar.png`); 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/team-list-item/styles.css: -------------------------------------------------------------------------------- 1 | .team-list-item { 2 | align-items: center; 3 | background: white; 4 | border-radius: 4px; 5 | display: flex; 6 | justify-content: center; 7 | height: 36px; 8 | overflow: hidden; 9 | width: 36px; 10 | } 11 | -------------------------------------------------------------------------------- /app/components/team-list-item/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/team-list-wrapper/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'canvas-web/config/environment'; 3 | import styles from './styles'; 4 | 5 | /** 6 | * A component which can wrap content in a team list 7 | * 8 | * @class CanvasWeb.TeamListWrapperComponent 9 | * @extends Ember.Component 10 | */ 11 | export default Ember.Component.extend({ 12 | changelogURL: ENV.changelogURL, 13 | localClassNames: ['team-list-wrapper'], 14 | styles 15 | }); 16 | -------------------------------------------------------------------------------- /app/components/team-list-wrapper/styles.css: -------------------------------------------------------------------------------- 1 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | @value --light-gray from 'canvas-colors/styles/colors'; 4 | 5 | @value --cell-padding: 1rem; 6 | 7 | .team-list-wrapper { 8 | display: flex; 9 | height: 100vh; 10 | } 11 | 12 | .aside { 13 | align-items: center; 14 | background: --extra-light-gray; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: space-between; 18 | padding: --cell-padding; 19 | } 20 | 21 | .aside-item { 22 | display: block; 23 | margin-bottom: 1rem; 24 | } 25 | 26 | .aside-item:last-child { 27 | margin-bottom: 0; 28 | } 29 | 30 | .aside-item svg { 31 | display: block; 32 | fill: --gray; 33 | } 34 | 35 | .main { 36 | background: white; 37 | border-bottom-left-radius: 4px; 38 | border-top-left-radius: 4px; 39 | box-shadow: -1px 0 2px --light-gray; 40 | display: flex; 41 | flex-direction: column; 42 | flex: 1; 43 | } 44 | 45 | .nav { 46 | border-bottom: 1px solid --light-gray; 47 | font-size: 0.875rem; 48 | } 49 | 50 | .content { 51 | display: flex; 52 | flex: 1; 53 | } 54 | -------------------------------------------------------------------------------- /app/components/team-list-wrapper/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{team-list teams=teams}} 4 |
5 | 6 |
7 | 8 |
9 | {{#intercom-launcher local-class='aside-item'}} 10 | {{svg-jar "Help" height="32" width="32"}} 11 | {{/intercom-launcher}} 12 | 13 |
14 | {{#basic-dropdown as |dropdown|}} 15 | {{#dropdown.trigger}} 16 | {{svg-jar "More" height="32" width="32"}} 17 | {{/dropdown.trigger}} 18 | 19 | {{#dropdown.content}} 20 | {{#ui-menu as |menu|}} 21 | {{#if changelogURL}} 22 | {{#menu.item link=changelogURL}}What's new?{{/menu.item}} 23 | {{/if}} 24 | 25 | {{#menu.item route=(array 'logout')}}Log Out...{{/menu.item}} 26 | {{/ui-menu}} 27 | {{/dropdown.content}} 28 | {{/basic-dropdown}} 29 |
30 |
31 |
32 | 33 |
34 | {{yield}} 35 |
36 | -------------------------------------------------------------------------------- /app/components/team-list/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | localClassNames: ['team-list'], 7 | slackTeams: computed.filterBy('teams', 'isSlack'), 8 | 9 | personalTeam: computed('teams.@each.isPersonal', function() { 10 | return this.get('teams').findBy('isPersonal'); 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /app/components/team-list/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | 4 | .item { 5 | display: block; 6 | margin-bottom: 1rem; 7 | } 8 | 9 | .item:last-child { 10 | margin-bottom: 0; 11 | } 12 | 13 | .item--active { 14 | position: relative; 15 | } 16 | 17 | .item--active::before { 18 | background: --blue; 19 | border-bottom-right-radius: 3px; 20 | border-top-right-radius: 3px; 21 | bottom: 0.125rem; 22 | content: ''; 23 | left: -1rem; 24 | position: absolute; 25 | top: 0.125rem; 26 | width: 3px; 27 | } 28 | 29 | .item--new { 30 | align-items: center; 31 | display: flex; 32 | justify-content: center; 33 | height: 36px; 34 | width: 36px; 35 | } 36 | 37 | .new-icon { 38 | display: block; 39 | fill: --gray; 40 | } 41 | 42 | .item--new:hover .new-icon { 43 | fill: --blue; 44 | } 45 | -------------------------------------------------------------------------------- /app/components/team-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if personalTeam}} 2 | {{#if personalTeam.domain}} 3 | {{#link-to "team.index" personalTeam 4 | local-class='item' 5 | activeClass=(local-class 'item--active')}} 6 | {{team-list-item team=personalTeam}} 7 | {{/link-to}} 8 | {{else}} 9 | {{#link-to "setup" 10 | local-class='item' 11 | activeClass=(local-class 'item--active')}} 12 | {{team-list-item team=personalTeam}} 13 | {{/link-to}} 14 | {{/if}} 15 | {{/if}} 16 | 17 | {{#each slackTeams as |team|}} 18 | {{#link-to "team.index" team 19 | local-class='item' 20 | activeClass=(local-class 'item--active')}} 21 | {{team-list-item team=team}} 22 | {{/link-to}} 23 | {{/each}} 24 | 25 | {{#link-to 'login' local-class='item item--new'}} 26 | {{svg-jar "Add" local-class='new-icon' height='32' width='32'}} 27 | {{/link-to}} 28 | -------------------------------------------------------------------------------- /app/components/team-nav/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed, inject } = Ember; 4 | 5 | export default Ember.Component.extend({ 6 | localClassNames: ['team-nav'], 7 | currentAccount: inject.service(), 8 | user: computed.readOnly('currentAccount.currentUser') 9 | }); 10 | -------------------------------------------------------------------------------- /app/components/team-nav/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | @value --extra-light-gray from 'canvas-colors/styles/colors'; 3 | @value --green from 'canvas-colors/styles/colors'; 4 | @value --light-gray from 'canvas-colors/styles/colors'; 5 | 6 | @value --cell-padding from 'canvas-web/components/routes/team-index/styles'; 7 | 8 | .team-nav { 9 | align-items: center; 10 | color: --dark-gray; 11 | display: flex; 12 | font-size: 0.875rem; 13 | justify-content: space-between; 14 | } 15 | 16 | .section { 17 | padding: --cell-padding; 18 | } 19 | 20 | .section--with-border { 21 | background: linear-gradient(white, --light-gray) top right no-repeat; 22 | background-size: 1px 100%; 23 | } 24 | 25 | .summary { 26 | align-items: center; 27 | display: flex; 28 | width: 12rem; 29 | } 30 | 31 | .avatar { 32 | background: --extra-light-gray; 33 | border-radius: 50%; 34 | display: block; 35 | height: 2rem; 36 | margin-right: 0.5rem; 37 | width: 2rem; 38 | } 39 | 40 | .summary-text { 41 | composes: flex-auto from 'canvas-web/styles/utilities'; 42 | } 43 | 44 | .name, 45 | .username { 46 | composes: truncate from 'canvas-web/styles/utilities'; 47 | } 48 | 49 | .name { 50 | font-weight: 600; 51 | } 52 | 53 | .username.is-online::before { 54 | background: --green; 55 | border-radius: 50%; 56 | content: ''; 57 | display: inline-block; 58 | height: 6px; 59 | margin-right: 4px; 60 | vertical-align: middle; 61 | width: 6px; 62 | } 63 | -------------------------------------------------------------------------------- /app/components/team-nav/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if team.isSlack}} 4 | 5 | {{/if}} 6 | 7 |
8 | {{#if team.isSlack}} 9 |
{{team.name}}
10 |
{{user.name}}
11 | {{else}} 12 |
{{team.domain}}
13 |
Personal Notes
14 | {{/if}} 15 |
16 |
17 |
18 | 19 |
20 | {{new-canvas-button 21 | channel=channel 22 | team=team 23 | didCreateCanvas=didCreateCanvas}} 24 |
25 | -------------------------------------------------------------------------------- /app/components/ui-billboard/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | fullScreen: false, 5 | localClassNames: ['ui-billboard'], 6 | localClassNameBindings: ['fullScreen:full-screen'] 7 | }); 8 | -------------------------------------------------------------------------------- /app/components/ui-billboard/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | 3 | .ui-billboard { 4 | align-items: center; 5 | background: white; 6 | display: flex; 7 | justify-content: center; 8 | text-align: center; 9 | } 10 | 11 | .ui-billboard.full-screen { 12 | min-height: 100vh; 13 | } 14 | 15 | .emoji { 16 | font-size: 3rem; 17 | } 18 | 19 | .heading { 20 | font-weight: 300; 21 | } 22 | 23 | .message { 24 | color: --dark-gray; 25 | line-height: 1.5; 26 | max-width: 35rem; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/ui-billboard/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{emoji}}
3 | 4 |

{{title}}

5 | 6 |
7 | {{yield}} 8 |
9 |
10 | -------------------------------------------------------------------------------- /app/components/ui-flash-message/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['ui-flash-message'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/ui-flash-message/styles.css: -------------------------------------------------------------------------------- 1 | .ui-flash-message { 2 | background: rgba(0, 0, 0, 0.8); 3 | border-radius: 1000px; 4 | color: white; 5 | font-size: 0.75rem; 6 | display: flex; 7 | padding: 0 0.25rem; 8 | } 9 | 10 | .cell { 11 | align-items: center; 12 | display: flex; 13 | padding: 0.5rem 0.25rem; 14 | } 15 | 16 | .emoji { 17 | composes: cell; 18 | align-items: center; 19 | display: flex; 20 | } 21 | 22 | .emoji-content { 23 | font-size: 1.75rem; 24 | line-height: 1; 25 | position: relative; 26 | top: 0.125rem; 27 | } 28 | 29 | .message { 30 | composes: cell; 31 | opacity: 0.8; 32 | } 33 | 34 | .action { 35 | composes: cell; 36 | border-left: 1px solid rgba(255, 255, 255, 0.25); 37 | cursor: pointer; 38 | } 39 | 40 | .action-icon { 41 | display: block; 42 | fill: white; 43 | } 44 | -------------------------------------------------------------------------------- /app/components/ui-flash-message/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{flash.emoji}} 4 |
5 |
6 | 7 |
8 |
9 | {{flash.message}} 10 |
11 |
12 | 13 | {{#if flash.action}} 14 |
15 |
16 | {{svg-jar flash.actionIcon height="32" width="32" local-class="action-icon"}} 17 |
18 |
19 | {{/if}} 20 | -------------------------------------------------------------------------------- /app/components/ui-list/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/ui-list/item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['ui-list-item'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/ui-list/item/styles.css: -------------------------------------------------------------------------------- 1 | @value --light-gray from 'canvas-colors/styles/colors'; 2 | 3 | .ui-list-item { 4 | border-bottom: 1px solid --light-gray; 5 | padding: 0.5em 0; 6 | 7 | &:last-child { 8 | border: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/components/ui-list/item/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /app/components/ui-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash item=(component 'ui-list/item'))}} 2 | -------------------------------------------------------------------------------- /app/components/ui-menu/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['ui-menu'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/ui-menu/item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /** 4 | * Renders a menu item inside of a `ui-menu`. 5 | * 6 | * The item can be used to render: 7 | * 8 | * 1. A regular link tag (i.e. ``) 9 | * 2. A `link-to` component 10 | * 3. Or calling a closure action 11 | * 12 | * 1. A UI menu as a link tag for external links: 13 | * 14 | * {{#ui-menu link='https://example.com'}} 15 | * Example 16 | * {{/ui-menu}} 17 | * 18 | * 2. A UI menu using a `link-to` component for internal routes: 19 | * 20 | * {{#ui-menu route=(array 'foo' 'bar' 'baz'}} 21 | * Example 22 | * {{/ui-menu}} 23 | * 24 | * 3. A UI menu using a closure action: 25 | * 26 | * {{#ui-menu action=(action 'beep')}} 27 | * Example 28 | * {{/ui-menu}} 29 | * 30 | * @class CanvasWeb.UIMenuItemComponent 31 | * @extends Ember.Component 32 | */ 33 | export default Ember.Component.extend({ 34 | localClassNames: ['ui-menu-item'], 35 | 36 | /** 37 | * @member {?Function} The closure action to be called: 38 | */ 39 | action: null, 40 | 41 | /** 42 | * @member {?string} The download attribute for a `link`: 43 | */ 44 | download: null, 45 | 46 | /** 47 | * @member {?string} A URL that will render the `ui-menu` item as a link: 48 | */ 49 | link: null, 50 | 51 | /** 52 | * @member {?Array} An array to be passted to a link-to component's `params` 53 | * property: 54 | */ 55 | route: null, 56 | 57 | /** 58 | * Calls the passed in `action` when the user clicks on the item. 59 | * 60 | * @method 61 | */ 62 | click() { 63 | const action = this.get('action'); 64 | if (action) action(); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /app/components/ui-menu/item/styles.css: -------------------------------------------------------------------------------- 1 | @value --blue from "canvas-colors/styles/colors"; 2 | 3 | .ui-menu-item { 4 | display: block; 5 | border-radius: 3px; 6 | margin-bottom: 0.125rem; 7 | padding: 0.25rem 0.5rem; 8 | } 9 | 10 | .ui-menu-item:hover { 11 | background: --blue; 12 | color: white; 13 | cursor: pointer; 14 | } 15 | 16 | .ui-menu-item:last-child { 17 | margin-bottom: 0; 18 | } 19 | 20 | .link { 21 | color: inherit; 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui-menu/item/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if link}} 2 | 3 | {{yield}} 4 | 5 | {{else if route}} 6 | {{#link-to params=route local-class='link'}} 7 | {{yield}} 8 | {{/link-to}} 9 | {{else}} 10 | {{yield}} 11 | {{/if}} 12 | -------------------------------------------------------------------------------- /app/components/ui-menu/styles.css: -------------------------------------------------------------------------------- 1 | .ui-menu { 2 | font-size: 0.875rem; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/ui-menu/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{yield (hash item=(component 'ui-menu/item'))}} 3 |
4 | -------------------------------------------------------------------------------- /app/components/ui-settings-list/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/ui-settings-list/item/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | localClassNames: ['root'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/ui-settings-list/item/styles.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | @value --light-gray from 'canvas-colors/styles/colors'; 3 | 4 | .root { 5 | border-bottom: 1px solid --light-gray; 6 | margin-bottom: 2rem; 7 | padding-bottom: 2rem; 8 | } 9 | 10 | .root:last-child { 11 | border: none; 12 | margin-bottom: 0; 13 | } 14 | 15 | .title { 16 | color: --dark-gray; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui-settings-list/item/template.hbs: -------------------------------------------------------------------------------- 1 |

{{title}}

2 | 3 | {{yield}} 4 | -------------------------------------------------------------------------------- /app/components/ui-settings-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash item=(component 'ui-settings-list/item'))}} 2 | -------------------------------------------------------------------------------- /app/components/yielding-link-to/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.LinkComponent.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/yielding-link-to/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash href=href)}} 2 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/controllers/not-found.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed, inject } = Ember; 4 | 5 | /** 6 | * A controller for our 404 page to give us access to the current user session. 7 | * 8 | * @class CanvasWeb.NotFoundController 9 | * @extends Ember.Controller 10 | */ 11 | export default Ember.Controller.extend({ 12 | /** 13 | * @member {CanvasWeb.CurrentAccountService} A service exposing the current 14 | * user account 15 | */ 16 | currentAccount: inject.service(), 17 | 18 | /** 19 | * @member {boolean} Whether the current user is logged in 20 | */ 21 | loggedIn: computed.readOnly('currentAccount.loggedIn') 22 | }); 23 | -------------------------------------------------------------------------------- /app/controllers/team/canvas/history.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { toShareDBBlock } from 'canvas-web/components/routes/canvas-show/component'; 3 | 4 | /** 5 | * A controller for the canvas history route 6 | * 7 | * @class CanvasWeb.CanvasHistoryController 8 | * @extends Ember.Controller 9 | */ 10 | export default Ember.Controller.extend({ 11 | queryParams: 'version'.w(), 12 | version: null, 13 | 14 | actions: { 15 | /** 16 | * Called when the user wishes to clone the canvas at the current history 17 | * point. 18 | * 19 | * @method 20 | */ 21 | clone() { 22 | const canvas = this.get('model'); 23 | const blocks = this.get('model.blocks').map(toShareDBBlock); 24 | 25 | this.get('store').createRecord('canvas', { 26 | blocks, 27 | isTemplate: canvas.get('isTemplate'), 28 | linkAccess: canvas.get('linkAccess'), 29 | slackChannelIds: canvas.get('slackChannelIds'), 30 | team: canvas.get('team') 31 | }).save().then(clonedCanvas => { 32 | this.transitionToRoute('team.canvas.show', clonedCanvas); 33 | }); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /app/controllers/team/canvas/show.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Controller.extend({ 6 | queryParams: 'block filter'.w(), 7 | block: null, 8 | filter: '', 9 | blockID: computed.readOnly('block') 10 | }); 11 | -------------------------------------------------------------------------------- /app/controllers/team/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | queryParams: ['channel'], 5 | channel: null 6 | }); 7 | -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/helpers/diff.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Helper.helper(([arr1 = [], arr2 = []]) => 3 | arr2.filter(val => !arr1.includes(val))); 4 | -------------------------------------------------------------------------------- /app/helpers/is-dismissed.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Helper.extend({ 3 | uiDismissals: Ember.inject.service(), 4 | dismissalsChanged: Ember.observer('uiDismissals.dismissals.isFulfilled', 5 | 'uiDismissals.dismissals.content.[]', function() { 6 | this.recompute(); 7 | }), 8 | compute([scope, ui]) { 9 | const dismissals = this.get('uiDismissals.dismissals'); 10 | ui = ui || scope; 11 | if (dismissals.get('isFulfilled')) { 12 | const combinations = generateCombinations(scope, ui); 13 | return dismissals.mapBy('identifier').any(i => combinations.includes(i)); 14 | } 15 | return true; 16 | } 17 | }); 18 | 19 | function generateCombinations(scope, ui) { 20 | const clients = ['*', 'web']; 21 | return [`${scope}.${ui}`, ui].reduce((p, n) => 22 | p.concat(clients.map(c => `${c}.${n}`)) 23 | , []); 24 | } 25 | -------------------------------------------------------------------------------- /app/helpers/symmetric-diff.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Helper.helper(([arr1 = [], arr2 = []]) => 3 | arr1.filter(val => !arr2.includes(val)) 4 | .concat(arr2.filter(val => !arr1.includes(val)))); 5 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Canvas 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | {{content-for "sentry"}} 22 | 23 | 24 | {{content-for "body-footer"}} 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/initializers/error-handling.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Raven from 'raven'; 3 | import RSVP from 'rsvp'; 4 | 5 | export default { 6 | name: 'error-handling', 7 | 8 | initialize() { 9 | this._oldWindowOnerror = window.onerror; 10 | 11 | Ember.onerror = this.handleEmberError.bind(this); 12 | RSVP.on('error', this.handleRSVPError.bind(this)); 13 | window.onerror = this.handleWindowError.bind(this); 14 | }, 15 | 16 | handleEmberError(err) { 17 | throw err; 18 | }, 19 | 20 | handleRSVPError(reason) { 21 | if (reason && reason.name === 'TransitionAborted') return; 22 | 23 | const context = 'Unhandled Promise error detected'; 24 | 25 | if (reason instanceof Error) { 26 | Raven.captureException(reason, { extra: { context } }); 27 | } else { 28 | Raven.captureMessage(context, { extra: { reason } }); 29 | } 30 | }, 31 | 32 | handleWindowError(/* msg, source, line, col, err */) { 33 | this._oldWindowOnerror(...arguments); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /app/initializers/sharedb-fuzz.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Base62UUID from 'canvas-editor/lib/base62-uuid'; 3 | 4 | export default { 5 | name: 'sharedb-fuzz', 6 | 7 | initialize() { 8 | window.shareDBFuzz = shareDBFuzz; 9 | window.shareDBFuzzBurst = shareDBFuzzBurst; 10 | } 11 | }; 12 | 13 | function shareDBFuzz(text) { 14 | const firstBlock = Ember.$('.canvas-block-paragraph-content').get(0); 15 | const $firstBlock = Ember.$(firstBlock); 16 | 17 | $firstBlock.text(`${$firstBlock.text()}${text} `); 18 | $firstBlock.trigger('input'); 19 | } 20 | 21 | function shareDBFuzzBurst(interval = 10, duration = 5000) { 22 | const id = Base62UUID.generate(); 23 | let index = 0; 24 | 25 | const intervalID = window.setInterval(_ => { 26 | shareDBFuzz(`${id}.${index}`); 27 | index += 1; 28 | }, interval); 29 | 30 | setTimeout(_ => window.clearInterval(intervalID), duration); 31 | 32 | console.info(`Fuzzing as: ${id}`); // eslint-disable-line no-console 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/copy-text.js: -------------------------------------------------------------------------------- 1 | export default function copyText(text) { 2 | const node = document.createElement('div'); 3 | node.appendChild(document.createTextNode(text)); 4 | document.body.appendChild(node); 5 | 6 | const range = document.createRange(); 7 | range.selectNodeContents(node); 8 | 9 | getSelection().removeAllRanges(); 10 | getSelection().addRange(range); 11 | 12 | document.execCommand('copy'); 13 | 14 | node.remove(); 15 | getSelection().removeAllRanges(); 16 | } 17 | -------------------------------------------------------------------------------- /app/lib/ns-events.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function nsEvents(object, events) { 4 | const guid = Ember.guidFor(object); 5 | return events.split(' ').map(event => `${event}.${guid}`).join(' '); 6 | } 7 | -------------------------------------------------------------------------------- /app/lib/preload.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import RSVP from 'rsvp'; 3 | 4 | const { get, isArray, typeOf } = Ember; 5 | 6 | export default function preload(object, toPreload) { 7 | return RSVP.resolve(object).then(result => { 8 | if (isArray(result)) { 9 | return preloadAll(result, toPreload); 10 | } 11 | 12 | return preloadRecord(result, toPreload); 13 | }); 14 | } 15 | 16 | function preloadAll(records, toPreload) { 17 | return RSVP.all(records.map(record => preload(record, toPreload))); 18 | } 19 | 20 | function preloadRecord(record, toPreload) { 21 | if (!record) return RSVP.resolve(record); 22 | 23 | const type = typeOf(toPreload); 24 | 25 | switch (type) { 26 | case 'string': 27 | return getPromise(record, toPreload).then(_ => record); 28 | case 'array': 29 | return RSVP.all(toPreload.map(preloadItem => { 30 | return preloadRecord(record, preloadItem); 31 | })).then(_ => record); 32 | case 'object': 33 | return RSVP.all(Object.keys(toPreload).map(preloadKey => { 34 | return getPromise(record, preloadKey).then(data => { 35 | return preload(data, toPreload[preloadKey]); 36 | }); 37 | })).then(_ => record); 38 | default: 39 | throw new Error(`Inrecognized preload type: "${type}"`); 40 | } 41 | } 42 | 43 | function getPromise(object, property) { 44 | return RSVP.resolve(get(object, property)); 45 | } 46 | -------------------------------------------------------------------------------- /app/mixins/channel-ids.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | channelIDs: Ember.computed('channel', 'team.channels.[]', function() { 5 | const channelName = this.get('channel'); 6 | if (!channelName) return []; 7 | const channel = this.get('team.channels').findBy('name', channelName); 8 | if (!channel) return []; 9 | return [channel.get('id')]; 10 | }) 11 | }); 12 | -------------------------------------------------------------------------------- /app/mixins/oauth.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Mixin.create({ 6 | /** 7 | * An OAuth client ID 8 | * @member {string} 9 | */ 10 | clientID: '', 11 | 12 | /** 13 | * An endpoint for authorizing 14 | * @member {string} 15 | */ 16 | endpoint: '', 17 | 18 | /** 19 | * An OAuth redirect URL 20 | * @member {string} 21 | */ 22 | redirectURL: '', 23 | 24 | /** 25 | * A list of OAuth identity scopes 26 | * @member {Array} 27 | */ 28 | scope: computed(_ => []), 29 | 30 | /** 31 | * A string used to identify OAuth flow state 32 | * @member {string} 33 | */ 34 | state: '', 35 | 36 | /** 37 | * OAuth scopes formatted as a query parameter 38 | * @member {string} 39 | */ 40 | scopeParam: computed('scope.[]', function() { 41 | return this.get('scope').join(','); 42 | }), 43 | 44 | 45 | /** 46 | * A URL used to sign in 47 | * @member {string} 48 | */ 49 | authorizeURL: computed('endpoint', 'scopeParam', function() { 50 | return `${this.get('endpoint')}?scope=${this.get('scopeParam')}\ 51 | &client_id=${this.get('clientID')}\ 52 | &state=${this.get('state')}\ 53 | &redirect_uri=${this.get('redirectURL')}`; 54 | }) 55 | }); 56 | -------------------------------------------------------------------------------- /app/mixins/ui-dropdown.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import nsEvents from 'canvas-web/lib/ns-events'; 3 | 4 | const { $, on } = Ember; 5 | 6 | export default Ember.Mixin.create({ 7 | eventList: 'click touchstart', 8 | isOpen: false, 9 | onClose: Ember.K, 10 | onOpen: Ember.K, 11 | 12 | setUpEventHandlers: on('didInsertElement', function() { 13 | $(document).on(nsEvents(this, this.get('eventList')), 14 | Ember.run.bind(this, 'dropdownClick')); 15 | }), 16 | 17 | tearDownEventHandlers: on('willDestroyElement', function() { 18 | $(document).off(nsEvents(this, this.get('eventList'))); 19 | }), 20 | 21 | click(event) { 22 | event.stopPropagation(); 23 | }, 24 | 25 | dropdownClick() { 26 | this.send('closeDropdown'); 27 | }, 28 | 29 | actions: { 30 | closeDropdown() { 31 | this.set('isOpen', false); 32 | this.get('onClose')(); 33 | }, 34 | 35 | openDropdown() { 36 | this.set('isOpen', true); 37 | this.get('onOpen')(); 38 | }, 39 | 40 | toggleDropdown() { 41 | this.toggleProperty('isOpen'); 42 | 43 | if (this.get('isOpen')) { 44 | this.get('onOpen')(); 45 | } else { 46 | this.get('onClose')(); 47 | } 48 | } 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/models/.gitkeep -------------------------------------------------------------------------------- /app/models/account.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { attr } = DS; 4 | 5 | export default DS.Model.extend({ 6 | intercomHash: attr(), 7 | insertedAt: attr('date'), 8 | updatedAt: attr('date') 9 | }); 10 | -------------------------------------------------------------------------------- /app/models/block.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({}); 4 | -------------------------------------------------------------------------------- /app/models/canvas-watch.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | 4 | export default DS.Model.extend({ 5 | canvasID: Ember.computed('canvas', function() { 6 | return this.belongsTo('canvas').id(); 7 | }), 8 | canvas: DS.belongsTo('canvas') 9 | }); 10 | -------------------------------------------------------------------------------- /app/models/comment.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | 4 | export default DS.Model.extend({ 5 | blocks: DS.attr(), 6 | insertedAt: DS.attr('date', { defaultValue() { return new Date(); } }), 7 | updatedAt: DS.attr('date'), 8 | canvas: DS.belongsTo('canvas'), 9 | block: DS.belongsTo('block'), 10 | creator: DS.belongsTo('user'), 11 | blockID: Ember.computed.readOnly('block.id') 12 | }); 13 | -------------------------------------------------------------------------------- /app/models/custom-inflector-rules.js: -------------------------------------------------------------------------------- 1 | import Inflector from 'ember-inflector'; 2 | 3 | const inflector = Inflector.inflector; 4 | 5 | inflector.irregular('canvas', 'canvases'); 6 | 7 | // Meet Ember Inspector's expectation of an export 8 | export default {}; 9 | -------------------------------------------------------------------------------- /app/models/op.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { attr, belongsTo } = DS; 4 | 5 | /** 6 | * A model representing a single OT operation on a canvas. 7 | * 8 | * @class CanvasWeb.Op 9 | * @extends DS.Model 10 | */ 11 | export default DS.Model.extend({ 12 | components: attr(), 13 | version: attr(), 14 | 15 | canvas: belongsTo('canvas', { async: true }), 16 | 17 | insertedAt: attr('date'), 18 | updatedAt: attr('date') 19 | }); 20 | -------------------------------------------------------------------------------- /app/models/pulse-event.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | 4 | const { attr, belongsTo } = DS; 5 | const { computed } = Ember; 6 | 7 | export default DS.Model.extend({ 8 | providerName: attr(), 9 | providerUrl: attr(), 10 | referencer: attr(), 11 | type: attr(), 12 | url: attr(), 13 | 14 | canvas: belongsTo('canvas', { async: true }), 15 | 16 | insertedAt: attr('date'), 17 | updatedAt: attr('date'), 18 | 19 | unfurled: computed('url', function() { 20 | return this.store.findRecord('unfurl', this.get('url')); 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /app/models/slack-channel.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | import RSVP from 'rsvp'; 4 | import ENV from 'canvas-web/config/environment'; 5 | 6 | const { attr, belongsTo } = DS; 7 | const { computed } = Ember; 8 | 9 | let TOPIC_REGEX; 10 | if (ENV.isElectron) { 11 | TOPIC_REGEX = 12 | new RegExp(`https?://pro.usecanvas.com/([^/]+)/([a-z0-9]{22})`, 'i'); 13 | } else { 14 | TOPIC_REGEX = 15 | new RegExp(`https?://${window.location.host}/([^/]+)/([a-z0-9]{22})`, 'i'); 16 | } 17 | 18 | export default DS.Model.extend({ 19 | name: attr(), 20 | topic: attr(), 21 | 22 | team: belongsTo('team', { async: true, inverse: 'channels' }), 23 | 24 | topicCanvas: computed('topic', function() { 25 | const topic = this.get('topic') || ''; 26 | const match = topic.match(TOPIC_REGEX); 27 | if (!match) return RSVP.resolve(null); 28 | 29 | const canvasID = match[2]; 30 | 31 | return this.get('store').findRecord('canvas', canvasID, { 32 | adapterOptions: { team: this.get('team') } 33 | }); 34 | }) 35 | }).reopenClass({ 36 | modelName: 'slack-channel' 37 | }); 38 | -------------------------------------------------------------------------------- /app/models/team.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | 4 | const { attr, belongsTo, hasMany } = DS; 5 | const { computed } = Ember; 6 | 7 | export default DS.Model.extend({ 8 | domain: attr(), 9 | name: attr(), 10 | needsSlackToken: attr(), 11 | images: attr(), 12 | isInTeam: attr(), 13 | slackId: attr(), 14 | slackScopes: attr(), 15 | 16 | accountUser: belongsTo('user', { async: true }), 17 | canvases: hasMany('canvas', { async: true }), 18 | channels: hasMany('slackChannel', { async: true }), 19 | 20 | insertedAt: attr('date'), 21 | updatedAt: attr('date'), 22 | 23 | hasChannelsRead: hasScope('channels:read'), 24 | hasChannelsHistory: hasScope('channels:history'), 25 | 26 | image88: computed('images.[]', function() { 27 | return this.get('images.image_88'); 28 | }), 29 | 30 | isPersonal: computed('slackId', function() { 31 | return !this.get('slackId'); 32 | }), 33 | 34 | isSlack: computed.not('isPersonal'), 35 | 36 | fetchTemplates() { 37 | return this.store.adapterFor('team').fetchTemplates(this); 38 | }, 39 | 40 | getDomainError() { 41 | const errors = this.get('errors').errorsFor('domain'); 42 | if (!errors.length) return null; 43 | return errors[0].message; 44 | } 45 | }); 46 | 47 | function hasScope(scope) { 48 | return Ember.computed('slackScopes.[]', function() { 49 | return (this.get('slackScopes') || []).includes(scope); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /app/models/thread-subscription.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | subscribed: DS.attr(), 5 | canvas: DS.belongsTo('canvas') 6 | }); 7 | -------------------------------------------------------------------------------- /app/models/token.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | token: DS.attr(), 5 | expiresAt: DS.attr('date') 6 | }); 7 | -------------------------------------------------------------------------------- /app/models/ui-dismissal.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | export default DS.Model.extend({ 3 | identifier: DS.attr() 4 | }); 5 | -------------------------------------------------------------------------------- /app/models/unfurl.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { attr } = DS; 4 | 5 | /** 6 | * A URL unfurl 7 | * 8 | * @class Unfurl 9 | * @extends DS.Model 10 | */ 11 | export default DS.Model.extend({ 12 | attachments: attr(), 13 | fetched: attr(), 14 | fields: attr(), 15 | height: attr(), 16 | html: attr(), 17 | labels: attr(), 18 | providerIconUrl: attr(), 19 | providerName: attr(), 20 | providerUrl: attr(), 21 | text: attr(), 22 | thumbnailUrl: attr(), 23 | title: attr(), 24 | type: attr(), 25 | width: attr(), 26 | url: attr() 27 | }); 28 | -------------------------------------------------------------------------------- /app/models/upload-signature.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { attr } = DS; 4 | 5 | export default DS.Model.extend({ 6 | uploadUrl: attr(), 7 | signature: attr(), 8 | policy: attr() 9 | }); 10 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | 4 | const { attr, belongsTo, hasMany } = DS; 5 | const { computed } = Ember; 6 | 7 | export default DS.Model.extend({ 8 | avatarUrl: attr(), 9 | email: attr(), 10 | images: attr(), 11 | name: attr(), 12 | slackId: attr(), 13 | 14 | canvases: hasMany('canvas', { async: true }), 15 | team: belongsTo('team', { async: true }), 16 | 17 | insertedAt: attr('date'), 18 | updatedAt: attr('date'), 19 | 20 | image72: computed('images.[]', function() { 21 | return this.get('images.image_72'); 22 | }) 23 | }); 24 | -------------------------------------------------------------------------------- /app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { // eslint-disable-line array-callback-return 10 | this.route('post-auth'); 11 | this.route('login'); 12 | this.route('logout'); 13 | this.route('setup'); 14 | this.route('beta', { path: '/beta' }); 15 | this.route('team', { path: '/:domain' }, function() { 16 | this.route('slack', { path: '/slack' }); 17 | this.route('settings', { path: '/settings' }); 18 | 19 | this.route('canvas', { path: '/:id' }, function() { 20 | this.route('show', { path: '/' }); 21 | this.route('history', { path: '/history' }); 22 | this.route('pulse', { path: '/pulse' }); 23 | this.route('settings', { path: '/settings' }); 24 | }); 25 | }); 26 | 27 | // For rendering 500 in error handler 28 | this.route('server-error'); 29 | 30 | // Catch all 31 | this.route('not-found', { path: '*path' }); 32 | }); 33 | 34 | export default Router; 35 | -------------------------------------------------------------------------------- /app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/routes/.gitkeep -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | const model = this.modelFor('application'); 6 | if (!model) return this.transitionTo('login'); 7 | 8 | const teams = model.teams; 9 | let team; 10 | try { 11 | team = teams.findBy('id', localStorage.lastTeamID); 12 | } catch (_err) { 13 | // Ignore 14 | } 15 | 16 | team = team || teams.filterBy('isSlack').get('firstObject'); 17 | return this.replaceWith('team', team.get('domain')); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /app/routes/logout.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | /** 5 | * Service for the signed-in account 6 | * @member {Ember.Service} 7 | */ 8 | currentAccount: Ember.inject.service(), 9 | 10 | /** 11 | * Callback called before the model is loaded in order to log the user out. 12 | * 13 | * @method 14 | * @memberof Web.ApplicationRoute 15 | * @instance 16 | * @private 17 | * @returns {Ember.RSVP.Promise} A promise resolving when the user has been 18 | * successfully logged out 19 | */ 20 | beforeModel() { 21 | return this.get('currentAccount') 22 | .logout() 23 | .then(_ => this.transitionTo('login')) 24 | .then(_ => window.location.reload()) 25 | .catch(err => { 26 | throw err; 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/routes/post-auth.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | beforeModel() { 5 | window.close(); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/setup.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed, inject } = Ember; 4 | 5 | /** 6 | * The personal team setup route 7 | * 8 | * @class CanvasWeb.SetupRoute 9 | * @extends Ember.Route 10 | */ 11 | export default Ember.Route.extend({ 12 | currentAccount: inject.service(), 13 | teamsList: inject.service(), 14 | 15 | /** 16 | * The current account's personal team 17 | * 18 | * @member {CanvasWeb.Team} 19 | */ 20 | personalTeam: computed('teamsList.teams.[]', function() { 21 | return this.get('teamsList.teams').findBy('isPersonal'); 22 | }), 23 | 24 | model() { 25 | return { 26 | personalTeam: this.get('personalTeam'), 27 | teams: this.get('teamsList.teams') 28 | }; 29 | }, 30 | 31 | actions: { 32 | /** 33 | * Redirect to the personal team when it is updated. 34 | * 35 | * @method 36 | * @param {CanvasWeb.Team} team The team to redirect to 37 | */ 38 | teamUpdated(team) { 39 | return this.transitionTo('team.index', team); 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /app/routes/team/canvas.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import preload from 'canvas-web/lib/preload'; 3 | 4 | export default Ember.Route.extend({ 5 | model({ id }) { 6 | return this.get('store').findRecord('canvas', id, 7 | { adapterOptions: { team: this.modelFor('team') } }); 8 | }, 9 | 10 | afterModel() { 11 | const team = this.modelFor('team'); 12 | if (team.get('hasChannelsRead') && team.get('isInTeam')) { 13 | return preload(this.modelFor('team'), ['channels']); 14 | } 15 | 16 | return null; 17 | }, 18 | 19 | titleToken(canvas) { 20 | return canvas.get('title'); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/routes/team/canvas/history.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /** 4 | * The canvas history route 5 | * 6 | * @class CanvasWeb.CanvasHistoryRoute 7 | * @extends Ember.Route 8 | */ 9 | export default Ember.Route.extend({ 10 | segment: Ember.inject.service(), 11 | 12 | model() { 13 | this.get('segment').trackEvent('Entered Time Machine'); 14 | return this.modelFor('team.canvas') 15 | .reload() 16 | .then(canvas => { 17 | return canvas.hasMany('ops').reload().then(_ => canvas); 18 | }); 19 | }, 20 | 21 | queryParams: { 22 | version: { 23 | replace: true 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/routes/team/canvas/pulse.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | const canvas = this.modelFor('team.canvas'); 6 | 7 | return canvas.get('pulseEvents').then(pulseEvents => { 8 | return [this.get('store').createRecord('pulseEvent', { 9 | providerName: 'Canvas', 10 | providerUrl: 'https://pro.usecanvas.com', 11 | type: 'canvas_created', 12 | referencer: { 13 | id: canvas.get('creator.id'), 14 | avatarUrl: canvas.get('creator.avatarUrl'), 15 | email: canvas.get('creator.email'), 16 | name: canvas.get('creator.name'), 17 | url: `mailto:${canvas.get('creator.email')}` 18 | } 19 | })].unshiftObjects(pulseEvents.toArray()); 20 | }).then(pulseEvents => ({ canvas, pulseEvents })); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/routes/team/canvas/settings.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.modelFor('team.canvas'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/team/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | didTransition() { 6 | const team = this.controller.get('model'); 7 | 8 | try { 9 | localStorage.lastTeamID = team.get('id'); 10 | } catch (_err) { 11 | // Ignore 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/routes/team/settings.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.JSONAPISerializer.extend({ 4 | keyForAttribute(attribute) { 5 | return attribute.underscore(); 6 | }, 7 | 8 | keyForRelationship(relationship) { 9 | return relationship.underscore(); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /app/serializers/canvas.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from 'canvas-web/serializers/application'; 2 | 3 | export default ApplicationSerializer.extend({ 4 | /** 5 | * Do not set "blocks" on an update (they are changed in realtime). 6 | * 7 | * @method 8 | * @override 9 | */ 10 | normalizeUpdateRecordResponse(store, klass, payload) { 11 | Reflect.deleteProperty(payload.data.attributes, 'blocks'); 12 | return this._super(...arguments); 13 | }, 14 | 15 | /** 16 | * Get the model name from the payload type. 17 | * 18 | * @method 19 | * @override 20 | */ 21 | modelNameFromPayloadKey(payloadKey) { 22 | return payloadKey; 23 | }, 24 | 25 | /** 26 | * Get the payload type from the canvas model name. 27 | * 28 | * @method 29 | * @override 30 | */ 31 | payloadKeyFromModelName(modelName) { 32 | return modelName; 33 | }, 34 | 35 | /** 36 | * Do not attempt to serialize and send "blocks" in a save. 37 | * 38 | * @method 39 | * @override 40 | */ 41 | serializeAttribute(snapshot, json, key, attributes) { 42 | if (snapshot.record.get('isNew')) { 43 | this._super(...arguments); 44 | return; 45 | } 46 | 47 | if (key === 'blocks') return; 48 | 49 | if (snapshot.record.get('isNew') || snapshot.changedAttributes()[key]) { 50 | this._super(snapshot, json, key, attributes); 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /app/serializers/comment.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from 'canvas-web/serializers/application'; 2 | 3 | export default ApplicationSerializer.extend({ 4 | /** 5 | * Get the model name from the payload type. 6 | * 7 | * @method 8 | * @override 9 | */ 10 | modelNameFromPayloadKey(payloadKey) { 11 | return payloadKey; 12 | }, 13 | 14 | /** 15 | * Get the payload type from the model name. 16 | * 17 | * @method 18 | * @override 19 | */ 20 | payloadKeyFromModelName(modelName) { 21 | return modelName; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /app/serializers/thread-subscription.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from 'canvas-web/serializers/application'; 2 | 3 | export default ApplicationSerializer.extend({ 4 | /** 5 | * Get the model name from the payload type. 6 | * 7 | * @method 8 | * @override 9 | */ 10 | modelNameFromPayloadKey(payloadKey) { 11 | return payloadKey; 12 | }, 13 | 14 | /** 15 | * Get the payload type from the model name. 16 | * 17 | * @method 18 | * @override 19 | */ 20 | payloadKeyFromModelName(modelName) { 21 | return modelName; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /app/services/csrf-token.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'cookies'; 2 | import Ember from 'ember'; 3 | import ENV from 'canvas-web/config/environment'; 4 | 5 | const { computed } = Ember; 6 | let token; 7 | 8 | if (window.requireNode) { 9 | const storage = window.requireNode('electron-json-storage'); 10 | storage.get('csrf', (_, data) => { 11 | token = data; 12 | }); 13 | } 14 | 15 | export default Ember.Service.extend({ 16 | token: computed(function() { 17 | return ENV.isElectron ? token : Cookies.get('csrf_token'); 18 | }).volatile() 19 | }); 20 | -------------------------------------------------------------------------------- /app/services/current-account.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Raven from 'raven'; 3 | 4 | const { inject } = Ember; 5 | 6 | export default Ember.Service.extend({ 7 | /** 8 | * The logged-in account. 9 | * @member {CanvasWeb.Account} 10 | */ 11 | currentAccount: null, 12 | 13 | /** 14 | * The active team user. 15 | * @member {CanvasWeb.User} 16 | */ 17 | currentUser: null, 18 | 19 | /** 20 | * The CSRF token service 21 | * @member {Ember.Service} 22 | */ 23 | csrfToken: inject.service(), 24 | 25 | dexie: inject.service(), 26 | 27 | loggedIn: false, 28 | 29 | /** 30 | * The data store 31 | * @member {DS.Store} 32 | */ 33 | store: inject.service(), 34 | 35 | /** 36 | * Fetch and set the logged-in account. 37 | * 38 | * @method 39 | * @returns {Ember.RSVP.Promise} A promise resolving once the user is fetched 40 | * and set 41 | */ 42 | fetch() { 43 | return this.get('store').queryRecord('account', 'me').then(account => { 44 | this.set('loggedIn', true); 45 | this.set('currentAccount', account); 46 | Raven.setUserContext(account.getProperties('id')); 47 | return account; 48 | }); 49 | }, 50 | 51 | /** 52 | * Log out the logged-in account. 53 | * 54 | * @method 55 | * @returns {Ember.RSVP.Promise} A promise resolving once the account is 56 | * logged out 57 | */ 58 | logout() { 59 | return this.get('store').adapterFor('session').logout().then(_ => { 60 | this.set('loggedIn', false); 61 | this.set('currentAccount', null); 62 | return this.get('dexie.db').clear(); 63 | }); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /app/services/desktop-menus.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'canvas-web/config/environment'; 3 | 4 | /* globals requireNode */ 5 | 6 | const { computed, observer } = Ember; 7 | 8 | export default Ember.Service.extend({ 9 | setup() { 10 | if (!ENV.isElectron) return; 11 | 12 | const { remote } = requireNode('electron'); 13 | const { Menu } = remote; 14 | const template = this.get('defaultTemplate'); 15 | 16 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 17 | }, 18 | 19 | onMenuUpdate: observer('editMenu.[]', function() { 20 | if (!ENV.isElectron) return; 21 | this.setup(); 22 | }), 23 | 24 | defaultTemplate: computed('applicationMenu.[]', 'editMenu.[]', function() { 25 | return [{ 26 | label: 'Application', 27 | submenu: this.get('applicationMenu') 28 | }, { 29 | label: 'Edit', 30 | submenu: this.get('editMenu') 31 | }]; 32 | }), 33 | 34 | applicationMenu: computed(function() { 35 | const quit = this.quit.bind(this); 36 | 37 | return [ 38 | { label: 'About Canvas', selector: 'orderFrontStandardAboutPanel:' }, 39 | { type: 'separator' }, 40 | { label: 'Quit', accelerator: 'Command+Q', click: quit } 41 | ]; 42 | }), 43 | 44 | editMenu: computed(function() { 45 | return [ 46 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, 47 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, 48 | { type: 'separator' }, 49 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, 50 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, 51 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, 52 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' } 53 | ]; 54 | }), 55 | 56 | quit() { 57 | const { remote } = requireNode('electron'); 58 | remote.app.quit(); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /app/services/dexie.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | import Ember from 'ember'; 3 | 4 | export default Ember.Service.extend({ 5 | db: Ember.computed(function() { 6 | const db = new Dexie('canvas_pro'); 7 | db.version(1).stores({ 8 | snapshots: 'id' 9 | }); 10 | return db.snapshots; 11 | }), 12 | 13 | persist(id, blocks) { 14 | const db = this.get('db'); 15 | const maxSize = 50; 16 | return db.get(id).then(item => { 17 | const hist = item ? item.hist.slice(-maxSize) : []; 18 | hist.push(blocks); 19 | return db.put({ id, hist }); 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/services/phoenix-socket.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import RSVP from 'rsvp'; 3 | import ENV from 'canvas-web/config/environment'; 4 | import Phoenix from 'canvas-web/lib/phoenix'; 5 | import { task } from 'ember-concurrency'; 6 | 7 | const { computed, inject, run } = Ember; 8 | 9 | export default Ember.Service.extend({ 10 | currentAccount: inject.service(), 11 | store: inject.service(), 12 | 13 | liveURL: computed(_ => { 14 | const host = ENV.apiURL.replace(/^.+?\/\//, '').replace(/\/v1\/$/, ''); 15 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 16 | return `${protocol}//${host}/socket`; 17 | }), 18 | 19 | /** 20 | * @member {Ember.Object} An object containing socket params that will be 21 | * read anew each time the socket attempts to connect or reconnect 22 | */ 23 | socketParams: Ember.computed(_ => Ember.Object.create()), 24 | 25 | socket: computed('currentAccount.loggedIn', function() { 26 | if (!this.get('currentAccount.loggedIn')) return RSVP.Promise.resolve(null); 27 | 28 | return this.get('setSocketToken').perform().then(_ => { 29 | const socket = new Phoenix.Socket(this.get('liveURL'), { 30 | heartbeatIntervalMs: 15000, 31 | params: this.get('socketParams') 32 | }); 33 | 34 | socket.connect(); 35 | 36 | socket.onClose(run.bind(this, function() { 37 | this.get('setSocketToken').perform(); 38 | })); 39 | 40 | this._socket = socket; 41 | return socket; 42 | }); 43 | }), 44 | 45 | /** 46 | * Create an access token and set it as a property on the socket params. 47 | * 48 | * @method 49 | */ 50 | setSocketToken: task(function *() { 51 | try { 52 | const token = yield this.get('store').createRecord('token', {}).save(); 53 | this.set('socketParams.token', token.get('token')); 54 | } catch (_err) { 55 | // Ignore failed token creation 56 | } 57 | }) 58 | }); 59 | -------------------------------------------------------------------------------- /app/services/team-query.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | store: Ember.inject.service(), 5 | 6 | findByDomain(domain) { 7 | return this.get('store') 8 | .queryRecord('team', { filter: { domain } }) 9 | .then(team => { 10 | if (!team) { 11 | const err = new Error('Not found'); 12 | err.status = 404; 13 | throw err; 14 | } 15 | return team; 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/services/teams-list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed } = Ember; 4 | 5 | export default Ember.Service.extend({ 6 | currentAccount: Ember.inject.service(), 7 | store: Ember.inject.service(), 8 | 9 | teams: computed('currentAccount.currentAccount', function() { 10 | return this.get('store').findAll('team'); 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /app/services/ui-dismissals.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Service.extend({ 3 | store: Ember.inject.service(), 4 | 5 | dismissals: Ember.computed(function() { 6 | return this.get('store').findAll('ui-dismissal'); 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /app/services/unfurler.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { inject } = Ember; 4 | 5 | export default Ember.Service.extend({ 6 | store: inject.service(), 7 | 8 | unfurl(url) { 9 | return this.get('store').findRecord('unfurl', url); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | @value --black from 'canvas-colors/styles/colors'; 2 | @value --blue from 'canvas-colors/styles/colors'; 3 | 4 | *, 5 | *::after, 6 | *::before { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body { 12 | color: --black; 13 | font-family: -apple-system, BlinkMacSystemFont, 14 | "Segoe UI", "Roboto", "Oxygen", 15 | "Ubuntu", "Cantarell", "Fira Sans", 16 | "Droid Sans", "Helvetica Neue", sans-serif; 17 | font-size: 16px; 18 | } 19 | 20 | a { 21 | color: --blue; 22 | text-decoration: none; 23 | } 24 | 25 | strong { 26 | font-weight: 600; 27 | } 28 | -------------------------------------------------------------------------------- /app/styles/application.css: -------------------------------------------------------------------------------- 1 | .flash-messages { 2 | bottom: 1rem; 3 | left: 1rem; 4 | position: absolute; 5 | width: 28rem; 6 | } 7 | 8 | .flash-messages-item { 9 | margin-bottom: 0.5rem; 10 | } 11 | 12 | .flash-messages-item:last-child { 13 | margin-bottom: 0; 14 | } 15 | -------------------------------------------------------------------------------- /app/styles/ember-basic-dropdown.css: -------------------------------------------------------------------------------- 1 | @value --gray from 'canvas-colors/styles/colors'; 2 | 3 | :global { 4 | .ember-basic-dropdown-trigger { 5 | outline: none; 6 | } 7 | 8 | /** 9 | * Add custom styles to the dropdown content area. 10 | * 11 | * 1. Use transform instead of top because ember-basic-dropdown uses JS logic 12 | * to set the top property. 13 | */ 14 | .ember-basic-dropdown-content { 15 | background: white; 16 | border-radius: 4px; 17 | box-shadow: 0 2px 8px --gray; 18 | padding: 0.25rem; 19 | transform: translateY(0.5rem); /* 1 */ 20 | width: 12rem; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/styles/team/canvas.css: -------------------------------------------------------------------------------- 1 | @value --dark-gray from 'canvas-colors/styles/colors'; 2 | @value --gray from 'canvas-colors/styles/colors'; 3 | @value --light-gray from 'canvas-colors/styles/colors'; 4 | 5 | .container { 6 | display: flex; 7 | height: 100vh; 8 | } 9 | 10 | .aside { 11 | border-right: 1px solid --light-gray; 12 | align-items: center; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: space-between; 16 | padding: 1rem; 17 | } 18 | 19 | .aside-item { 20 | display: block; 21 | margin-bottom: 1rem; 22 | } 23 | 24 | .aside-item:last-child { 25 | margin-bottom: 0; 26 | } 27 | 28 | .aside-item svg { 29 | fill: --gray; 30 | } 31 | 32 | .aside-item--active svg { 33 | fill: --dark-gray; 34 | } 35 | 36 | .main { 37 | flex: 1; 38 | overflow: auto; 39 | -webkit-overflow-scrolling: touch; 40 | padding: 0 2rem; 41 | } 42 | -------------------------------------------------------------------------------- /app/styles/utilities.css: -------------------------------------------------------------------------------- 1 | .button-reset { 2 | background: none; 3 | border: none; 4 | outline: 0; 5 | padding: 0; 6 | } 7 | 8 | .scroll-container { 9 | overflow-y: auto; 10 | -webkit-overflow-scrolling: touch; 11 | } 12 | 13 | .truncate { 14 | max-width: 100%; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | } 19 | 20 | .flex-auto { 21 | flex: 1 1 auto; 22 | min-width: 0; 23 | min-height: 0; 24 | } 25 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | 3 | {{#if flashMessages.queue}} 4 |
5 | {{#each flashMessages.queue as |flash|}} 6 | {{#flash-message flash=flash local-class="flash-messages-item"}} 7 | {{ui-flash-message flash=flash}} 8 | {{/flash-message}} 9 | {{/each}} 10 |
11 | {{/if}} -------------------------------------------------------------------------------- /app/templates/beta.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='️🙅' fullScreen=true title='Looks like you\'re not on the list'}} 2 |

3 | Canvas is in private beta and your team doesn't yet have access.
4 | If you want in, 5 | fill out this form 6 | and we'll get the ball rolling. 7 |

8 | {{/ui-billboard}} 9 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /app/templates/login.hbs: -------------------------------------------------------------------------------- 1 | {{routes/login-x}} 2 | -------------------------------------------------------------------------------- /app/templates/logout.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /app/templates/not-found.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='🕵️' fullScreen=true title='Uh oh, four-oh-four'}} 2 |

3 | We couldn't find the page you're looking for.
4 | We even checked under the sofa. 5 | 6 | {{#if loggedIn}} 7 | Maybe {{link-to 'return home' 'index'}}? 8 | {{/if}} 9 |

10 | 11 | {{#unless loggedIn}} 12 |

13 | Perhaps you need to sign in? 14 |

15 | 16 | {{sign-in-with-slack}} 17 | {{/unless}} 18 | {{/ui-billboard}} 19 | -------------------------------------------------------------------------------- /app/templates/post-auth.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /app/templates/server-error.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='💣' fullScreen=true title='Whoops, five-oh-something'}} 2 |

3 | Things have gone wrong. It's probably Bob's fault.
4 | We've asked him to fix it. Return to safety? 5 |

6 | {{/ui-billboard}} -------------------------------------------------------------------------------- /app/templates/setup.hbs: -------------------------------------------------------------------------------- 1 | {{routes/setup-x 2 | personalTeam=model.personalTeam 3 | teams=model.teams}} 4 | -------------------------------------------------------------------------------- /app/templates/team/canvas.hbs: -------------------------------------------------------------------------------- 1 |
2 | 33 | 34 |
35 | {{outlet}} 36 |
37 |
38 | -------------------------------------------------------------------------------- /app/templates/team/canvas/history.hbs: -------------------------------------------------------------------------------- 1 | {{routes/canvas-history 2 | canvas=model 3 | version=version 4 | onClone=(action 'clone')}} 5 | -------------------------------------------------------------------------------- /app/templates/team/canvas/pulse.hbs: -------------------------------------------------------------------------------- 1 | {{routes/canvas-pulse canvas=model.canvas pulseEvents=model.pulseEvents}} -------------------------------------------------------------------------------- /app/templates/team/canvas/settings.hbs: -------------------------------------------------------------------------------- 1 | {{routes/canvas-settings canvas=model}} 2 | -------------------------------------------------------------------------------- /app/templates/team/canvas/show.hbs: -------------------------------------------------------------------------------- 1 | {{routes/canvas-show 2 | canvas=model 3 | connected=model.connected 4 | blockID=blockID 5 | filterQueryParam=filter 6 | onDeleteCanvas=(route-action 'onDeleteCanvas')}} 7 | -------------------------------------------------------------------------------- /app/templates/team/index.hbs: -------------------------------------------------------------------------------- 1 | {{routes/team-index 2 | channel=channel 3 | team=model 4 | didCreateCanvas="didCreateCanvas"}} 5 | -------------------------------------------------------------------------------- /app/templates/team/settings.hbs: -------------------------------------------------------------------------------- 1 | {{routes/team-settings team=model}} 2 | -------------------------------------------------------------------------------- /app/templates/team/slack.hbs: -------------------------------------------------------------------------------- 1 | {{#ui-billboard emoji='🙌' fullScreen=true title='Set up your new team'}} 2 |

3 | Before you can get started you have to link your Slack and Canvas teams.
4 | It'll only take a few seconds. 5 |

6 | 7 | {{add-to-slack}} 8 | {{/ui-billboard}} 9 | -------------------------------------------------------------------------------- /assets/icons/Canvas-macOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/assets/icons/Canvas-macOS.png -------------------------------------------------------------------------------- /assets/icons/Canvas.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/assets/icons/Canvas.icns -------------------------------------------------------------------------------- /assets/icons/make-iconset.sh: -------------------------------------------------------------------------------- 1 | mkdir Canvas.iconset 2 | sips -z 16 16 Canvas-macOS.png --out Canvas.iconset/icon_16x16.png 3 | sips -z 32 32 Canvas-macOS.png --out Canvas.iconset/icon_16x16@2x.png 4 | sips -z 32 32 Canvas-macOS.png --out Canvas.iconset/icon_32x32.png 5 | sips -z 64 64 Canvas-macOS.png --out Canvas.iconset/icon_32x32@2x.png 6 | sips -z 128 128 Canvas-macOS.png --out Canvas.iconset/icon_128x128.png 7 | sips -z 256 256 Canvas-macOS.png --out Canvas.iconset/icon_128x128@2x.png 8 | sips -z 256 256 Canvas-macOS.png --out Canvas.iconset/icon_256x256.png 9 | sips -z 512 512 Canvas-macOS.png --out Canvas.iconset/icon_256x256@2x.png 10 | sips -z 512 512 Canvas-macOS.png --out Canvas.iconset/icon_512x512.png 11 | sips -z 1024 1024 Canvas-macOS.png --out Canvas.iconset/icon_512x512@2x.png 12 | cp Canvas-macOS.png Canvas.iconset/icon_512x512@2x.png 13 | iconutil -c icns Canvas.iconset 14 | rm -R Canvas.iconset 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas-web", 3 | "dependencies": { 4 | "dexie": "2.0.0-beta.10", 5 | "uuid.js": "3.3.0", 6 | "base62": "0.5.0", 7 | "rangy": "1.3.0", 8 | "reconnectingWebsocket": "^1.0.0", 9 | "raven-js": "^3.7.0", 10 | "js-cookie": "^2.1.3", 11 | "qs": "ljharb/qs#8aa9c26", 12 | "markdown-it": "^8.2.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - node_modules 4 | - bower_components 5 | post: 6 | - bower install 7 | -------------------------------------------------------------------------------- /mirage/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | camelcase: off 3 | -------------------------------------------------------------------------------- /mirage/config.js: -------------------------------------------------------------------------------- 1 | export default function sharedConfig() { // eslint-ignore-line no-empty-function 2 | } 3 | 4 | export function testConfig() { 5 | this.namespace = '/v1'; 6 | 7 | this.get('/account', _ => { 8 | return this.create('account'); 9 | }); 10 | 11 | this.get('/teams', schema => { 12 | return schema.teams.all(); 13 | }); 14 | 15 | this.get('/comments', schema => { 16 | return schema.comments.all(); 17 | }); 18 | 19 | this.post('/tokens', schema => { 20 | return schema.create('token'); 21 | }); 22 | 23 | this.get('/canvas-watches', schema => { 24 | return schema.canvasWatches.all(); 25 | }); 26 | 27 | this.get('/thread-subscriptions', schema => { 28 | return schema.threadSubscriptions.all(); 29 | }); 30 | 31 | this.get('/ui-dismissals', schema => { 32 | return schema.uiDismissals.all(); 33 | }); 34 | 35 | this.get('/teams/:domain', (schema, req) => { 36 | return schema.teams.findBy({ domain: req.params.domain }); 37 | }); 38 | 39 | this.get('/teams/:id/user', (schema, req) => { 40 | const team = schema.teams.find(req.params.id); 41 | return team.users.models[0]; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /mirage/factories/account.js: -------------------------------------------------------------------------------- 1 | import { Factory } from 'ember-cli-mirage'; 2 | 3 | export default Factory.extend({ 4 | intercom_hash: 'intercom_hash', 5 | 6 | inserted_at() { return new Date().toISOString(); }, 7 | updated_at() { return new Date().toISOString(); } 8 | }); 9 | -------------------------------------------------------------------------------- /mirage/factories/team.js: -------------------------------------------------------------------------------- 1 | import { Factory } from 'ember-cli-mirage'; 2 | 3 | export default Factory.extend({ 4 | domain(i) { return `domain-${i}`; }, 5 | needs_slack_token: false, 6 | is_in_team: true, 7 | images: {}, 8 | name(i) { return `name-${i}`; }, 9 | slack_id(i) { return `slack_id-${i}`; }, 10 | 11 | inserted_at() { return new Date().toISOString(); }, 12 | updated_at() { return new Date().toISOString(); }, 13 | 14 | afterCreate(team, server) { 15 | server.create('user', { team }); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /mirage/factories/user.js: -------------------------------------------------------------------------------- 1 | import { Factory } from 'ember-cli-mirage'; 2 | 3 | export default Factory.extend({ 4 | avatar_url(i) { return `https://gravatar.com/avatar/${i}`; }, 5 | email(i) { return `user-${i}@example.com`; }, 6 | images: {}, 7 | name(i) { return `User ${i}`; }, 8 | slack_id(i) { return `slack_id-${i}`; }, 9 | 10 | inserted_at() { return new Date().toISOString(); }, 11 | updated_at() { return new Date().toISOString(); } 12 | }); 13 | -------------------------------------------------------------------------------- /mirage/models/account.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /mirage/models/canvas-watch.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({}); 4 | -------------------------------------------------------------------------------- /mirage/models/comment.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({}); 4 | -------------------------------------------------------------------------------- /mirage/models/team.js: -------------------------------------------------------------------------------- 1 | import { Model, hasMany } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | users: hasMany() 5 | }); 6 | -------------------------------------------------------------------------------- /mirage/models/thread-subscription.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({}); 4 | -------------------------------------------------------------------------------- /mirage/models/ui-dismissal.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({}); 4 | -------------------------------------------------------------------------------- /mirage/models/user.js: -------------------------------------------------------------------------------- 1 | import { Model, belongsTo } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | team: belongsTo() 5 | }); 6 | -------------------------------------------------------------------------------- /mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function(/* server */) { 2 | 3 | /* 4 | Seed your development database using your factories. 5 | This data will not be loaded in your tests. 6 | 7 | Make sure to define a factory for each model you want to create. 8 | */ 9 | 10 | // server.createList('post', 10); 11 | } 12 | -------------------------------------------------------------------------------- /mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { JSONAPISerializer } from 'ember-cli-mirage'; 2 | 3 | export default JSONAPISerializer.extend({ 4 | keyForAttribute(attribute) { 5 | return attribute.underscore(); 6 | }, 7 | 8 | keyForRelationship(relationship) { 9 | return relationship.underscore(); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/personal-notes-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/public/images/personal-notes-avatar.png -------------------------------------------------------------------------------- /public/primaries/Activity.svg: -------------------------------------------------------------------------------- 1 | Icons 300 -------------------------------------------------------------------------------- /public/primaries/Add.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/AlignLeft.svg: -------------------------------------------------------------------------------- 1 | Icons 200 -------------------------------------------------------------------------------- /public/primaries/ArrowLeft.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/Bookmark.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/Branch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 200 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/primaries/Close.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/Delete.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/Document.svg: -------------------------------------------------------------------------------- 1 | Icons 200 -------------------------------------------------------------------------------- /public/primaries/Error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 200 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/primaries/Help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 200 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/primaries/Home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 100 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/primaries/More.svg: -------------------------------------------------------------------------------- 1 | Icons 200 -------------------------------------------------------------------------------- /public/primaries/Notification.svg: -------------------------------------------------------------------------------- 1 | Icons 100 -------------------------------------------------------------------------------- /public/primaries/Receipt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 200 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/primaries/SlidersVertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Icons 400 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "https_only": true, 3 | "proxies": { 4 | "/v1/": { 5 | "origin": "${API_URL}" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint-disable camelcase */ 3 | 4 | module.exports = { 5 | framework: 'qunit', 6 | test_page: 'tests/index.html?hidepassed', 7 | disable_watching: true, 8 | launch_in_ci: [ 9 | 'Chrome' 10 | ], 11 | launch_in_dev: [ 12 | 'Chrome' 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /tests/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | embertest: true 3 | rules: 4 | camelcase: off 5 | consistent-return: off 6 | newline-after-var: off 7 | object-curly-newline: off 8 | one-var: off 9 | require-jsdoc: off 10 | -------------------------------------------------------------------------------- /tests/acceptance/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | globals: 2 | server: true 3 | -------------------------------------------------------------------------------- /tests/acceptance/logged-out-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import { Response } from 'ember-cli-mirage'; 3 | import moduleForAcceptance from 'canvas-web/tests/helpers/module-for-acceptance'; 4 | 5 | moduleForAcceptance('Acceptance | logged out'); 6 | 7 | test('visiting a route while logged out redirects to /login', assert => { 8 | server.get('/account', _ => { 9 | return new Response(401); 10 | }); 11 | 12 | visit('/usecanvas'); 13 | 14 | andThen(_ => { 15 | assert.equal(currentURL(), '/login'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/acceptance/needs-slack-id-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from 'canvas-web/tests/helpers/module-for-acceptance'; 3 | 4 | moduleForAcceptance('Acceptance | needs slack id'); 5 | 6 | test('visiting a team that needs a Slack ID', assert => { 7 | server.create('team', { domain: 'usecanvas', needs_slack_token: true }); 8 | 9 | visit('/usecanvas'); 10 | 11 | andThen(function() { 12 | assert.equal(currentURL(), '/usecanvas'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/electron.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const BrowserWindow = require('electron').BrowserWindow; 4 | const app = require('electron').app; 5 | 6 | let mainWindow = null; 7 | 8 | app.on('window-all-closed', function onWindowAllClosed() { 9 | if (process.platform !== 'darwin') { 10 | app.quit(); 11 | } 12 | }); 13 | 14 | app.on('ready', function onReady() { 15 | mainWindow = new BrowserWindow({ 16 | width: 800, 17 | height: 600 18 | }); 19 | 20 | Reflect.deleteProperty(mainWindow, 'module'); 21 | 22 | if (process.env.EMBER_ENV === 'test') { 23 | mainWindow.loadURL(`file://${__dirname}/index.html`); 24 | } else { 25 | mainWindow.loadURL(`file://${__dirname}/dist/index.html`); 26 | } 27 | 28 | mainWindow.on('closed', function onClosed() { 29 | mainWindow = null; 30 | }); 31 | }); 32 | 33 | /* jshint undef: true */ 34 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/flash-message.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import FlashObject from 'ember-cli-flash/flash/object'; 3 | 4 | const { K } = Ember; 5 | 6 | FlashObject.reopen({ init: K }); 7 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return Reflect.apply(options.beforeEach, this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | const afterEach = 20 | options.afterEach && Reflect.apply(options.afterEach, this, arguments); 21 | return Promise.resolve(afterEach).then(_ => destroyApp(this.application)); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Canvas Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/add-to-slack/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('add-to-slack', 'Integration | Component | add to slack', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | this.render(hbs`{{add-to-slack}}`); 12 | assert.equal(this.$().text().trim(), ''); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/canvas-block-actions/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('canvas-block-actions', 5 | 'Integration | Component | canvas block actions', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | this.render(hbs`{{canvas-block-actions}}`); 11 | assert.ok(true); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/integration/components/canvas-block-filter/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import Ember from 'ember'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | moduleForComponent('canvas-block-filter', 6 | 'Integration | Component | canvas block filter', { 7 | integration: true 8 | }); 9 | 10 | test('it binds a filter term', function(assert) { 11 | this.set('filterTerm', 'Foo'); 12 | this.render(hbs`{{canvas-block-filter filterTerm=filterTerm}}`); 13 | assert.equal(this.$('input').val(), 'Foo'); 14 | this.$('input').val('Bar').trigger('input'); 15 | assert.equal(this.get('filterTerm'), 'Bar'); 16 | }); 17 | 18 | test('it clears the filter when closing', function(assert) { 19 | this.set('filterTerm', 'Foo'); 20 | this.set('onCloseFilter', Ember.K); 21 | this.render(hbs`{{canvas-block-filter 22 | onCloseFilter=onCloseFilter 23 | filterTerm=filterTerm}}`); 24 | this.$('button').click(); 25 | assert.equal(this.get('filterTerm'), ''); 26 | }); 27 | 28 | test('it calls a close callback', function(assert) { 29 | this.set('onCloseFilter', _ => assert.ok(true)); 30 | this.render(hbs`{{canvas-block-filter onCloseFilter=onCloseFilter}}`); 31 | this.$('button').click(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/integration/components/canvas-channel-list/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('canvas-channel-list', 5 | 'Integration | Component | canvas channel list', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | this.render(hbs`{{canvas-channel-list}}`); 13 | assert.ok(/Add Channels/.test(this.$().text())); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/canvas-list-filter/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('canvas-list-filter', 5 | 'Integration | Component | channel list', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.render(hbs`{{canvas-list-filter}}`); 14 | assert.ok(/All Canvases/.test(this.$().text())); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/integration/components/canvas-pulse-item/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'canvas-web/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('canvas-pulse-item', 6 | 'Integration | Component | canvas pulse item', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | this.set('pulseEvent', { 12 | type: 'reference_added', 13 | providerName: 'Provider', 14 | referencer: { 15 | name: 'Author' 16 | } 17 | }); 18 | 19 | this.render(hbs`{{canvas-pulse-item pulseEvent=pulseEvent}}`); 20 | 21 | assert.equal(this.$(testSelector('summary')).text() 22 | .replace(/\s+/gm, ' ') 23 | .trim(), 'Author referenced this canvas on Provider.'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/channel-list-add-to-slack/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('channel-list-add-to-slack', 5 | 'Integration | Component | channel list add to slack', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | this.render(hbs`{{channel-list-add-to-slack}}`); 11 | assert.ok(/Set up the Slack integration/.test(this.$().text())); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/integration/components/channel-topic-canvas/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import RealtimeCanvas from 'canvas-editor/lib/realtime-canvas'; 4 | 5 | moduleForComponent('channel-topic-canvas', 6 | 'Integration | Component | channel topic canvas', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | // Set any properties with this.set('myProperty', 'value'); 12 | // Handle any actions with this.on('myAction', function(val) { ... }); 13 | this.set('canvas', RealtimeCanvas.create({ slackChannelIds: [] })); 14 | this.render(hbs`{{channel-topic-canvas canvas=canvas}}`); 15 | assert.ok(/Last edited/.test(this.$().text().trim())); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/integration/components/comment-thread/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('comment-thread', 5 | 'Integration | Component | comment thread', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | 11 | // Set any properties with this.set('myProperty', 'value'); 12 | // Handle any actions with this.on('myAction', function(val) { ... }); 13 | 14 | this.render(hbs`{{comment-thread}}`); 15 | 16 | assert.ok(this.$()); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/integration/components/comment-thread/item/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('comment-thread/item', 5 | 'Integration | Component | comment thread/item', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | 11 | // Set any properties with this.set('myProperty', 'value'); 12 | // Handle any actions with this.on('myAction', function(val) { ... }); 13 | 14 | this.render(hbs`{{comment-thread/item}}`); 15 | 16 | assert.ok(this.$()); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/integration/components/drop-zone/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('drop-zone', 'Integration | Component | drop zone', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | this.render(hbs`{{drop-zone}}`); 12 | assert.equal(this.$().text().trim(), ''); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/github-oauth/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('github-oauth', 'Integration | Component | github oauth', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | this.render(hbs`{{github-oauth}}`); 12 | assert.equal(this.$().text().trim(), 'Authorize with GitHub'); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/intercom-launcher/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('intercom-launcher', 5 | 'Integration | Component | intercom launcher', { 6 | integration: true 7 | }); 8 | 9 | test('it renders a #customer-intercom-link', function(assert) { 10 | this.set('intercomAppID', 'foobar'); 11 | 12 | this.render(hbs` 13 | {{#intercom-launcher intercomAppID=intercomAppID}} 14 | template block text 15 | {{/intercom-launcher}} 16 | `); 17 | 18 | /* 19 | * The #custom-intercom-link is set in Segment and is required for the 20 | * integration to work. 21 | */ 22 | assert.equal( 23 | this.$('#custom-intercom-link').text().trim(), 'template block text' 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/integration/components/personal-team-form/component-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleForComponent, test } from 'ember-qunit'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | moduleForComponent('personal-team-form', 6 | 'Integration | Component | personal team form', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | this.set('team', Ember.Object.create({ domain: '~domain' })); 12 | this.render(hbs`{{personal-team-form team=team}}`); 13 | 14 | assert.ok(this.$().text().trim().includes( 15 | 'Where should your notes live? Dashes and numbers are A-OK!')); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/integration/components/pulse-settings/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('pulse-settings', 5 | 'Integration | Component | pulse settings', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | this.render(hbs`{{pulse-settings}}`); 11 | assert.ok(/Set Up/.test(this.$().text())); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/integration/components/routes/canvas-history/component-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import { moduleForComponent, test } from 'ember-qunit'; 4 | 5 | moduleForComponent('routes/canvas-history', 6 | 'Integration | Component | routes/canvas history', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | const canvas = Ember.Object.create( 12 | { blocks: [{ content: 'Canvas Title' }], ops: [] }); 13 | this.set('canvas', canvas); 14 | this.render(hbs`{{routes/canvas-history canvas=canvas}}`); 15 | assert.ok(this.$().text().includes('Canvas Title')); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/integration/components/routes/canvas-pulse/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'canvas-web/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('routes/canvas-pulse', 6 | 'Integration | Component | canvas pulse route', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | this.render(hbs`{{routes/canvas-pulse}}`); 12 | assert.ok(this.$(testSelector('pulse')).get(0)); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/routes/canvas-settings/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'canvas-web/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('routes/canvas-settings', 6 | 'Integration | Component | canvas settings route', { 7 | integration: true 8 | }); 9 | 10 | test('it renders', function(assert) { 11 | // Set any properties with this.set('myProperty', 'value'); 12 | // Handle any actions with this.on('myAction', function(val) { ... }); 13 | 14 | this.render(hbs`{{routes/canvas-settings}}`); 15 | assert.ok(this.$(testSelector('settings')).get(0)); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/integration/components/routes/canvas-show/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import Ember from 'ember'; 4 | import RealtimeCanvas from 'canvas-editor/lib/realtime-canvas'; 5 | import ShareDB from 'sharedb'; 6 | 7 | moduleForComponent('routes/canvas-show', 8 | 'Integration | Component | canvas show route', { 9 | integration: true, 10 | beforeEach() { 11 | // stub out route actions as they do not resolve in component tests 12 | this.container.registry.registrations['helper:route-action'] = 13 | Ember.Helper.helper(_ => Ember.RSVP.resolve({})); 14 | } 15 | }); 16 | 17 | test('it renders', function(assert) { 18 | // Set any properties with this.set('myProperty', 'value'); 19 | // Handle any actions with this.on('myAction', function(val) { ... }); 20 | this.set('canvas', 21 | RealtimeCanvas.create({ 22 | shareDBDoc: new ShareDB.Doc(), 23 | slackChannelIds: [], 24 | blocks: [{ 25 | type: 'title', 26 | content: 'Hello, World', 27 | meta: {} 28 | }] 29 | })); 30 | this.render(hbs`{{routes/canvas-show canvas=canvas}}`); 31 | assert.ok(/Hello, World/.test(this.$().text())); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/integration/components/routes/setup-x/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import Ember from 'ember'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | moduleForComponent('routes/setup-x', 6 | 'Integration | Component | routes/setup x', { 7 | integration: true, 8 | beforeEach() { 9 | // stub out route actions as they do not resolve in component tests 10 | this.container.registry.registrations['helper:route-action'] = 11 | Ember.Helper.helper(_ => Ember.RSVP.resolve({})); 12 | } 13 | }); 14 | 15 | test('it renders', function(assert) { 16 | this.set('teams', []); 17 | this.set('personalTeam', Ember.Object.create({ domain: 'domain' })); 18 | this.render(hbs`{{routes/setup-x personalTeam=personalTeam teams=teams}}`); 19 | assert.ok(this.$().text().trim().includes( 20 | 'Set up your space for personal notes')); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/integration/components/sign-in-with-slack/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('sign-in-with-slack', 5 | 'Integration | Component | sign in with slack', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | this.render(hbs`{{sign-in-with-slack}}`); 13 | assert.equal(this.$().text().trim(), ''); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/team-list-wrapper/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('team-list-wrapper', 5 | 'Integration | Component | team list wrapper', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.set('teams', []); 14 | 15 | // Template block usage: 16 | this.render(hbs` 17 | {{#team-list-wrapper teams=teams}} 18 | template block text 19 | {{/team-list-wrapper}} 20 | `); 21 | 22 | assert.ok(this.$().text().trim().includes('template block text')); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/integration/components/ui-list/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('ui-list', 'Integration | Component | ui list', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.render(hbs`{{ui-list}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage: 18 | this.render(hbs` 19 | {{#ui-list}} 20 | template block text 21 | {{/ui-list}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/integration/components/ui-list/item/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('ui-list/item', 'Integration | Component | ui list/item', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.render(hbs`{{ui-list/item}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage: 18 | this.render(hbs` 19 | {{#ui-list/item}} 20 | template block text 21 | {{/ui-list/item}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/integration/components/ui-menu/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('ui-menu', 'Integration | Component | ui menu', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{ui-menu}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#ui-menu}} 19 | template block text 20 | {{/ui-menu}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/ui-menu/item/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('ui-menu/item', 'Integration | Component | ui menu/item', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Set any properties with this.set('myProperty', 'value'); 10 | // Handle any actions with this.on('myAction', function(val) { ... }); 11 | 12 | this.render(hbs`{{ui-menu/item}}`); 13 | 14 | assert.equal(this.$().text().trim(), ''); 15 | 16 | // Template block usage: 17 | this.render(hbs` 18 | {{#ui-menu/item}} 19 | template block text 20 | {{/ui-menu/item}} 21 | `); 22 | 23 | assert.equal(this.$().text().trim(), 'template block text'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-electron-test", 3 | "main": "electron.js" 4 | } 5 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import './helpers/flash-message'; 3 | 4 | import { 5 | setResolver 6 | } from 'ember-qunit'; 7 | 8 | setResolver(resolver); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/adapters/account-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('adapter:account', 'Unit | Adapter | account', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['serializer:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const adapter = this.subject(); 11 | assert.ok(adapter); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/adapters/canvas-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('adapter:canvas', 'Unit | Adapter | canvas', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['serializer:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const adapter = this.subject(); 11 | assert.ok(adapter); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/adapters/user-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('adapter:user', 'Unit | Adapter | user', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['serializer:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const adapter = this.subject(); 11 | assert.ok(adapter); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/controllers/team/index-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:team/index', 'Unit | Controller | team/index', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/initializers/error-handling-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ErrorHandlingInitializer from 'canvas-web/initializers/error-handling'; 3 | import { module, test } from 'qunit'; 4 | 5 | let application; 6 | 7 | module('Unit | Initializer | error handling', { 8 | beforeEach() { 9 | Ember.run(function() { 10 | application = Ember.Application.create(); 11 | application.deferReadiness(); 12 | }); 13 | } 14 | }); 15 | 16 | // Replace this with your real tests. 17 | test('it works', function(assert) { 18 | ErrorHandlingInitializer.initialize(application); 19 | 20 | // you would normally confirm the results of the initializer here 21 | assert.ok(true); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/initializers/sharedb-fuzz-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import SharedbFuzzInitializer from 'canvas-web/initializers/sharedb-fuzz'; 3 | import { module, test } from 'qunit'; 4 | 5 | let application; 6 | 7 | module('Unit | Initializer | sharedb fuzz', { 8 | beforeEach() { 9 | Ember.run(function() { 10 | application = Ember.Application.create(); 11 | application.deferReadiness(); 12 | }); 13 | } 14 | }); 15 | 16 | // Replace this with your real tests. 17 | test('it works', function(assert) { 18 | SharedbFuzzInitializer.initialize(application); 19 | 20 | // you would normally confirm the results of the initializer here 21 | assert.ok(true); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/mixins/oauth-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import OauthMixin from 'canvas-web/mixins/oauth'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | oauth'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | const OauthObject = Ember.Object.extend(OauthMixin); 10 | const subject = OauthObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/mixins/with-dropzone-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import WithDropzoneMixin from 'canvas-web/mixins/with-dropzone'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | with dropzone'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | const WithDropzoneObject = Ember.Object.extend(WithDropzoneMixin); 10 | const subject = WithDropzoneObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/account-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('account', 'Unit | Model | account', { 4 | // Specify the other units that are required for this test. 5 | needs: [] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | // let store = this.store(); 11 | assert.ok(Boolean(model)); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/canvas-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('canvas', 'Unit | Model | canvas', { 4 | // Specify the other units that are required for this test. 5 | needs: 'model:comment model:op model:pulseEvent model:team model:user'.w() 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | assert.ok(Boolean(model)); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/models/op-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('op', 'Unit | Model | op', { 4 | // Specify the other units that are required for this test. 5 | needs: 'model:canvas'.w() 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | assert.ok(Boolean(model)); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/models/pulse-event-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('pulse-event', 'Unit | Model | pulse event', { 4 | // Specify the other units that are required for this test. 5 | needs: ['model:canvas'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | assert.ok(Boolean(model)); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/models/team-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | import 'canvas-web/models/custom-inflector-rules'; 3 | 4 | moduleForModel('team', 'Unit | Model | team', { 5 | // Specify the other units that are required for this test. 6 | needs: 'model:canvas model:slack-channel model:user'.w() 7 | }); 8 | 9 | test('it exists', function(assert) { 10 | const model = this.subject(); 11 | // let store = this.store(); 12 | assert.ok(Boolean(model)); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/unit/models/unfurl-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('unfurl', 'Unit | Model | unfurl', { 4 | // Specify the other units that are required for this test. 5 | needs: [] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | // let store = this.store(); 11 | assert.ok(Boolean(model)); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/user-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('user', 'Unit | Model | user', { 4 | // Specify the other units that are required for this test. 5 | needs: 'model:canvas model:team'.w() 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const model = this.subject(); 10 | // let store = this.store(); 11 | assert.ok(Boolean(model)); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/routes/application-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:application', 'Unit | Route | application', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/routes/index-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:index', 'Unit | Route | index', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/routes/logout-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:logout', 'Unit | Route | logout', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/routes/post-auth-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:post-auth', 'Unit | Route | post auth', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | const route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/services/csrf-token-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:csrf-token', 'Unit | Service | csrf token', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/current-account-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:current-account', 'Unit | Service | current account', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/desktop-menus-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:desktop-menus', 'Unit | Service | desktop menus', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/team-query-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:team-query', 'Unit | Service | team query', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/teams-list-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:teams-list', 'Unit | Service | teams list', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/unfurler-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:unfurler', 'Unit | Service | unfurler', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | const service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usecanvas/web-v2/29fc0f4d1b342bbc6a459fbfe7789ff0553a9aa4/vendor/.gitkeep -------------------------------------------------------------------------------- /vendor/shims/dexie.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('dexie', [], function() { 3 | return { default: window.Dexie }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/diff-match-patch.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('diff-match-patch', [], function() { 3 | return { default: window.diff_match_patch }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/js-cookie.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('cookies', [], function() { 3 | return { default: window.Cookies }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/qs.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('qs', [], function() { 3 | return { default: window.Qs }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/raven.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('raven', [], function() { 3 | return { default: window.Raven }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/reconnecting-websocket.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('reconnecting-websocket', [], function() { 3 | return { default: window.ReconnectingWebSocket }; 4 | }); 5 | }()); 6 | -------------------------------------------------------------------------------- /vendor/shims/sharedb.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | define('sharedb', [], function() { 3 | return { default: window.ShareDB }; 4 | }); 5 | }()); 6 | --------------------------------------------------------------------------------