diff --git a/app.js b/app.js index 8d8517d..3f781f5 100644 --- a/app.js +++ b/app.js @@ -9,11 +9,17 @@ var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var hbs = require('hbs'); -var indexRoute = require('./routes/get-features'); -var featuresRoute = require('./routes/features'); +var handlebarHelpers = require(path.join(__dirname,'views', 'helpers')); + +// Projects route, current Index. +var projectsRoute = require('./routes/projects'); + +// The invidual project route. +var projectRoute = require('./routes/project'); + +// The individual feature/markdown file route. var featureRoute = require('./routes/feature'); -var handlebarHelpers = require(path.join(__dirname,'views', 'helpers')); var app = express(); @@ -58,16 +64,19 @@ app.use('/public', express.static(path.join(__dirname, 'public'))); /* Routes. */ -// Front page, currently the 'get features' page. -app.use('/', indexRoute); +// Front page is the projects page. +// http://host/ +app.use(projectsRoute); -// List of features. -app.use('/features', featuresRoute); +// Individual project. +// http://host/ +app.use(projectRoute); -// Individual feature. -app.use('/features', featureRoute); +// Files of interest +// htpp://host// +app.use(featureRoute); -// Special resources in node_modules routes. +// Special resources in node_modules/ routes. app.get('/github-markdown-css/github-markdown.css', function(req, res, next) { var cssPath = path.join(__dirname, 'node_modules','github-markdown-css','github-markdown.css'); res.sendFile(cssPath, {}, function(err) { diff --git a/features-support/step_definitions/features.js b/features-support/step_definitions/features.js index 58630ae..af8f381 100644 --- a/features-support/step_definitions/features.js +++ b/features-support/step_definitions/features.js @@ -5,11 +5,11 @@ var request = require('request'); var should = require('should'); // Test helper. -function getFeaturesFromUrl(callback) { +function getProjectFromUrl(callback) { var world = this; - var projectFeaturesUrl = 'http://localhost:' + world.appPort + '/?repo_url=' + encodeURIComponent(world.repoUrl); + var projectRetrievalUrl = 'http://localhost:' + world.appPort + '/?repo_url=' + encodeURIComponent(world.repoUrl); request - .get(projectFeaturesUrl, function(error, response, body) { + .get(projectRetrievalUrl, function(error, response, body) { if (error) { callback(error); return; @@ -24,7 +24,21 @@ function getFeaturesFromUrl(callback) { }); } -var staticTestDataExists = false; +// Cache the state of the static test data +// at the module level (on first require). +var fakeProjectMetadataExists; +var fakeProjectMetadata = { + repoName: 'made-up', + repoUrl: 'http//example.com', + head: 'testing!', + localName: 'not a real repo' +}; + +function getFakeProjectUrl(appPort, projectName) { + return 'http://localhost:' + appPort + '/' + projectName; + // DEBUG + //return 'http://localhost:' + appPort + '/features'; +} module.exports = function () { @@ -35,7 +49,7 @@ module.exports = function () { // Only generate the static test data once for the feature // as opposed to the @cleanSlate tag which removes it for // each scenario. - if (staticTestDataExists) { + if (fakeProjectMetadataExists) { callback(); return; } @@ -43,10 +57,10 @@ module.exports = function () { // Make sure the test data is removed. world.deleteTestSpecs() .then(function() { - return world.createSpecsForTesting(); + return world.createSpecsForTesting(fakeProjectMetadata); }) .then(function() { - staticTestDataExists = true; + fakeProjectMetadataExists = true; callback(); }) .catch(function(err) { @@ -56,8 +70,9 @@ module.exports = function () { this.When(/^an interested party attempts to view them\.?$/, function (callback) { var world = this; + var fakeProjectUrl = getFakeProjectUrl(world.appPort, fakeProjectMetadata.repoName); request - .get('http://localhost:' + world.appPort + '/features', function(error, response, body) { + .get(fakeProjectUrl, function(error, response, body) { if (error) { callback(error); return; @@ -83,7 +98,8 @@ module.exports = function () { this.Given(/^a list of feature files is displayed\.?$/, function (callback) { var world = this; - request.get('http://localhost:' + world.appPort + '/features', function(error, response, body) { + var fakeProjectUrl = getFakeProjectUrl(world.appPort, fakeProjectMetadata.repoName); + request.get(fakeProjectUrl, function(error, response, body) { if (error) { callback(error); return; @@ -122,6 +138,6 @@ module.exports = function () { callback(); }); - this.When(/^an interested party wants to view the features in that repo\.?$/, getFeaturesFromUrl); - this.When(/^they request the features for the same repository again\.?$/, getFeaturesFromUrl); + this.When(/^an interested party wants to view the features in that repo\.?$/, getProjectFromUrl); + this.When(/^they request the features for the same repository again\.?$/, getProjectFromUrl); }; diff --git a/features-support/world.js b/features-support/world.js index 0d0bbd4..4454809 100644 --- a/features-support/world.js +++ b/features-support/world.js @@ -22,11 +22,12 @@ module.exports = function() { * * @return Promise for operation completion. */ - this.createSpecsForTesting = function createSpecsForTesting() { + this.createSpecsForTesting = function createSpecsForTesting(fakeProjectMetadata) { var world = this; return fs.makeTree(world.paths.public) .then(function() { - return fs.copyTree(world.paths.features, world.paths.public); + var fakeProjectPath = path.join(world.paths.public, fakeProjectMetadata.repoName); + return fs.copyTree(world.paths.features, fakeProjectPath); }) .then(function() { @@ -36,12 +37,7 @@ module.exports = function() { // Pass an object of made up repo data to be decorated with // feature file paths and return a promise for completion // of storage of that data. - return configuredDeriveAndStore({ - repoName: 'made up', - repoUrl: 'http//example.com', - head: 'testing!', - localName: 'not a real repo' - }); + return configuredDeriveAndStore(fakeProjectMetadata); }); }; diff --git a/lib/specifications/getProject.js b/lib/specifications/getProject.js index 33a8ca2..13c6bfb 100644 --- a/lib/specifications/getProject.js +++ b/lib/specifications/getProject.js @@ -12,7 +12,7 @@ var featureFileRoot = path.join(__dirname, '..', '..', 'public', 'feature-files' /** * Get a copy of the project, derive metadata, store metadata. * - * @return a promise for the completion of repo metadata storage. + * @return a promise for the project metadata on storage completion. */ function getProject(repoUrl) { var repoName = /\/([^\/]+?)(?:\.git)?\/?$/.exec(repoUrl); diff --git a/lib/specifications/projectDataStorage.js b/lib/specifications/projectDataStorage.js index 1503879..febb3ad 100644 --- a/lib/specifications/projectDataStorage.js +++ b/lib/specifications/projectDataStorage.js @@ -12,12 +12,15 @@ var projectDataDirectory = path.join(appRoot, 'project-data'); module.exports = { - // Return promise for write completion. + // Return promise for the metadata on write completion. persist: function(projectData) { // Ensure project data directory exists and write the file. fs.existsSync(projectDataDirectory) || fs.mkdirSync(projectDataDirectory); - return qfs.write(path.join(projectDataDirectory, projectData.name + '.data'), JSON.stringify(projectData)); + return qfs.write(path.join(projectDataDirectory, projectData.name + '.data'), JSON.stringify(projectData)) + .then(function() { + return projectData; + }); }, // Return promise for an array of paths in data directory diff --git a/lib/specifications/projectMetaData.js b/lib/specifications/projectMetaData.js index 864295f..3b0c4de 100644 --- a/lib/specifications/projectMetaData.js +++ b/lib/specifications/projectMetaData.js @@ -9,18 +9,20 @@ var projectDataStorage = require('./projectDataStorage'); // Get a function for use with Array.prototype.map to // convert file paths for feature files to express routes. -function pathsToRoutes(featureFileRoot) { +function pathsToRoutes(featureFileRoot, projectName) { return function(featurePath) { - // Remove the filesystem root. - featurePath = featurePath.replace(featureFileRoot, ''); + // Remove the filesystem root and the project name + // so that we have URLs relative the project page. + var pathRoot = path.posix.join(featureFileRoot, projectName) + '/'; + var featureRoute = featurePath.replace(pathRoot, ''); - // Prefix with the 'features' route, always use backslashes. - featurePath = path.posix.join('features', featurePath); + // Remove the .feature for a slightly shorter display name. + var featureName = featureRoute.replace('.feature', ''); return { - featureRoute: featurePath, - featureName: featurePath.replace('.feature', '').replace('features/', '') + featureRoute: featureRoute, + featureName: featureName }; } } @@ -41,7 +43,7 @@ function deriveProjectData(featureFileRoot, repoData) { commit: repoData.commit, shortCommit: repoData.shortCommit, localName: repoData.localName, - projectLink: '/features#' + repoData.repoName, + projectLink: '/' + repoData.repoName, featureFilePaths: [] } @@ -50,7 +52,7 @@ function deriveProjectData(featureFileRoot, repoData) { .then(function(featureFilePaths) { // Map from the storage directory to the Express route for creating links. - projectData.featureFilePaths = featureFilePaths.map(pathsToRoutes(featureFileRoot)); + projectData.featureFilePaths = featureFilePaths.map(pathsToRoutes(featureFileRoot, projectData.name)); return projectData; }); } @@ -59,7 +61,7 @@ function deriveProjectData(featureFileRoot, repoData) { /** * Return a function which takes the repo data, derives the project data and stores it. * - * @return A promise for completion of the storage of the project data. + * @return A promise for the project data on completion of the storage. */ function deriveAndStore(featureFileRoot) { return function(repoData) { diff --git a/package.json b/package.json index 43ee52b..489a8f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "specs", - "version": "0.4.0", + "version": "0.5.0", "private": true, "bin": { "module-name": "./bin/www" diff --git a/public/css/main.css b/public/css/main.css index df994ca..47d254e 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -96,25 +96,27 @@ button.call-to-action:active { /* Top nav. */ .header-nav { - width: 100%; + width: 70%; + margin: 0 auto; position: relative; top: 0.8em; - text-align: center; + text-align: left; font-size: 1.2em; list-style: none; } -.header-nav hr { - max-width: 70%; -} .header-nav a { font-weight: bold; - color: green; + color: blue; text-decoration: none; } .header-nav li { display: inline; margin-left: 0.2em; } +.header-nav li:hover a { + text-decoration: underline; + color: green; +} .header-nav li.primary { font-size: 1.2em; margin-right: 1em; diff --git a/public/css/features.css b/public/css/project.css similarity index 63% rename from public/css/features.css rename to public/css/project.css index 5d72c01..a4b89d5 100644 --- a/public/css/features.css +++ b/public/css/project.css @@ -2,35 +2,21 @@ margin-bottom: 1em; } -.project-title { - display: inline-block; -} -.quick-link { - text-decoration: none; - color: blue; -} -.project-title .quick-link-icon { - font-size: 1.3em; - opacity: 0.4; - color: blue; -} -.project-title:hover .quick-link-icon { - opacity: 1; - color: green; -} .project-name { - margin-left: 0.5em; + font-size: 2.5em; + display: inline-block; } .project-commit { - margin-left: 0.3em; + position: relative; + top: -0.1em; + margin-left: 0.5em; + margin-right: 1em; } .repo-link { - position: relative; - top: -0.1em; font-size: 0.7em; - margin-left: 2em; + margin-right: 3em; font-weight: bold; text-transform: uppercase; } @@ -40,12 +26,12 @@ } .repo-link:hover a { color: green; + text-decoration: underline; } .project-update { margin-top: 1em; margin-bottom: 1em; - margin-left: 2em; } .project-update a { @@ -56,3 +42,9 @@ .links-container { margin-left: 3em; } + +@media (min-width: 800px) { + .project-update { + float: right; + } +} diff --git a/public/css/get-features.css b/public/css/projects.css similarity index 67% rename from public/css/get-features.css rename to public/css/projects.css index 45141c9..641325d 100644 --- a/public/css/get-features.css +++ b/public/css/projects.css @@ -1,23 +1,23 @@ -.get-features > section { +.projects > section { float: left; margin-right: 3em; } -.get-features form { +.projects form { display: inline-block; } -.get-features form { +.projects form { margin-bottom: 1.2em; } -.get-features input { +.projects input { margin-top: 1em; margin-right: 0.5em; margin-bottom: 1em; } -.get-features button { +.projects button { font-size: 1.1em; } @@ -25,7 +25,7 @@ * Wider viewport (e.g. monitor, tv). */ @media (min-width: 1280px) { - .get-features input { + .projects input { margin-top: 0; } } diff --git a/routes/feature.js b/routes/feature.js index 13231c6..91097d0 100644 --- a/routes/feature.js +++ b/routes/feature.js @@ -1,6 +1,8 @@ "use strict"; /* eslint new-cap: 0 */ +var path = require('path'); + var express = require('express'); var router = express.Router(); @@ -9,20 +11,21 @@ var markdown = require( "markdown" ).markdown; var getFeatureFile = require("../lib/specifications/getFeatureFile"); var GherkinParser = require('../lib/parser/gherkin.js'); -// Match all routes with something after the slash -// and display an individual feature. -router.get(/^\/(.+)/, function(req, res, next) { - var featureFilePath = req.params[0]; +// Display an individual feature in a project. +// htpp://host// +router.get('/:projectName/*', function(req, res, next) { + var projectName = req.params.projectName; + var filePath = req.params[0]; // Skip the rendering for query param ?plain=true ?plain=1 etc. var renderPlainFile = req.query.plain === 'true' || !!parseInt(req.query.plain); - getFeatureFile(featureFilePath) + getFeatureFile(path.join(projectName, filePath)) .then(function(fileContents) { var parser; var features; - var isFeatureFile = /.*\.feature/.test(featureFilePath); - var isMarkdownFile = /.*\.md/.test(featureFilePath); + var isFeatureFile = /.*\.feature/.test(filePath); + var isMarkdownFile = /.*\.md/.test(filePath); if (isFeatureFile && !renderPlainFile) { parser = new GherkinParser(); diff --git a/routes/features.js b/routes/features.js deleted file mode 100644 index 0368d0c..0000000 --- a/routes/features.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; -/* eslint new-cap: 0 */ - -var express = require('express'); -var router = express.Router(); - -var Q = require('q'); -var getProjectMetaData = require('../lib/specifications/projectMetaData').getAll; -var updateProject = require('../lib/specifications/getProject').update; - -// List of available features in each known project. -router.get('/', function(req, res, next) { - var projectToUpdate = req.query.update; - - function render(projectData) { - var data = {}; - if (projectData.length) { - data = {projects: projectData} - } - res.render('features', data); - } - - function passError(err) { - next(err) - } - - if (projectToUpdate) { - updateProject(projectToUpdate) - .then(getProjectMetaData) - .then(render) - .catch(passError); - } else { - getProjectMetaData() - .then(render) - .catch(passError); - } -}); - -module.exports = router; diff --git a/routes/project.js b/routes/project.js new file mode 100644 index 0000000..90b3175 --- /dev/null +++ b/routes/project.js @@ -0,0 +1,44 @@ +"use strict"; +/* eslint new-cap: 0 */ + +var express = require('express'); +var router = express.Router(); + +var Q = require('q'); +var getProjectMetaDataByName = require('../lib/specifications/projectMetaData').getByName; +var updateProject = require('../lib/specifications/getProject').update; + +// List of available features in a project. +router.get('/:projectName', function(req, res, next) { + + var projectName = req.params.projectName; + + var projectShouldUpdate = (req.query.update === 'true' || !!parseInt(req.query.update)); + + function render(projectData) { + var data = {}; + if (projectData) { + data = {project: projectData} + } + res.render('project', data); + } + + function passError(err) { + next(err) + } + + if (projectShouldUpdate) { + updateProject(projectName) + .then(function() { + return getProjectMetaDataByName(projectName); + }) + .then(render) + .catch(passError); + } else { + getProjectMetaDataByName(projectName) + .then(render) + .catch(passError); + } +}); + +module.exports = router; diff --git a/routes/get-features.js b/routes/projects.js similarity index 74% rename from routes/get-features.js rename to routes/projects.js index 047e23c..3db4b4c 100644 --- a/routes/get-features.js +++ b/routes/projects.js @@ -4,15 +4,19 @@ var express = require('express'); var router = express.Router(); + var getProject = require('../lib/specifications/getProject'); var getProjectMetaData = require('../lib/specifications/projectMetaData').getAll; + +// Projects page. +// http://host/ router.get('/', function(req, res, next) { var repoUrl = req.query.repo_url; var projects; // If there is no URL query param then - // render the index page. + // render the projects page. if (!repoUrl) { getProjectMetaData() .then(function(projectData) { @@ -20,7 +24,7 @@ router.get('/', function(req, res, next) { if (projectData.length) { data = {projects: projectData} } - res.render('get-features', data); + res.render('projects', data); }) .catch(function(err) { // Pass on to the error handling route. @@ -29,12 +33,12 @@ router.get('/', function(req, res, next) { return; } - // Else get the project and load the features page. + // Else get the project and load the individual project page. getProject.get(repoUrl) - .then(function() { + .then(function(projectMetadata) { - // Redirect to the features page. - res.redirect('/features'); + // Redirect to the project page. + res.redirect(projectMetadata.projectLink); }) .catch(function(err) { // Pass on to the error handling route. @@ -42,4 +46,5 @@ router.get('/', function(req, res, next) { }); }); + module.exports = router; diff --git a/views/features.hbs b/views/features.hbs deleted file mode 100644 index 8ce5739..0000000 --- a/views/features.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{> layout_start_document}} - -{{> layout_start_body}} -
-
-

Available Specifications

-
- {{#each projects}} -
-
-

- {{this.name}} -

- @{{this.shortCommit}} - (visit repository) - -
- -
- {{else}} -

Sorry, no projects were found

- {{/each}} -
-{{> layout_end}} diff --git a/views/partials/header-navigation.hbs b/views/partials/header-navigation.hbs index 516818a..5e51469 100644 --- a/views/partials/header-navigation.hbs +++ b/views/partials/header-navigation.hbs @@ -3,7 +3,6 @@
  1. Specs
  2. -
  3. features

diff --git a/views/project.hbs b/views/project.hbs new file mode 100644 index 0000000..09835bf --- /dev/null +++ b/views/project.hbs @@ -0,0 +1,28 @@ +{{> layout_start_document}} + +{{> layout_start_body}} +
+
+

Specifications

+
+ {{#if project}} +
+
+

{{project.name}}

+ ( @{{project.shortCommit}} ) + visit repository + +
+ +
+ {{else}} +

Sorry, no project data was found

+ {{/if}} +
+{{> layout_end}} diff --git a/views/get-features.hbs b/views/projects.hbs similarity index 69% rename from views/get-features.hbs rename to views/projects.hbs index bb01f76..0c5428a 100644 --- a/views/get-features.hbs +++ b/views/projects.hbs @@ -1,17 +1,18 @@ {{> layout_start_document}} - + {{> layout_start_body}} -
-

Get features from a project

+
+

Projects

+

Add a Project

- +
-

Projects

+

Available Projects

{{#if projects}}
    {{#each projects}}