diff --git a/ui/core/app/components/form/project/index.hbs b/ui/core/app/components/form/project/index.hbs
new file mode 100644
index 000000000..50c4e9249
--- /dev/null
+++ b/ui/core/app/components/form/project/index.hbs
@@ -0,0 +1,44 @@
+
diff --git a/ui/core/app/router.js b/ui/core/app/router.js
index a20befcc5..68162d24d 100644
--- a/ui/core/app/router.js
+++ b/ui/core/app/router.js
@@ -9,7 +9,11 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('orgs', function () {
this.route('org', { path: ':org_id' }, function () {
- this.route('projects');
+ this.route('projects', function () {
+ this.route('project', { path: ':project_id' });
+ this.route('new');
+ });
});
});
+ this.route('projects');
});
diff --git a/ui/core/app/routes/orgs/org.js b/ui/core/app/routes/orgs/org.js
index d1fcd307a..fcedca1e1 100644
--- a/ui/core/app/routes/orgs/org.js
+++ b/ui/core/app/routes/orgs/org.js
@@ -9,12 +9,21 @@ export default class OrgsOrgRoute extends Route {
// =methods
/**
- * Sets the scope to the specified organization and returns the org.
+ * Returns a mock organization object containing the specified ID.
+ * TODO: load a real org instance from the API.
* @param {object} params
* @return {object}
*/
model({ org_id: id }) {
- this.scope.org = { id };
- return this.scope.org;
+ return { id };
+ }
+
+ /**
+ * Adds the org to the scope.
+ * @param {object} org
+ */
+ afterModel(org) {
+ this.scope.org = org;
+ return super.afterModel(...arguments);
}
}
diff --git a/ui/core/app/routes/orgs/org/projects/index.js b/ui/core/app/routes/orgs/org/projects/index.js
new file mode 100644
index 000000000..be2068f6d
--- /dev/null
+++ b/ui/core/app/routes/orgs/org/projects/index.js
@@ -0,0 +1,3 @@
+import Route from '@ember/routing/route';
+
+export default class OrgsOrgProjectsIndexRoute extends Route {}
diff --git a/ui/core/app/routes/orgs/org/projects/new.js b/ui/core/app/routes/orgs/org/projects/new.js
new file mode 100644
index 000000000..23261b7ee
--- /dev/null
+++ b/ui/core/app/routes/orgs/org/projects/new.js
@@ -0,0 +1,46 @@
+import Route from '@ember/routing/route';
+import { action } from '@ember/object';
+
+export default class OrgsOrgProjectsNewRoute extends Route {
+ // =methods
+
+ /**
+ * Returns a new unsaved project.
+ * @return {Promise{ProjectModel}}
+ */
+ model() {
+ return this.store.createRecord('project');
+ }
+
+ // =actions
+
+ /**
+ * Rollback changes on project (which destroys unsaved instances)
+ * and redirects to projects index.
+ * @param {ProjectModel} project
+ */
+ @action
+ cancel(project) {
+ project.rollbackAttributes();
+ this.transitionTo('orgs.org.projects');
+ }
+
+ /**
+ * Handle save project.
+ * @param {ProjectModel} project
+ * @param {Event} e
+ */
+ @action
+ async save(project, e) {
+ // Prevent default behavior, since this was trigger from a form submission.
+ e.preventDefault();
+ try {
+ await project.save();
+ this.transitionTo('orgs.org.projects.project', project);
+ } catch (e) {
+ // TODO: error handling
+ throw e;
+ }
+ }
+
+}
diff --git a/ui/core/app/routes/orgs/org/projects/project.js b/ui/core/app/routes/orgs/org/projects/project.js
new file mode 100644
index 000000000..e9cdf887a
--- /dev/null
+++ b/ui/core/app/routes/orgs/org/projects/project.js
@@ -0,0 +1,73 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+export default class OrgsOrgProjectsProjectRoute extends Route {
+ // =services
+
+ @service scope;
+
+ // =methods
+
+ /**
+ * Returns a project by id.
+ * @param {object} params
+ * @return {Promise{ProjectModel}}
+ */
+ model({ project_id: id }) {
+ return this.store.findRecord('project', id);
+ }
+
+ /**
+ * Adds the project to the scope.
+ * @param {ProjectModel} project
+ */
+ afterModel(project) {
+ this.scope.project = project;
+ return super.afterModel(...arguments);
+ }
+
+ // =actions
+
+ /**
+ * Rollback changes on project.
+ * @param {ProjectModel} project
+ */
+ @action
+ cancel(project) {
+ project.rollbackAttributes();
+ }
+
+ /**
+ * Handle save project.
+ * @param {ProjectModel} project
+ * @param {Event} e
+ */
+ @action
+ async save(project, e) {
+ // Prevent default behavior, since this was trigger from a form submission.
+ e.preventDefault();
+ try {
+ await project.save();
+ } catch (e) {
+ // TODO: error handling
+ throw e;
+ }
+ }
+
+ /**
+ * Deletes the project and redirects to index.
+ * @param {ProjectModel} project
+ */
+ @action
+ async delete(project) {
+ try {
+ await project.destroyRecord();
+ this.transitionTo('orgs.org.projects');
+ } catch (e) {
+ // TODO: error handling
+ throw e;
+ }
+ }
+
+}
diff --git a/ui/core/app/templates/orgs/org/projects.hbs b/ui/core/app/templates/orgs/org/projects.hbs
index 777cc4e17..c24cd6895 100644
--- a/ui/core/app/templates/orgs/org/projects.hbs
+++ b/ui/core/app/templates/orgs/org/projects.hbs
@@ -1,9 +1 @@
-{{t 'resources.projects'}}
-
-
- {{#each @model as |project|}}
- - {{project.name}}
- {{/each}}
-
-
{{outlet}}
diff --git a/ui/core/app/templates/orgs/org/projects/index.hbs b/ui/core/app/templates/orgs/org/projects/index.hbs
new file mode 100644
index 000000000..67fb74a45
--- /dev/null
+++ b/ui/core/app/templates/orgs/org/projects/index.hbs
@@ -0,0 +1,13 @@
+{{t "resources.projects"}}
+
+{{t "titles.new"}}
+
+
+ {{#each @model as |project|}}
+ -
+
+ {{project.name}}
+
+
+ {{/each}}
+
diff --git a/ui/core/app/templates/orgs/org/projects/new.hbs b/ui/core/app/templates/orgs/org/projects/new.hbs
new file mode 100644
index 000000000..d848be793
--- /dev/null
+++ b/ui/core/app/templates/orgs/org/projects/new.hbs
@@ -0,0 +1,11 @@
+{{t "resources.projects"}}
+>
+{{t "titles.new"}}
+
+{{t "titles.new"}}
+
+
diff --git a/ui/core/app/templates/orgs/org/projects/project.hbs b/ui/core/app/templates/orgs/org/projects/project.hbs
new file mode 100644
index 000000000..368276ddd
--- /dev/null
+++ b/ui/core/app/templates/orgs/org/projects/project.hbs
@@ -0,0 +1,19 @@
+{{t "resources.projects"}}
+>
+{{@model.name}}
+
+{{@model.name}}
+
+
+
+
+ {{t 'actions.delete'}}
+
diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js
index 323eca350..ce0e06960 100644
--- a/ui/core/mirage/config.js
+++ b/ui/core/mirage/config.js
@@ -7,7 +7,7 @@ export default function() {
// make this `/api`, for example, if your API is namespaced
this.namespace = config.api.namespace;
// delay for each request, automatically set to 0 during testing
- this.timing = 50;
+ this.timing = 350;
/*
Shorthand cheatsheet:
@@ -25,4 +25,8 @@ export default function() {
this.passthrough();
this.get('/orgs/:org_id/projects');
+ this.post('/orgs/:org_id/projects');
+ this.get('/orgs/:org_id/projects/:id');
+ this.patch('/orgs/:org_id/projects/:id');
+ this.del('/orgs/:org_id/projects/:id');
}
diff --git a/ui/core/mirage/serializers/application.js b/ui/core/mirage/serializers/application.js
index a0b104e4a..bed25389e 100644
--- a/ui/core/mirage/serializers/application.js
+++ b/ui/core/mirage/serializers/application.js
@@ -40,6 +40,22 @@ export default RestSerializer.extend({
// If array, root it under a standard `items` key
if (json.length) json = {items: json};
return json;
+ },
+
+ /**
+ * Adds a root to the payload, since `root: false` doesn't appear to affect
+ * the normalize method.
+ * @param {object} json
+ * @return {object}
+ */
+ normalize(json) {
+ let newJSON = json;
+ if (this.modelName) {
+ newJSON = {};
+ newJSON[this.modelName] = json;
+ }
+ const value = RestSerializer.prototype.normalize.apply(this, [newJSON]);
+ return value;
}
});
diff --git a/ui/core/mirage/serializers/project.js b/ui/core/mirage/serializers/project.js
new file mode 100644
index 000000000..370fb5cf9
--- /dev/null
+++ b/ui/core/mirage/serializers/project.js
@@ -0,0 +1,5 @@
+import ApplicationSerializer from './application';
+
+export default ApplicationSerializer.extend({
+ modelName: 'project'
+});
diff --git a/ui/core/package.json b/ui/core/package.json
index b8ff1d161..15cf43ef8 100644
--- a/ui/core/package.json
+++ b/ui/core/package.json
@@ -63,6 +63,7 @@
"ember-page-title": "^5.2.1",
"ember-qunit": "^4.6.0",
"ember-resolver": "^7.0.0",
+ "ember-route-action-helper": "^2.0.8",
"ember-source": "~3.16.0",
"ember-welcome-page": "^4.0.0",
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774",
diff --git a/ui/core/tests/integration/components/form/project-test.js b/ui/core/tests/integration/components/form/project-test.js
new file mode 100644
index 000000000..9c4073d47
--- /dev/null
+++ b/ui/core/tests/integration/components/form/project-test.js
@@ -0,0 +1,24 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | form/project', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+ this.submit = () => {};
+ this.cancel = () => {};
+
+ await render(hbs`
+ `);
+
+ assert.ok(find('form'));
+ });
+
+});
diff --git a/ui/core/tests/unit/routes/orgs/org/projects/index-test.js b/ui/core/tests/unit/routes/orgs/org/projects/index-test.js
new file mode 100644
index 000000000..2bc8af956
--- /dev/null
+++ b/ui/core/tests/unit/routes/orgs/org/projects/index-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | orgs/org/projects/index', function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup('route:orgs/org/projects/index');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/routes/orgs/org/projects/new-test.js b/ui/core/tests/unit/routes/orgs/org/projects/new-test.js
new file mode 100644
index 000000000..fe57f0d9e
--- /dev/null
+++ b/ui/core/tests/unit/routes/orgs/org/projects/new-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | orgs/org/projects/new', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:orgs/org/projects/new');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/routes/orgs/org/projects/project-test.js b/ui/core/tests/unit/routes/orgs/org/projects/project-test.js
new file mode 100644
index 000000000..a1e01801f
--- /dev/null
+++ b/ui/core/tests/unit/routes/orgs/org/projects/project-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | orgs/org/projects/project', function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup('route:orgs/org/projects/project');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index e7823c763..0e2b062ad 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -1,2 +1,12 @@
+actions:
+ cancel: Cancel
+ delete: Delete
+ save: Save
resources:
projects: Projects
+titles:
+ new: New
+form:
+ label:
+ name: Name
+ description: Description
diff --git a/yarn.lock b/yarn.lock
index d6e455ee3..c60ae7ac4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7921,6 +7921,13 @@ ember-export-application-global@^2.0.1:
resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-2.0.1.tgz#b120a70e322ab208defc9e2daebe8d0dfc2dcd46"
integrity sha512-B7wiurPgsxsSGzJuPFkpBWnaeuCu2PGpG2BjyrfA1VcL7//o+5RSnZqiCEY326y7qmxb2GoCgo0ft03KBU0rRw==
+ember-factory-for-polyfill@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/ember-factory-for-polyfill/-/ember-factory-for-polyfill-1.3.1.tgz#b446ed64916d293c847a4955240eb2c993b86eae"
+ integrity sha512-y3iG2iCzH96lZMTWQw6LWNLAfOmDC4pXKbZP6FxG8lt7GGaNFkZjwsf+Z5GAe7kxfD7UG4lVkF7x37K82rySGA==
+ dependencies:
+ ember-cli-version-checker "^2.1.0"
+
ember-fetch@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-7.1.0.tgz#17549e0df0f8c402b68612aa87f77957fa956ebf"
@@ -7948,6 +7955,14 @@ ember-get-config@0.2.4, ember-get-config@^0.2.2, ember-get-config@^0.2.4:
broccoli-file-creator "^1.1.1"
ember-cli-babel "^6.3.0"
+ember-getowner-polyfill@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-2.2.0.tgz#38e7dccbcac69d5ec694000329ec0b2be651d2b2"
+ integrity sha512-rwGMJgbGzxIAiWYjdpAh04Abvt0s3HuS/VjHzUFhVyVg2pzAuz45B9AzOxYXzkp88vFC7FPaiA4kE8NxNk4A4Q==
+ dependencies:
+ ember-cli-version-checker "^2.1.0"
+ ember-factory-for-polyfill "^1.3.1"
+
"ember-inflector@^2.0.0 || ^3.0.0", ember-inflector@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-3.0.1.tgz#04be6df4d7e4000f6d6bd70787cdc995f77be4ab"
@@ -8072,6 +8087,14 @@ ember-rfc176-data@^0.3.13:
resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.13.tgz#ed1712a26e65fec703655f35410414aa1982cf3b"
integrity sha512-m9JbwQlT6PjY7x/T8HslnXP7Sz9bx/pz3FrNfNi2NesJnbNISly0Lix6NV1fhfo46572cpq4jrM+/6yYlMefTQ==
+ember-route-action-helper@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/ember-route-action-helper/-/ember-route-action-helper-2.0.8.tgz#f227fcccb73e839b65e9b814e241b322fe8c02fc"
+ integrity sha512-V+4uKwqaYveriVt2rl4e+9mzHJiQOr1B8dCPQQ2TS3iAcmi5RD2giRDFGtCK9d2XY9Arb/f9hJh0obP20iyt3A==
+ dependencies:
+ ember-cli-babel "^6.8.1"
+ ember-getowner-polyfill "^2.0.0"
+
ember-router-generator@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee"