├── src ├── index.ts ├── receiver.ts ├── resources │ ├── gen │ │ ├── batch_api_base.ts │ │ ├── jobs_base.ts │ │ ├── organization_exports_base.ts │ │ ├── custom_field_settings_base.ts │ │ ├── events_base.ts │ │ ├── project_memberships_base.ts │ │ ├── user_task_lists_base.ts │ │ ├── time_periods_base.ts │ │ ├── attachments_base.ts │ │ ├── workspace_memberships_base.ts │ │ ├── project_briefs_base.ts │ │ ├── project_statuses_base.ts │ │ ├── status_updates_base.ts │ │ ├── typeahead_base.ts │ │ ├── audit_log_api_base.ts │ │ ├── stories_base.ts │ │ ├── workspaces_base.ts │ │ ├── project_templates_base.ts │ │ ├── webhooks_base.ts │ │ ├── portfolio_memberships_base.ts │ │ ├── team_memberships_base.ts │ │ ├── teams_base.ts │ │ ├── users_base.ts │ │ ├── sections_base.ts │ │ ├── tags_base.ts │ │ ├── custom_fields_base.ts │ │ └── portfolios_base.ts │ ├── interfaces.ts │ ├── time_periods.ts │ ├── portfolio_memberships.ts │ ├── sections.ts │ ├── project_briefs.ts │ ├── project_memberships.ts │ ├── workspaces.ts │ ├── teams.ts │ ├── project_templates.ts │ ├── users.ts │ ├── organization_exports.ts │ ├── user_task_lists.ts │ ├── workspace_memberships.ts │ ├── project_statuses.ts │ ├── attachments.ts │ ├── audit_log_api.ts │ ├── jobs.ts │ ├── custom_field_settings.ts │ ├── tags.ts │ ├── goals.ts │ ├── events.ts │ ├── status_updates.ts │ ├── custom_fields.ts │ ├── portfolios.ts │ ├── helpers.ts │ ├── webhooks.ts │ ├── stories.ts │ └── projects.ts ├── class_names.ts ├── explorer.ts ├── constants.ts ├── components │ ├── resource_entry.ts │ ├── property_entry.ts │ ├── paginate_entry.ts │ ├── route_entry.ts │ ├── json_response.ts │ ├── parameter_entry.ts │ └── extra_parameter_entry.ts ├── asana.d.ts ├── resources.ts └── credentials.ts ├── test ├── env.ts ├── receiver_spec.ts ├── class_names_spec.ts ├── helpers.ts ├── components │ ├── resource_entry_spec.ts │ ├── parameter_entry_spec.ts │ ├── property_entry_spec.ts │ ├── paginate_entry_spec.ts │ ├── route_entry_spec.ts │ ├── json_response_spec.ts │ └── extra_parameter_entry_spec.ts └── credentials_spec.ts ├── swagger_templates ├── api-explorer-config.json └── api.service.mustache ├── public ├── index.html └── popup_receiver.html ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── tsd.json ├── CODE_OF_CONDUCT.md ├── tslint.json ├── package.json └── README.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export import Receiver = require("./receiver"); 2 | export import Explorer = require("./explorer"); 3 | export var name = "api-explorer"; 4 | -------------------------------------------------------------------------------- /test/env.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import jsdom = require("jsdom-global") 3 | let test = jsdom( 4 | ``, 5 | { 6 | url: "http://localhost:8338/" 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /swagger_templates/api-explorer-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceFolder": "lib", 3 | "packageName" : "api-explorer", 4 | "templateDir": "swagger_templates", 5 | "hideGenerationTimestamp": true, 6 | "apiTests": false, 7 | "apiDocs": false 8 | } 9 | -------------------------------------------------------------------------------- /src/receiver.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Asana = require("asana"); 3 | 4 | /** 5 | * Runs the receiver code to send the Oauth result to the requesting tab. 6 | * Note: This logic is handled entirely within the `Asana.auth` module. 7 | */ 8 | export function run(): void { 9 | Asana.auth.PopupFlow.runReceiver(); 10 | } 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Asana API Explorer 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Loading react...
12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/resources/gen/batch_api_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "batch_api", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createBatchRequest", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/batch", 15 | "params": [ 16 | ], 17 | "comment": "Submit parallel requests" 18 | }, 19 | ] 20 | } 21 | export = resourceBase; 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | quote_type = double 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.ts] 18 | curly_bracket_next_line = false 19 | continuation_indent_size = 4 20 | indent_brace_style = BSD KNF 21 | indent_size = 4 22 | max_line_length = 140 23 | spaces_around_brackets = outside 24 | spaces_around_operators = true 25 | -------------------------------------------------------------------------------- /public/popup_receiver.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Asana Oauth Example: Browser Popup Receiver 5 | 6 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/resources/gen/jobs_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "jobs", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getJob", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/jobs/%s", 15 | "params": [ 16 | { 17 | "name": "job_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the job.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a job by id" 25 | }, 26 | ] 27 | } 28 | export = resourceBase; 29 | -------------------------------------------------------------------------------- /test/receiver_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Asana = require("asana"); 3 | import sinon = require("sinon"); 4 | 5 | import Receiver = require("../src/receiver"); 6 | import {SinonFakeServer} from "sinon"; 7 | 8 | describe("Receiver", () => { 9 | var sand: SinonFakeServer; 10 | 11 | beforeEach(() => { 12 | sand = sinon.fakeServer.create(); 13 | }); 14 | 15 | afterEach(() => { 16 | sand.restore(); 17 | }); 18 | 19 | describe("#run", () => { 20 | it("should pass through call to the client's runReceiver", () => { 21 | var receiverStub = sinon.stub(Asana.auth.PopupFlow, "runReceiver"); 22 | 23 | Receiver.run(); 24 | sinon.assert.called(receiverStub); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/resources/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable variable-name */ 2 | interface Action { 3 | name: string; 4 | class: string; 5 | method: string; 6 | path: string; 7 | notes?: string[]; 8 | comment: string; 9 | params?: Parameter[]; 10 | collection?: boolean; 11 | collection_cannot_paginate?: boolean; 12 | no_code?: boolean; 13 | } 14 | 15 | interface Parameter { 16 | name: string; 17 | type: string; 18 | example_values?: string[]; 19 | comment: string; 20 | required?: boolean; 21 | explicit?: boolean; 22 | } 23 | interface Property { 24 | name: string; 25 | comment: string; 26 | type: string; 27 | example_values: string[]; 28 | values?: any[]; 29 | } 30 | 31 | interface Resource { 32 | name: string; 33 | comment: string; 34 | properties: Property[]; 35 | actions: Action[]; 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Artifacts 30 | dist 31 | build 32 | *.d.ts 33 | *.js 34 | typings 35 | 36 | # Vim 37 | *.sw[op] 38 | 39 | .DS_Store 40 | 41 | # TypeScript 42 | tsconfig.json 43 | 44 | # Intellij 45 | .idea 46 | 47 | # nyc 48 | .nyc_output 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Cache node_modules and typings for future runs. This does make the builds 2 | # sometimes fail because of a bad cache but usually this is a huge test 3 | # performance win. 4 | cache: 5 | directories: 6 | - node_modules 7 | - typings 8 | # Set up environment variables for tests 9 | env: 10 | global: 11 | # Use a better mocha reporter for Travis 12 | - MOCHA_REPORTER=spec 13 | # Use the Travis Docker build system which is faster 14 | sudo: false 15 | # Specify to use node 16 | language: node_js 17 | # Specify the node versions to run on 18 | node_js: 19 | - "10.16.0" 20 | before_deploy: 21 | - npm run web 22 | # Push to gh-pages 23 | # TODO: Re-enable automatic push to gh-pages 24 | # after_success: 25 | # - git config --global user.email "git@asana.com" 26 | # - git config --global user.name "Asana" 27 | # # only publish to gh-pages from the master branch. 28 | # - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && gulp gh-pages 29 | -------------------------------------------------------------------------------- /test/class_names_spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require("chai"); 2 | import cx = require("../src/class_names"); 3 | 4 | var assert = chai.assert; 5 | 6 | suite("classNames", () => { 7 | 8 | test("should filter to return only truthy strings", () => { 9 | assert.equal(cx({ 10 | foo: true, 11 | bar: undefined, 12 | baz: true, 13 | fire: null, 14 | foobar: false 15 | }), "foo baz"); 16 | }); 17 | 18 | test("should append extra class strings", () => { 19 | assert.equal(cx({ 20 | baz: false, 21 | boo: true, 22 | bar: true 23 | }, "a", "b", "c"), "boo bar a b c"); 24 | }); 25 | 26 | test("should also accept a string as the first argument", () => { 27 | assert.equal(cx("foobar", "foo", "baz"), "foobar foo baz"); 28 | }); 29 | 30 | test("should filter out falsy values", () => { 31 | assert.equal(cx(null, "foo", undefined, "bar"), "foo bar"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/resources/gen/organization_exports_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "organization_exports", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createOrganizationExport", 12 | "method": "POST", 13 | "collection": true||false, 14 | "path": "/organization_exports", 15 | "params": [ 16 | ], 17 | "comment": "Create an organization export request" 18 | }, 19 | { 20 | "name": "getOrganizationExport", 21 | "method": "GET", 22 | "collection": false, 23 | "path": "/organization_exports/%s", 24 | "params": [ 25 | { 26 | "name": "organization_export_gid", 27 | "type": "string", 28 | "example_values": ["12345"], 29 | "comment": "Globally unique identifier for the organization export.", 30 | "required": true 31 | }, 32 | ], 33 | "comment": "Get details on an org export request" 34 | }, 35 | ] 36 | } 37 | export = resourceBase; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Asana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/class_names.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility to concatenate multiple classNames into a single string. 3 | * The first parameter may also be a "classSet", an object with classNames 4 | * as keys mapping to boolean values indicating whether or not the given 5 | * className should be included. 6 | * @param {(ClassSet | string)} arg - classSet or first className 7 | * @param {...string} className - extra className to append 8 | * @return {string} the complete concatenated className 9 | */ 10 | function classNames( 11 | arg: ({ [key: string]: boolean } | string), 12 | ...extraClasses: string[] 13 | ): string { 14 | 15 | var className = ""; 16 | if (typeof arg === "object") { 17 | for (var name in arg) { 18 | if (arg.hasOwnProperty(name) && arg[name]) { 19 | className += " " + name; 20 | } 21 | } 22 | } else if (arg) { 23 | className = " " + arg; 24 | } 25 | 26 | var i: number; 27 | for (i = 0; i < extraClasses.length; ++i) { 28 | if (extraClasses[i]) { 29 | className += " " + extraClasses[i]; 30 | } 31 | } 32 | return className.substr(1); 33 | } 34 | 35 | export = classNames; 36 | -------------------------------------------------------------------------------- /src/resources/time_periods.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/time_periods_base"); 2 | resourceBase.comment = "A time_period is an object that represents a domain-scoped date range that can be set on _Goals_.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "resource_type", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "display_name", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "end_on", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "parent", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "period", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "start_on", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | ]; 47 | export = resourceBase; 48 | -------------------------------------------------------------------------------- /src/resources/gen/custom_field_settings_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "custom_field_settings", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getCustomFieldSettingsForPortfolio", 12 | "method": "GET", 13 | "collection": true||false, 14 | "path": "/portfolios/%s/custom_field_settings", 15 | "params": [ 16 | { 17 | "name": "portfolio_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the portfolio.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a portfolio's custom fields" 25 | }, 26 | { 27 | "name": "getCustomFieldSettingsForProject", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/projects/%s/custom_field_settings", 31 | "params": [ 32 | { 33 | "name": "project_gid", 34 | "type": "string", 35 | "example_values": ["1331"], 36 | "comment": "Globally unique identifier for the project.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Get a project's custom fields" 41 | }, 42 | ] 43 | } 44 | export = resourceBase; 45 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "Asana/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "chai/chai.d.ts": { 9 | "commit": "016db435a6d07149b825d2f2b0bf2f639cff3986" 10 | }, 11 | "mocha/mocha.d.ts": { 12 | "commit": "66d8808c98856feae7d5a2d5acfc955110fb126d" 13 | }, 14 | "node/node.d.ts": { 15 | "commit": "66d8808c98856feae7d5a2d5acfc955110fb126d" 16 | }, 17 | "sinon/sinon.d.ts": { 18 | "commit": "66d8808c98856feae7d5a2d5acfc955110fb126d" 19 | }, 20 | "bluebird/bluebird.d.ts": { 21 | "commit": "01ce3ccf7f071514ff5057ef32a4550bf0b81dfe" 22 | }, 23 | "chai-as-promised/chai-as-promised.d.ts": { 24 | "commit": "7bab855ae33d79e86da1eb6c73a7f7eab2676ddb" 25 | }, 26 | "jsdom/jsdom.d.ts": { 27 | "commit": "84db5f1b5934ab9048185e784a34c545f1d705af" 28 | }, 29 | "lodash/lodash.d.ts": { 30 | "commit": "3882d337bb0808cde9fe4c08012508a48c135482" 31 | }, 32 | "react/react.d.ts": { 33 | "commit": "57340eca1e3aac68d59cc981b441da834ca38235" 34 | }, 35 | "react/react-addons.d.ts": { 36 | "commit": "57340eca1e3aac68d59cc981b441da834ca38235" 37 | }, 38 | "marked/marked.d.ts": { 39 | "commit": "66d8808c98856feae7d5a2d5acfc955110fb126d" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /swagger_templates/api.service.mustache: -------------------------------------------------------------------------------- 1 | {{#operations}} 2 | /// 3 | 4 | /* tslint:disable:max-line-length */ 5 | /* tslint:disable:eofline */ 6 | var resourceBase = { 7 | "name": "{{toSnakeCase pathPrefix}}", 8 | "comment": "", 9 | "properties":[], 10 | "actions": [ 11 | {{#operation}}{{^formParams}} 12 | {{#contents}} 13 | { 14 | "name": "{{operationId}}", 15 | "method": "{{toUpperCase httpMethod}}", 16 | "collection": {{#queryParams}}{{#eq baseName "limit"}}true||{{/eq}}{{/queryParams}}false, 17 | "path": "{{parsePath path}}", 18 | "params": [ 19 | {{#pathParams}} 20 | { 21 | "name": "{{baseName}}", 22 | "type": "{{dataType}}", 23 | "example_values": [{{&getExampleFromJson jsonSchema}}], 24 | "comment": "{{description}}", 25 | "required": {{required}} 26 | }, 27 | {{/pathParams}} 28 | {{#queryParams}} 29 | {{#neq baseName "limit"}} 30 | { 31 | "name": "{{baseName}}", 32 | "type": "{{dataType}}", 33 | "example_values": [{{&getExampleFromJson jsonSchema}}], 34 | "comment": "{{description}}", 35 | "required": {{required}} 36 | }, 37 | {{/neq}} 38 | {{/queryParams}} 39 | ], 40 | "comment": "{{summary}}" 41 | }, 42 | {{/contents}} 43 | {{/formParams}}{{/operation}} 44 | ] 45 | } 46 | {{/operations}} 47 | export = resourceBase; 48 | -------------------------------------------------------------------------------- /src/resources/gen/events_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "events", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getEvents", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/events", 15 | "params": [ 16 | { 17 | "name": "sync", 18 | "type": "string", 19 | "example_values": ["de4774f6915eae04714ca93bb2f5ee81"], 20 | "comment": "A sync token received from the last request, or none on first sync. Events will be returned from the point in time that the sync token was generated. *Note: On your first request, omit the sync token. The response will be the same as for an expired sync token, and will include a new valid sync token.If the sync token is too old (which may happen from time to time) the API will return a `412 Precondition Failed` error, and include a fresh sync token in the response.*", 21 | "required": false 22 | }, 23 | { 24 | "name": "resource", 25 | "type": "string", 26 | "example_values": ["12345"], 27 | "comment": "A resource ID to subscribe to. The resource can be a task or project.", 28 | "required": true 29 | }, 30 | ], 31 | "comment": "Get events on a resource" 32 | }, 33 | ] 34 | } 35 | export = resourceBase; 36 | -------------------------------------------------------------------------------- /src/resources/portfolio_memberships.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/portfolio_memberships_base"); 2 | resourceBase.comment = "This object determines if a user is a member of a portfolio.\n" 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the portfolio membership.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `portfolio_membership`.\n", 16 | "example_values": [ 17 | "\"portfolio_membership\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "portfolio_membership", 22 | "comment": "A portfolio membership resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "user", 28 | "type": "User", 29 | "example_values": [ 30 | "{ id: 12345, gid: \"12345\", name: \"Tim Bizarro\" }" 31 | ], 32 | "comment": "The user in the membership.\n" 33 | }, 34 | { 35 | "name": "portfolio", 36 | "type": "Project", 37 | "example_values": [ 38 | "{ id: 1234, gid: \"1234\", name: 'Progress Tracking' }" 39 | ], 40 | "comment": "[Opt In](https://asana.com/developers/documentation/getting-started/input-output-options). The portfolio the user is a member of.\n" 41 | } 42 | ] 43 | 44 | export = resourceBase; -------------------------------------------------------------------------------- /src/explorer.ts: -------------------------------------------------------------------------------- 1 | import ReactDOM = require("react-dom"); 2 | import {OAuth2AuthCodePKCE} from "@bity/oauth2-auth-code-pkce" 3 | import Explorer = require("./components/explorer"); 4 | import constants = require("./constants"); 5 | 6 | 7 | /** 8 | * Creates and renders the API Explorer component. 9 | */ 10 | 11 | const oauth = new OAuth2AuthCodePKCE({ 12 | authorizationUrl: "https://app.asana.com/-/oauth_authorize", 13 | tokenUrl: constants.TOKEN_URL, 14 | clientId: constants.CLIENT_ID, 15 | redirectUrl: window.location.href.split("?")[0], 16 | scopes: [], 17 | onInvalidGrant(){ 18 | console.log("Expired! Auth code or refresh token needs to be renewed."); 19 | }, 20 | onAccessTokenExpiry(refreshAccessToken) { 21 | console.log("Expired! Access token needs to be renewed."); 22 | return refreshAccessToken() 23 | } 24 | }); 25 | 26 | 27 | export function run(initialResource?: string, initialRoute?: string): void { 28 | const explorer = ReactDOM.render(Explorer.create({ 29 | initialResourceString: initialResource, 30 | initialRoute: initialRoute, 31 | oauth 32 | }), document.getElementById("tab-explorer")); 33 | 34 | oauth.isReturningFromAuthServer().then(() => { 35 | return oauth.getAccessToken().then((tokenResponse) => { 36 | explorer.setCredentialsFromOAuth(tokenResponse.token.value) 37 | }); 38 | }) 39 | .catch((potentialError) => { 40 | if (potentialError) { console.log(potentialError); } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/resources/gen/project_memberships_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "project_memberships", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getProjectMembership", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/project_memberships/%s", 15 | "params": [ 16 | { 17 | "name": "project_membership_gid", 18 | "type": "string", 19 | "example_values": ["1331"], 20 | "comment": "", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a project membership" 25 | }, 26 | { 27 | "name": "getProjectMembershipsForProject", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/projects/%s/project_memberships", 31 | "params": [ 32 | { 33 | "name": "project_gid", 34 | "type": "string", 35 | "example_values": ["1331"], 36 | "comment": "Globally unique identifier for the project.", 37 | "required": true 38 | }, 39 | { 40 | "name": "user", 41 | "type": "string", 42 | "example_values": ["me"], 43 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 44 | "required": false 45 | }, 46 | ], 47 | "comment": "Get memberships from a project" 48 | }, 49 | ] 50 | } 51 | export = resourceBase; 52 | -------------------------------------------------------------------------------- /src/resources/gen/user_task_lists_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "user_task_lists", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getUserTaskList", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/user_task_lists/%s", 15 | "params": [ 16 | { 17 | "name": "user_task_list_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the user task list.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a user task list" 25 | }, 26 | { 27 | "name": "getUserTaskListForUser", 28 | "method": "GET", 29 | "collection": false, 30 | "path": "/users/%s/user_task_list", 31 | "params": [ 32 | { 33 | "name": "user_gid", 34 | "type": "string", 35 | "example_values": ["me"], 36 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 37 | "required": true 38 | }, 39 | { 40 | "name": "workspace", 41 | "type": "string", 42 | "example_values": ["1234"], 43 | "comment": "The workspace in which to get the user task list.", 44 | "required": true 45 | }, 46 | ], 47 | "comment": "Get a user's task list" 48 | }, 49 | ] 50 | } 51 | export = resourceBase; 52 | -------------------------------------------------------------------------------- /src/resources/sections.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/sections_base"); 2 | resourceBase.comment = "A _section_ is a subdivision of a project that groups tasks together. It can\neither be a header above a list of tasks in a list view or a column in a\nboard view of a project.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the section.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `section`.\n", 16 | "example_values": [ 17 | "\"section\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "section", 22 | "comment": "A section resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "'Next Actions'" 31 | ], 32 | "comment": "The name of the section (i.e. the text displayed as the section header).\n" 33 | }, 34 | { 35 | "name": "project", 36 | "type": "Project", 37 | "example_values": [ 38 | "{ id: 1234, gid: \"1234\", name: 'Bugs' }" 39 | ], 40 | "comment": "The project which contains the section.\n" 41 | }, 42 | { 43 | "name": "created_at", 44 | "type": "String", 45 | "example_values": [ 46 | "'2012-02-22T02:06:58.147Z'" 47 | ], 48 | "comment": "The time at which the section was created.\n" 49 | } 50 | ] 51 | export = resourceBase; 52 | -------------------------------------------------------------------------------- /src/resources/project_briefs.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/project_briefs_base"); 2 | resourceBase.comment = "A _project brief_ object represents a rich text document that describes a project.\n\nPlease note that this API is in _preview_, and is expected to change. This API is to be used for development and testing only as an advance view into the upcoming rich text format experience in the task description. For more information, see [this post](https://forum.asana.com/t/project-brief-api-now-available-as-a-preview/150885) in the developer forum.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "resource_type", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "html_text", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "title", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "permalink_url", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "project", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "text", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | ]; 47 | export = resourceBase; 48 | -------------------------------------------------------------------------------- /src/resources/gen/time_periods_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "time_periods", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getTimePeriod", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/time_periods/%s", 15 | "params": [ 16 | { 17 | "name": "time_period_gid", 18 | "type": "string", 19 | "example_values": ["917392"], 20 | "comment": "Globally unique identifier for the time period.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a time period" 25 | }, 26 | { 27 | "name": "getTimePeriods", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/time_periods", 31 | "params": [ 32 | { 33 | "name": "workspace", 34 | "type": "string", 35 | "example_values": ["31326"], 36 | "comment": "Globally unique identifier for the workspace.", 37 | "required": true 38 | }, 39 | { 40 | "name": "end_on", 41 | "type": "string", 42 | "example_values": ["2019-09-15"], 43 | "comment": "ISO 8601 date string", 44 | "required": false 45 | }, 46 | { 47 | "name": "start_on", 48 | "type": "string", 49 | "example_values": ["2019-09-15"], 50 | "comment": "ISO 8601 date string", 51 | "required": false 52 | }, 53 | ], 54 | "comment": "Get time periods" 55 | }, 56 | ] 57 | } 58 | export = resourceBase; 59 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | interface BaseConstants { 2 | LOCALSTORAGE_KEY: string; 3 | CLIENT_ID: string; 4 | REDIRECT_URI: string; 5 | TOKEN_URL: string; 6 | INITIAL_PAGINATION_LIMIT: number; 7 | } 8 | 9 | var ghPagesConstants: BaseConstants = { 10 | LOCALSTORAGE_KEY: "api_explorer_credentials", 11 | CLIENT_ID: "29147353239426", 12 | REDIRECT_URI: "https://asana.github.io/api-explorer/popup_receiver.html", 13 | TOKEN_URL: "https://ccbv8pweoe.execute-api.us-east-1.amazonaws.com/default/api_explorer_oauth_beta", 14 | INITIAL_PAGINATION_LIMIT: 10 15 | }; 16 | 17 | var localhostConstants: BaseConstants = { 18 | LOCALSTORAGE_KEY: "api_explorer_credentials", 19 | CLIENT_ID: "23824292948206", 20 | REDIRECT_URI: "http://localhost:8338/popup_receiver.html", 21 | TOKEN_URL: "https://ccbv8pweoe.execute-api.us-east-1.amazonaws.com/default/api_explorer_oauth_beta", 22 | INITIAL_PAGINATION_LIMIT: 10 23 | }; 24 | 25 | var production: BaseConstants = { 26 | LOCALSTORAGE_KEY: "api_explorer_credentials", 27 | CLIENT_ID: "38682966449842", 28 | REDIRECT_URI: "https://asana.github.io/developer-docs/explorer/popup_receiver.html", 29 | TOKEN_URL: "https://8fuawabj9d.execute-api.us-east-1.amazonaws.com/default/api_explorer_oauth_beta", 30 | INITIAL_PAGINATION_LIMIT: 10 31 | }; 32 | 33 | var constant: BaseConstants; 34 | switch (process.env.CONSTANTS_TYPE) { 35 | case "gh_pages": 36 | constant = ghPagesConstants; 37 | break; 38 | case "production": 39 | constant = production; 40 | break; 41 | // case "localhost": 42 | default: 43 | constant = localhostConstants; 44 | break; 45 | } 46 | 47 | export = constant; 48 | -------------------------------------------------------------------------------- /src/components/resource_entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import React = require("react"); 3 | import marked = require("marked"); 4 | 5 | import ResourcesHelpers = require("../resources/helpers"); 6 | 7 | let r = React.createElement; 8 | 9 | /** 10 | * The resource entry area 11 | */ 12 | class ResourceEntry extends React.Component { 13 | static create = React.createFactory(ResourceEntry); 14 | 15 | render() { 16 | return r("div", { }, 17 | this.renderSelectResource(), 18 | this.renderResourceInfo() 19 | ); 20 | } 21 | 22 | private renderSelectResource = () => { 23 | return r("p", { }, 24 | r("select", { 25 | className: "select-resource", 26 | onChange: this.props.onResourceChange, 27 | value: ResourcesHelpers.resourceNameFromResource(this.props.resource), 28 | children: ResourcesHelpers.names().map(resource => { 29 | return r("option", { 30 | value: resource 31 | }, resource); 32 | }) 33 | }) 34 | ); 35 | } 36 | 37 | private renderResourceInfo = () => { 38 | var resource = this.props.resource; 39 | 40 | return r("div", { }, 41 | r("div", { }, 42 | r("h3", { }, "Resource description"), 43 | r("div", { dangerouslySetInnerHTML: { 44 | __html: marked(resource.comment) } 45 | }) 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | module ResourceEntry { 52 | export interface Props { 53 | resource: Resource; 54 | onResourceChange: (event?: React.FormEvent) => void; 55 | } 56 | } 57 | 58 | export = ResourceEntry; 59 | -------------------------------------------------------------------------------- /src/asana.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for node-asana 2 | // Project: https://github.com/asana/node-asana/ 3 | // Note: These type definitions are incomplete; only used sections of the API will be defined. 4 | /* tslint:disable variable-name */ 5 | 6 | declare module "asana" { 7 | 8 | export class Client { 9 | constructor(dispatcher: Dispatcher, options?: any); 10 | dispatcher: Dispatcher; 11 | workspaces: resources.Workspaces; 12 | static create(options?: any): Client; 13 | useOauth(options?: any): Client; 14 | useAccessToken(accessToken: String): Client; 15 | authorize(): Promise; 16 | } 17 | 18 | export class Dispatcher { 19 | authenticator: auth.Authenticator; 20 | get(path: string, query: any, dispatchOptions: any): Promise; 21 | handleUnauthorized(): Promise; 22 | url(path: string): string; 23 | } 24 | 25 | export module auth { 26 | interface Credentials { 27 | access_token: string; 28 | expires_in: number; 29 | expiry_timestamp?: number; 30 | } 31 | 32 | class Authenticator { 33 | } 34 | 35 | class OauthAuthenticator extends Authenticator { 36 | credentials: Credentials; 37 | } 38 | 39 | class PopupFlow { 40 | constructor(options?: any); 41 | static runReceiver(): void; 42 | } 43 | } 44 | 45 | export module errors { 46 | } 47 | 48 | export module resources { 49 | interface Resources { 50 | data: T[]; 51 | } 52 | interface Workspace { 53 | gid: string; 54 | name: string; 55 | } 56 | 57 | class Workspaces { 58 | findAll(): Promise>; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/resources/gen/attachments_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "attachments", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "deleteAttachment", 12 | "method": "DELETE", 13 | "collection": false, 14 | "path": "/attachments/%s", 15 | "params": [ 16 | { 17 | "name": "attachment_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the attachment.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Delete an attachment" 25 | }, 26 | { 27 | "name": "getAttachment", 28 | "method": "GET", 29 | "collection": false, 30 | "path": "/attachments/%s", 31 | "params": [ 32 | { 33 | "name": "attachment_gid", 34 | "type": "string", 35 | "example_values": ["12345"], 36 | "comment": "Globally unique identifier for the attachment.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Get an attachment" 41 | }, 42 | { 43 | "name": "getAttachmentsForObject", 44 | "method": "GET", 45 | "collection": true||false, 46 | "path": "/attachments", 47 | "params": [ 48 | { 49 | "name": "parent", 50 | "type": "string", 51 | "example_values": ["159874"], 52 | "comment": "Globally unique identifier for object to fetch statuses from. Must be a GID for a task or project_brief.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Get attachments from an object" 57 | }, 58 | ] 59 | } 60 | export = resourceBase; 61 | -------------------------------------------------------------------------------- /src/components/property_entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import React = require("react"); 3 | 4 | var r = React.createElement; 5 | 6 | /** 7 | * The property toggling area 8 | */ 9 | class PropertyEntry extends React.Component { 10 | static create = React.createFactory(PropertyEntry); 11 | 12 | render() { 13 | return r("p", { 14 | className: "property-entry-" + this.props.classSuffix, 15 | children: [ 16 | this.props.text, 17 | r("span", { 18 | className: "property-checkboxes-" + this.props.classSuffix 19 | }, this.props.properties.map(this.renderPropertyCheckbox)) 20 | ] 21 | } 22 | ); 23 | } 24 | 25 | private renderPropertyCheckbox = (property: Property) => { 26 | return r("span", {key: property.name}, 27 | [ 28 | r("input", { 29 | type: "checkbox", 30 | id: "property_checkbox_" + property.name, 31 | className: "property-checkbox-" + this.props.classSuffix, 32 | checked: this.props.useProperty(property.name), 33 | onChange: this.props.isPropertyChecked, 34 | value: property.name 35 | }), 36 | r("div", {}, " " + property.name) 37 | ] 38 | ); 39 | } 40 | } 41 | 42 | module PropertyEntry { 43 | export interface Props { 44 | classSuffix: string; 45 | text: React.DOMElement; 46 | properties: Property[]; 47 | useProperty: (property: string) => boolean; 48 | isPropertyChecked: (event?: React.FormEvent) => void; 49 | } 50 | } 51 | export = PropertyEntry; 52 | -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- 1 | export import Attachments = require("./resources/attachments"); 2 | export import AuditLogApi = require("./resources/audit_log_api"); 3 | export import CustomFieldSettings = require("./resources/custom_field_settings"); 4 | export import CustomFields = require("./resources/custom_fields"); 5 | export import Events = require("./resources/events"); 6 | export import Goals = require("./resources/goals"); 7 | export import Jobs = require("./resources/jobs"); 8 | export import OrganizationExports = require("./resources/organization_exports"); 9 | export import Portfolios = require("./resources/portfolios"); 10 | export import PortfolioMemberships = require("./resources/portfolio_memberships"); 11 | export import Projects = require("./resources/projects"); 12 | export import ProjectBriefs = require("./resources/project_briefs"); 13 | export import ProjectMemberships = require("./resources/project_memberships"); 14 | export import ProjectStatuses = require("./resources/project_statuses"); 15 | export import ProjectTemplates = require("./resources/project_templates"); 16 | export import Sections = require("./resources/sections"); 17 | export import StatusUpdates = require("./resources/status_updates"); 18 | export import Stories = require("./resources/stories"); 19 | export import Tags = require("./resources/tags"); 20 | export import Tasks = require("./resources/tasks"); 21 | export import Teams = require("./resources/teams"); 22 | export import TimePeriods = require("./resources/time_periods"); 23 | export import Users = require("./resources/users"); 24 | export import UserTaskLists = require("./resources/user_task_lists"); 25 | export import Webhooks = require("./resources/webhooks"); 26 | export import Workspaces = require("./resources/workspaces"); 27 | export import WorkspaceMemberships = require("./resources/workspace_memberships"); 28 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Asana = require("asana"); 3 | 4 | var noop = () => { return; }; 5 | var noopString = (val: string) => { return val; }; 6 | 7 | /** 8 | * Creates credentials with a given expiry timestamp. 9 | * 10 | * @param expiry_ts 11 | * @returns {Asana.auth.Credentials} 12 | */ 13 | export function createCredentials(expiry_ts: number): Asana.auth.Credentials { 14 | return { 15 | access_token: "token", 16 | expires_in: 3600, 17 | expiry_timestamp: expiry_ts 18 | }; 19 | } 20 | 21 | /** 22 | * Creates an asana oauth client, with given credentials. 23 | * 24 | * @param {Asana.auth.Credentials} creds 25 | * @returns {Client} 26 | */ 27 | export function createOauthClient(credentials?: Asana.auth.Credentials): Asana.Client { 28 | return Asana.Client.create({ 29 | clientId: "client_id", 30 | redirectUri: "redirect_uri" 31 | }).useOauth({ credentials: credentials }); 32 | } 33 | 34 | /** 35 | * Creates an interface that has required attributes for Storage. 36 | * This is useful when mocking localStorage. 37 | * 38 | * @returns {Storage} 39 | */ 40 | export function createFakeStorage(): Storage { 41 | return { 42 | getItem: noopString, 43 | setItem: noopString, 44 | removeItem: noopString, 45 | clear: noop, 46 | length: null, 47 | key: null, 48 | remainingSpace: null 49 | }; 50 | } 51 | 52 | /** 53 | * Finds the react component for the given element 54 | * 55 | * @returns {Component} 56 | */ 57 | export function findReactComponent(el: Element) { 58 | for (const key in el) { 59 | if (key.startsWith("__reactInternalInstance$")) { 60 | const fiberNode = (el)[key]; 61 | 62 | return fiberNode && fiberNode.return && fiberNode.return.stateNode; 63 | } 64 | } 65 | return null; 66 | } 67 | -------------------------------------------------------------------------------- /src/resources/project_memberships.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/project_memberships_base"); 2 | resourceBase.comment = "With the introduction of \"comment-only\" projects in Asana, a user's membership\nin a project comes with associated permissions. These permissions (whether a\nuser has full access to the project or comment-only access) are accessible\nthrough the project memberships endpoints described here.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the project membership.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `project_membership`.\n", 16 | "example_values": [ 17 | "\"project_membership\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "project_membership", 22 | "comment": "A project membership resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "user", 28 | "type": "User", 29 | "example_values": [ 30 | "{ id: 12345, gid: \"12345\", name: \"Tim Bizarro\" }" 31 | ], 32 | "comment": "The user in the membership.\n" 33 | }, 34 | { 35 | "name": "project", 36 | "type": "Project", 37 | "example_values": [ 38 | "{ id: 1234, gid: \"1234\", name: 'Bugs' }" 39 | ], 40 | "comment": "[Opt In](https://asana.com/developers/documentation/getting-started/input-output-options). The project the user is a member of.\n" 41 | }, 42 | { 43 | "name": "write_access", 44 | "type": "Enum", 45 | "example_values": [ 46 | "'full_write'", 47 | "'comment_only'" 48 | ], 49 | "comment": "Whether the user has full access to the project or has comment-only access.\n" 50 | } 51 | ] 52 | 53 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/workspaces.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/workspaces_base"); 2 | resourceBase.comment = "A _workspace_ is the highest-level organizational unit in Asana. All projects\nand tasks have an associated workspace.\n\nAn _organization_ is a special kind of workspace that represents a company.\nIn an organization, you can group your projects into teams. You can read\nmore about how organizations work on the Asana Guide.\nTo tell if your workspace is an organization or not, check its\n`is_organization` property.\n\nOver time, we intend to migrate most workspaces into organizations and to\nrelease more organization-specific functionality. We may eventually deprecate\nusing workspace-based APIs for organizations. Currently, and until after\nsome reasonable grace period following any further announcements, you can\nstill reference organizations in any `workspace` parameter.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the workspace.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `workspace`.\n", 16 | "example_values": [ 17 | "\"workspace\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "workspace", 22 | "comment": "A workspace resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "'My Favorite Workspace'" 31 | ], 32 | "comment": "The name of the workspace.\n" 33 | }, 34 | { 35 | "name": "is_organization", 36 | "type": "Boolean", 37 | "example_values": [ 38 | "false" 39 | ], 40 | "comment": "Whether the workspace is an _organization_.\n" 41 | } 42 | ]; 43 | 44 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/teams.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/teams_base"); 2 | resourceBase.comment = "A _team_ is used to group related projects and people together within an\norganization. Each project in an organization is associated with a team.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the team.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `team`.\n", 16 | "example_values": [ 17 | "\"team\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "team", 22 | "comment": "A team resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "'Engineering'" 31 | ], 32 | "comment": "The name of the team.\n" 33 | }, 34 | { 35 | "name": "description", 36 | "type": "String", 37 | "example_values": [ 38 | "'All developers should be members of this team.'" 39 | ], 40 | "comment": "[Opt In](/developers/documentation/getting-started/input-output-options). The description of the team.\n" 41 | }, 42 | { 43 | "name": "html_description", 44 | "type": "String", 45 | "example_values": [ 46 | "'<body><em>All</em> developers should be members of this team.</body>'" 47 | ], 48 | "comment": "[Opt In](/developers/documentation/getting-started/input-output-options). The description of the team with formatting as HTML.\n" 49 | }, 50 | { 51 | "name": "organization", 52 | "type": "Workspace", 53 | "example_values": [ 54 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 55 | ], 56 | "comment": "The organization the team belongs to.\n" 57 | } 58 | ] 59 | 60 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/project_templates.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/project_templates_base"); 2 | resourceBase.comment = "A _project template_ is an object that allows new projects to be created with a predefined setup, which may include tasks, sections, Rules, etc. It simplifies the process of running a workflow that involves a similar set of work every time.\n\nProject templates in organizations are shared with a single team. You cannot currently change the team of a project template via the API.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "resource_type", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "color", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "description", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "html_description", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "name", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "owner", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | { 47 | "name": "public", 48 | "type": "", 49 | "example_values": [], 50 | "comment": "", 51 | }, 52 | { 53 | "name": "requested_dates", 54 | "type": "", 55 | "example_values": [], 56 | "comment": "", 57 | }, 58 | { 59 | "name": "team", 60 | "type": "", 61 | "example_values": [], 62 | "comment": "", 63 | }, 64 | ]; 65 | export = resourceBase; 66 | -------------------------------------------------------------------------------- /src/resources/users.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/users_base"); 2 | resourceBase.comment = "A _user_ object represents an account in Asana that can be given access to\nvarious workspaces, projects, and tasks.\n\nLike other objects in the system, users are referred to by numerical IDs.\nHowever, the special string identifier `me` can be used anywhere\na user ID is accepted, to refer to the current authenticated user.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the user.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `user`.\n", 16 | "example_values": [ 17 | "\"user\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "user", 22 | "comment": "A user resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "'Greg Sanchez'" 31 | ], 32 | "comment": "The user's name.\n" 33 | }, 34 | { 35 | "name": "email", 36 | "type": "String", 37 | "example_values": [ 38 | "'gsanchez@example.com'" 39 | ], 40 | "comment": "The user's email address.\n" 41 | }, 42 | { 43 | "name": "photo", 44 | "type": "Struct", 45 | "example_values": [ 46 | "{ 'image_21x21': 'https://...', ... }" 47 | ], 48 | "comment": "A map of the user's profile photo in various sizes, or `null` if no photo\nis set. Sizes provided are 21, 27, 36, 60, and 128. Images are in\nPNG format.\n" 49 | }, 50 | { 51 | "name": "workspaces", 52 | "type": "Array", 53 | "example_values": [ 54 | "[ { id: 14916, gid:\"14916\" name: 'My Workspace' }, ... ]" 55 | ], 56 | "comment": "Workspaces and organizations this user may access.\n", 57 | } 58 | ]; 59 | 60 | export = resourceBase; -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 23 | -------------------------------------------------------------------------------- /src/components/paginate_entry.ts: -------------------------------------------------------------------------------- 1 | import React = require("react"); 2 | 3 | var r = React.createElement; 4 | 5 | /** 6 | * Allows users to change their pagination settings. 7 | */ 8 | class PaginateEntry extends React.Component { 9 | static create = React.createFactory(PaginateEntry); 10 | 11 | render() { 12 | return r("div", { 13 | className: "paginate-entry", 14 | children: [ 15 | this.props.text, 16 | this.props.canPaginate ? 17 | this.renderPaginateInputs() : 18 | r("small", { }, "Pagination is not available on this route.") 19 | ] 20 | } 21 | ); 22 | } 23 | 24 | private renderPaginateInputs = () => { 25 | // If the values are not set, or 0, display empty string. 26 | var limitValue = !this.props.paginateParams.limit ? 27 | "" : this.props.paginateParams.limit.toString(); 28 | var offsetValue = !this.props.paginateParams.offset ? 29 | "" : this.props.paginateParams.offset; 30 | 31 | return r("span", {}, 32 | r("label", { }, 33 | "Limit" 34 | ), 35 | r("input", { 36 | type: "number", 37 | className: "paginate-entry-limit", 38 | min: "0", 39 | onChange: this.props.onPaginateChange("limit"), 40 | placeholder: "Limit", 41 | value: limitValue 42 | }), 43 | r("label", { }, 44 | "Offset" 45 | ), 46 | r("input", { 47 | type: "text", 48 | className: "paginate-entry-offset", 49 | onChange: this.props.onPaginateChange("offset"), 50 | placeholder: "Offset", 51 | value: offsetValue 52 | }) 53 | ); 54 | }; 55 | } 56 | 57 | module PaginateEntry { 58 | export interface Props { 59 | canPaginate: boolean; 60 | onPaginateChange: (limitOrOffset: string) => 61 | (event?: React.FormEvent) => void; 62 | paginateParams: { 63 | limit?: number; 64 | offset?: string; 65 | }; 66 | text: React.DOMElement; 67 | } 68 | } 69 | export = PaginateEntry; 70 | -------------------------------------------------------------------------------- /src/components/route_entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import React = require("react"); 3 | import marked = require("marked"); 4 | 5 | import ResourcesHelpers = require("../resources/helpers"); 6 | 7 | var r = React.createElement; 8 | 9 | /** 10 | * The route entry area. 11 | */ 12 | class RouteEntry extends React.Component { 13 | static create = React.createFactory(RouteEntry); 14 | 15 | render() { 16 | return r("div", { 17 | className: "route-entry", 18 | children: [ 19 | r("hr", { }), 20 | this.renderSelectRoute(), 21 | this.renderRouteInfo() 22 | ] 23 | }); 24 | } 25 | 26 | private renderSelectRoute = () => { 27 | return r("p", { }, 28 | r("select", { 29 | className: "select-route", 30 | onChange: this.props.onActionChange, 31 | value: this.props.action.name, 32 | // Only include read paths in route select 33 | children: this.props.resource.actions.filter((action) => {return action.method === "GET"}).map( 34 | action => { 35 | return r("option", { 36 | value: action.name 37 | }, action.method + " " + ResourcesHelpers.pathForAction(action)); 38 | }) 39 | }) 40 | ); 41 | } 42 | 43 | private renderRouteInfo = () => { 44 | return r("div", { }, 45 | r("h3", { }, "Route description"), 46 | r("div", { dangerouslySetInnerHTML: { 47 | __html: marked(this.props.action.comment) } 48 | }), 49 | r("hr", { }), 50 | r("h3", { }, "Current request URL"), 51 | r("p", { }, 52 | r("pre", { }, 53 | r("code", { }, this.props.action.method + " " + this.props.currentRequestUrl) 54 | ) 55 | ), 56 | r("hr", { }) 57 | ); 58 | } 59 | } 60 | 61 | module RouteEntry { 62 | export interface Props { 63 | action: Action; 64 | currentRequestUrl: string; 65 | onActionChange: (event?: React.FormEvent) => void; 66 | resource: Resource; 67 | } 68 | } 69 | 70 | export = RouteEntry; 71 | -------------------------------------------------------------------------------- /src/resources/organization_exports.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/organization_exports_base"); 2 | resourceBase.comment = "An _organization_export_ object represents a request to export the complete data of an Organization\nin JSON format.\n\nTo export an Organization using this API:\n\n* Create an `organization_export` [request](#create) and store the id that is returned.\\\n* Request the `organization_export` every few minutes, until the `state` field contains 'finished'.\\\n* Download the file located at the URL in the `download_url` field.\n\nExports can take a long time, from several minutes to a few hours for large Organizations.\n\n**Note:** These endpoints are only available to [Service Accounts](/guide/help/premium/service-accounts)\nof an [Enterprise](/enterprise) Organization.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the Organization export.\n" 11 | }, 12 | { 13 | "name": "created_at", 14 | "type": "String", 15 | "example_values": [ 16 | "'2012-02-22T02:06:58.147Z'" 17 | ], 18 | "comment": "The time at which this export was requested.\n" 19 | }, 20 | { 21 | "name": "download_url", 22 | "type": "String", 23 | "example_values": [ 24 | "null" 25 | ], 26 | "comment": "Download this URL to retreive the full export of the organization in JSON format. It will be\ncompressed in a gzip (.gz) container.\n", 27 | }, 28 | { 29 | "name": "state", 30 | "type": "String", 31 | "example_values": [ 32 | "'pending'", 33 | "'started'", 34 | "'finished'", 35 | "'error'" 36 | ], 37 | "comment": "The current state of the export.\n" 38 | }, 39 | { 40 | "name": "organization", 41 | "type": "Workspace", 42 | "example_values": [ 43 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 44 | ], 45 | "comment": "The Organization that is being exported. This can only be specified at create time.\n" 46 | } 47 | ] 48 | 49 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/gen/workspace_memberships_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "workspace_memberships", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getWorkspaceMembership", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/workspace_memberships/%s", 15 | "params": [ 16 | { 17 | "name": "workspace_membership_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a workspace membership" 25 | }, 26 | { 27 | "name": "getWorkspaceMembershipsForUser", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/users/%s/workspace_memberships", 31 | "params": [ 32 | { 33 | "name": "user_gid", 34 | "type": "string", 35 | "example_values": ["me"], 36 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Get workspace memberships for a user" 41 | }, 42 | { 43 | "name": "getWorkspaceMembershipsForWorkspace", 44 | "method": "GET", 45 | "collection": true||false, 46 | "path": "/workspaces/%s/workspace_memberships", 47 | "params": [ 48 | { 49 | "name": "workspace_gid", 50 | "type": "string", 51 | "example_values": ["12345"], 52 | "comment": "Globally unique identifier for the workspace or organization.", 53 | "required": true 54 | }, 55 | { 56 | "name": "user", 57 | "type": "string", 58 | "example_values": ["me"], 59 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 60 | "required": false 61 | }, 62 | ], 63 | "comment": "Get the workspace memberships for a workspace" 64 | }, 65 | ] 66 | } 67 | export = resourceBase; 68 | -------------------------------------------------------------------------------- /src/resources/gen/project_briefs_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "project_briefs", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createProjectBrief", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/projects/%s/project_briefs", 15 | "params": [ 16 | { 17 | "name": "project_gid", 18 | "type": "string", 19 | "example_values": ["1331"], 20 | "comment": "Globally unique identifier for the project.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Create a project brief" 25 | }, 26 | { 27 | "name": "deleteProjectBrief", 28 | "method": "DELETE", 29 | "collection": false, 30 | "path": "/project_briefs/%s", 31 | "params": [ 32 | { 33 | "name": "project_brief_gid", 34 | "type": "string", 35 | "example_values": ["12345"], 36 | "comment": "Globally unique identifier for the project brief.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Delete a project brief" 41 | }, 42 | { 43 | "name": "getProjectBrief", 44 | "method": "GET", 45 | "collection": false, 46 | "path": "/project_briefs/%s", 47 | "params": [ 48 | { 49 | "name": "project_brief_gid", 50 | "type": "string", 51 | "example_values": ["12345"], 52 | "comment": "Globally unique identifier for the project brief.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Get a project brief" 57 | }, 58 | { 59 | "name": "updateProjectBrief", 60 | "method": "PUT", 61 | "collection": false, 62 | "path": "/project_briefs/%s", 63 | "params": [ 64 | { 65 | "name": "project_brief_gid", 66 | "type": "string", 67 | "example_values": ["12345"], 68 | "comment": "Globally unique identifier for the project brief.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Update a project brief" 73 | }, 74 | ] 75 | } 76 | export = resourceBase; 77 | -------------------------------------------------------------------------------- /src/resources/gen/project_statuses_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "project_statuses", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createProjectStatusForProject", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/projects/%s/project_statuses", 15 | "params": [ 16 | { 17 | "name": "project_gid", 18 | "type": "string", 19 | "example_values": ["1331"], 20 | "comment": "Globally unique identifier for the project.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Create a project status" 25 | }, 26 | { 27 | "name": "deleteProjectStatus", 28 | "method": "DELETE", 29 | "collection": false, 30 | "path": "/project_statuses/%s", 31 | "params": [ 32 | { 33 | "name": "project_status_gid", 34 | "type": "string", 35 | "example_values": ["321654"], 36 | "comment": "The project status update to get.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Delete a project status" 41 | }, 42 | { 43 | "name": "getProjectStatus", 44 | "method": "GET", 45 | "collection": false, 46 | "path": "/project_statuses/%s", 47 | "params": [ 48 | { 49 | "name": "project_status_gid", 50 | "type": "string", 51 | "example_values": ["321654"], 52 | "comment": "The project status update to get.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Get a project status" 57 | }, 58 | { 59 | "name": "getProjectStatusesForProject", 60 | "method": "GET", 61 | "collection": true||false, 62 | "path": "/projects/%s/project_statuses", 63 | "params": [ 64 | { 65 | "name": "project_gid", 66 | "type": "string", 67 | "example_values": ["1331"], 68 | "comment": "Globally unique identifier for the project.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get statuses from a project" 73 | }, 74 | ] 75 | } 76 | export = resourceBase; 77 | -------------------------------------------------------------------------------- /src/resources/gen/status_updates_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "status_updates", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createStatusForObject", 12 | "method": "POST", 13 | "collection": true||false, 14 | "path": "/status_updates", 15 | "params": [ 16 | ], 17 | "comment": "Create a status update" 18 | }, 19 | { 20 | "name": "deleteStatus", 21 | "method": "DELETE", 22 | "collection": false, 23 | "path": "/status_updates/%s", 24 | "params": [ 25 | { 26 | "name": "status_gid", 27 | "type": "string", 28 | "example_values": ["321654"], 29 | "comment": "The status update to get.", 30 | "required": true 31 | }, 32 | ], 33 | "comment": "Delete a status update" 34 | }, 35 | { 36 | "name": "getStatus", 37 | "method": "GET", 38 | "collection": false, 39 | "path": "/status_updates/%s", 40 | "params": [ 41 | { 42 | "name": "status_gid", 43 | "type": "string", 44 | "example_values": ["321654"], 45 | "comment": "The status update to get.", 46 | "required": true 47 | }, 48 | ], 49 | "comment": "Get a status update" 50 | }, 51 | { 52 | "name": "getStatusesForObject", 53 | "method": "GET", 54 | "collection": true||false, 55 | "path": "/status_updates", 56 | "params": [ 57 | { 58 | "name": "created_since", 59 | "type": "Date", 60 | "example_values": ["2012-02-22T02:06:58.158Z"], 61 | "comment": "Only return statuses that have been created since the given time.", 62 | "required": false 63 | }, 64 | { 65 | "name": "parent", 66 | "type": "string", 67 | "example_values": ["159874"], 68 | "comment": "Globally unique identifier for object to fetch statuses from. Must be a GID for a project, portfolio, or goal.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get status updates from an object" 73 | }, 74 | ] 75 | } 76 | export = resourceBase; 77 | -------------------------------------------------------------------------------- /src/components/json_response.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import cx = require("../class_names"); 3 | import React = require("react"); 4 | 5 | const r = React.createElement; 6 | 7 | /** 8 | * The JSON response code block. 9 | */ 10 | class JsonResponse extends React.Component { 11 | static create = React.createFactory(JsonResponse); 12 | 13 | render() { 14 | const rawResponse = this.props.response.rawResponse; 15 | 16 | const jsonString = rawResponse === undefined ? null : 17 | JSON.stringify(rawResponse, undefined, 2); 18 | 19 | return r("div", { }, 20 | this.renderResponseHeaderInfo(), 21 | r("pre", { 22 | className: cx({ 23 | "json-response-block": true, 24 | "json-error": this.props.response.error !== undefined, 25 | "json-loading": this.props.response.isLoading || false 26 | }), 27 | children: [ 28 | r("code", { 29 | className: "json" 30 | }, jsonString) 31 | ] 32 | }) 33 | ); 34 | } 35 | 36 | private renderResponseHeaderInfo = () => { 37 | const action = this.props.response.action; 38 | 39 | return action === undefined ? null : 40 | r("p", { 41 | className: "json-response-info", 42 | children: [ 43 | action.method + " " + this.props.response.route, 44 | action.method !== "GET" ? "" : 45 | r("a", { 46 | className: "raw-route-link", 47 | href: this.props.response.routeUrl, 48 | target: "_blank" 49 | }, r("small", { }, " (open raw response)")) 50 | ] 51 | }); 52 | } 53 | } 54 | 55 | module JsonResponse { 56 | /** 57 | * Stores information about the response of a request. 58 | * This is set after the user submits a query. 59 | */ 60 | export interface ResponseData { 61 | action?: Action; 62 | error?: any; 63 | isLoading?: boolean; 64 | rawResponse?: any; 65 | route?: string; 66 | routeUrl?: string; 67 | } 68 | 69 | export interface Props { 70 | response: ResponseData; 71 | } 72 | } 73 | 74 | export = JsonResponse; 75 | -------------------------------------------------------------------------------- /src/resources/user_task_lists.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/user_task_lists_base"); 2 | resourceBase.comment = "A _user task list_ represents the tasks assigned to a particular user. It provides API access to a user's \"My Tasks\" view in Asana.\n\nA user's \"My Tasks\" represent all of the tasks assigned to that user. It is\nvisually divided into regions based on the task's\n[`assignee_status`](/developers/api-reference/tasks#field-assignee_status)\nfor Asana users to triage their tasks based on when they can address them.\nWhen building an integration it's worth noting that tasks with due dates will\nautomatically move through `assignee_status` states as their due dates\napproach; read up on [task\nauto-promotion](/guide/help/fundamentals/my-tasks#gl-auto-promote) for more\ninfomation.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the user task list.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `user_task_list`.\n", 16 | "example_values": [ 17 | "\"user_task_list\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "user_task_list", 22 | "comment": "A user_task_list resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "\"My Tasks\"" 31 | ], 32 | "comment": "The name of the user task list.\n", 33 | }, 34 | { 35 | "name": "owner", 36 | "type": "User", 37 | "example_values": [ 38 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }", 39 | "null" 40 | ], 41 | "comment": "The owner of the user task list, i.e. the person whose My Tasks is represented by this resource.\n", 42 | }, 43 | { 44 | "name": "workspace", 45 | "type": "Workspace", 46 | "example_values": [ 47 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 48 | ], 49 | "comment": "The workspace in which the user task list is located.\n" 50 | } 51 | ]; 52 | export = resourceBase; 53 | -------------------------------------------------------------------------------- /src/resources/gen/typeahead_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "typeahead", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "typeaheadForWorkspace", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/workspaces/%s/typeahead", 15 | "params": [ 16 | { 17 | "name": "workspace_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the workspace or organization.", 21 | "required": true 22 | }, 23 | { 24 | "name": "count", 25 | "type": "number", 26 | "example_values": [20], 27 | "comment": "The number of results to return. The default is 20 if this parameter is omitted, with a minimum of 1 and a maximum of 100. If there are fewer results found than requested, all will be returned.", 28 | "required": false 29 | }, 30 | { 31 | "name": "query", 32 | "type": "string", 33 | "example_values": ["Greg"], 34 | "comment": "The string that will be used to search for relevant objects. If an empty string is passed in, the API will return results.", 35 | "required": false 36 | }, 37 | { 38 | "name": "type", 39 | "type": "string", 40 | "example_values": [], 41 | "comment": "*Deprecated: new integrations should prefer the resource_type field.*", 42 | "required": false 43 | }, 44 | { 45 | "name": "resource_type", 46 | "type": "string", 47 | "example_values": [], 48 | "comment": "The type of values the typeahead should return. You can choose from one of the following: `custom_field`, `project`, `project_template`, `portfolio`, `tag`, `task`, and `user`. Note that unlike in the names of endpoints, the types listed here are in singular form (e.g. `task`). Using multiple types is not yet supported.", 49 | "required": true 50 | }, 51 | ], 52 | "comment": "Get objects via typeahead" 53 | }, 54 | ] 55 | } 56 | export = resourceBase; 57 | -------------------------------------------------------------------------------- /src/resources/workspace_memberships.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/workspace_memberships_base"); 2 | resourceBase.comment = "This object determines if a user is a member of a workspace.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the workspace membership.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `workspace_membership`.\n", 16 | "example_values": [ 17 | "\"workspace_membership\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "workspace_membership", 22 | "comment": "A workspace membership resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "user", 28 | "type": "User", 29 | "example_values": [ 30 | "{ id: 12345, gid: \"12345\", name: \"Tim Bizarro\" }" 31 | ], 32 | "comment": "The user in the membership.\n" 33 | }, 34 | { 35 | "name": "workspace", 36 | "type": "Workspace", 37 | "example_values": [ 38 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 39 | ], 40 | "comment": "The workspace the user is a member of.\n" 41 | }, 42 | { 43 | "name": "user_task_list", 44 | "type": "UserTaskList", 45 | "example_values": [ 46 | "{ gid: \"12345\", resource_type: \"user_task_list\", name: 'My Tasks' }" 47 | ], 48 | "comment": "The user's \"My Tasks\" in the workspace.\n" 49 | }, 50 | { 51 | "name": "is_active", 52 | "type": "Boolean", 53 | "example_values": [ 54 | "false" 55 | ], 56 | "comment": "Reflects if this user still a member of the workspace.\n" 57 | }, 58 | { 59 | "name": "is_admin", 60 | "type": "Boolean", 61 | "example_values": [ 62 | "false" 63 | ], 64 | "comment": "Reflects if this user is an admin of the workspace.\n" 65 | }, 66 | { 67 | "name": "is_guest", 68 | "type": "Boolean", 69 | "example_values": [ 70 | "false" 71 | ], 72 | "comment": "Reflects if this user is a guest of the workspace.\n" 73 | } 74 | ] 75 | 76 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/gen/audit_log_api_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "audit_log_api", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getAuditLogEvents", 12 | "method": "GET", 13 | "collection": true||false, 14 | "path": "/workspaces/%s/audit_log_events", 15 | "params": [ 16 | { 17 | "name": "workspace_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the workspace or organization.", 21 | "required": true 22 | }, 23 | { 24 | "name": "resource_gid", 25 | "type": "string", 26 | "example_values": [], 27 | "comment": "Filter to events with this resource ID.", 28 | "required": false 29 | }, 30 | { 31 | "name": "actor_gid", 32 | "type": "string", 33 | "example_values": [], 34 | "comment": "Filter to events triggered by the actor with this ID.", 35 | "required": false 36 | }, 37 | { 38 | "name": "actor_type", 39 | "type": "string", 40 | "example_values": [], 41 | "comment": "Filter to events with an actor of this type. This only needs to be included if querying for actor types without an ID. If `actor_gid` is included, this should be excluded.", 42 | "required": false 43 | }, 44 | { 45 | "name": "event_type", 46 | "type": "string", 47 | "example_values": [], 48 | "comment": "Filter to events of this type. Refer to the [Supported AuditLogEvents](/docs/supported-auditlogevents) for a full list of values.", 49 | "required": false 50 | }, 51 | { 52 | "name": "end_at", 53 | "type": "Date", 54 | "example_values": [], 55 | "comment": "Filter to events created before this time (exclusive).", 56 | "required": false 57 | }, 58 | { 59 | "name": "start_at", 60 | "type": "Date", 61 | "example_values": [], 62 | "comment": "Filter to events created after this time (inclusive).", 63 | "required": false 64 | }, 65 | ], 66 | "comment": "Get audit log events" 67 | }, 68 | ] 69 | } 70 | export = resourceBase; 71 | -------------------------------------------------------------------------------- /src/resources/project_statuses.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/project_statuses_base"); 2 | resourceBase.comment = "A _project status_ is an update on the progress of a particular project, and is sent out to all project\nfollowers when created. These updates include both text describing the update and a color code intended to\nrepresent the overall state of the project: \"green\" for projects that are on track, \"yellow\" for projects\nat risk, and \"red\" for projects that are behind.\n\nProject statuses can be created and deleted, but not modified.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the project status update.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `project_status`.\n", 16 | "example_values": [ 17 | "\"project_status\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "project_status", 22 | "comment": "A project status resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "title", 28 | "type": "String", 29 | "comment": "The title of the project status update.\n", 30 | "example_values": [ 31 | "'Status Update - Jun 15'" 32 | ] 33 | }, 34 | { 35 | "name": "text", 36 | "type": "String", 37 | "comment": "The text content of the status update.\n", 38 | "example_values": [ 39 | "'The project is moving forward according to plan...'" 40 | ] 41 | }, 42 | { 43 | "name": "color", 44 | "type": "Enum", 45 | "comment": "The color associated with the status update.\n", 46 | "example_values": [ 47 | "'green'", 48 | "'yellow'", 49 | "'red'" 50 | ] 51 | }, 52 | { 53 | "name": "created_by", 54 | "type": "User", 55 | "example_values": [ 56 | "{ id: 12345, name: 'Tim Bizarro' }" 57 | ], 58 | "comment": "The creator of the status update.\n" 59 | }, 60 | { 61 | "name": "created_at", 62 | "type": "String", 63 | "example_values": [ 64 | "'2012-02-22T02:06:58.147Z'" 65 | ], 66 | "comment": "The time at which the status update was created.\n" 67 | } 68 | ] 69 | 70 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/attachments.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/attachments_base"); 2 | resourceBase.comment = "An _attachment_ object represents any file attached to a task in Asana,\nwhether it's an uploaded file or one associated via a third-party service\nsuch as Dropbox or Google Drive.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the attachment.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `attachment`.\n", 16 | "example_values": [ 17 | "\"attachment\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "attachment", 22 | "comment": "An attachment resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "created_at", 28 | "type": "String", 29 | "example_values": [ 30 | "'2012-02-22T02:06:58.147Z'" 31 | ], 32 | "comment": "The time at which this attachment was uploaded.\n" 33 | }, 34 | { 35 | "name": "download_url", 36 | "type": "String", 37 | "example_values": [ 38 | "'https://www.dropbox.com/s/123/Screenshot.png?dl=1'", 39 | "null" 40 | ], 41 | "comment": "The URL containing the content of the attachment.\n", 42 | }, 43 | { 44 | "name": "host", 45 | "type": "String", 46 | "example_values": [ 47 | "'dropbox'" 48 | ], 49 | "comment": "The service hosting the attachment. Valid values are `asana`, `dropbox`,\n`gdrive` and `box`.\n" 50 | }, 51 | { 52 | "name": "name", 53 | "type": "String", 54 | "example_values": [ 55 | "'Screenshot.png'" 56 | ], 57 | "comment": "The name of the file.\n" 58 | }, 59 | { 60 | "name": "parent", 61 | "type": "Task", 62 | "example_values": [ 63 | "{ id: 1234, gid: \"1234\", name: 'Bug task' }" 64 | ], 65 | "comment": "The task this attachment is attached to.\n" 66 | }, 67 | { 68 | "name": "view_url", 69 | "type": "String", 70 | "example_values": [ 71 | "'https://www.dropbox.com/s/123/Screenshot.png'", 72 | "null" 73 | ], 74 | "comment": "The URL where the attachment can be viewed, which may be friendlier to\nusers in a browser than just directing them to a raw file.\n" 75 | } 76 | ]; 77 | 78 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/gen/stories_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "stories", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createStoryForTask", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/tasks/%s/stories", 15 | "params": [ 16 | { 17 | "name": "task_gid", 18 | "type": "string", 19 | "example_values": ["321654"], 20 | "comment": "The task to operate on.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Create a story on a task" 25 | }, 26 | { 27 | "name": "deleteStory", 28 | "method": "DELETE", 29 | "collection": false, 30 | "path": "/stories/%s", 31 | "params": [ 32 | { 33 | "name": "story_gid", 34 | "type": "string", 35 | "example_values": ["35678"], 36 | "comment": "Globally unique identifier for the story.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Delete a story" 41 | }, 42 | { 43 | "name": "getStoriesForTask", 44 | "method": "GET", 45 | "collection": true||false, 46 | "path": "/tasks/%s/stories", 47 | "params": [ 48 | { 49 | "name": "task_gid", 50 | "type": "string", 51 | "example_values": ["321654"], 52 | "comment": "The task to operate on.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Get stories from a task" 57 | }, 58 | { 59 | "name": "getStory", 60 | "method": "GET", 61 | "collection": true||false, 62 | "path": "/stories/%s", 63 | "params": [ 64 | { 65 | "name": "story_gid", 66 | "type": "string", 67 | "example_values": ["35678"], 68 | "comment": "Globally unique identifier for the story.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get a story" 73 | }, 74 | { 75 | "name": "updateStory", 76 | "method": "PUT", 77 | "collection": false, 78 | "path": "/stories/%s", 79 | "params": [ 80 | { 81 | "name": "story_gid", 82 | "type": "string", 83 | "example_values": ["35678"], 84 | "comment": "Globally unique identifier for the story.", 85 | "required": true 86 | }, 87 | ], 88 | "comment": "Update a story" 89 | }, 90 | ] 91 | } 92 | export = resourceBase; 93 | -------------------------------------------------------------------------------- /src/resources/gen/workspaces_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "workspaces", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "addUserForWorkspace", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/workspaces/%s/addUser", 15 | "params": [ 16 | { 17 | "name": "workspace_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the workspace or organization.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Add a user to a workspace or organization" 25 | }, 26 | { 27 | "name": "getWorkspace", 28 | "method": "GET", 29 | "collection": false, 30 | "path": "/workspaces/%s", 31 | "params": [ 32 | { 33 | "name": "workspace_gid", 34 | "type": "string", 35 | "example_values": ["12345"], 36 | "comment": "Globally unique identifier for the workspace or organization.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Get a workspace" 41 | }, 42 | { 43 | "name": "getWorkspaces", 44 | "method": "GET", 45 | "collection": true||false, 46 | "path": "/workspaces", 47 | "params": [ 48 | ], 49 | "comment": "Get multiple workspaces" 50 | }, 51 | { 52 | "name": "removeUserForWorkspace", 53 | "method": "POST", 54 | "collection": false, 55 | "path": "/workspaces/%s/removeUser", 56 | "params": [ 57 | { 58 | "name": "workspace_gid", 59 | "type": "string", 60 | "example_values": ["12345"], 61 | "comment": "Globally unique identifier for the workspace or organization.", 62 | "required": true 63 | }, 64 | ], 65 | "comment": "Remove a user from a workspace or organization" 66 | }, 67 | { 68 | "name": "updateWorkspace", 69 | "method": "PUT", 70 | "collection": false, 71 | "path": "/workspaces/%s", 72 | "params": [ 73 | { 74 | "name": "workspace_gid", 75 | "type": "string", 76 | "example_values": ["12345"], 77 | "comment": "Globally unique identifier for the workspace or organization.", 78 | "required": true 79 | }, 80 | ], 81 | "comment": "Update a workspace" 82 | }, 83 | ] 84 | } 85 | export = resourceBase; 86 | -------------------------------------------------------------------------------- /src/resources/gen/project_templates_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "project_templates", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getProjectTemplate", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/project_templates/%s", 15 | "params": [ 16 | { 17 | "name": "project_template_gid", 18 | "type": "string", 19 | "example_values": ["1331"], 20 | "comment": "Globally unique identifier for the project template.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a project template" 25 | }, 26 | { 27 | "name": "getProjectTemplates", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/project_templates", 31 | "params": [ 32 | { 33 | "name": "team", 34 | "type": "string", 35 | "example_values": ["14916"], 36 | "comment": "The team to filter projects on.", 37 | "required": false 38 | }, 39 | { 40 | "name": "workspace", 41 | "type": "string", 42 | "example_values": ["12345"], 43 | "comment": "The workspace to filter results on.", 44 | "required": false 45 | }, 46 | ], 47 | "comment": "Get multiple project templates" 48 | }, 49 | { 50 | "name": "getProjectTemplatesForTeam", 51 | "method": "GET", 52 | "collection": true||false, 53 | "path": "/teams/%s/project_templates", 54 | "params": [ 55 | { 56 | "name": "team_gid", 57 | "type": "string", 58 | "example_values": ["159874"], 59 | "comment": "Globally unique identifier for the team.", 60 | "required": true 61 | }, 62 | ], 63 | "comment": "Get a team's project templates" 64 | }, 65 | { 66 | "name": "instantiateProject", 67 | "method": "POST", 68 | "collection": false, 69 | "path": "/project_templates/%s/instantiateProject", 70 | "params": [ 71 | { 72 | "name": "project_template_gid", 73 | "type": "string", 74 | "example_values": ["1331"], 75 | "comment": "Globally unique identifier for the project template.", 76 | "required": true 77 | }, 78 | ], 79 | "comment": "Instantiate a project from a project template" 80 | }, 81 | ] 82 | } 83 | export = resourceBase; 84 | -------------------------------------------------------------------------------- /src/resources/audit_log_api.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/audit_log_api_base"); 2 | resourceBase.comment = "Asana's Audit Log is an immutable log of [important events](/docs/supported-auditlogevents) in your organization's Asana instance.\n\nThe Audit Log API allows you to monitor and act upon important security and compliance-related changes. Organizations might use this API endpoint to:\n\n* Set up proactive alerting with a Security Information and Event Management (SIEM) tool like [Splunk](https://asana.com/guide/help/api/splunk)\n* Conduct reactive investigations when a security incident takes place\n* Visualize key domain data in aggregate to identify security trends\n\nNote that since the API provides insight into what is happening in an Asana instance, the data is [read-only](/docs/get-audit-log-events). That is, there are no \"write\" or \"update\" endpoints for audit log events.\n\nOnly [Service Accounts](https://asana.com/guide/help/premium/service-accounts) in [Enterprise Domains](https://asana.com/enterprise) can access Audit Log API endpoints. Authentication with a Service Account's [Personal Access Token](/docs/personal-access-token) is required.\n\nFor a full list of supported events, see [Supported AuditLogEvents](/docs/supported-auditlogevents).\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "actor", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "context", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "created_at", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "details", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "event_category", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "event_type", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | { 47 | "name": "resource", 48 | "type": "", 49 | "example_values": [], 50 | "comment": "", 51 | }, 52 | ]; 53 | export = resourceBase; 54 | -------------------------------------------------------------------------------- /src/resources/gen/webhooks_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "webhooks", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createWebhook", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/webhooks", 15 | "params": [ 16 | ], 17 | "comment": "Establish a webhook" 18 | }, 19 | { 20 | "name": "deleteWebhook", 21 | "method": "DELETE", 22 | "collection": false, 23 | "path": "/webhooks/%s", 24 | "params": [ 25 | { 26 | "name": "webhook_gid", 27 | "type": "string", 28 | "example_values": ["12345"], 29 | "comment": "Globally unique identifier for the webhook.", 30 | "required": true 31 | }, 32 | ], 33 | "comment": "Delete a webhook" 34 | }, 35 | { 36 | "name": "getWebhook", 37 | "method": "GET", 38 | "collection": false, 39 | "path": "/webhooks/%s", 40 | "params": [ 41 | { 42 | "name": "webhook_gid", 43 | "type": "string", 44 | "example_values": ["12345"], 45 | "comment": "Globally unique identifier for the webhook.", 46 | "required": true 47 | }, 48 | ], 49 | "comment": "Get a webhook" 50 | }, 51 | { 52 | "name": "getWebhooks", 53 | "method": "GET", 54 | "collection": true||false, 55 | "path": "/webhooks", 56 | "params": [ 57 | { 58 | "name": "resource", 59 | "type": "string", 60 | "example_values": ["51648"], 61 | "comment": "Only return webhooks for the given resource.", 62 | "required": false 63 | }, 64 | { 65 | "name": "workspace", 66 | "type": "string", 67 | "example_values": ["1331"], 68 | "comment": "The workspace to query for webhooks in.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get multiple webhooks" 73 | }, 74 | { 75 | "name": "updateWebhook", 76 | "method": "PUT", 77 | "collection": false, 78 | "path": "/webhooks/%s", 79 | "params": [ 80 | { 81 | "name": "webhook_gid", 82 | "type": "string", 83 | "example_values": ["12345"], 84 | "comment": "Globally unique identifier for the webhook.", 85 | "required": true 86 | }, 87 | ], 88 | "comment": "Update a webhook" 89 | }, 90 | ] 91 | } 92 | export = resourceBase; 93 | -------------------------------------------------------------------------------- /src/resources/gen/portfolio_memberships_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "portfolio_memberships", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getPortfolioMembership", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/portfolio_memberships/%s", 15 | "params": [ 16 | { 17 | "name": "portfolio_membership_gid", 18 | "type": "string", 19 | "example_values": ["1331"], 20 | "comment": "", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a portfolio membership" 25 | }, 26 | { 27 | "name": "getPortfolioMemberships", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/portfolio_memberships", 31 | "params": [ 32 | { 33 | "name": "user", 34 | "type": "string", 35 | "example_values": ["me"], 36 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 37 | "required": false 38 | }, 39 | { 40 | "name": "workspace", 41 | "type": "string", 42 | "example_values": ["12345"], 43 | "comment": "The workspace to filter results on.", 44 | "required": false 45 | }, 46 | { 47 | "name": "portfolio", 48 | "type": "string", 49 | "example_values": ["12345"], 50 | "comment": "The portfolio to filter results on.", 51 | "required": false 52 | }, 53 | ], 54 | "comment": "Get multiple portfolio memberships" 55 | }, 56 | { 57 | "name": "getPortfolioMembershipsForPortfolio", 58 | "method": "GET", 59 | "collection": true||false, 60 | "path": "/portfolios/%s/portfolio_memberships", 61 | "params": [ 62 | { 63 | "name": "portfolio_gid", 64 | "type": "string", 65 | "example_values": ["12345"], 66 | "comment": "Globally unique identifier for the portfolio.", 67 | "required": true 68 | }, 69 | { 70 | "name": "user", 71 | "type": "string", 72 | "example_values": ["me"], 73 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 74 | "required": false 75 | }, 76 | ], 77 | "comment": "Get memberships from a portfolio" 78 | }, 79 | ] 80 | } 81 | export = resourceBase; 82 | -------------------------------------------------------------------------------- /test/components/resource_entry_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import chai = require("chai"); 3 | import * as ReactDOM from "react-dom"; 4 | import sinon = require("sinon"); 5 | 6 | import ResourceEntry = require("../../src/components/resource_entry"); 7 | import Resources = require("../../src/resources"); 8 | import * as ReactTestUtils from "react-dom/test-utils"; 9 | import ResourcesHelpers = require("../../src/resources/helpers"); 10 | import {SinonFakeServer, SinonStub} from "sinon"; 11 | 12 | var assert = chai.assert; 13 | var testUtils = ReactTestUtils; 14 | 15 | describe("ResourceEntryComponent", () => { 16 | var sand: SinonFakeServer; 17 | 18 | var initialResource: Resource; 19 | 20 | var onResourceChangeStub: SinonStub; 21 | 22 | var root: ResourceEntry; 23 | var selectResource: Element; 24 | 25 | beforeEach(() => { 26 | sand = sinon.fakeServer.create(); 27 | 28 | initialResource = Resources.Events; 29 | 30 | onResourceChangeStub = sinon.stub(); 31 | 32 | root = testUtils.renderIntoDocument( 33 | ResourceEntry.create({ 34 | resource: initialResource, 35 | onResourceChange: onResourceChangeStub 36 | }) 37 | ); 38 | selectResource = testUtils.findRenderedDOMComponentWithClass( 39 | root, 40 | "select-resource" 41 | ); 42 | }); 43 | 44 | afterEach(() => { 45 | sand.restore(); 46 | }); 47 | 48 | it("should select the current resource", () => { 49 | assert.include( 50 | (ReactDOM.findDOMNode(selectResource)).value, 51 | ResourcesHelpers.resourceNameFromResource(initialResource) 52 | ); 53 | }); 54 | 55 | it("should contain dropdown with other resources", () => { 56 | var children = >(ReactDOM.findDOMNode(selectResource) || {}).childNodes; 57 | var resourceNames = ResourcesHelpers.names(); 58 | 59 | assert.equal(children.length, resourceNames.length); 60 | resourceNames.forEach((resourceName, idx) => { 61 | assert.equal( 62 | (children.item(idx)).value, 63 | resourceName 64 | ); 65 | }); 66 | }); 67 | 68 | it("should trigger onResourceChange property on resource change", () => { 69 | testUtils.Simulate.change(selectResource); 70 | sinon.assert.called(onResourceChangeStub); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/components/parameter_entry_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import chai = require("chai"); 3 | import React = require("react"); 4 | import sinon = require("sinon"); 5 | import _ = require("lodash"); 6 | 7 | import ParameterEntry = require("../../src/components/parameter_entry"); 8 | import Resources = require("../../src/resources"); 9 | import * as ReactTestUtils from "react-dom/test-utils"; 10 | import {SinonFakeServer, SinonStub} from "sinon"; 11 | 12 | const assert = chai.assert; 13 | const r = React.createElement; 14 | const testUtils = ReactTestUtils; 15 | 16 | describe("ParameterEntryComponent", () => { 17 | let sand: SinonFakeServer; 18 | 19 | let parameters: Parameter[]; 20 | 21 | let onParameterChangeStub: SinonStub; 22 | 23 | let root: ParameterEntry; 24 | let inputs: Element[]; 25 | 26 | beforeEach(() => { 27 | sand = sinon.fakeServer.create(); 28 | 29 | parameters = Resources.Events.actions[0].params || []; 30 | 31 | onParameterChangeStub = sinon.stub(); 32 | 33 | root = testUtils.renderIntoDocument( 34 | ParameterEntry.create({ 35 | text: r("h3", {}, "this is a test"), 36 | parameters: parameters, 37 | onParameterChange: onParameterChangeStub, 38 | workspace: undefined, 39 | workspaces: undefined 40 | }) 41 | ); 42 | inputs = testUtils.scryRenderedDOMComponentsWithClass( 43 | root, 44 | "parameter-input" 45 | ); 46 | }); 47 | 48 | afterEach(() => { 49 | sand.restore(); 50 | }); 51 | 52 | it("should contain an input for each parameter", () => { 53 | const parameterNames = _.map(parameters, "name"); 54 | 55 | // Filter out the extra-param for this test. 56 | const parameterInputs = _.filter( 57 | inputs, input => !_.includes(input.className, "extra-param")); 58 | 59 | parameterInputs.forEach(input => { 60 | assert.include( 61 | parameterNames, 62 | (input).placeholder 63 | ); 64 | }); 65 | }); 66 | 67 | it("should trigger onChange parameter when text is entered", () => { 68 | inputs.forEach(input => { 69 | input.dispatchEvent(new Event("change", {bubbles: true})); 70 | }); 71 | 72 | parameters.forEach(parameter => { 73 | sinon.assert.calledWith(onParameterChangeStub, parameter); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/resources/jobs.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/jobs_base"); 2 | resourceBase.comment = "A _job_ represents a process that handles asynchronous work.\n\nJobs are created when an endpoint requests an action that will be handled asynchronously.\nSuch as project or task duplication.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the job.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `job`.\n", 16 | "example_values": [ 17 | "\"job\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "job", 22 | "comment": "A job resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "resource_subtype", 28 | "type": "Enum", 29 | "comment": "The type of job.\n", 30 | "example_values": [ 31 | "\"duplicate_project\"", 32 | "\"duplicate_task\"" 33 | ], 34 | "values": [ 35 | { 36 | "name": "duplicate_project", 37 | "comment": "A job that duplicates a project." 38 | }, 39 | { 40 | "name": "duplicate_task", 41 | "comment": "A job that duplicates a task." 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "status", 47 | "type": "Enum", 48 | "comment": "The status of the job.", 49 | "example_values": [ 50 | "\"not_started\"", 51 | "\"in_progress\"", 52 | "\"succeeded\"", 53 | "\"failed\"" 54 | ], 55 | "values": [ 56 | { 57 | "name": "not_started", 58 | "comment": "The job has not started." 59 | }, 60 | { 61 | "name": "in_progress", 62 | "comment": "The job is in progress." 63 | }, 64 | { 65 | "name": "succeeded", 66 | "comment": "The job has completed." 67 | }, 68 | { 69 | "name": "failed", 70 | "comment": "The job has not started." 71 | } 72 | ] 73 | }, 74 | { 75 | "name": "new_project", 76 | "type": "Project", 77 | "example_values": [ 78 | "{ id: 1234, gid: \"1234\", name: 'Bugs' }" 79 | ], 80 | "comment": "Contains the new project if the job created a new project.\n" 81 | }, 82 | { 83 | "name": "new_task", 84 | "type": "Task", 85 | "example_values": [ 86 | "{ id: 1234, gid: \"1234\", name: 'Bug task' }" 87 | ], 88 | "comment": "Contains the new task if the job created a new task.\n" 89 | } 90 | ] 91 | 92 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/custom_field_settings.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/custom_field_settings_base"); 2 | resourceBase.comment = "\nCustom fields are applied to a particular project or portfolio with the\nCustom Field Settings resource. This resource both represents the\nmany-to-many join of the Custom Field and Project or Portfolio as well as\nstores information that is relevant to that particular pairing; for instance,\nthe `is_important` property determines some possible application-specific\nhandling of that custom field and parent.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the custom field settings object.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `custom_field_setting`\n", 16 | "example_values": [ 17 | "\"custom_field_setting\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "custom_field_setting", 22 | "comment": "A custom_field_setting resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "created_at", 28 | "type": "String", 29 | "example_values": [ 30 | "'2012-02-22T02:06:58.147Z'" 31 | ], 32 | "comment": "The time at which this custom field setting was created.\n" 33 | }, 34 | { 35 | "name": "is_important", 36 | "type": "Boolean", 37 | "example_values": [ 38 | "false" 39 | ], 40 | "comment": "`is_important` is used in the Asana web application to determine if this\ncustom field is displayed in the task list (left pane) of a project. A\nproject can have a maximum of 5 custom field settings marked as\n`is_important`.\n" 41 | }, 42 | { 43 | "name": "parent", 44 | "type": "String", 45 | "example_values": [ 46 | "{id: 29485, gid: \"29485\", name: \"Marketing steps\", resource_type: \"project\"}", 47 | "{id: 36985, gid: \"36985\", name: \"Product launch\", resource_type: \"portfolio\"}" 48 | ], 49 | "comment": "The parent to which the custom field is applied. This can be a project\nor portfolio and indicates that the tasks or projects that the parent\ncontains may be given custom field values for this custom field.\n" 50 | }, 51 | { 52 | "name": "custom_field", 53 | "type": "CustomField", 54 | "example_values": [ 55 | "{ id: 1646, gid: \"1646\", name: 'Priority', type: 'enum' }" 56 | ], 57 | "comment": "The custom field that is applied to the `parent`.\n" 58 | } 59 | ]; 60 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/tags.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/tags_base"); 2 | resourceBase.comment = "A _tag_ is a label that can be attached to any task in Asana. It exists in a\nsingle workspace or organization.\n\nTags have some metadata associated with them, but it is possible that we will\nsimplify them in the future so it is not encouraged to rely too heavily on it.\nUnlike projects, tags do not provide any ordering on the tasks they\nare associated with.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the tag.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `tag`.\n", 16 | "example_values": [ 17 | "\"tag\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "tag", 22 | "comment": "A tag resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "created_at", 28 | "type": "String", 29 | "example_values": [ 30 | "'2012-02-22T02:06:58.147Z'" 31 | ], 32 | "comment": "The time at which this tag was created.\n" 33 | }, 34 | { 35 | "name": "followers", 36 | "type": "Array", 37 | "example_values": [ 38 | "[ { id: 1123, gid: \"1123\", resource_type: \"user\", name: 'Mittens' }, ... ]" 39 | ], 40 | "comment": "Array of users following this tag.\n" 41 | }, 42 | { 43 | "name": "name", 44 | "type": "String", 45 | "example_values": [ 46 | "'Stuff to buy'" 47 | ], 48 | "comment": "Name of the tag. This is generally a short sentence fragment that fits\non a line in the UI for maximum readability. However, it can be longer.\n" 49 | }, 50 | { 51 | "name": "color", 52 | "type": "Enum", 53 | "example_values": [ 54 | "'dark-purple'" 55 | ], 56 | "comment": "Color of the tag. Must be either `null` or one of: `dark-pink`,\n`dark-green`, `dark-blue`, `dark-red`, `dark-teal`, `dark-brown`,\n`dark-orange`, `dark-purple`, `dark-warm-gray`, `light-pink`, `light-green`,\n`light-blue`, `light-red`, `light-teal`, `light-yellow`, `light-orange`,\n`light-purple`, `light-warm-gray`.\n" 57 | }, 58 | { 59 | "name": "workspace", 60 | "type": "Workspace", 61 | "example_values": [ 62 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 63 | ], 64 | "comment": "The workspace or organization this tag is associated with. Once created,\ntags cannot be moved to a different workspace. This attribute can only\nbe specified at creation time.\n" 65 | } 66 | ]; 67 | 68 | export = resourceBase; -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "statements" 6 | ], 7 | "ban": false, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space", 12 | "check-uppercase" 13 | ], 14 | "curly": true, 15 | "eofline": false, 16 | "forin": true, 17 | "indent": [ 18 | true, 19 | 4 20 | ], 21 | "interface-name": false, 22 | "jsdoc-format": true, 23 | "label-position": true, 24 | "label-undefined": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-ordering": [ 30 | true, 31 | "public-before-private", 32 | "static-before-instance", 33 | "variables-before-functions" 34 | ], 35 | "no-any": false, 36 | "no-arg": true, 37 | "no-bitwise": true, 38 | "no-console": [ 39 | true, 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-consecutive-blank-lines": true, 47 | "no-construct": true, 48 | "no-constructor-vars": true, 49 | "no-debugger": true, 50 | "no-duplicate-key": true, 51 | "no-duplicate-variable": true, 52 | "no-empty": true, 53 | "no-eval": true, 54 | "no-string-literal": true, 55 | "no-switch-case-fall-through": true, 56 | "no-trailing-comma": true, 57 | "no-trailing-whitespace": true, 58 | "no-unreachable": true, 59 | "no-unused-expression": true, 60 | "no-unused-variable": true, 61 | "no-use-before-declare": true, 62 | "no-var-requires": true, 63 | "one-line": [ 64 | true, 65 | "check-open-brace", 66 | "check-catch", 67 | "check-else", 68 | "check-whitespace" 69 | ], 70 | "quotemark": [ 71 | true, 72 | "double" 73 | ], 74 | "radix": true, 75 | "semicolon": true, 76 | "switch-default": true, 77 | "triple-equals": true, 78 | "typedef": [ 79 | true, 80 | "property-declaration" 81 | ], 82 | "typedef-whitespace": [ 83 | true, { 84 | "call-signature": "nospace", 85 | "index-signature": "nospace", 86 | "parameter": "nospace", 87 | "property-declaration": "nospace", 88 | "variable-declaration": "nospace" 89 | } 90 | ], 91 | "use-strict": false, 92 | "variable-name": [ 93 | true, 94 | "allow-leading-underscore" 95 | ], 96 | "whitespace": [ 97 | true, 98 | "check-branch", 99 | "check-decl", 100 | "check-operator", 101 | "check-separator", 102 | "check-type" 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/credentials.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Asana = require("asana"); 3 | 4 | import constants = require("./constants"); 5 | 6 | // Allows us to mock out localStorage in tests. 7 | export var localStorage: Storage = window.localStorage; 8 | 9 | export enum AuthState { 10 | Unauthorized, 11 | Expired, 12 | Authorized 13 | } 14 | 15 | /* 16 | * The time to subtract before we actually consider credentials expired. 17 | */ 18 | var EXPIRY_BUFFER_MS = 5 * 60 * 1000; 19 | 20 | /** 21 | * Returns the authorization state based on credentials in the client. 22 | * 23 | * @param {Asana.Client} client 24 | * @returns {AuthState} 25 | */ 26 | export function authStateFromClient(client: Asana.Client): AuthState { 27 | var credentials = getFromClient(client); 28 | 29 | // If no credentials, then mark as unauthorized. 30 | if (credentials === null) { 31 | return AuthState.Unauthorized; 32 | } 33 | 34 | // If the credentials have expired, then mark as expired. 35 | var expiryTimestamp = credentials.expiry_timestamp; 36 | if (!expiryTimestamp || expiryTimestamp - Date.now() < EXPIRY_BUFFER_MS) { 37 | return AuthState.Expired; 38 | } 39 | 40 | return AuthState.Authorized; 41 | } 42 | 43 | /** 44 | * Fetches credentials from an asana client. 45 | * 46 | * @param {Asana.Client} client 47 | * @returns {Asana.auth.Credentials} 48 | */ 49 | function getFromClient(client: Asana.Client): Asana.auth.Credentials { 50 | // We know our authenticator is an oauth authenticator, so we typecast it as such. 51 | return (client.dispatcher.authenticator).credentials; 52 | } 53 | 54 | /** 55 | * Fetches credentials from localStorage, if such credentials exist. 56 | * Note: This may return expired credentials, so we always must re-check 57 | * before using them. 58 | * 59 | * @returns {Asana.auth.Credentials} 60 | */ 61 | export function getFromLocalStorage(): Asana.auth.Credentials { 62 | if (localStorage !== undefined) { 63 | return JSON.parse( 64 | localStorage.getItem(constants.LOCALSTORAGE_KEY) 65 | ); 66 | } 67 | } 68 | 69 | /** 70 | * Stores credentials from a client for use in later sessions. 71 | * 72 | * @param {Asana.Client} client 73 | */ 74 | export function storeFromClient(client: Asana.Client): void { 75 | if (localStorage !== undefined) { 76 | var credentials = getFromClient(client); 77 | 78 | if (credentials === null) { 79 | throw new Error("There are no credentials in the client."); 80 | } 81 | 82 | // Add expiry timestamp to credentials, for use when checking expiry. 83 | credentials.expiry_timestamp = Date.now() + credentials.expires_in * 1000; 84 | 85 | localStorage.setItem( 86 | constants.LOCALSTORAGE_KEY, 87 | JSON.stringify(credentials) 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/components/property_entry_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import chai = require("chai"); 3 | import React = require("react"); 4 | import sinon = require("sinon"); 5 | import _ = require("lodash"); 6 | import * as ReactDOM from "react-dom"; 7 | 8 | import PropertyEntry = require("../../src/components/property_entry"); 9 | import Resources = require("../../src/resources"); 10 | import * as ReactTestUtils from "react-dom/test-utils"; 11 | import {SinonFakeServer, SinonStub} from "sinon"; 12 | 13 | const assert = chai.assert; 14 | const r = React.createElement; 15 | const testUtils = ReactTestUtils; 16 | 17 | describe("PropertyEntryComponent", () => { 18 | let sand: SinonFakeServer; 19 | 20 | let properties: Property[]; 21 | 22 | let isPropertyCheckedStub: SinonStub; 23 | let usePropertyStub: SinonStub; 24 | 25 | let root: PropertyEntry; 26 | let checkboxes: Element[]; 27 | 28 | beforeEach(() => { 29 | sand = sinon.fakeServer.create(); 30 | 31 | properties = Resources.Events.properties; 32 | 33 | isPropertyCheckedStub = sinon.stub(); 34 | usePropertyStub = sinon.stub(); 35 | 36 | root = testUtils.renderIntoDocument( 37 | PropertyEntry.create({ 38 | classSuffix: "test", 39 | text: r("h3", {}, "this is a test"), 40 | properties: properties, 41 | isPropertyChecked: isPropertyCheckedStub, 42 | useProperty: usePropertyStub 43 | }) 44 | ); 45 | checkboxes = testUtils.scryRenderedDOMComponentsWithClass( 46 | root, 47 | "property-checkbox-test" 48 | ); 49 | }); 50 | 51 | afterEach(() => { 52 | sand.restore(); 53 | }); 54 | 55 | it("should contain a checkbox for each property", () => { 56 | const propertyNames = _.map(properties, "name"); 57 | 58 | assert.equal(checkboxes.length, properties.length); 59 | checkboxes.forEach(checkbox => { 60 | assert.include( 61 | propertyNames, 62 | ((ReactDOM.findDOMNode(checkbox))).value 63 | ); 64 | }); 65 | }); 66 | 67 | it("should verify checked state for each property", () => { 68 | properties.forEach(property => { 69 | sinon.assert.calledWith(usePropertyStub, property.name); 70 | }); 71 | }); 72 | 73 | it("should trigger onChange property on each check action", () => { 74 | checkboxes.forEach(checkbox => { 75 | const checkboxNode = ReactDOM.findDOMNode(checkbox); 76 | testUtils.Simulate.change(checkboxNode); 77 | }); 78 | sinon.assert.callCount(isPropertyCheckedStub, properties.length); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/parameter_entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import Asana = require("asana"); 4 | import cx = require("../class_names"); 5 | import React = require("react"); 6 | 7 | var r = React.createElement; 8 | 9 | /** 10 | * The parameter input area 11 | */ 12 | class ParameterEntry extends React.Component { 13 | static create = React.createFactory(ParameterEntry); 14 | 15 | render() { 16 | return r("div", { 17 | className: "parameter-entry", 18 | children: [ 19 | this.props.text, 20 | r("div", { 21 | className: "parameter-inputs" 22 | }, this.props.parameters === undefined ? "" : 23 | this.props.parameters.map(this.renderParameterInput)) 24 | ] 25 | } 26 | ); 27 | } 28 | 29 | private useWorkspaceDropdown = (parameter: Parameter) => { 30 | // Ensure workspaces have loaded successfully. 31 | if (this.props.workspaces === undefined) { 32 | return false; 33 | } 34 | 35 | return parameter.name === "workspace" || parameter.name === "organization"; 36 | } 37 | 38 | private renderParameterInput = (parameter: Parameter) => { 39 | var classes = cx({ 40 | "parameter-input": true, 41 | "required-param": parameter.required || false 42 | }); 43 | var id = "parameter_input_" + parameter.name; 44 | 45 | // We pre-fetch workspaces, so show a dropdown instead. 46 | if (this.useWorkspaceDropdown(parameter)) { 47 | return r("span", { key: parameter.name }, 48 | r("label", { }, 49 | "Workspace" 50 | ), 51 | r("select", { 52 | id: id, 53 | className: classes, 54 | onChange: this.props.onParameterChange(parameter), 55 | value: this.props.workspace ? this.props.workspace.gid.toString() : undefined, 56 | children: this.props.workspaces ? this.props.workspaces.map(workspace => { 57 | return r("option", { 58 | value: workspace.gid.toString() 59 | }, workspace.name); 60 | }) : undefined 61 | }) 62 | ); 63 | } else { 64 | return r("span", { key: parameter.name }, 65 | r("label", { }, 66 | parameter.name 67 | ), 68 | r("input", { 69 | placeholder: parameter.name, 70 | type: "text", 71 | id: id, 72 | className: classes, 73 | onChange: this.props.onParameterChange(parameter) 74 | }) 75 | ); 76 | } 77 | } 78 | } 79 | 80 | module ParameterEntry { 81 | export interface Props { 82 | text: React.DOMElement; 83 | parameters: Parameter[]; 84 | onParameterChange: 85 | (parameter: Parameter) => (event?: React.FormEvent) => void; 86 | workspace?: Asana.resources.Workspace; 87 | workspaces?: Asana.resources.Workspace[]; 88 | } 89 | } 90 | 91 | export = ParameterEntry; 92 | -------------------------------------------------------------------------------- /src/resources/gen/team_memberships_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "team_memberships", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getTeamMembership", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/team_memberships/%s", 15 | "params": [ 16 | { 17 | "name": "team_membership_gid", 18 | "type": "string", 19 | "example_values": ["724362"], 20 | "comment": "", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Get a team membership" 25 | }, 26 | { 27 | "name": "getTeamMemberships", 28 | "method": "GET", 29 | "collection": true||false, 30 | "path": "/team_memberships", 31 | "params": [ 32 | { 33 | "name": "workspace", 34 | "type": "string", 35 | "example_values": ["31326"], 36 | "comment": "Globally unique identifier for the workspace. This parameter must be used with the user parameter.", 37 | "required": false 38 | }, 39 | { 40 | "name": "user", 41 | "type": "string", 42 | "example_values": ["512241"], 43 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user. This parameter must be used with the workspace parameter.", 44 | "required": false 45 | }, 46 | { 47 | "name": "team", 48 | "type": "string", 49 | "example_values": ["159874"], 50 | "comment": "Globally unique identifier for the team.", 51 | "required": false 52 | }, 53 | ], 54 | "comment": "Get team memberships" 55 | }, 56 | { 57 | "name": "getTeamMembershipsForTeam", 58 | "method": "GET", 59 | "collection": true||false, 60 | "path": "/teams/%s/team_memberships", 61 | "params": [ 62 | { 63 | "name": "team_gid", 64 | "type": "string", 65 | "example_values": ["159874"], 66 | "comment": "Globally unique identifier for the team.", 67 | "required": true 68 | }, 69 | ], 70 | "comment": "Get memberships from a team" 71 | }, 72 | { 73 | "name": "getTeamMembershipsForUser", 74 | "method": "GET", 75 | "collection": true||false, 76 | "path": "/users/%s/team_memberships", 77 | "params": [ 78 | { 79 | "name": "user_gid", 80 | "type": "string", 81 | "example_values": ["me"], 82 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 83 | "required": true 84 | }, 85 | { 86 | "name": "workspace", 87 | "type": "string", 88 | "example_values": ["31326"], 89 | "comment": "Globally unique identifier for the workspace.", 90 | "required": true 91 | }, 92 | ], 93 | "comment": "Get memberships from a user" 94 | }, 95 | ] 96 | } 97 | export = resourceBase; 98 | -------------------------------------------------------------------------------- /src/resources/goals.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/goals_base"); 2 | resourceBase.comment = "A _goal_ is an object in the goal-tracking system that helps your organization drive measurable results.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "resource_type", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "due_on", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "html_notes", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "is_workspace_level", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "liked", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "name", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | { 47 | "name": "notes", 48 | "type": "", 49 | "example_values": [], 50 | "comment": "", 51 | }, 52 | { 53 | "name": "start_on", 54 | "type": "", 55 | "example_values": [], 56 | "comment": "", 57 | }, 58 | { 59 | "name": "status", 60 | "type": "", 61 | "example_values": [], 62 | "comment": "", 63 | }, 64 | { 65 | "name": "current_status_update", 66 | "type": "", 67 | "example_values": [], 68 | "comment": "", 69 | }, 70 | { 71 | "name": "followers", 72 | "type": "", 73 | "example_values": [], 74 | "comment": "", 75 | }, 76 | { 77 | "name": "likes", 78 | "type": "", 79 | "example_values": [], 80 | "comment": "", 81 | }, 82 | { 83 | "name": "metric", 84 | "type": "", 85 | "example_values": [], 86 | "comment": "", 87 | }, 88 | { 89 | "name": "num_likes", 90 | "type": "", 91 | "example_values": [], 92 | "comment": "", 93 | }, 94 | { 95 | "name": "owner", 96 | "type": "", 97 | "example_values": [], 98 | "comment": "", 99 | }, 100 | { 101 | "name": "team", 102 | "type": "", 103 | "example_values": [], 104 | "comment": "", 105 | }, 106 | { 107 | "name": "time_period", 108 | "type": "", 109 | "example_values": [], 110 | "comment": "", 111 | }, 112 | { 113 | "name": "workspace", 114 | "type": "", 115 | "example_values": [], 116 | "comment": "", 117 | }, 118 | ]; 119 | export = resourceBase; 120 | -------------------------------------------------------------------------------- /src/resources/events.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/events_base"); 2 | resourceBase.comment = "An _event_ is an object representing a change to a resource that was observed\nby an event subscription.\n\nIn general, requesting events on a resource is faster and subject to higher\nrate limits than requesting the resource itself. Additionally, change events\nbubble up - listening to events on a project would include when stories are\nadded to tasks in the project, even on subtasks.\n\nEstablish an initial sync token by making a request with no sync token.\nThe response will be a `412` error - the same as if the sync token had\nexpired.\n\nSubsequent requests should always provide the sync token from the immediately\npreceding call.\n\nSync tokens may not be valid if you attempt to go 'backward' in the history\nby requesting previous tokens, though re-requesting the current sync token\nis generally safe, and will always return the same results.\n\nWhen you receive a `412 Precondition Failed` error, it means that the\nsync token is either invalid or expired. If you are attempting to keep a set\nof data in sync, this signals you may need to re-crawl the data.\n\nSync tokens always expire after 24 hours, but may expire sooner, depending on\nload on the service.\n" 3 | resourceBase.properties = [ 4 | { 5 | "name": "user", 6 | "type": "User", 7 | "example_values": [ 8 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }", 9 | "null" 10 | ], 11 | "comment": "The user who triggered the event.\n", 12 | }, 13 | { 14 | "name": "resource", 15 | "type": "Task", 16 | "example_values": [ 17 | "{ id: 1234, gid: \"1234\", name: 'Bug task' }" 18 | ], 19 | "comment": "The resource the event occurred on.\n", 20 | }, 21 | { 22 | "name": "action", 23 | "type": "Enum", 24 | "example_values": [ 25 | "'changed'" 26 | ], 27 | "values": [ 28 | { 29 | "name": "changed", 30 | "comment": "A resource was changed." 31 | }, 32 | { 33 | "name": "added", 34 | "comment": "A resource was added." 35 | }, 36 | { 37 | "name": "removed", 38 | "comment": "A resource was removed." 39 | }, 40 | { 41 | "name": "deleted", 42 | "comment": "A resource was deleted." 43 | }, 44 | { 45 | "name": "undeleted", 46 | "comment": "A resource was undeleted." 47 | } 48 | ], 49 | "comment": "The type of action taken that triggered the event.\n" 50 | }, 51 | { 52 | "name": "parent", 53 | "type": "Project", 54 | "example_values": [ 55 | "{ id: 1234, gid: \"1234\", name: 'Bugs' }" 56 | ], 57 | "comment": "For added/removed events, the parent that resource was added to or\nremoved from. `null` for other event types.\n" 58 | }, 59 | { 60 | "name": "created_at", 61 | "type": "String", 62 | "example_values": [ 63 | "'2012-02-22T02:06:58.147Z'" 64 | ], 65 | "comment": "The timestamp when the event occurred.\n" 66 | } 67 | ]; 68 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/gen/teams_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "teams", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "addUserForTeam", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/teams/%s/addUser", 15 | "params": [ 16 | { 17 | "name": "team_gid", 18 | "type": "string", 19 | "example_values": ["159874"], 20 | "comment": "Globally unique identifier for the team.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Add a user to a team" 25 | }, 26 | { 27 | "name": "createTeam", 28 | "method": "POST", 29 | "collection": true||false, 30 | "path": "/teams", 31 | "params": [ 32 | ], 33 | "comment": "Create a team" 34 | }, 35 | { 36 | "name": "getTeam", 37 | "method": "GET", 38 | "collection": true||false, 39 | "path": "/teams/%s", 40 | "params": [ 41 | { 42 | "name": "team_gid", 43 | "type": "string", 44 | "example_values": ["159874"], 45 | "comment": "Globally unique identifier for the team.", 46 | "required": true 47 | }, 48 | ], 49 | "comment": "Get a team" 50 | }, 51 | { 52 | "name": "getTeamsForUser", 53 | "method": "GET", 54 | "collection": true||false, 55 | "path": "/users/%s/teams", 56 | "params": [ 57 | { 58 | "name": "user_gid", 59 | "type": "string", 60 | "example_values": ["me"], 61 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 62 | "required": true 63 | }, 64 | { 65 | "name": "organization", 66 | "type": "string", 67 | "example_values": ["1331"], 68 | "comment": "The workspace or organization to filter teams on.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get teams for a user" 73 | }, 74 | { 75 | "name": "getTeamsForWorkspace", 76 | "method": "GET", 77 | "collection": true||false, 78 | "path": "/workspaces/%s/teams", 79 | "params": [ 80 | { 81 | "name": "workspace_gid", 82 | "type": "string", 83 | "example_values": ["12345"], 84 | "comment": "Globally unique identifier for the workspace or organization.", 85 | "required": true 86 | }, 87 | ], 88 | "comment": "Get teams in a workspace" 89 | }, 90 | { 91 | "name": "removeUserForTeam", 92 | "method": "POST", 93 | "collection": false, 94 | "path": "/teams/%s/removeUser", 95 | "params": [ 96 | { 97 | "name": "team_gid", 98 | "type": "string", 99 | "example_values": ["159874"], 100 | "comment": "Globally unique identifier for the team.", 101 | "required": true 102 | }, 103 | ], 104 | "comment": "Remove a user from a team" 105 | }, 106 | ] 107 | } 108 | export = resourceBase; 109 | -------------------------------------------------------------------------------- /src/resources/status_updates.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/status_updates_base"); 2 | resourceBase.comment = "A _status update_ is an update on the progress of a particular object, and is sent out to all followers when created. These updates include both text describing the update and a status_type intended to represent the overall state of the project. These include: on_track for projects that are on track, at_risk for projects at risk, off_track for projects that are behind, and on_hold for projects on hold.\n\nStatus updates can be created and deleted, but not modified.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "", 7 | "example_values": [], 8 | "comment": "", 9 | }, 10 | { 11 | "name": "resource_type", 12 | "type": "", 13 | "example_values": [], 14 | "comment": "", 15 | }, 16 | { 17 | "name": "html_text", 18 | "type": "", 19 | "example_values": [], 20 | "comment": "", 21 | }, 22 | { 23 | "name": "resource_subtype", 24 | "type": "", 25 | "example_values": [], 26 | "comment": "", 27 | }, 28 | { 29 | "name": "status_type", 30 | "type": "", 31 | "example_values": [], 32 | "comment": "", 33 | }, 34 | { 35 | "name": "text", 36 | "type": "", 37 | "example_values": [], 38 | "comment": "", 39 | }, 40 | { 41 | "name": "title", 42 | "type": "", 43 | "example_values": [], 44 | "comment": "", 45 | }, 46 | { 47 | "name": "author", 48 | "type": "", 49 | "example_values": [], 50 | "comment": "", 51 | }, 52 | { 53 | "name": "created_at", 54 | "type": "", 55 | "example_values": [], 56 | "comment": "", 57 | }, 58 | { 59 | "name": "created_by", 60 | "type": "", 61 | "example_values": [], 62 | "comment": "", 63 | }, 64 | { 65 | "name": "hearted", 66 | "type": "", 67 | "example_values": [], 68 | "comment": "", 69 | }, 70 | { 71 | "name": "hearts", 72 | "type": "", 73 | "example_values": [], 74 | "comment": "", 75 | }, 76 | { 77 | "name": "liked", 78 | "type": "", 79 | "example_values": [], 80 | "comment": "", 81 | }, 82 | { 83 | "name": "likes", 84 | "type": "", 85 | "example_values": [], 86 | "comment": "", 87 | }, 88 | { 89 | "name": "modified_at", 90 | "type": "", 91 | "example_values": [], 92 | "comment": "", 93 | }, 94 | { 95 | "name": "num_hearts", 96 | "type": "", 97 | "example_values": [], 98 | "comment": "", 99 | }, 100 | { 101 | "name": "num_likes", 102 | "type": "", 103 | "example_values": [], 104 | "comment": "", 105 | }, 106 | { 107 | "name": "parent", 108 | "type": "", 109 | "example_values": [], 110 | "comment": "", 111 | }, 112 | ]; 113 | export = resourceBase; 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asana-api-explorer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "A node.js client for the Asana API Explorer", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "clean": "rm -rf build coverage dist *.js *.d.ts", 9 | "precompile": "npm run clean", 10 | "compile": "find src test -name \"*.ts\" | xargs tsc --module commonjs --target es5 --noImplicitAny --outDir build", 11 | "coveralls": "cat ./coverage/lcov.info | coveralls", 12 | "lint": "find src test -name \"*.ts\" | sed 's/^/--file=/g' | xargs tslint", 13 | "setup": "rm -rf node_modules typings && npm install", 14 | "prebrowserify": "mkdir -p dist/", 15 | "browserify": "browserify build/src/index.js -t [ envify purge ] --standalone AsanaExplorer --outfile dist/asana-explorer.js", 16 | "postbrowserify": "cp -r public/* dist/.", 17 | "pretest": "npm run compile -- --sourceMap && find build -type f -name *.js -exec sed -i .bak -e '1s/^var __extends/\\/\\* nyc ignore next \\*\\/\rvar __extends/; 1s/^/require(\"source-map-support\").install();\r/' {} \\;", 18 | "test": " nyc mocha --timeout 100000 -r build/test/env.js 'build/test/components/*_spec.js'", 19 | "// TODO: Raise threshold for nyc coverage to 100.": null, 20 | "posttest": "npm run lint && nyc check-coverage --statements 90 --branches 90 --functions 90 --lines 90", 21 | "update": "git fetch && git merge origin master && npm run setup", 22 | "// TODO: Use a server and browser-sync (gulp?) for a better experience.": null, 23 | "web": "npm run compile && npm run browserify", 24 | "release": "CONSTANTS_TYPE=production npm run web && uglifyjs dist/asana-explorer.js -o dist/asana-explorer-min.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/Asana/api-explorer.git" 29 | }, 30 | "keywords": [ 31 | "asana", 32 | "typescript", 33 | "api" 34 | ], 35 | "author": "Asana", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Asana/api-explorer/issues" 39 | }, 40 | "homepage": "https://github.com/Asana/api-explorer", 41 | "devDependencies": { 42 | "@types/bluebird": "^3.5.27", 43 | "@types/chai": "^4.2.1", 44 | "@types/jsdom": "^12.2.4", 45 | "@types/marked": "^0.6.5", 46 | "@types/mocha": "^5.2.7", 47 | "@types/node": "^12.7.5", 48 | "@types/react": "^16.9.2", 49 | "@types/react-addons-update": "^0.14.20", 50 | "@types/sinon": "^9.0.0", 51 | "@types/react-dom": "^16.9.0", 52 | "@types/lodash": "^4.14.149", 53 | "browserify": "^16.5.0", 54 | "chai": "^2.3.0", 55 | "chai-as-promised": "^4.1.1", 56 | "coveralls": "^3.0.6", 57 | "envify": "^4.1.0", 58 | "mocha": "^6.2.0", 59 | "nyc": "^14.1.1", 60 | "sinon": "^9.0.2", 61 | "source-map-support": "^0.2.10", 62 | "tslint": "5.19.0", 63 | "uglify-js": "^2.4.23" 64 | }, 65 | "dependencies": { 66 | "@bity/oauth2-auth-code-pkce": "^2.9.0", 67 | "asana": "^0.17.3", 68 | "bluebird": "^2.11.0", 69 | "immutability-helper": "^3.0.2", 70 | "jsdom": "^16.2.2", 71 | "jsdom-global": "^3.0.2", 72 | "lodash": "^4.17.19", 73 | "marked": "^0.7.0", 74 | "mocha-jsdom": "^2.0.0", 75 | "react": "^16.13.1", 76 | "react-dom": "^16.13.1", 77 | "typescript": "^3.8.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/resources/gen/users_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "users", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "getFavoritesForUser", 12 | "method": "GET", 13 | "collection": false, 14 | "path": "/users/%s/favorites", 15 | "params": [ 16 | { 17 | "name": "user_gid", 18 | "type": "string", 19 | "example_values": ["me"], 20 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 21 | "required": true 22 | }, 23 | { 24 | "name": "workspace", 25 | "type": "string", 26 | "example_values": ["1234"], 27 | "comment": "The workspace in which to get favorites.", 28 | "required": true 29 | }, 30 | { 31 | "name": "resource_type", 32 | "type": "string", 33 | "example_values": [], 34 | "comment": "The resource type of favorites to be returned.", 35 | "required": true 36 | }, 37 | ], 38 | "comment": "Get a user's favorites" 39 | }, 40 | { 41 | "name": "getUser", 42 | "method": "GET", 43 | "collection": false, 44 | "path": "/users/%s", 45 | "params": [ 46 | { 47 | "name": "user_gid", 48 | "type": "string", 49 | "example_values": ["me"], 50 | "comment": "A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.", 51 | "required": true 52 | }, 53 | ], 54 | "comment": "Get a user" 55 | }, 56 | { 57 | "name": "getUsers", 58 | "method": "GET", 59 | "collection": true||false, 60 | "path": "/users", 61 | "params": [ 62 | { 63 | "name": "team", 64 | "type": "string", 65 | "example_values": ["15627"], 66 | "comment": "The team ID to filter users on.", 67 | "required": false 68 | }, 69 | { 70 | "name": "workspace", 71 | "type": "string", 72 | "example_values": ["1331"], 73 | "comment": "The workspace or organization ID to filter users on.", 74 | "required": false 75 | }, 76 | ], 77 | "comment": "Get multiple users" 78 | }, 79 | { 80 | "name": "getUsersForTeam", 81 | "method": "GET", 82 | "collection": false, 83 | "path": "/teams/%s/users", 84 | "params": [ 85 | { 86 | "name": "team_gid", 87 | "type": "string", 88 | "example_values": ["159874"], 89 | "comment": "Globally unique identifier for the team.", 90 | "required": true 91 | }, 92 | ], 93 | "comment": "Get users in a team" 94 | }, 95 | { 96 | "name": "getUsersForWorkspace", 97 | "method": "GET", 98 | "collection": false, 99 | "path": "/workspaces/%s/users", 100 | "params": [ 101 | { 102 | "name": "workspace_gid", 103 | "type": "string", 104 | "example_values": ["12345"], 105 | "comment": "Globally unique identifier for the workspace or organization.", 106 | "required": true 107 | }, 108 | ], 109 | "comment": "Get users in a workspace or organization" 110 | }, 111 | ] 112 | } 113 | export = resourceBase; 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **_NOTE:_** This repository has been archived and is no longer being maintained. Asana's API explorer is available at [https://developers.asana.com/docs/api-explorer](https://developers.asana.com/docs/api-explorer). To access up-to-date versions of Asana's OpenAPI Specifications, visit the [openapi](https://github.com/asana/openapi) repository. 2 | 3 | # api-explorer [![Build Status][travis-image]][travis-url] 4 | 5 | The Asana Api Explorer is a React component that was built to allow one to explore the Asana Api. It is built in typescript with React to allow easy integration with the [Asana Developers Site](https://developers.asana.com/docs), and uses the [node.js asana client](https://github.com/Asana/node-asana). To populate the API Explorer, we use metadata from the [asana-api-meta](https://github.com/Asana/asana-api-meta) repository, which contains structural information of the various resources and endpoints in the API. 6 | 7 | We avoid using the convenience methods provided by the node.js client (e.g. `client.users.me()`), in favor of explicit GET requests with the dispatcher (`dispatcher.get('/users/me', params, null)`). This allows us to consistently cover all endpoint and parameter permutations across the API instead of mixing the two styles throughout. 8 | 9 | # Setup 10 | ### Node 11 | If you do not have Node, run the following commands 12 | 13 | ``` 14 | curl https://raw.githubusercontent.com/creationix/nvm/v0.25.4/install.sh | bash 15 | nvm install 0.12 16 | nvm use 0.12 17 | nvm alias default 0.12 18 | ``` 19 | 20 | ## Api Explorer 21 | ``` 22 | git clone git@github.com:Asana/api-explorer.git 23 | cd api-explorer 24 | npm run update 25 | 26 | ``` 27 | 28 | # Developing 29 | 30 | ## Running locally 31 | Since the base of the API Explorer is a React component, we've created a sample HTML page to run the explorer locally. After making some changes, run: 32 | 33 | ``` 34 | # Clean setup to have the latest version of everything 35 | npm run setup 36 | # Compile the typescript and browserify the output 37 | npm run web 38 | # Start a server 39 | cd dist && python -m http.server 8338 40 | # Now, you can open a web browser to http://localhost:8338/ 41 | ``` 42 | 43 | ## Testing locally 44 | To run the test suite locally, just run `npm run test`. 45 | 46 | ## Updating resource metadata 47 | 48 | The asana-api-meta repository generates [resource files](https://github.com/Asana/api-explorer/tree/master/src/resources/gen) from [templates](https://github.com/Asana/api-explorer/tree/master/src/resources/templates), and these resources are used to populate the API Explorer. These generated resource files should not be changed directly. Instead, they should be updated within the `asana-api-meta` repository using `gulp deploy-api_explorer`. 49 | 50 | ## Application constants 51 | When generating a bundle, we use an environment variable to decide which [set of constants](https://github.com/Asana/api-explorer/blob/master/src/constants.ts) to use. This allows us to easily transition from testing the app locally, and running it in a production environment. In order to switch which constants you run, simply run `CONSTANTS_TYPE=localhost npm run web`. 52 | 53 | ## Generating minified release 54 | If you want to use this live, you can minify the javascript file with `npm run release`. By default, this uses the production set of constants. 55 | 56 | [travis-url]: http://travis-ci.org/Asana/api-explorer 57 | [travis-image]: https://travis-ci.org/Asana/api-explorer.svg?branch=master 58 | -------------------------------------------------------------------------------- /src/resources/custom_fields.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/custom_fields_base"); 2 | resourceBase.comment = "\nCustom Fields store the metadata that is used in order to add user-specified\ninformation to tasks in Asana. Be sure to reference the [Custom\nFields](/developers/documentation/getting-started/custom-fields) developer\ndocumentation for more information about how custom fields relate to various\nresources in Asana.\n\nUsers in Asana can [lock custom\nfields](/guide/help/premium/custom-fields#gl-lock-fields), which will make\nthem read-only when accessed by other users. Attempting to edit a locked\ncustom field will return HTTP error code `403 Forbidden`.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the custom field.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `custom_field`.\n", 16 | "example_values": [ 17 | "\"custom_field\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "custom_field", 22 | "comment": "A custom field resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "resource_subtype", 28 | "type": "Enum", 29 | "comment": "The type of custom field. Must be one of the given values.\n", 30 | "example_values": [ 31 | "\"text\"", 32 | "\"number\"", 33 | "\"enum\"" 34 | ], 35 | "values": [ 36 | { 37 | "name": "text", 38 | "comment": "A custom field of subtype `text`. Text custom fields store strings of text in Asana and have very few restrictions on content." 39 | }, 40 | { 41 | "name": "number", 42 | "comment": "A custom field of subtype `number`. Number custom fields must contain only valid numbers and are constrained to a predefined precision." 43 | }, 44 | { 45 | "name": "enum", 46 | "comment": "A custom field of subtype `enum`. Enum custom fields are constrained to one of a set of predefined values." 47 | } 48 | ] 49 | }, 50 | { 51 | "name": "name", 52 | "type": "String", 53 | "example_values": [ 54 | "'Priority'" 55 | ], 56 | "comment": "The name of the custom field.\n" 57 | }, 58 | { 59 | "name": "description", 60 | "type": "String", 61 | "example_values": [ 62 | "'Development team priority'" 63 | ], 64 | "comment": "[Opt In](/developers/documentation/getting-started/input-output-options). The description of the custom field.\n" 65 | }, 66 | { 67 | "name": "enum_options", 68 | "type": "String", 69 | "example_values": [ 70 | "[ { id: 789, gid: \"789\", name: 'Low', enabled: 'true', color: 'blue' }, ... ]" 71 | ], 72 | "comment": "Only relevant for custom fields of type 'enum'. This array specifies the possible values which an `enum` custom field can adopt. To modify the enum options, refer to [working with enum options](#enum-options).\n" 73 | }, 74 | { 75 | "name": "precision", 76 | "type": "Integer", 77 | "example_values": [ 78 | "2" 79 | ], 80 | "comment": "Only relevant for custom fields of type 'Number'. This field dictates the number of places after the decimal to round to, i.e. 0 is integer values, 1 rounds to the nearest tenth, and so on. Must be between 0 and 6, inclusive.\n" 81 | } 82 | ] 83 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/portfolios.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/portfolios_base"); 2 | resourceBase.comment = "A _portfolio_ gives a high-level overview of the status of multiple\ninitiatives in Asana. Portfolios provide a dashboard overview of the state\nof multiple items, including a progress report and the most recent\n[project status](/developers/api-reference/project_statuses) update.\n\nPortfolios have some restrictions on size. Each portfolio has a maximum of 250\nitems and, like projects, a maximum of 20 custom fields.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the portfolio.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `portfolio`.\n", 16 | "example_values": [ 17 | "\"portfolio\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "portfolio", 22 | "comment": "A portfolio resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "\"Product launch\"" 31 | ], 32 | "comment": "Name of the portfolio.\n" 33 | }, 34 | { 35 | "name": "owner", 36 | "type": "Struct", 37 | "example_values": [ 38 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }" 39 | ], 40 | "comment": "The current owner of the portfolio. Cannot be null.\n" 41 | }, 42 | { 43 | "name": "created_at", 44 | "type": "String", 45 | "example_values": [ 46 | "'2012-02-22T02:06:58.147Z'" 47 | ], 48 | "comment": "The time at which this portfolio was created.\n" 49 | }, 50 | { 51 | "name": "created_by", 52 | "type": "User", 53 | "example_values": [ 54 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }", 55 | "null" 56 | ], 57 | "comment": "The user that created the portfolio.\n" 58 | }, 59 | { 60 | "name": "custom_field_settings", 61 | "type": "Array", 62 | "example_values": [ 63 | "[ { id: 258147, gid: \"258147\", custom_field: {id: 1646, gid: \"1646\", name: \"Priority\", type: \"enum\"}, parent: {id: 36985, gid: \"36985\", name: \"Product launch\"} }, ...]" 64 | ], 65 | "comment": "Array of custom field settings applied to the portfolio.\n" 66 | }, 67 | { 68 | "name": "color", 69 | "type": "String", 70 | "example_values": [ 71 | "\"dark-red\"", 72 | "\"light-blue\"", 73 | "\"none\"" 74 | ], 75 | "comment": "Must be either `none` or one of: `dark-pink`, `dark-green`, `dark-blue`,\n`dark-red`, `dark-teal`, `dark-brown`, `dark-orange`, `dark-purple`,\n`dark-warm-gray`, `light-pink`, `light-green`, `light-blue`, `light-red`,\n`light-teal`, `light-yellow`, `light-orange`, `light-purple`,\n`light-warm-gray`.\n" 76 | }, 77 | { 78 | "name": "workspace", 79 | "type": "Workspace", 80 | "example_values": [ 81 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 82 | ], 83 | "comment": "The workspace or organization that the portfolio belongs to.\n" 84 | }, 85 | { 86 | "name": "members", 87 | "type": "Array", 88 | "example_values": [ 89 | "[ { id: 1123, gid: \"1123\", resource_type: \"user\", name: 'Mittens' }, ... ]" 90 | ], 91 | "comment": "Members of the portfolio.\n" 92 | } 93 | ]; 94 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/gen/sections_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "sections", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "addTaskForSection", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/sections/%s/addTask", 15 | "params": [ 16 | { 17 | "name": "section_gid", 18 | "type": "string", 19 | "example_values": ["321654"], 20 | "comment": "The globally unique identifier for the section.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Add task to section" 25 | }, 26 | { 27 | "name": "createSectionForProject", 28 | "method": "POST", 29 | "collection": false, 30 | "path": "/projects/%s/sections", 31 | "params": [ 32 | { 33 | "name": "project_gid", 34 | "type": "string", 35 | "example_values": ["1331"], 36 | "comment": "Globally unique identifier for the project.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Create a section in a project" 41 | }, 42 | { 43 | "name": "deleteSection", 44 | "method": "DELETE", 45 | "collection": false, 46 | "path": "/sections/%s", 47 | "params": [ 48 | { 49 | "name": "section_gid", 50 | "type": "string", 51 | "example_values": ["321654"], 52 | "comment": "The globally unique identifier for the section.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Delete a section" 57 | }, 58 | { 59 | "name": "getSection", 60 | "method": "GET", 61 | "collection": false, 62 | "path": "/sections/%s", 63 | "params": [ 64 | { 65 | "name": "section_gid", 66 | "type": "string", 67 | "example_values": ["321654"], 68 | "comment": "The globally unique identifier for the section.", 69 | "required": true 70 | }, 71 | ], 72 | "comment": "Get a section" 73 | }, 74 | { 75 | "name": "getSectionsForProject", 76 | "method": "GET", 77 | "collection": true||false, 78 | "path": "/projects/%s/sections", 79 | "params": [ 80 | { 81 | "name": "project_gid", 82 | "type": "string", 83 | "example_values": ["1331"], 84 | "comment": "Globally unique identifier for the project.", 85 | "required": true 86 | }, 87 | ], 88 | "comment": "Get sections in a project" 89 | }, 90 | { 91 | "name": "insertSectionForProject", 92 | "method": "POST", 93 | "collection": false, 94 | "path": "/projects/%s/sections/insert", 95 | "params": [ 96 | { 97 | "name": "project_gid", 98 | "type": "string", 99 | "example_values": ["1331"], 100 | "comment": "Globally unique identifier for the project.", 101 | "required": true 102 | }, 103 | ], 104 | "comment": "Move or Insert sections" 105 | }, 106 | { 107 | "name": "updateSection", 108 | "method": "PUT", 109 | "collection": false, 110 | "path": "/sections/%s", 111 | "params": [ 112 | { 113 | "name": "section_gid", 114 | "type": "string", 115 | "example_values": ["321654"], 116 | "comment": "The globally unique identifier for the section.", 117 | "required": true 118 | }, 119 | ], 120 | "comment": "Update a section" 121 | }, 122 | ] 123 | } 124 | export = resourceBase; 125 | -------------------------------------------------------------------------------- /src/resources/gen/tags_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "tags", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createTag", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/tags", 15 | "params": [ 16 | ], 17 | "comment": "Create a tag" 18 | }, 19 | { 20 | "name": "createTagForWorkspace", 21 | "method": "POST", 22 | "collection": false, 23 | "path": "/workspaces/%s/tags", 24 | "params": [ 25 | { 26 | "name": "workspace_gid", 27 | "type": "string", 28 | "example_values": ["12345"], 29 | "comment": "Globally unique identifier for the workspace or organization.", 30 | "required": true 31 | }, 32 | ], 33 | "comment": "Create a tag in a workspace" 34 | }, 35 | { 36 | "name": "deleteTag", 37 | "method": "DELETE", 38 | "collection": true||false, 39 | "path": "/tags/%s", 40 | "params": [ 41 | { 42 | "name": "tag_gid", 43 | "type": "string", 44 | "example_values": ["11235"], 45 | "comment": "Globally unique identifier for the tag.", 46 | "required": true 47 | }, 48 | ], 49 | "comment": "Delete a tag" 50 | }, 51 | { 52 | "name": "getTag", 53 | "method": "GET", 54 | "collection": true||false, 55 | "path": "/tags/%s", 56 | "params": [ 57 | { 58 | "name": "tag_gid", 59 | "type": "string", 60 | "example_values": ["11235"], 61 | "comment": "Globally unique identifier for the tag.", 62 | "required": true 63 | }, 64 | ], 65 | "comment": "Get a tag" 66 | }, 67 | { 68 | "name": "getTags", 69 | "method": "GET", 70 | "collection": true||false, 71 | "path": "/tags", 72 | "params": [ 73 | { 74 | "name": "workspace", 75 | "type": "string", 76 | "example_values": ["1331"], 77 | "comment": "The workspace to filter tags on.", 78 | "required": false 79 | }, 80 | ], 81 | "comment": "Get multiple tags" 82 | }, 83 | { 84 | "name": "getTagsForTask", 85 | "method": "GET", 86 | "collection": true||false, 87 | "path": "/tasks/%s/tags", 88 | "params": [ 89 | { 90 | "name": "task_gid", 91 | "type": "string", 92 | "example_values": ["321654"], 93 | "comment": "The task to operate on.", 94 | "required": true 95 | }, 96 | ], 97 | "comment": "Get a task's tags" 98 | }, 99 | { 100 | "name": "getTagsForWorkspace", 101 | "method": "GET", 102 | "collection": true||false, 103 | "path": "/workspaces/%s/tags", 104 | "params": [ 105 | { 106 | "name": "workspace_gid", 107 | "type": "string", 108 | "example_values": ["12345"], 109 | "comment": "Globally unique identifier for the workspace or organization.", 110 | "required": true 111 | }, 112 | ], 113 | "comment": "Get tags in a workspace" 114 | }, 115 | { 116 | "name": "updateTag", 117 | "method": "PUT", 118 | "collection": true||false, 119 | "path": "/tags/%s", 120 | "params": [ 121 | { 122 | "name": "tag_gid", 123 | "type": "string", 124 | "example_values": ["11235"], 125 | "comment": "Globally unique identifier for the tag.", 126 | "required": true 127 | }, 128 | ], 129 | "comment": "Update a tag" 130 | }, 131 | ] 132 | } 133 | export = resourceBase; 134 | -------------------------------------------------------------------------------- /src/resources/helpers.ts: -------------------------------------------------------------------------------- 1 | // 2 | import util = require("util"); 3 | import _ = require("lodash"); 4 | 5 | import Resources = require("../resources"); 6 | 7 | /** 8 | * Returns the names of all valid resources. 9 | * 10 | * @returns {string[]} 11 | */ 12 | export function names(): string[] { 13 | return Object.keys(Resources); 14 | } 15 | 16 | /** 17 | * Returns the resource information for a given resource name. 18 | * 19 | * @param name 20 | * @returns {any} 21 | */ 22 | export function resourceFromResourceName(name: string): Resource { 23 | return (Resources)[name]; 24 | } 25 | 26 | /** 27 | * Returns the resource name (key of Resources) for a given resource. 28 | * 29 | * @param resource 30 | * @returns {any} 31 | */ 32 | export function resourceNameFromResource(resource: Resource): string { 33 | return Object.keys(Resources).filter( 34 | key => { return (Resources)[key] === resource; } 35 | )[0]; 36 | } 37 | 38 | /** 39 | * Return the action for a given resource and path string. 40 | * 41 | */ 42 | export function actionFromResourcePath(resource: Resource, path: string): Action { 43 | return resource.actions.filter( 44 | action => { return action.method === "GET" && path === action.path; } 45 | )[0]; 46 | } 47 | 48 | /** 49 | * Returns the action by its resource and name. 50 | */ 51 | export function actionFromResourceAndName(resource: Resource, action_name: string): Action { 52 | return resource.actions.filter( 53 | action => { return action_name === action.name; } 54 | )[0]; 55 | } 56 | 57 | /** 58 | * Returns the action to use as a default for a given resource. 59 | * 60 | * We prefer GET actions, so if one exists on the resource use that. 61 | * Otherwise, we use the first action in the list. 62 | */ 63 | export function defaultActionFromResource(resource: Resource): Action { 64 | var getActions = resource.actions.filter( 65 | action => { return action.method === "GET"; } 66 | ); 67 | 68 | return getActions.length > 0 ? getActions[0] : resource.actions[0]; 69 | } 70 | 71 | /** 72 | * Given an action, return the path after replacing a required param value. 73 | * If no param value is given, and one is needed, then use a placeholder value. 74 | * 75 | * Assumes we have at-most one required parameter to put in the URL. 76 | * 77 | * @param action 78 | * @param param_value? 79 | * @returns {string} 80 | */ 81 | export function pathForAction(action: Action, paramValue?: string): string { 82 | // If there's a placeholder, then replace it with its required param. 83 | if (pathForActionContainsRequiredParam(action)) { 84 | var requiredParam = _.find(action.params, "required"); 85 | if (requiredParam === undefined) { 86 | throw new Error("Placeholder in path but there's no required param."); 87 | } 88 | 89 | // If a param_value is given, use it. Otherwise, use a placeholder. 90 | if (paramValue !== undefined) { 91 | return util.format(action.path, paramValue); 92 | } else { 93 | // Use the parameter name as a placeholder in the URL. 94 | return util.format(action.path, ":" + requiredParam.name); 95 | } 96 | } 97 | 98 | // Otherwise, we just return the path. 99 | return action.path; 100 | } 101 | 102 | /** 103 | * Given an action, checks the path to check if there's a placeholder value for 104 | * a required param. 105 | * 106 | * @param action 107 | * @returns {boolean} 108 | */ 109 | export function pathForActionContainsRequiredParam(action: Action): boolean { 110 | return action.path.match(/%/g) !== null; 111 | } 112 | -------------------------------------------------------------------------------- /test/components/paginate_entry_spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require("chai"); 2 | import React = require("react"); 3 | import sinon = require("sinon"); 4 | import _ = require("lodash"); 5 | 6 | import PaginateEntry = require("../../src/components/paginate_entry"); 7 | import * as ReactTestUtils from "react-dom/test-utils"; 8 | import {SinonFakeServer, SinonStub} from "sinon"; 9 | 10 | const assert = chai.assert; 11 | const r = React.createElement; 12 | const testUtils = ReactTestUtils; 13 | 14 | describe("PaginateEntryComponent", () => { 15 | let sand: SinonFakeServer; 16 | 17 | let onPaginateChangeStub: SinonStub; 18 | 19 | let root: PaginateEntry; 20 | let limitInput: Element; 21 | let offsetInput: Element; 22 | 23 | beforeEach(() => { 24 | sand = sinon.fakeServer.create(); 25 | 26 | onPaginateChangeStub = sinon.stub().returns(_.noop); 27 | }); 28 | 29 | afterEach(() => { 30 | sand.restore(); 31 | }); 32 | 33 | describe("when can paginate", () => { 34 | beforeEach(() => { 35 | root = testUtils.renderIntoDocument( 36 | PaginateEntry.create({ 37 | canPaginate: true, 38 | onPaginateChange: onPaginateChangeStub, 39 | paginateParams: { 40 | limit: 5, 41 | offset: "initial value" 42 | }, 43 | text: r("h3", {}, "this is a test") 44 | }) 45 | ); 46 | limitInput = testUtils.findRenderedDOMComponentWithClass( 47 | root, 48 | "paginate-entry-limit" 49 | ); 50 | offsetInput = testUtils.findRenderedDOMComponentWithClass( 51 | root, 52 | "paginate-entry-offset" 53 | ); 54 | }); 55 | 56 | it("should set initial values of input fields from state", () => { 57 | assert.equal((limitInput).value, "5"); 58 | assert.equal((offsetInput).value, "initial value"); 59 | }); 60 | 61 | it("should trigger onChange property for limit/offset fields", () => { 62 | limitInput.dispatchEvent(new Event("change", {bubbles: true})); 63 | sinon.assert.calledWith(onPaginateChangeStub, "limit"); 64 | assert(true); 65 | 66 | offsetInput.dispatchEvent(new Event("change", {bubbles: true})); 67 | sinon.assert.calledWith(onPaginateChangeStub, "offset"); 68 | }); 69 | }); 70 | 71 | describe("when cannot paginate", () => { 72 | beforeEach(() => { 73 | root = testUtils.renderIntoDocument( 74 | PaginateEntry.create({ 75 | canPaginate: false, 76 | onPaginateChange: onPaginateChangeStub, 77 | paginateParams: { 78 | limit: 5, 79 | offset: "initial value" 80 | }, 81 | text: r("h3", {}, "this is a test") 82 | }) 83 | ); 84 | }); 85 | 86 | it("should hide pagination input fields", () => { 87 | const limitInputs = testUtils.scryRenderedDOMComponentsWithClass( 88 | root, 89 | "paginate-entry-limit" 90 | ); 91 | assert.lengthOf(limitInputs, 0); 92 | 93 | const offsetInputs = testUtils.scryRenderedDOMComponentsWithClass( 94 | root, 95 | "paginate-entry-offset" 96 | ); 97 | assert.lengthOf(offsetInputs, 0); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/components/route_entry_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import chai = require("chai"); 3 | import sinon = require("sinon"); 4 | import _ = require("lodash"); 5 | 6 | import Resources = require("../../src/resources"); 7 | import RouteEntry = require("../../src/components/route_entry"); 8 | import {SinonFakeServer, SinonStub} from "sinon"; 9 | import * as ReactTestUtils from "react-dom/test-utils"; 10 | import * as ReactDOM from "react-dom"; 11 | 12 | var assert = chai.assert; 13 | var testUtils = ReactTestUtils; 14 | 15 | describe("RouteEntryComponent", () => { 16 | var sand: SinonFakeServer; 17 | 18 | var initialAction: Action; 19 | var initialResource: Resource; 20 | 21 | var onActionChangeStub: SinonStub; 22 | 23 | var root: RouteEntry; 24 | var selectRoute: Element; 25 | 26 | beforeEach(() => { 27 | sand = sinon.fakeServer.create(); 28 | 29 | initialResource = Resources.Projects; 30 | let i = 0; 31 | initialAction = initialResource.actions[i]; 32 | while(initialAction.method !== "GET") { 33 | i += 1; 34 | initialAction = initialResource.actions[i] 35 | } 36 | 37 | onActionChangeStub = sinon.stub(); 38 | 39 | root = testUtils.renderIntoDocument( 40 | RouteEntry.create({ 41 | action: initialAction, 42 | currentRequestUrl: "URL_HERE", 43 | onActionChange: onActionChangeStub, 44 | resource: initialResource 45 | }) 46 | ); 47 | selectRoute = testUtils.findRenderedDOMComponentWithClass( 48 | root, 49 | "select-route" 50 | ); 51 | }); 52 | 53 | afterEach(() => { 54 | sand.restore(); 55 | }); 56 | 57 | it("should select the current route", () => { 58 | 59 | let selectRouteNode = (ReactDOM.findDOMNode(selectRoute)); 60 | if (selectRouteNode === null) { 61 | assert(false); 62 | return; 63 | } 64 | assert.include( 65 | selectRouteNode.value || "", 66 | initialAction.name 67 | ); 68 | }); 69 | 70 | it("should display the current route url", () => { 71 | let rootNode = ReactDOM.findDOMNode(root); 72 | if (rootNode === null) { 73 | assert(false); 74 | return; 75 | } 76 | assert.include( 77 | rootNode.textContent || "", 78 | initialAction.method + " " + "URL_HERE" 79 | ); 80 | }); 81 | 82 | it("should contain dropdown with other routes", () => { 83 | let selectRouteNode = ReactDOM.findDOMNode(selectRoute); 84 | 85 | if (selectRouteNode === null) { 86 | assert(false); 87 | return; 88 | } 89 | var children = selectRouteNode.childNodes; 90 | 91 | const getActions = initialResource.actions.filter((action) => {return action.method === "GET"}) 92 | 93 | assert.equal(children.length, getActions.length); 94 | getActions.forEach((action, idx) => { 95 | var childItem = (children.item(idx)); 96 | 97 | // Replace any placeholders with their required param name. 98 | // NB: We use replace rather than util.format in order to ignore 99 | // Paths that do not contain a placeholder. 100 | var requiredParam = _.find(action.params, "required"); 101 | var actionPath = requiredParam !== undefined ? 102 | action.path.replace("%s", ":" + requiredParam.name) : action.path; 103 | 104 | assert.equal(childItem.value, action.name); 105 | assert.equal(childItem.text, action.method + " " + actionPath); 106 | }); 107 | }); 108 | 109 | it("should trigger onRouteChange property on route change", () => { 110 | testUtils.Simulate.change(selectRoute); 111 | sinon.assert.called(onActionChangeStub); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/resources/gen/custom_fields_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "custom_fields", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "createCustomField", 12 | "method": "POST", 13 | "collection": true||false, 14 | "path": "/custom_fields", 15 | "params": [ 16 | ], 17 | "comment": "Create a custom field" 18 | }, 19 | { 20 | "name": "createEnumOptionForCustomField", 21 | "method": "POST", 22 | "collection": true||false, 23 | "path": "/custom_fields/%s/enum_options", 24 | "params": [ 25 | { 26 | "name": "custom_field_gid", 27 | "type": "string", 28 | "example_values": ["12345"], 29 | "comment": "Globally unique identifier for the custom field.", 30 | "required": true 31 | }, 32 | ], 33 | "comment": "Create an enum option" 34 | }, 35 | { 36 | "name": "deleteCustomField", 37 | "method": "DELETE", 38 | "collection": false, 39 | "path": "/custom_fields/%s", 40 | "params": [ 41 | { 42 | "name": "custom_field_gid", 43 | "type": "string", 44 | "example_values": ["12345"], 45 | "comment": "Globally unique identifier for the custom field.", 46 | "required": true 47 | }, 48 | ], 49 | "comment": "Delete a custom field" 50 | }, 51 | { 52 | "name": "getCustomField", 53 | "method": "GET", 54 | "collection": false, 55 | "path": "/custom_fields/%s", 56 | "params": [ 57 | { 58 | "name": "custom_field_gid", 59 | "type": "string", 60 | "example_values": ["12345"], 61 | "comment": "Globally unique identifier for the custom field.", 62 | "required": true 63 | }, 64 | ], 65 | "comment": "Get a custom field" 66 | }, 67 | { 68 | "name": "getCustomFieldsForWorkspace", 69 | "method": "GET", 70 | "collection": true||false, 71 | "path": "/workspaces/%s/custom_fields", 72 | "params": [ 73 | { 74 | "name": "workspace_gid", 75 | "type": "string", 76 | "example_values": ["12345"], 77 | "comment": "Globally unique identifier for the workspace or organization.", 78 | "required": true 79 | }, 80 | ], 81 | "comment": "Get a workspace's custom fields" 82 | }, 83 | { 84 | "name": "insertEnumOptionForCustomField", 85 | "method": "POST", 86 | "collection": false, 87 | "path": "/custom_fields/%s/enum_options/insert", 88 | "params": [ 89 | { 90 | "name": "custom_field_gid", 91 | "type": "string", 92 | "example_values": ["12345"], 93 | "comment": "Globally unique identifier for the custom field.", 94 | "required": true 95 | }, 96 | ], 97 | "comment": "Reorder a custom field's enum" 98 | }, 99 | { 100 | "name": "updateCustomField", 101 | "method": "PUT", 102 | "collection": false, 103 | "path": "/custom_fields/%s", 104 | "params": [ 105 | { 106 | "name": "custom_field_gid", 107 | "type": "string", 108 | "example_values": ["12345"], 109 | "comment": "Globally unique identifier for the custom field.", 110 | "required": true 111 | }, 112 | ], 113 | "comment": "Update a custom field" 114 | }, 115 | { 116 | "name": "updateEnumOption", 117 | "method": "PUT", 118 | "collection": false, 119 | "path": "/enum_options/%s", 120 | "params": [ 121 | { 122 | "name": "enum_option_gid", 123 | "type": "string", 124 | "example_values": ["124578"], 125 | "comment": "Globally unique identifier for the enum option.", 126 | "required": true 127 | }, 128 | ], 129 | "comment": "Update an enum option" 130 | }, 131 | ] 132 | } 133 | export = resourceBase; 134 | -------------------------------------------------------------------------------- /src/components/extra_parameter_entry.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import React = require("react"); 3 | import _ = require("lodash"); 4 | import update from "immutability-helper"; 5 | 6 | var r = React.createElement; 7 | 8 | /** 9 | * The extra parameter input area 10 | */ 11 | class ExtraParameterEntry extends React.Component { 12 | static create = React.createFactory(ExtraParameterEntry); 13 | 14 | constructor(props: ExtraParameterEntry.Props, context: any) { 15 | super(props, context); 16 | this.state = { 17 | extraParams: [] 18 | }; 19 | } 20 | 21 | render() { 22 | return r("div", { 23 | className: "parameter-entry", 24 | children: [ 25 | this.props.text, 26 | this.state.extraParams.map(this.renderExtraParameterInput), 27 | this._renderAddNewExtraParameterLink() 28 | ] 29 | } 30 | ); 31 | } 32 | 33 | private setStateAndPropagate = (newState: ExtraParameterEntry.State) => { 34 | this.setState(newState); 35 | 36 | // Pass new list of extra params to prop function, so we can propagate 37 | // Changes in the parent component. 38 | this.props.syncExtraParameters(newState.extraParams); 39 | } 40 | 41 | private _renderAddNewExtraParameterLink = () => { 42 | return r("a", { 43 | className: "add-extra-param", 44 | href: "#", 45 | onClick: (e: any) => { 46 | e.preventDefault(); 47 | this.setState(update(this.state, { 48 | extraParams: { 49 | $push: [{ 50 | key: "", 51 | value: "" 52 | }] 53 | } 54 | })); 55 | } 56 | }, "Add new parameter!"); 57 | } 58 | 59 | private renderExtraParameterInput = (extraParam: ExtraParameterEntry.ExtraParameter, idx: number) => { 60 | var idPrefix = "extra_param_" + idx; 61 | 62 | return r("p", { 63 | key: idPrefix, 64 | className: "extra-param", 65 | children: [ 66 | r("input", { 67 | placeholder: "Key", 68 | type: "text", 69 | id: idPrefix + "_key", 70 | className: "parameter-input extra-param-key", 71 | value: this.state.extraParams[idx].key, 72 | onChange: (event: React.FormEvent) => { 73 | this.setStateAndPropagate(update(this.state, { 74 | extraParams: _.zipObject( 75 | [idx.toString()], 76 | [{ key: { $set: (event.target).value } }] 77 | ) 78 | })); 79 | } 80 | }), 81 | ":", 82 | r("input", { 83 | placeholder: "Value", 84 | type: "text", 85 | id: idPrefix + "_value", 86 | className: "parameter-input extra-param-value", 87 | value: this.state.extraParams[idx].value, 88 | onChange: (event: React.FormEvent) => { 89 | this.setStateAndPropagate(update(this.state, { 90 | extraParams: _.zipObject( 91 | [idx.toString()], 92 | [{ value: { $set: (event.target).value } }] 93 | ) 94 | })); 95 | } 96 | }), 97 | r("span", { 98 | className: "delete-extra-param", 99 | onClick: () => { 100 | this.setStateAndPropagate(update(this.state, { 101 | extraParams: { 102 | $splice: [[ idx, 1 ]] 103 | } 104 | })); 105 | } 106 | }, "\u2715") 107 | ] 108 | }); 109 | } 110 | } 111 | 112 | module ExtraParameterEntry { 113 | export interface ExtraParameter { 114 | key: string; 115 | value: string; 116 | } 117 | 118 | export interface Props { 119 | text: React.DOMElement; 120 | syncExtraParameters: (parameters: ExtraParameter[]) => void; 121 | } 122 | 123 | export interface State { 124 | extraParams: ExtraParameter[]; 125 | } 126 | } 127 | 128 | export = ExtraParameterEntry; 129 | -------------------------------------------------------------------------------- /src/resources/webhooks.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/webhooks_base"); 2 | resourceBase.comment = "Webhooks allow an application to be notified of changes. This is in addition\nto the ability to fetch those changes directly as\n[Events](/developers/api-reference/events) - in fact, Webhooks are just a way\nto receive Events via HTTP POST at the time they occur instead of polling for\nthem. For services accessible via HTTP this is often vastly more convenient,\nand if events are not too frequent can be significantly more efficient.\n\nIn both cases, however, changes are represented as Event objects - refer to\nthe [Events documentation](/developers/api-reference/events) for more\ninformation on what data these events contain.\n\n**NOTE:** While Webhooks send arrays of Event objects to their target, the\nEvent objects themselves contain *only IDs*, rather than the actual resource\nthey are referencing. So while a normal event you receive via GET /events\nwould look like this:\n\n {\\\n \"resource\": {\\\n \"id\": 1337,\\\n \"resource_type\": \"task\",\\\n \"name\": \"My Task\"\\\n },\\\n \"parent\": null,\\\n \"created_at\": \"2013-08-21T18:20:37.972Z\",\\\n \"user\": {\\\n \"id\": 1123,\\\n \"resource_type\": \"user\",\\\n \"name\": \"Tom Bizarro\"\\\n },\\\n \"action\": \"changed\",\\\n \"type\": \"task\"\\\n }\n\nIn a Webhook payload you would instead receive this:\n\n {\\\n \"resource\": 1337,\\\n \"parent\": null,\\\n \"created_at\": \"2013-08-21T18:20:37.972Z\",\\\n \"user\": 1123,\\\n \"action\": \"changed\",\\\n \"type\": \"task\"\\\n }\n\nWebhooks themselves contain only the information necessary to deliver the\nevents to the desired target as they are generated.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the webhook.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `webhook`.\n", 16 | "example_values": [ 17 | "\"webhook\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "webhook", 22 | "comment": "A webhook resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "resource", 28 | "type": "Task", 29 | "example_values": [ 30 | "{ id: 1234, gid: \"1234\", name: 'Bug task' }" 31 | ], 32 | "comment": "The resource the webhook is subscribed to.\n" 33 | }, 34 | { 35 | "name": "target", 36 | "type": "String", 37 | "example_values": [ 38 | "'https://example.com/receive-webhook/7654'" 39 | ], 40 | "comment": "The URL to receive the HTTP POST.\n" 41 | }, 42 | { 43 | "name": "active", 44 | "type": "Boolean", 45 | "example_values": [ 46 | "false" 47 | ], 48 | "comment": "If true, the webhook will send events - if false it is considered\ninactive and will not generate events.\n" 49 | }, 50 | { 51 | "name": "created_at", 52 | "type": "String", 53 | "example_values": [ 54 | "'2012-02-22T02:06:58.147Z'" 55 | ], 56 | "comment": "The timestamp when the webhook was created.\n" 57 | }, 58 | { 59 | "name": "last_success_at", 60 | "type": "String", 61 | "example_values": [ 62 | "'2012-02-22T02:06:58.147Z'" 63 | ], 64 | "comment": "The timestamp when the webhook last successfully sent an event to the\ntarget.\n" 65 | }, 66 | { 67 | "name": "last_failure_at", 68 | "type": "String", 69 | "example_values": [ 70 | "'2012-02-22T02:06:58.147Z'" 71 | ], 72 | "comment": "The timestamp when the webhook last received an error when sending an\nevent to the target.\n" 73 | }, 74 | { 75 | "name": "last_failure_content", 76 | "type": "String", 77 | "example_values": [ 78 | "'500 Server Error\\n\\nCould not complete the request'" 79 | ], 80 | "comment": "The contents of the last error response sent to the webhook when\nattempting to deliver events to the target.\n" 81 | } 82 | ] 83 | export = resourceBase; -------------------------------------------------------------------------------- /src/resources/stories.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/stories_base"); 2 | resourceBase.comment = "A _story_ represents an activity associated with an object in the Asana\nsystem. Stories are generated by the system whenever users take actions such\nas creating or assigning tasks, or moving tasks between projects. _Comments_\nare also a form of user-generated story.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the story.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `story`.\n", 16 | "example_values": [ 17 | "\"story\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "story", 22 | "comment": "A story resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "resource_subtype", 28 | "type": "Enum", 29 | "comment": "The type of story. This provides fine-grained information about what triggered the story's creation. There are many story subtypes, so inspect the data returned from Asana's API to find the value for your use case.\n", 30 | "example_values": [ 31 | "\"comment_added\"", 32 | "\"description_changed\"", 33 | "\"liked\"", 34 | "..." 35 | ] 36 | }, 37 | { 38 | "name": "created_at", 39 | "type": "String", 40 | "example_values": [ 41 | "'2012-02-22T02:06:58.147Z'" 42 | ], 43 | "comment": "The time at which this story was created.\n" 44 | }, 45 | { 46 | "name": "created_by", 47 | "type": "User", 48 | "example_values": [ 49 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }", 50 | "null" 51 | ], 52 | "comment": "The user who created the story.\n" 53 | }, 54 | { 55 | "name": "liked", 56 | "type": "Boolean", 57 | "example_values": [ 58 | "false" 59 | ], 60 | "comment": "True if the story is liked by the authorized user, false if not.\n", 61 | }, 62 | { 63 | "name": "likes", 64 | "type": "Array", 65 | "example_values": [ 66 | "[ { id: 1123, gid: \"1123\", resource_type: \"user\", name: 'Mittens' }, ... ]" 67 | ], 68 | "comment": "Array of users who have liked this story.\n", 69 | }, 70 | { 71 | "name": "num_likes", 72 | "type": "Integer", 73 | "example_values": [ 74 | "5" 75 | ], 76 | "comment": "The number of users who have liked this story.\n", 77 | }, 78 | { 79 | "name": "text", 80 | "type": "String", 81 | "example_values": [ 82 | "'marked today'" 83 | ], 84 | "comment": "Human-readable text for the story or comment. This will not include the\nname of the creator.\n", 85 | }, 86 | { 87 | "name": "html_text", 88 | "type": "String", 89 | "example_values": [ 90 | "'<body>Get whatever <a href='https://app.asana.com/0/1123/list'>Sashimi</a> has.</body>'" 91 | ], 92 | "comment": "[Opt In](https://asana.com/developers/documentation/getting-started/input-output-options). HTML formatted text for a comment.\n" 93 | }, 94 | { 95 | "name": "target", 96 | "type": "Task", 97 | "example_values": [ 98 | "{ id: 1234, gid: \"1234\", name: 'Bug task' }" 99 | ], 100 | "comment": "The object this story is associated with. Currently may only be a task.\n" 101 | }, 102 | { 103 | "name": "is_pinned", 104 | "type": "Boolean", 105 | "example_values": [ 106 | "false" 107 | ], 108 | "comment": "Whether the story is pinned on the target.\n", 109 | }, 110 | { 111 | "name": "is_edited", 112 | "type": "Boolean", 113 | "example_values": [ 114 | "false" 115 | ], 116 | "comment": "Whether the text of the story has been edited after creation.\n", 117 | }, 118 | { 119 | "name": "source", 120 | "type": "Enum", 121 | "example_values": [ 122 | "'web'" 123 | ], 124 | "values": [ 125 | { 126 | "name": "web", 127 | "comment": "Via the Asana web app." 128 | }, 129 | { 130 | "name": "email", 131 | "comment": "Via email." 132 | }, 133 | { 134 | "name": "mobile", 135 | "comment": "Via the Asana mobile app." 136 | }, 137 | { 138 | "name": "api", 139 | "comment": "Via the Asana API." 140 | }, 141 | { 142 | "name": "unknown", 143 | "comment": "Unknown or unrecorded." 144 | } 145 | ], 146 | "comment": "The component of the Asana product the user used to create the story.\n" 147 | }, 148 | ] 149 | 150 | export = resourceBase; -------------------------------------------------------------------------------- /test/credentials_spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require("chai"); 2 | import sinon = require("sinon"); 3 | 4 | import Credentials = require("../src/credentials"); 5 | import constants = require("../src/constants"); 6 | import helpers = require("./helpers"); 7 | import {SinonFakeTimers, SinonFakeServer} from "sinon"; 8 | 9 | var assert = chai.assert; 10 | 11 | var MINUTE_IN_MS = 1000 * 60; 12 | 13 | describe("Credentials", () => { 14 | var sand: SinonFakeServer; 15 | var clock: SinonFakeTimers; 16 | 17 | beforeEach(() => { 18 | sand = sinon.fakeServer.create(); 19 | 20 | clock = sinon.useFakeTimers(); 21 | }); 22 | 23 | afterEach(() => { 24 | clock.restore(); 25 | 26 | sand.restore(); 27 | }); 28 | 29 | describe("#authStateFromClient", () => { 30 | it("should return AuthState.Unauthorized with null credentials", () => { 31 | var client = helpers.createOauthClient(undefined); 32 | assert.equal( 33 | Credentials.authStateFromClient(client), 34 | Credentials.AuthState.Unauthorized); 35 | }); 36 | 37 | it("should return AuthState.Expired with expired credentials", () => { 38 | var client = helpers.createOauthClient( 39 | helpers.createCredentials(Date.now()) 40 | ); 41 | clock.tick(500); 42 | assert.equal( 43 | Credentials.authStateFromClient(client), 44 | Credentials.AuthState.Expired); 45 | }); 46 | 47 | it("should return AuthState.Expired with soon-to-expire credentials", () => { 48 | var client = helpers.createOauthClient( 49 | helpers.createCredentials(Date.now() + 2 * MINUTE_IN_MS) 50 | ); 51 | 52 | assert.equal( 53 | Credentials.authStateFromClient(client), 54 | Credentials.AuthState.Expired); 55 | }); 56 | 57 | it("should return AuthState.Authorized with long-to-expire credentials", () => { 58 | var client = helpers.createOauthClient( 59 | helpers.createCredentials(Date.now() + 20 * MINUTE_IN_MS) 60 | ); 61 | 62 | assert.equal( 63 | Credentials.authStateFromClient(client), 64 | Credentials.AuthState.Authorized); 65 | }); 66 | }); 67 | 68 | describe("localStorage", () => { 69 | var oldStorage: Storage; 70 | var localStorage: Storage; 71 | 72 | beforeEach(() => { 73 | // We need to mock localStorage, which is a browser-only api. 74 | // So we create a fake storage, and then mock methods within it. 75 | oldStorage = Credentials.localStorage; 76 | localStorage = helpers.createFakeStorage(); 77 | Credentials.localStorage = localStorage; 78 | }); 79 | 80 | afterEach(() => { 81 | // We want test isolation, so we restore the original localStorage. 82 | Credentials.localStorage = oldStorage; 83 | }); 84 | 85 | describe("#getFromLocalStorage", () => { 86 | it("should return null when localStorage is empty", () => { 87 | var getItemStub = sinon.stub(localStorage, "getItem"); 88 | var parseStub = sinon.spy(JSON, "parse"); 89 | 90 | getItemStub.returns(null); 91 | assert.equal(Credentials.getFromLocalStorage(), null); 92 | 93 | sinon.assert.called(getItemStub); 94 | sinon.assert.calledWith(parseStub, "null"); 95 | }); 96 | 97 | it("should fetch result when localStorage is not empty", () => { 98 | var getItemStub = sinon.stub(localStorage, "getItem"); 99 | var parseStub = sinon.stub(JSON, "parse"); 100 | 101 | getItemStub.returns("hi"); 102 | Credentials.getFromLocalStorage(); 103 | 104 | sinon.assert.called(getItemStub); 105 | sinon.assert.calledWith(parseStub, "hi"); 106 | }); 107 | }); 108 | 109 | describe("#storeFromClient", () => { 110 | it("should throw when no credentials are in the client", () => { 111 | var client = helpers.createOauthClient(undefined); 112 | 113 | assert.throws( 114 | () => Credentials.storeFromClient(client), 115 | "no credentials in the client" 116 | ); 117 | }); 118 | 119 | it("should store credentials from the client", () => { 120 | var credentials = helpers.createCredentials(Date.now()); 121 | var client = helpers.createOauthClient(credentials); 122 | var setItemStub = sinon.stub(localStorage, "setItem"); 123 | 124 | Credentials.storeFromClient(client); 125 | 126 | sinon.assert.calledWithExactly( 127 | setItemStub, 128 | constants.LOCALSTORAGE_KEY, 129 | JSON.stringify(credentials) 130 | ); 131 | }); 132 | 133 | it("should add expiry timestamp to credentials", () => { 134 | var credentials = helpers.createCredentials(Date.now()); 135 | var client = helpers.createOauthClient(credentials); 136 | 137 | Credentials.storeFromClient(client); 138 | 139 | assert.equal( 140 | credentials.expiry_timestamp, 141 | Date.now() + credentials.expires_in * 1000 142 | ); 143 | }); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/resources/gen/portfolios_base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* tslint:disable:max-line-length */ 4 | /* tslint:disable:eofline */ 5 | var resourceBase = { 6 | "name": "portfolios", 7 | "comment": "", 8 | "properties":[], 9 | "actions": [ 10 | { 11 | "name": "addCustomFieldSettingForPortfolio", 12 | "method": "POST", 13 | "collection": false, 14 | "path": "/portfolios/%s/addCustomFieldSetting", 15 | "params": [ 16 | { 17 | "name": "portfolio_gid", 18 | "type": "string", 19 | "example_values": ["12345"], 20 | "comment": "Globally unique identifier for the portfolio.", 21 | "required": true 22 | }, 23 | ], 24 | "comment": "Add a custom field to a portfolio" 25 | }, 26 | { 27 | "name": "addItemForPortfolio", 28 | "method": "POST", 29 | "collection": false, 30 | "path": "/portfolios/%s/addItem", 31 | "params": [ 32 | { 33 | "name": "portfolio_gid", 34 | "type": "string", 35 | "example_values": ["12345"], 36 | "comment": "Globally unique identifier for the portfolio.", 37 | "required": true 38 | }, 39 | ], 40 | "comment": "Add a portfolio item" 41 | }, 42 | { 43 | "name": "addMembersForPortfolio", 44 | "method": "POST", 45 | "collection": false, 46 | "path": "/portfolios/%s/addMembers", 47 | "params": [ 48 | { 49 | "name": "portfolio_gid", 50 | "type": "string", 51 | "example_values": ["12345"], 52 | "comment": "Globally unique identifier for the portfolio.", 53 | "required": true 54 | }, 55 | ], 56 | "comment": "Add users to a portfolio" 57 | }, 58 | { 59 | "name": "createPortfolio", 60 | "method": "POST", 61 | "collection": false, 62 | "path": "/portfolios", 63 | "params": [ 64 | ], 65 | "comment": "Create a portfolio" 66 | }, 67 | { 68 | "name": "deletePortfolio", 69 | "method": "DELETE", 70 | "collection": false, 71 | "path": "/portfolios/%s", 72 | "params": [ 73 | { 74 | "name": "portfolio_gid", 75 | "type": "string", 76 | "example_values": ["12345"], 77 | "comment": "Globally unique identifier for the portfolio.", 78 | "required": true 79 | }, 80 | ], 81 | "comment": "Delete a portfolio" 82 | }, 83 | { 84 | "name": "getItemsForPortfolio", 85 | "method": "GET", 86 | "collection": true||false, 87 | "path": "/portfolios/%s/items", 88 | "params": [ 89 | { 90 | "name": "portfolio_gid", 91 | "type": "string", 92 | "example_values": ["12345"], 93 | "comment": "Globally unique identifier for the portfolio.", 94 | "required": true 95 | }, 96 | ], 97 | "comment": "Get portfolio items" 98 | }, 99 | { 100 | "name": "getPortfolio", 101 | "method": "GET", 102 | "collection": false, 103 | "path": "/portfolios/%s", 104 | "params": [ 105 | { 106 | "name": "portfolio_gid", 107 | "type": "string", 108 | "example_values": ["12345"], 109 | "comment": "Globally unique identifier for the portfolio.", 110 | "required": true 111 | }, 112 | ], 113 | "comment": "Get a portfolio" 114 | }, 115 | { 116 | "name": "getPortfolios", 117 | "method": "GET", 118 | "collection": true||false, 119 | "path": "/portfolios", 120 | "params": [ 121 | { 122 | "name": "owner", 123 | "type": "string", 124 | "example_values": ["14916"], 125 | "comment": "The user who owns the portfolio. Currently, API users can only get a list of portfolios that they themselves own.", 126 | "required": true 127 | }, 128 | { 129 | "name": "workspace", 130 | "type": "string", 131 | "example_values": ["1331"], 132 | "comment": "The workspace or organization to filter portfolios on.", 133 | "required": true 134 | }, 135 | ], 136 | "comment": "Get multiple portfolios" 137 | }, 138 | { 139 | "name": "removeCustomFieldSettingForPortfolio", 140 | "method": "POST", 141 | "collection": false, 142 | "path": "/portfolios/%s/removeCustomFieldSetting", 143 | "params": [ 144 | { 145 | "name": "portfolio_gid", 146 | "type": "string", 147 | "example_values": ["12345"], 148 | "comment": "Globally unique identifier for the portfolio.", 149 | "required": true 150 | }, 151 | ], 152 | "comment": "Remove a custom field from a portfolio" 153 | }, 154 | { 155 | "name": "removeItemForPortfolio", 156 | "method": "POST", 157 | "collection": false, 158 | "path": "/portfolios/%s/removeItem", 159 | "params": [ 160 | { 161 | "name": "portfolio_gid", 162 | "type": "string", 163 | "example_values": ["12345"], 164 | "comment": "Globally unique identifier for the portfolio.", 165 | "required": true 166 | }, 167 | ], 168 | "comment": "Remove a portfolio item" 169 | }, 170 | { 171 | "name": "removeMembersForPortfolio", 172 | "method": "POST", 173 | "collection": false, 174 | "path": "/portfolios/%s/removeMembers", 175 | "params": [ 176 | { 177 | "name": "portfolio_gid", 178 | "type": "string", 179 | "example_values": ["12345"], 180 | "comment": "Globally unique identifier for the portfolio.", 181 | "required": true 182 | }, 183 | ], 184 | "comment": "Remove users from a portfolio" 185 | }, 186 | { 187 | "name": "updatePortfolio", 188 | "method": "PUT", 189 | "collection": false, 190 | "path": "/portfolios/%s", 191 | "params": [ 192 | { 193 | "name": "portfolio_gid", 194 | "type": "string", 195 | "example_values": ["12345"], 196 | "comment": "Globally unique identifier for the portfolio.", 197 | "required": true 198 | }, 199 | ], 200 | "comment": "Update a portfolio" 201 | }, 202 | ] 203 | } 204 | export = resourceBase; 205 | -------------------------------------------------------------------------------- /test/components/json_response_spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require("chai"); 2 | import sinon = require("sinon"); 3 | 4 | import JsonResponse = require("../../src/components/json_response"); 5 | import Resources = require("../../src/resources"); 6 | import {SinonFakeServer, SinonSpy} from "sinon"; 7 | import * as ReactTestUtils from "react-dom/test-utils"; 8 | import * as ReactDOM from "react-dom"; 9 | 10 | const assert = chai.assert; 11 | const testUtils = ReactTestUtils; 12 | 13 | describe("JsonResponseComponent", () => { 14 | let sand: SinonFakeServer; 15 | 16 | let root: JsonResponse; 17 | let responseBlock: Element; 18 | let stringifySpy = sinon.spy(JSON, "stringify"); 19 | 20 | beforeEach(() => { 21 | sand = sinon.fakeServer.create(); 22 | }); 23 | 24 | function renderWithProps(props?: JsonResponse.Props) { 25 | props = props || { 26 | response: { 27 | action: undefined, 28 | rawResponse: undefined, 29 | route: undefined, 30 | routeUrl: undefined 31 | } 32 | }; 33 | 34 | root = testUtils.renderIntoDocument( 35 | JsonResponse.create(props) 36 | ); 37 | responseBlock = testUtils.findRenderedDOMComponentWithClass( 38 | root, 39 | "json-response-block" 40 | ); 41 | } 42 | 43 | afterEach(() => { 44 | sand.restore(); 45 | }); 46 | 47 | it("should show an empty json response when undefined", () => { 48 | renderWithProps(); 49 | 50 | // We shouldn't stringify an undefined string. 51 | sinon.assert.notCalled(stringifySpy); 52 | 53 | // Verify the DOM is as expected. 54 | const node = ReactDOM.findDOMNode(responseBlock); 55 | 56 | if (node !== null) { 57 | assert.equal(node.nodeName, "PRE"); 58 | assert.equal(node.childNodes.length, 1); 59 | assert.equal(node.childNodes[0].nodeName, "CODE"); 60 | assert.equal(node.childNodes[0].textContent, ""); 61 | } else { 62 | assert(false); 63 | } 64 | }); 65 | 66 | it("should show error json response when response fails", () => { 67 | const action = Resources.Attachments.actions[0]; 68 | 69 | // Set the props to have a non-empty response. 70 | const rawResponse = {error: {again: 2}}; 71 | renderWithProps({ 72 | response: { 73 | action: action, 74 | error: "something", 75 | rawResponse: rawResponse, 76 | route: action.path, 77 | routeUrl: "https://app.asana.com/api/1.0/" + action.path 78 | } 79 | }); 80 | 81 | // We should stringify a non-empty string. 82 | sinon.assert.calledWith(stringifySpy, rawResponse); 83 | 84 | // Verify the DOM for the json response block. 85 | const node = ReactDOM.findDOMNode(responseBlock); 86 | if (node === null) { 87 | assert(false); 88 | return; 89 | } 90 | 91 | assert.equal(node.nodeName, "PRE"); 92 | assert.equal(node.childNodes.length, 1); 93 | assert.equal(node.childNodes[0].nodeName, "CODE"); 94 | assert.equal( 95 | node.childNodes[0].textContent, 96 | JSON.stringify(rawResponse, undefined, 2) 97 | ); 98 | 99 | // Should contain error class for styling. 100 | assert.include((node as HTMLElement).className, "json-error"); 101 | 102 | // Verify the DOM for the response header info. 103 | const responseInfo = testUtils.findRenderedDOMComponentWithClass( 104 | root, 105 | "json-response-info" 106 | ); 107 | const textNode = ReactDOM.findDOMNode(responseInfo); 108 | 109 | if (textNode === null) { 110 | assert(false); 111 | return; 112 | } 113 | const textContent = textNode.textContent; 114 | 115 | if (textContent === null) { 116 | assert(false); 117 | return; 118 | } 119 | 120 | assert.include(textContent, action.method); 121 | assert.include(textContent, action.path); 122 | }); 123 | 124 | it("should show non-empty json response after updating props", () => { 125 | const action = Resources.Attachments.actions[0]; 126 | 127 | // Set the props to have a non-empty response. 128 | const rawResponse = {test: {again: 2}}; 129 | renderWithProps({ 130 | response: { 131 | action: action, 132 | rawResponse: rawResponse, 133 | route: action.path, 134 | routeUrl: "https://app.asana.com/api/1.0/" + action.path 135 | } 136 | }); 137 | 138 | // We should stringify a non-empty string. 139 | sinon.assert.calledWith(stringifySpy, rawResponse); 140 | 141 | // Verify the DOM for the json response block. 142 | const node = ReactDOM.findDOMNode(responseBlock); 143 | 144 | if (node !== null) { 145 | assert.equal(node.nodeName, "PRE"); 146 | assert.equal(node.childNodes.length, 1); 147 | assert.equal(node.childNodes[0].nodeName, "CODE"); 148 | assert.equal( 149 | node.childNodes[0].textContent, 150 | JSON.stringify(rawResponse, undefined, 2) 151 | ); 152 | } 153 | 154 | // Verify the DOM for the response header info. 155 | const responseInfo = testUtils.findRenderedDOMComponentWithClass( 156 | root, 157 | "json-response-info" 158 | ); 159 | let domNode = ReactDOM.findDOMNode(responseInfo); 160 | 161 | if (domNode !== null) { 162 | let textContent = domNode.textContent; 163 | 164 | if (textContent !== null) { 165 | assert.include(textContent, action.method); 166 | assert.include(textContent, action.path); 167 | } 168 | } 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/components/extra_parameter_entry_spec.ts: -------------------------------------------------------------------------------- 1 | import chai = require("chai"); 2 | import React = require("react"); 3 | import sinon = require("sinon"); 4 | import _ = require("lodash"); 5 | 6 | import ExtraParameterEntry = require("../../src/components/extra_parameter_entry"); 7 | import * as ReactTestUtils from "react-dom/test-utils"; 8 | import {SinonFakeServer, SinonStub} from "sinon"; 9 | 10 | import Helpers = require("../helpers"); 11 | 12 | const assert = chai.assert; 13 | const r = React.createElement; 14 | const testUtils = ReactTestUtils; 15 | 16 | describe("ExtraParameterEntryComponent", () => { 17 | let sand: SinonFakeServer; 18 | 19 | let syncExtraParametersStub: SinonStub; 20 | 21 | let root: ExtraParameterEntry; 22 | let addExtraParam: Element; 23 | 24 | beforeEach(() => { 25 | sand = sinon.fakeServer.create(); 26 | 27 | syncExtraParametersStub = sinon.stub(); 28 | 29 | root = testUtils.renderIntoDocument( 30 | ExtraParameterEntry.create({ 31 | text: r("h3", {}, "this is a test"), 32 | syncExtraParameters: syncExtraParametersStub 33 | }) 34 | ); 35 | addExtraParam = testUtils.findRenderedDOMComponentWithClass( 36 | root, 37 | "add-extra-param" 38 | ); 39 | }); 40 | 41 | afterEach(() => { 42 | sand.restore(); 43 | }); 44 | 45 | function assertNumberOfExtraParams(n: number) { 46 | assert.lengthOf(root.state.extraParams, n); 47 | 48 | assert.lengthOf( 49 | testUtils.scryRenderedDOMComponentsWithClass(root, "extra-param-key"), 50 | n 51 | ); 52 | assert.lengthOf( 53 | testUtils.scryRenderedDOMComponentsWithClass(root, "extra-param-value"), 54 | n 55 | ); 56 | assert.lengthOf( 57 | testUtils.scryRenderedDOMComponentsWithClass(root, "extra-param"), 58 | n 59 | ); 60 | } 61 | 62 | it("should initialize with no extra parameter fields", () => { 63 | assertNumberOfExtraParams(0); 64 | }); 65 | 66 | it("should add a parameter field after clicking link", () => { 67 | testUtils.Simulate.click(addExtraParam); 68 | assertNumberOfExtraParams(1); 69 | 70 | testUtils.Simulate.click(addExtraParam); 71 | assertNumberOfExtraParams(2); 72 | }); 73 | 74 | it("should update state and trigger sync when text is entered", () => { 75 | // First add a few parameters so we can enter text in those fields. 76 | testUtils.Simulate.click(addExtraParam); 77 | testUtils.Simulate.click(addExtraParam); 78 | testUtils.Simulate.click(addExtraParam); 79 | const extraParams = testUtils.scryRenderedDOMComponentsWithClass( 80 | root, 81 | "extra-param" 82 | ); 83 | 84 | 85 | const extraParamKeys = testUtils.scryRenderedDOMComponentsWithClass( 86 | root, 87 | "extra-param-key" 88 | ); 89 | 90 | const extraParamValues = testUtils.scryRenderedDOMComponentsWithClass( 91 | root, 92 | "extra-param-value" 93 | ); 94 | 95 | // For each extra parameter, we'll input data and verify it updated. 96 | extraParamKeys.forEach((keyInput, i) => { 97 | 98 | const uniqueKey = _.uniqueId(); 99 | const input = (keyInput) 100 | testUtils.Simulate.change(input, { 101 | target: { value: uniqueKey } 102 | } as any) 103 | 104 | assert.include( 105 | root.state.extraParams, 106 | {key: uniqueKey, value: ""} 107 | ); 108 | sinon.assert.calledWith( 109 | syncExtraParametersStub, 110 | root.state.extraParams); 111 | 112 | const uniqueValue = _.uniqueId(); 113 | 114 | testUtils.Simulate.change( extraParamValues[i], { 115 | target: { value: uniqueValue } 116 | } as any) 117 | 118 | assert.include( 119 | root.state.extraParams, 120 | {key: uniqueKey, value: uniqueValue} 121 | ); 122 | sinon.assert.calledWith( 123 | syncExtraParametersStub, 124 | root.state.extraParams); 125 | }) 126 | }); 127 | 128 | it("should remove extra param after clicking link", () => { 129 | // First add a few parameters so we can enter text in those fields. 130 | testUtils.Simulate.click(addExtraParam); 131 | testUtils.Simulate.click(addExtraParam); 132 | testUtils.Simulate.click(addExtraParam); 133 | const extraParams = testUtils.scryRenderedDOMComponentsWithClass( 134 | root, 135 | "extra-param" 136 | ); 137 | 138 | const extraParamKeys = testUtils.scryRenderedDOMComponentsWithClass( 139 | root, 140 | "extra-param-key" 141 | ); 142 | 143 | const extraParamValues = testUtils.scryRenderedDOMComponentsWithClass( 144 | root, 145 | "extra-param-value" 146 | ); 147 | 148 | // Now enter text in parameters to differentiate them. 149 | extraParamKeys.forEach((keyInput, i) => { 150 | keyInput.dispatchEvent(new Event("change", {bubbles: true})); 151 | extraParamValues[i].dispatchEvent(new Event("change", {bubbles: true})); 152 | }) 153 | 154 | // Now, we can remove each of the fields and verify. 155 | const extraParamsCopy = _.cloneDeep(root.state.extraParams); 156 | while (root.state.extraParams.length > 0) { 157 | const deleteLink = testUtils.scryRenderedDOMComponentsWithClass( 158 | root, 159 | "delete-extra-param" 160 | ); 161 | testUtils.Simulate.click(deleteLink[0]); 162 | 163 | extraParamsCopy.shift(); 164 | assert.deepEqual(root.state.extraParams, extraParamsCopy); 165 | sinon.assert.calledWith( 166 | syncExtraParametersStub, 167 | root.state.extraParams 168 | ); 169 | } 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/resources/projects.ts: -------------------------------------------------------------------------------- 1 | import resourceBase = require("./gen/projects_base"); 2 | resourceBase.comment = "A _project_ represents a prioritized list of tasks in Asana or a board with\ncolumns of tasks represented as cards. It exists in a single workspace or\norganization and is accessible to a subset of users in that workspace or\norganization, depending on its permissions.\n\nProjects in organizations are shared with a single team. You cannot currently\nchange the team of a project via the API. Non-organization workspaces do not\nhave teams and so you should not specify the team of project in a regular\nworkspace.\n"; 3 | resourceBase.properties = [ 4 | { 5 | "name": "gid", 6 | "type": "Gid", 7 | "example_values": [ 8 | "\"1234\"" 9 | ], 10 | "comment": "Globally unique ID of the project.\n" 11 | }, 12 | { 13 | "name": "resource_type", 14 | "type": "Enum", 15 | "comment": "The resource type of this resource. The value for this resource is always `project`. To distinguish between board and list projects see the `layout` property.\n", 16 | "example_values": [ 17 | "\"project\"" 18 | ], 19 | "values": [ 20 | { 21 | "name": "project", 22 | "comment": "A project resource type." 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "name", 28 | "type": "String", 29 | "example_values": [ 30 | "'Stuff to buy'" 31 | ], 32 | "comment": "Name of the project. This is generally a short sentence fragment that fits\non a line in the UI for maximum readability. However, it can be longer.\n" 33 | }, 34 | { 35 | "name": "owner", 36 | "type": "User", 37 | "example_values": [ 38 | "{ id: 12345, gid: \"12345\", resource_type: \"user\", name: 'Tim Bizarro' }", 39 | "null" 40 | ], 41 | "comment": "The current owner of the project, may be null.\n" 42 | }, 43 | { 44 | "name": "current_status", 45 | "type": "Struct", 46 | "example_values": [ 47 | "{ 'color': 'green', 'title': 'Status Update - Jun 15', ... } " 48 | ], 49 | "comment": "The most recently created status update for the project, or `null` if no update exists. See also the\ndocumentation for [project status updates](/developers/api-reference/project_statuses).\n" 50 | }, 51 | { 52 | "name": "due_on", 53 | "type": "String", 54 | "example_values": [ 55 | "'2012-03-26'" 56 | ], 57 | "comment": "The day on which this project is due. This takes a date with format YYYY-MM-DD.\n" 58 | }, 59 | { 60 | "name": "start_on", 61 | "type": "String", 62 | "example_values": [ 63 | "'2012-03-26'" 64 | ], 65 | "comment": "The day on which this project starts. This takes a date with format YYYY-MM-DD.\n" 66 | }, 67 | { 68 | "name": "created_at", 69 | "type": "String", 70 | "example_values": [ 71 | "'2012-02-22T02:06:58.147Z'" 72 | ], 73 | "comment": "The time at which this project was created.\n" 74 | }, 75 | { 76 | "name": "modified_at", 77 | "type": "String", 78 | "example_values": [ 79 | "'2012-02-22T02:06:58.147Z'" 80 | ], 81 | "comment": "The time at which this project was last modified.\n", 82 | }, 83 | { 84 | "name": "archived", 85 | "type": "Boolean", 86 | "example_values": [ 87 | "false" 88 | ], 89 | "comment": "True if the project is archived, false if not. Archived projects do not\nshow in the UI by default and may be treated differently for queries.\n" 90 | }, 91 | { 92 | "name": "public", 93 | "type": "Boolean", 94 | "example_values": [ 95 | "false" 96 | ], 97 | "comment": "True if the project is public to the organization. If false, do not share this project with other users in this organization without explicitly checking to see if they have access." 98 | }, 99 | { 100 | "name": "members", 101 | "type": "Array", 102 | "example_values": [ 103 | "[ { id: 1123, gid: \"1123\", resource_type: \"user\", name: 'Mittens' }, ... ]" 104 | ], 105 | "comment": "Array of users who are members of this project.\n" 106 | }, 107 | { 108 | "name": "followers", 109 | "type": "Array", 110 | "example_values": [ 111 | "[ { id: 1123, gid: \"1123\", resource_type: \"user\", name: 'Mittens' }, ... ]" 112 | ], 113 | "comment": "Array of users following this project. Followers are a subset of members who receive all notifications for a\nproject, the default notification setting when adding members to a project in-product.\n" 114 | }, 115 | { 116 | "name": "custom_fields", 117 | "type": "Array", 118 | "example_values": [ 119 | "[ { id: 1646, gid: \"1646\", name: 'Priority', type: 'enum', enum_value: { id: 126, gid: \"126\", name: 'P1' } }, ...]" 120 | ], 121 | "comment": "Array of custom field values set on the project for a custom field applied to a parent portfolio. Take care to avoid confusing these custom field values with the custom field settings in the [custom_field_settings](#field-custom_field_settings) property. Please note that the `gid` returned on each custom field value is identical to the `gid` of the custom field, which allows referencing the custom field through the [/custom_fields/{custom_field_gid}](/developers/api-reference/custom_fields#get-single) endpoint.\n" 122 | }, 123 | { 124 | "name": "custom_field_settings", 125 | "type": "Array", 126 | "example_values": [ 127 | "[ { id: 258147, gid: \"258147\", custom_field: {id: 1646, gid: \"1646\", name: 'Priority', type: 'enum'}, project: {id: 13309, gid: \"13309\", name: 'Bugs'} }, ...]" 128 | ], 129 | "comment": "Array of custom field settings in compact form. These represent the association of custom fields with this project. Take care to avoid confusing these custom field settings with the custom field values in the [custom_fields](#field-custom_fields) property.\n" 130 | }, 131 | { 132 | "name": "color", 133 | "type": "Enum", 134 | "example_values": [ 135 | "'dark-purple'" 136 | ], 137 | "comment": "Color of the project. Must be either `null` or one of: `dark-pink`,\n`dark-green`, `dark-blue`, `dark-red`, `dark-teal`, `dark-brown`,\n`dark-orange`, `dark-purple`, `dark-warm-gray`, `light-pink`, `light-green`,\n`light-blue`, `light-red`, `light-teal`, `light-yellow`, `light-orange`,\n`light-purple`, `light-warm-gray`.\n" 138 | }, 139 | { 140 | "name": "notes", 141 | "type": "String", 142 | "example_values": [ 143 | "'These are things we need to purchase.'" 144 | ], 145 | "comment": "More detailed, free-form textual information associated with the project.\n" 146 | }, 147 | { 148 | "name": "html_notes", 149 | "type": "String", 150 | "example_values": [ 151 | "'<body>Get whatever <a href='https://app.asana.com/0/1123/list'>Sashimi</a> has.</body>'" 152 | ], 153 | "comment": "[Opt In](https://asana.com/developers/documentation/getting-started/input-output-options). The notes of the project with formatting as HTML.\n" 154 | }, 155 | { 156 | "name": "workspace", 157 | "type": "Workspace", 158 | "example_values": [ 159 | "{ id: 14916, gid: \"14916\", name: 'My Workspace' }" 160 | ], 161 | "comment": "The workspace or organization this project is associated with. Once created,\nprojects cannot be moved to a different workspace. This attribute can only\nbe specified at creation time.\n" 162 | }, 163 | { 164 | "name": "team", 165 | "type": "Team", 166 | "example_values": [ 167 | "{ id: 692353, gid: \"692353\", name: 'organization.com Marketing' }" 168 | ], 169 | "comment": "The team that this project is shared with. This field only exists for\nprojects in organizations.\n" 170 | }, 171 | { 172 | "name": "layout", 173 | "type": "Enum", 174 | "example_values": [ 175 | "'board'", 176 | "'list'" 177 | ], 178 | "comment": "The layout (board or list view) of the project.\n" 179 | } 180 | ] 181 | 182 | export = resourceBase; --------------------------------------------------------------------------------