├── .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 |
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 |
8 | {{svg-jar 'Close' height=24 width=24 local-class='close-icon'}}
9 |
--------------------------------------------------------------------------------
/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 |
Done
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 |
31 | {{#unless static}}
32 | {{#if canvasChannels}}
33 | Edit
34 | {{else}}
35 | Add Channels
36 | {{/if}}
37 | {{/unless}}
38 |
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 |
12 | {{svg-jar "Delete" height="20" width="20"}}
13 |
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 | Use Template
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 |
6 |
7 |
8 | Set up the Slack integration:
9 |
10 |
11 |
12 | Organize with #channels
13 | /canvas new from Slack
14 | Work with @mentions
15 | Get notifications in Slack
16 |
17 |
18 |
19 | {{add-to-slack currentScopes=currentScopes}}
20 |
21 |
22 |
--------------------------------------------------------------------------------
/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 |
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 | Set Up
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 |
10 | {{svg-jar 'Branch' height=24 width=24 local-class='clone-icon'}}
11 |
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 |
15 | {{#each permissions key='@item' as |permissionChoice|}}
16 |
17 | {{permissionChoice}}
18 |
19 | {{/each}}
20 |
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 |
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 |
9 | {{#if exportCanvases.isRunning}}
10 | Exporting...
11 | {{else}}
12 | Start Export
13 | {{/if}}
14 |
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 |
3 |
4 |
5 | {{#link-to (if model.team.isInTeam 'team' 'index')}}
6 | {{svg-jar 'Home' height='32' width='32'}}
7 | {{/link-to}}
8 |
9 |
10 |
11 | {{#if model.team.isInTeam}}
12 |
13 | {{#link-to 'team.canvas.show' local-class='aside-item' activeClass=(local-class 'aside-item--active')}}
14 | {{svg-jar 'AlignLeft' height='32' width='32'}}
15 | {{/link-to}}
16 |
17 | {{#link-to 'team.canvas.pulse' local-class='aside-item' activeClass=(local-class 'aside-item--active')}}
18 | {{svg-jar 'Activity' height='32' width='32'}}
19 | {{/link-to}}
20 |
21 | {{#link-to 'team.canvas.settings' local-class='aside-item' activeClass=(local-class 'aside-item--active')}}
22 | {{svg-jar "Gear" height="32" width="32"}}
23 | {{/link-to}}
24 |
25 | {{/if}}
26 |
27 |
28 | {{#intercom-launcher}}
29 | {{svg-jar "Help" height="32" width="32"}}
30 | {{/intercom-launcher}}
31 |
32 |
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 |
--------------------------------------------------------------------------------