Starting to add schema validation to API endpoints
							parent
							
								
									de5a01c671
								
							
						
					
					
						commit
						fc1580ddd6
					
				
							
								
								
									
										42
									
								
								app.js
								
								
								
								
							
							
						
						
									
										42
									
								
								app.js
								
								
								
								
							|  | @ -13,6 +13,7 @@ const colors = require('colors'); | ||||||
| const cron = require('node-cron'); | const cron = require('node-cron'); | ||||||
| const common = require('./lib/common'); | const common = require('./lib/common'); | ||||||
| const { runIndexing } = require('./lib/indexing'); | const { runIndexing } = require('./lib/indexing'); | ||||||
|  | const { addSchemas } = require('./lib/schema'); | ||||||
| const { initDb } = require('./lib/db'); | const { initDb } = require('./lib/db'); | ||||||
| let handlebars = require('express-handlebars'); | let handlebars = require('express-handlebars'); | ||||||
| 
 | 
 | ||||||
|  | @ -78,7 +79,7 @@ app.set('view engine', 'hbs'); | ||||||
| // helpers for the handlebar templating platform
 | // helpers for the handlebar templating platform
 | ||||||
| handlebars = handlebars.create({ | handlebars = handlebars.create({ | ||||||
|     helpers: { |     helpers: { | ||||||
|         perRowClass: function(numProducts){ |         perRowClass: (numProducts) => { | ||||||
|             if(parseInt(numProducts) === 1){ |             if(parseInt(numProducts) === 1){ | ||||||
|                 return'col-md-12 col-xl-12 col m12 xl12 product-item'; |                 return'col-md-12 col-xl-12 col m12 xl12 product-item'; | ||||||
|             } |             } | ||||||
|  | @ -94,7 +95,7 @@ handlebars = handlebars.create({ | ||||||
| 
 | 
 | ||||||
|             return'col-md-6 col-xl-6 col m6 xl6 product-item'; |             return'col-md-6 col-xl-6 col m6 xl6 product-item'; | ||||||
|         }, |         }, | ||||||
|         menuMatch: function(title, search){ |         menuMatch: (title, search) => { | ||||||
|             if(!title || !search){ |             if(!title || !search){ | ||||||
|                 return''; |                 return''; | ||||||
|             } |             } | ||||||
|  | @ -103,22 +104,22 @@ handlebars = handlebars.create({ | ||||||
|             } |             } | ||||||
|             return''; |             return''; | ||||||
|         }, |         }, | ||||||
|         getTheme: function(view){ |         getTheme: (view) => { | ||||||
|             return`themes/${config.theme}/${view}`; |             return`themes/${config.theme}/${view}`; | ||||||
|         }, |         }, | ||||||
|         formatAmount: function(amt){ |         formatAmount: (amt) => { | ||||||
|             if(amt){ |             if(amt){ | ||||||
|                 return numeral(amt).format('0.00'); |                 return numeral(amt).format('0.00'); | ||||||
|             } |             } | ||||||
|             return'0.00'; |             return'0.00'; | ||||||
|         }, |         }, | ||||||
|         amountNoDecimal: function(amt){ |         amountNoDecimal: (amt) => { | ||||||
|             if(amt){ |             if(amt){ | ||||||
|                 return handlebars.helpers.formatAmount(amt).replace('.', ''); |                 return handlebars.helpers.formatAmount(amt).replace('.', ''); | ||||||
|             } |             } | ||||||
|             return handlebars.helpers.formatAmount(amt); |             return handlebars.helpers.formatAmount(amt); | ||||||
|         }, |         }, | ||||||
|         getStatusColor: function (status){ |         getStatusColor: (status) => { | ||||||
|             switch(status){ |             switch(status){ | ||||||
|             case'Paid': |             case'Paid': | ||||||
|                 return'success'; |                 return'success'; | ||||||
|  | @ -138,52 +139,58 @@ handlebars = handlebars.create({ | ||||||
|                 return'danger'; |                 return'danger'; | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         checkProductOptions: function (opts){ |         checkProductOptions: (opts) => { | ||||||
|             if(opts){ |             if(opts){ | ||||||
|                 return'true'; |                 return'true'; | ||||||
|             } |             } | ||||||
|             return'false'; |             return'false'; | ||||||
|         }, |         }, | ||||||
|         currencySymbol: function(value){ |         currencySymbol: (value) => { | ||||||
|             if(typeof value === 'undefined' || value === ''){ |             if(typeof value === 'undefined' || value === ''){ | ||||||
|                 return'$'; |                 return'$'; | ||||||
|             } |             } | ||||||
|             return value; |             return value; | ||||||
|         }, |         }, | ||||||
|         objectLength: function(obj){ |         objectLength: (obj) => { | ||||||
|             if(obj){ |             if(obj){ | ||||||
|                 return Object.keys(obj).length; |                 return Object.keys(obj).length; | ||||||
|             } |             } | ||||||
|             return 0; |             return 0; | ||||||
|         }, |         }, | ||||||
|         checkedState: function (state){ |         stringify: (obj) => { | ||||||
|  |             if(obj){ | ||||||
|  |                 return JSON.stringify(obj); | ||||||
|  |             } | ||||||
|  |             return''; | ||||||
|  |         }, | ||||||
|  |         checkedState: (state) => { | ||||||
|             if(state === 'true' || state === true){ |             if(state === 'true' || state === true){ | ||||||
|                 return'checked'; |                 return'checked'; | ||||||
|             } |             } | ||||||
|             return''; |             return''; | ||||||
|         }, |         }, | ||||||
|         selectState: function (state, value){ |         selectState: (state, value) => { | ||||||
|             if(state === value){ |             if(state === value){ | ||||||
|                 return'selected'; |                 return'selected'; | ||||||
|             } |             } | ||||||
|             return''; |             return''; | ||||||
|         }, |         }, | ||||||
|         isNull: function (value, options){ |         isNull: (value, options) => { | ||||||
|             if(typeof value === 'undefined' || value === ''){ |             if(typeof value === 'undefined' || value === ''){ | ||||||
|                 return options.fn(this); |                 return options.fn(this); | ||||||
|             } |             } | ||||||
|             return options.inverse(this); |             return options.inverse(this); | ||||||
|         }, |         }, | ||||||
|         toLower: function (value){ |         toLower: (value) => { | ||||||
|             if(value){ |             if(value){ | ||||||
|                 return value.toLowerCase(); |                 return value.toLowerCase(); | ||||||
|             } |             } | ||||||
|             return null; |             return null; | ||||||
|         }, |         }, | ||||||
|         formatDate: function (date, format){ |         formatDate: (date, format) => { | ||||||
|             return moment(date).format(format); |             return moment(date).format(format); | ||||||
|         }, |         }, | ||||||
|         ifCond: function (v1, operator, v2, options){ |         ifCond: (v1, operator, v2, options) => { | ||||||
|             switch(operator){ |             switch(operator){ | ||||||
|             case'==': |             case'==': | ||||||
|                 return(v1 === v2) ? options.fn(this) : options.inverse(this); |                 return(v1 === v2) ? options.fn(this) : options.inverse(this); | ||||||
|  | @ -207,7 +214,7 @@ handlebars = handlebars.create({ | ||||||
|                 return options.inverse(this); |                 return options.inverse(this); | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         isAnAdmin: function (value, options){ |         isAnAdmin: (value, options) => { | ||||||
|             if(value === 'true' || value === true){ |             if(value === 'true' || value === true){ | ||||||
|                 return options.fn(this); |                 return options.fn(this); | ||||||
|             } |             } | ||||||
|  | @ -355,6 +362,9 @@ initDb(config.databaseConnectionString, async (err, db) => { | ||||||
|         config.trackStock = true; |         config.trackStock = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Process schemas
 | ||||||
|  |     await addSchemas(); | ||||||
|  | 
 | ||||||
|     // We index when not in test env
 |     // We index when not in test env
 | ||||||
|     if(process.env.NODE_ENV !== 'test'){ |     if(process.env.NODE_ENV !== 'test'){ | ||||||
|         try{ |         try{ | ||||||
|  |  | ||||||
|  | @ -40,6 +40,18 @@ const mongoSanitize = (param) => { | ||||||
|     return param; |     return param; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const safeParseInt = (param) => { | ||||||
|  |     if(param){ | ||||||
|  |         try{ | ||||||
|  |             return parseInt(param); | ||||||
|  |         }catch(ex){ | ||||||
|  |             return param; | ||||||
|  |         } | ||||||
|  |     }else{ | ||||||
|  |         return param; | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const checkboxBool = (param) => { | const checkboxBool = (param) => { | ||||||
|     if(param && param === 'on'){ |     if(param && param === 'on'){ | ||||||
|         return true; |         return true; | ||||||
|  | @ -47,6 +59,13 @@ const checkboxBool = (param) => { | ||||||
|     return false; |     return false; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const convertBool = (value) => { | ||||||
|  |     if(value === 'true' || value === true){ | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const showCartCloseBtn = (page) => { | const showCartCloseBtn = (page) => { | ||||||
|     let showCartCloseButton = true; |     let showCartCloseButton = true; | ||||||
|     if(page === 'checkout' || page === 'pay'){ |     if(page === 'checkout' || page === 'pay'){ | ||||||
|  | @ -512,7 +531,9 @@ module.exports = { | ||||||
|     fileSizeLimit, |     fileSizeLimit, | ||||||
|     cleanHtml, |     cleanHtml, | ||||||
|     mongoSanitize, |     mongoSanitize, | ||||||
|  |     safeParseInt, | ||||||
|     checkboxBool, |     checkboxBool, | ||||||
|  |     convertBool, | ||||||
|     showCartCloseBtn, |     showCartCloseBtn, | ||||||
|     addSitemapProducts, |     addSitemapProducts, | ||||||
|     clearSessionValue, |     clearSessionValue, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | const path = require('path'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const _ = require('lodash'); | ||||||
|  | const Validator = require('jsonschema').Validator; | ||||||
|  | const v = new Validator(); | ||||||
|  | const glob = require('glob-fs')(); | ||||||
|  | 
 | ||||||
|  | const addSchemas = () => { | ||||||
|  |     const schemaFiles = glob.readdirSync('./lib/**/*.json'); | ||||||
|  |     _.forEach(schemaFiles, (file) => { | ||||||
|  |         const fileData = JSON.parse(fs.readFileSync(file, 'utf-8')); | ||||||
|  |         v.addSchema(fileData, path.basename(schemaFiles[0], '.json')); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const validateJson = (schema, json) => { | ||||||
|  |     return v.validate(json, schema); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |     addSchemas, | ||||||
|  |     validateJson | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | { | ||||||
|  |     "id": "/editProduct", | ||||||
|  |     "type": "object", | ||||||
|  |     "properties": { | ||||||
|  |         "productPermalink": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productTitle": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productPrice": { | ||||||
|  |             "type": "number" | ||||||
|  |         }, | ||||||
|  |         "productDescription": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productPublished": { | ||||||
|  |             "type": "boolean" | ||||||
|  |         }, | ||||||
|  |         "productTags": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productOptions": { | ||||||
|  |             "type": ["object", "string", "null"] | ||||||
|  |         }, | ||||||
|  |         "productComment": { | ||||||
|  |             "type": "boolean" | ||||||
|  |         }, | ||||||
|  |         "productStock": { | ||||||
|  |             "type": ["number", "null"] | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "required": [ | ||||||
|  |         "productPermalink", | ||||||
|  |         "productTitle", | ||||||
|  |         "productPrice", | ||||||
|  |         "productDescription", | ||||||
|  |         "productPublished" | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | { | ||||||
|  |     "id": "/newProduct", | ||||||
|  |     "type": "object", | ||||||
|  |     "properties": { | ||||||
|  |         "productPermalink": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productTitle": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productPrice": { | ||||||
|  |             "type": "number" | ||||||
|  |         }, | ||||||
|  |         "productDescription": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productPublished": { | ||||||
|  |             "type": "boolean" | ||||||
|  |         }, | ||||||
|  |         "productTags": { | ||||||
|  |             "type": "string" | ||||||
|  |         }, | ||||||
|  |         "productOptions": { | ||||||
|  |             "type": ["object", "string", "null"] | ||||||
|  |         }, | ||||||
|  |         "productComment": { | ||||||
|  |             "type": "boolean" | ||||||
|  |         }, | ||||||
|  |         "productStock": { | ||||||
|  |             "type": ["number", "null"] | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "required": [ | ||||||
|  |         "productPermalink", | ||||||
|  |         "productTitle", | ||||||
|  |         "productPrice", | ||||||
|  |         "productDescription", | ||||||
|  |         "productPublished" | ||||||
|  |     ] | ||||||
|  | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -24,8 +24,10 @@ | ||||||
|     "express-handlebars": "^3.1.0", |     "express-handlebars": "^3.1.0", | ||||||
|     "express-session": "^1.16.1", |     "express-session": "^1.16.1", | ||||||
|     "glob": "^7.1.4", |     "glob": "^7.1.4", | ||||||
|  |     "glob-fs": "^0.1.7", | ||||||
|     "helmet": "^3.18.0", |     "helmet": "^3.18.0", | ||||||
|     "html-entities": "^1.2.0", |     "html-entities": "^1.2.0", | ||||||
|  |     "jsonschema": "^1.2.4", | ||||||
|     "lodash": "^4.17.11", |     "lodash": "^4.17.11", | ||||||
|     "lunr": "^2.3.6", |     "lunr": "^2.3.6", | ||||||
|     "mime-db": "^1.40.0", |     "mime-db": "^1.40.0", | ||||||
|  |  | ||||||
|  | @ -103,7 +103,7 @@ $(document).ready(function (){ | ||||||
|         $(this).addClass('table table-hover'); |         $(this).addClass('table table-hover'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $('#frmProductTags').tokenfield(); |     $('#productTags').tokenfield(); | ||||||
| 
 | 
 | ||||||
|     $(document).on('click', '.dashboard_list', function(e){ |     $(document).on('click', '.dashboard_list', function(e){ | ||||||
|         window.document.location = $(this).attr('href'); |         window.document.location = $(this).attr('href'); | ||||||
|  | @ -245,8 +245,8 @@ $(document).ready(function (){ | ||||||
|         var optOptions = $('#product_optOptions').val(); |         var optOptions = $('#product_optOptions').val(); | ||||||
| 
 | 
 | ||||||
|         var optJson = {}; |         var optJson = {}; | ||||||
|         if($('#productOptJson').val() !== ''){ |         if($('#productOptions').val() !== ''){ | ||||||
|             optJson = JSON.parse($('#productOptJson').val()); |             optJson = JSON.parse($('#productOptions').val()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var html = '<li class="list-group-item">'; |         var html = '<li class="list-group-item">'; | ||||||
|  | @ -271,7 +271,7 @@ $(document).ready(function (){ | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // write new json back to field
 |         // write new json back to field
 | ||||||
|         $('#productOptJson').val(JSON.stringify(optJson)); |         $('#productOptions').val(JSON.stringify(optJson)); | ||||||
| 
 | 
 | ||||||
|         // clear inputs
 |         // clear inputs
 | ||||||
|         $('#product_optName').val(''); |         $('#product_optName').val(''); | ||||||
|  | @ -593,14 +593,15 @@ $(document).ready(function (){ | ||||||
| 
 | 
 | ||||||
| 	// Call to API to check if a permalink is available
 | 	// Call to API to check if a permalink is available
 | ||||||
|     $(document).on('click', '#validate_permalink', function(e){ |     $(document).on('click', '#validate_permalink', function(e){ | ||||||
|         if($('#frmProductPermalink').val() !== ''){ |         if($('#productPermalink').val() !== ''){ | ||||||
|             $.ajax({ |             $.ajax({ | ||||||
|                 method: 'POST', |                 method: 'POST', | ||||||
|                 url: '/admin/api/validate_permalink', |                 url: '/admin/api/validate_permalink', | ||||||
|                 data: { 'permalink': $('#frmProductPermalink').val(), 'docId': $('#frmProductId').val() } |                 data: { 'permalink': $('#productPermalink').val(), 'docId': $('#frmProductId').val() } | ||||||
|             }) |             }) | ||||||
|             .done(function(msg){ |             .done(function(msg){ | ||||||
|                 showNotification(msg, 'success'); |                 console.log('msg', msg); | ||||||
|  |                 showNotification(msg.message, 'success'); | ||||||
|             }) |             }) | ||||||
|             .fail(function(msg){ |             .fail(function(msg){ | ||||||
|                 showNotification(msg.responseJSON.message, 'danger'); |                 showNotification(msg.responseJSON.message, 'danger'); | ||||||
|  | @ -654,8 +655,8 @@ $(document).ready(function (){ | ||||||
| 
 | 
 | ||||||
|     // create a permalink from the product title if no permalink has already been set
 |     // create a permalink from the product title if no permalink has already been set
 | ||||||
|     $(document).on('click', '#frm_edit_product_save', function(e){ |     $(document).on('click', '#frm_edit_product_save', function(e){ | ||||||
|         if($('#frmProductPermalink').val() === '' && $('#frmProductTitle').val() !== ''){ |         if($('#productPermalink').val() === '' && $('#productTitle').val() !== ''){ | ||||||
|             $('#frmProductPermalink').val(slugify($('#frmProductTitle').val())); |             $('#productPermalink').val(slugify($('#productTitle').val())); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -147,10 +147,7 @@ router.get('/product/:id', (req, res) => { | ||||||
|         if(err || result == null || result.productPublished === 'false'){ |         if(err || result == null || result.productPublished === 'false'){ | ||||||
|             res.render('error', { title: 'Not found', message: 'Product not found', helpers: req.handlebars.helpers, config }); |             res.render('error', { title: 'Not found', message: 'Product not found', helpers: req.handlebars.helpers, config }); | ||||||
|         }else{ |         }else{ | ||||||
|             let productOptions = {}; |             let productOptions = result.productOptions; | ||||||
|             if(result.productOptions){ |  | ||||||
|                 productOptions = JSON.parse(result.productOptions); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             // If JSON query param return json instead
 |             // If JSON query param return json instead
 | ||||||
|             if(req.query.json === 'true'){ |             if(req.query.json === 'true'){ | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const common = require('../lib/common'); | const common = require('../lib/common'); | ||||||
| const { restrict, checkAccess } = require('../lib/auth'); | const { restrict, checkAccess } = require('../lib/auth'); | ||||||
|  | const { indexProducts } = require('../lib/indexing'); | ||||||
|  | const { validateJson } = require('../lib/schema'); | ||||||
| const colors = require('colors'); | const colors = require('colors'); | ||||||
| const rimraf = require('rimraf'); | const rimraf = require('rimraf'); | ||||||
| const fs = require('fs'); | const fs = require('fs'); | ||||||
|  | @ -78,76 +80,132 @@ router.get('/admin/product/new', restrict, checkAccess, (req, res) => { | ||||||
| router.post('/admin/product/insert', restrict, checkAccess, (req, res) => { | router.post('/admin/product/insert', restrict, checkAccess, (req, res) => { | ||||||
|     const db = req.app.db; |     const db = req.app.db; | ||||||
| 
 | 
 | ||||||
|  |     // Process supplied options
 | ||||||
|  |     let productOptions = req.body.productOptions; | ||||||
|  |     if(productOptions && typeof productOptions !== 'object'){ | ||||||
|  |         try{ | ||||||
|  |             productOptions = JSON.parse(req.body.productOptions); | ||||||
|  |         }catch(ex){ | ||||||
|  |             console.log('Failure to parse options'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     let doc = { |     let doc = { | ||||||
|         productPermalink: req.body.frmProductPermalink, |         productPermalink: req.body.productPermalink, | ||||||
|         productTitle: common.cleanHtml(req.body.frmProductTitle), |         productTitle: common.cleanHtml(req.body.productTitle), | ||||||
|         productPrice: req.body.frmProductPrice, |         productPrice: common.safeParseInt(req.body.productPrice), | ||||||
|         productDescription: common.cleanHtml(req.body.frmProductDescription), |         productDescription: common.cleanHtml(req.body.productDescription), | ||||||
|         productPublished: req.body.frmProductPublished, |         productPublished: common.convertBool(req.body.productPublished), | ||||||
|         productTags: req.body.frmProductTags, |         productTags: req.body.productTags, | ||||||
|         productOptions: common.cleanHtml(req.body.productOptJson), |         productOptions: productOptions || null, | ||||||
|         productComment: common.checkboxBool(req.body.frmProductComment), |         productComment: common.checkboxBool(req.body.productComment), | ||||||
|         productAddedDate: new Date(), |         productAddedDate: new Date(), | ||||||
|         productStock: req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null |         productStock: common.safeParseInt(req.body.productStock) || null | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     db.products.count({ 'productPermalink': req.body.frmProductPermalink }, (err, product) => { |     // Validate the body again schema
 | ||||||
|  |     const schemaResult = validateJson('newProduct', doc); | ||||||
|  |     if(!schemaResult.valid){ | ||||||
|  |         // If API request, return json
 | ||||||
|  |         if(req.apiAuthenticated){ | ||||||
|  |             res.status(400).json(schemaResult.errors); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log('schemaResult errors', schemaResult.errors); | ||||||
|  |         req.session.message = 'Form invalid. Please check values and try again.'; | ||||||
|  |         req.session.messageType = 'danger'; | ||||||
|  | 
 | ||||||
|  |         // keep the current stuff
 | ||||||
|  |         req.session.productTitle = req.body.productTitle; | ||||||
|  |         req.session.productDescription = req.body.productDescription; | ||||||
|  |         req.session.productPrice = req.body.productPrice; | ||||||
|  |         req.session.productPermalink = req.body.productPermalink; | ||||||
|  |         req.session.productOptions = productOptions; | ||||||
|  |         req.session.productComment = common.checkboxBool(req.body.productComment); | ||||||
|  |         req.session.productTags = req.body.productTags; | ||||||
|  |         req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null; | ||||||
|  | 
 | ||||||
|  |         // redirect to insert
 | ||||||
|  |         res.redirect('/admin/product/new'); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     db.products.count({ 'productPermalink': req.body.productPermalink }, (err, product) => { | ||||||
|         if(err){ |         if(err){ | ||||||
|             console.info(err.stack); |             console.info(err.stack); | ||||||
|         } |         } | ||||||
|         if(product > 0 && req.body.frmProductPermalink !== ''){ |         if(product > 0 && req.body.productPermalink !== ''){ | ||||||
|             // permalink exits
 |             // permalink exits
 | ||||||
|             req.session.message = 'Permalink already exists. Pick a new one.'; |             req.session.message = 'Permalink already exists. Pick a new one.'; | ||||||
|             req.session.messageType = 'danger'; |             req.session.messageType = 'danger'; | ||||||
| 
 | 
 | ||||||
|             // keep the current stuff
 |             // keep the current stuff
 | ||||||
|             req.session.productTitle = req.body.frmProductTitle; |             req.session.productTitle = req.body.productTitle; | ||||||
|             req.session.productDescription = req.body.frmProductDescription; |             req.session.productDescription = req.body.productDescription; | ||||||
|             req.session.productPrice = req.body.frmProductPrice; |             req.session.productPrice = req.body.productPrice; | ||||||
|             req.session.productPermalink = req.body.frmProductPermalink; |             req.session.productPermalink = req.body.productPermalink; | ||||||
|             req.session.productPermalink = req.body.productOptJson; |             req.session.productOptions = productOptions; | ||||||
|             req.session.productComment = common.checkboxBool(req.body.frmProductComment); |             req.session.productComment = common.checkboxBool(req.body.productComment); | ||||||
|             req.session.productTags = req.body.frmProductTags; |             req.session.productTags = req.body.productTags; | ||||||
|             req.session.productStock = req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null; |             req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null; | ||||||
|  | 
 | ||||||
|  |             // If API request, return json
 | ||||||
|  |             if(req.apiAuthenticated){ | ||||||
|  |                 res.status(400).json({ error: 'Permalink already exists. Pick a new one.' }); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             // redirect to insert
 |             // redirect to insert
 | ||||||
|             res.redirect('/admin/insert'); |             res.redirect('/admin/product/new'); | ||||||
|         }else{ |             return; | ||||||
|             db.products.insert(doc, (err, newDoc) => { |  | ||||||
|                 if(err){ |  | ||||||
|                     console.log(colors.red('Error inserting document: ' + err)); |  | ||||||
| 
 |  | ||||||
|                     // keep the current stuff
 |  | ||||||
|                     req.session.productTitle = req.body.frmProductTitle; |  | ||||||
|                     req.session.productDescription = req.body.frmProductDescription; |  | ||||||
|                     req.session.productPrice = req.body.frmProductPrice; |  | ||||||
|                     req.session.productPermalink = req.body.frmProductPermalink; |  | ||||||
|                     req.session.productPermalink = req.body.productOptJson; |  | ||||||
|                     req.session.productComment = common.checkboxBool(req.body.frmProductComment); |  | ||||||
|                     req.session.productTags = req.body.frmProductTags; |  | ||||||
|                     req.session.productStock = req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null; |  | ||||||
| 
 |  | ||||||
|                     req.session.message = 'Error: Inserting product'; |  | ||||||
|                     req.session.messageType = 'danger'; |  | ||||||
| 
 |  | ||||||
|                     // redirect to insert
 |  | ||||||
|                     res.redirect('/admin/product/new'); |  | ||||||
|                 }else{ |  | ||||||
|                     // get the new ID
 |  | ||||||
|                     let newId = newDoc.insertedIds[0]; |  | ||||||
| 
 |  | ||||||
|                     // add to lunr index
 |  | ||||||
|                     common.indexProducts(req.app) |  | ||||||
|                     .then(() => { |  | ||||||
|                         req.session.message = 'New product successfully created'; |  | ||||||
|                         req.session.messageType = 'success'; |  | ||||||
| 
 |  | ||||||
|                         // redirect to new doc
 |  | ||||||
|                         res.redirect('/admin/product/edit/' + newId); |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         } |         } | ||||||
|  |         db.products.insert(doc, (err, newDoc) => { | ||||||
|  |             if(err){ | ||||||
|  |                 console.log(colors.red('Error inserting document: ' + err)); | ||||||
|  | 
 | ||||||
|  |                 // keep the current stuff
 | ||||||
|  |                 req.session.productTitle = req.body.productTitle; | ||||||
|  |                 req.session.productDescription = req.body.productDescription; | ||||||
|  |                 req.session.productPrice = req.body.productPrice; | ||||||
|  |                 req.session.productPermalink = req.body.productPermalink; | ||||||
|  |                 req.session.productOptions = productOptions; | ||||||
|  |                 req.session.productComment = common.checkboxBool(req.body.productComment); | ||||||
|  |                 req.session.productTags = req.body.productTags; | ||||||
|  |                 req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null; | ||||||
|  | 
 | ||||||
|  |                 req.session.message = 'Error: Inserting product'; | ||||||
|  |                 req.session.messageType = 'danger'; | ||||||
|  | 
 | ||||||
|  |                 // If API request, return json
 | ||||||
|  |                 if(req.apiAuthenticated){ | ||||||
|  |                     res.status(400).json({ error: `Error inserting document: ${err}` }); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // redirect to insert
 | ||||||
|  |                 res.redirect('/admin/product/new'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // get the new ID
 | ||||||
|  |             let newId = newDoc.insertedIds[0]; | ||||||
|  | 
 | ||||||
|  |             // add to lunr index
 | ||||||
|  |             indexProducts(req.app) | ||||||
|  |             .then(() => { | ||||||
|  |                 req.session.message = 'New product successfully created'; | ||||||
|  |                 req.session.messageType = 'success'; | ||||||
|  | 
 | ||||||
|  |                 // If API request, return json
 | ||||||
|  |                 if(req.apiAuthenticated){ | ||||||
|  |                     res.status(200).json({ error: 'New product successfully created' }); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // redirect to new doc
 | ||||||
|  |                 res.redirect('/admin/product/edit/' + newId); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -162,7 +220,7 @@ router.get('/admin/product/edit/:id', restrict, checkAccess, (req, res) => { | ||||||
|             } |             } | ||||||
|             let options = {}; |             let options = {}; | ||||||
|             if(result.productOptions){ |             if(result.productOptions){ | ||||||
|                 options = JSON.parse(result.productOptions); |                 options = result.productOptions; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             res.render('product_edit', { |             res.render('product_edit', { | ||||||
|  | @ -194,7 +252,7 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => { | ||||||
|             res.redirect('/admin/product/edit/' + req.body.frmProductId); |             res.redirect('/admin/product/edit/' + req.body.frmProductId); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         db.products.count({ 'productPermalink': req.body.frmProductPermalink, _id: { $ne: common.getId(product._id) } }, (err, count) => { |         db.products.count({ 'productPermalink': req.body.productPermalink, _id: { $ne: common.getId(product._id) } }, (err, count) => { | ||||||
|             if(err){ |             if(err){ | ||||||
|                 console.info(err.stack); |                 console.info(err.stack); | ||||||
|                 req.session.message = 'Failed updating product.'; |                 req.session.message = 'Failed updating product.'; | ||||||
|  | @ -203,37 +261,75 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if(count > 0 && req.body.frmProductPermalink !== ''){ |             if(count > 0 && req.body.productPermalink !== ''){ | ||||||
|                 // permalink exits
 |                 // permalink exits
 | ||||||
|                 req.session.message = 'Permalink already exists. Pick a new one.'; |                 req.session.message = 'Permalink already exists. Pick a new one.'; | ||||||
|                 req.session.messageType = 'danger'; |                 req.session.messageType = 'danger'; | ||||||
| 
 | 
 | ||||||
|                 // keep the current stuff
 |                 // keep the current stuff
 | ||||||
|                 req.session.productTitle = req.body.frmProductTitle; |                 req.session.productTitle = req.body.productTitle; | ||||||
|                 req.session.productDescription = req.body.frmProductDescription; |                 req.session.productDescription = req.body.productDescription; | ||||||
|                 req.session.productPrice = req.body.frmProductPrice; |                 req.session.productPrice = req.body.productPrice; | ||||||
|                 req.session.productPermalink = req.body.frmProductPermalink; |                 req.session.productPermalink = req.body.productPermalink; | ||||||
|                 req.session.productTags = req.body.frmProductTags; |                 req.session.productTags = req.body.productTags; | ||||||
|                 req.session.productOptions = req.body.productOptJson; |                 req.session.productOptions = req.body.productOptions; | ||||||
|                 req.session.productComment = common.checkboxBool(req.body.frmProductComment); |                 req.session.productComment = common.checkboxBool(req.body.productComment); | ||||||
|                 req.session.productStock = req.body.frmProductStock ? req.body.frmProductStock : null; |                 req.session.productStock = req.body.productStock ? req.body.productStock : null; | ||||||
| 
 | 
 | ||||||
|                 // redirect to insert
 |                 // redirect to insert
 | ||||||
|                 res.redirect('/admin/product/edit/' + req.body.frmProductId); |                 res.redirect('/admin/product/edit/' + req.body.frmProductId); | ||||||
|             }else{ |             }else{         | ||||||
|                 common.getImages(req.body.frmProductId, req, res, (images) => { |                 common.getImages(req.body.frmProductId, req, res, (images) => { | ||||||
|  |                     // Process supplied options
 | ||||||
|  |                     let productOptions = req.body.productOptions; | ||||||
|  |                     if(productOptions && typeof productOptions !== 'object'){ | ||||||
|  |                         try{ | ||||||
|  |                             productOptions = JSON.parse(req.body.productOptions); | ||||||
|  |                         }catch(ex){ | ||||||
|  |                             console.log('Failure to parse options'); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                     let productDoc = { |                     let productDoc = { | ||||||
|                         productTitle: common.cleanHtml(req.body.frmProductTitle), |                         productPermalink: req.body.productPermalink, | ||||||
|                         productDescription: common.cleanHtml(req.body.frmProductDescription), |                         productTitle: common.cleanHtml(req.body.productTitle), | ||||||
|                         productPublished: req.body.frmProductPublished, |                         productPrice: common.safeParseInt(req.body.productPrice), | ||||||
|                         productPrice: req.body.frmProductPrice, |                         productDescription: common.cleanHtml(req.body.productDescription), | ||||||
|                         productPermalink: req.body.frmProductPermalink, |                         productPublished: common.convertBool(req.body.productPublished), | ||||||
|                         productTags: common.cleanHtml(req.body.frmProductTags), |                         productTags: req.body.productTags, | ||||||
|                         productOptions: common.cleanHtml(req.body.productOptJson), |                         productOptions: productOptions || null, | ||||||
|                         productComment: common.checkboxBool(req.body.frmProductComment), |                         productComment: common.checkboxBool(req.body.productComment), | ||||||
|                         productStock: req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null |                         productStock: common.safeParseInt(req.body.productStock) || null | ||||||
|                     }; |                     }; | ||||||
| 
 | 
 | ||||||
|  |                     // Validate the body again schema
 | ||||||
|  |                     const schemaResult = validateJson('editProduct', productDoc); | ||||||
|  |                     if(!schemaResult.valid){ | ||||||
|  |                         // If API request, return json
 | ||||||
|  |                         if(req.apiAuthenticated){ | ||||||
|  |                             res.status(400).json(schemaResult.errors); | ||||||
|  |                             return; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         console.log('schemaResult errors', schemaResult.errors); | ||||||
|  |                         req.session.message = 'Form invalid. Please check values and try again.'; | ||||||
|  |                         req.session.messageType = 'danger'; | ||||||
|  | 
 | ||||||
|  |                         // keep the current stuff
 | ||||||
|  |                         req.session.productTitle = req.body.productTitle; | ||||||
|  |                         req.session.productDescription = req.body.productDescription; | ||||||
|  |                         req.session.productPrice = req.body.productPrice; | ||||||
|  |                         req.session.productPermalink = req.body.productPermalink; | ||||||
|  |                         req.session.productOptions = productOptions; | ||||||
|  |                         req.session.productComment = common.checkboxBool(req.body.productComment); | ||||||
|  |                         req.session.productTags = req.body.productTags; | ||||||
|  |                         req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null; | ||||||
|  | 
 | ||||||
|  |                         // redirect to insert
 | ||||||
|  |                         res.redirect('/admin/product/edit/' + req.body.frmProductId); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                     // if no featured image
 |                     // if no featured image
 | ||||||
|                     if(!product.productImage){ |                     if(!product.productImage){ | ||||||
|                         if(images.length > 0){ |                         if(images.length > 0){ | ||||||
|  | @ -253,7 +349,7 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => { | ||||||
|                             res.redirect('/admin/product/edit/' + req.body.frmProductId); |                             res.redirect('/admin/product/edit/' + req.body.frmProductId); | ||||||
|                         }else{ |                         }else{ | ||||||
|                             // Update the index
 |                             // Update the index
 | ||||||
|                             common.indexProducts(req.app) |                             indexProducts(req.app) | ||||||
|                             .then(() => { |                             .then(() => { | ||||||
|                                 req.session.message = 'Successfully saved'; |                                 req.session.message = 'Successfully saved'; | ||||||
|                                 req.session.messageType = 'success'; |                                 req.session.messageType = 'success'; | ||||||
|  | @ -283,7 +379,7 @@ router.get('/admin/product/delete/:id', restrict, checkAccess, (req, res) => { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // remove the index
 |             // remove the index
 | ||||||
|             common.indexProducts(req.app) |             indexProducts(req.app) | ||||||
|             .then(() => { |             .then(() => { | ||||||
|                 // redirect home
 |                 // redirect home
 | ||||||
|                 req.session.message = 'Product successfully deleted'; |                 req.session.message = 'Product successfully deleted'; | ||||||
|  |  | ||||||
|  | @ -10,24 +10,24 @@ | ||||||
|                 <h2>Edit product</h2> |                 <h2>Edit product</h2> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductTitle" class="col-sm-2 control-label">Product title *</label> |                 <label for="productTitle" class="col-sm-2 control-label">Product title *</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <input type="text" name="frmProductTitle" class="form-control" minlength="5" maxlength="200" value="{{result.productTitle}}" required/> |                     <input type="text" name="productTitle" class="form-control" minlength="5" maxlength="200" value="{{result.productTitle}}" required/> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductPrice" class="col-sm-2 control-label">Product price *</label> |                 <label for="productPrice" class="col-sm-2 control-label">Product price *</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <div class="input-group"> |                     <div class="input-group"> | ||||||
|                         <span class="input-group-addon">{{currencySymbol config.currencySymbol}}</span> |                         <span class="input-group-addon">{{currencySymbol config.currencySymbol}}</span> | ||||||
|                         <input type="number" name="frmProductPrice" class="form-control" value="{{result.productPrice}}" step="any" required/> |                         <input type="number" name="productPrice" class="form-control" value="{{result.productPrice}}" step="any" required/> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductPublished" class="col-sm-2 control-label">Status</label> |                 <label for="productPublished" class="col-sm-2 control-label">Status</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <select class="form-control" id="frmProductPublished" name="frmProductPublished"> |                     <select class="form-control" id="productPublished" name="productPublished"> | ||||||
|                         <option value="true"  {{selectState result.productPublished "true"}}>Published</option> |                         <option value="true"  {{selectState result.productPublished "true"}}>Published</option> | ||||||
|                         <option value="false" {{selectState result.productPublished "false"}}>Draft</option> |                         <option value="false" {{selectState result.productPublished "false"}}>Draft</option> | ||||||
|                     </select> |                     </select> | ||||||
|  | @ -35,23 +35,23 @@ | ||||||
|             </div> |             </div> | ||||||
|             {{#if config.trackStock}} |             {{#if config.trackStock}} | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductStock" class="col-sm-2 control-label">Stock level</label> |                 <label for="productStock" class="col-sm-2 control-label">Stock level</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <input type="number" name="frmProductStock" class="form-control" value="{{result.productStock}}" step="any" /> |                     <input type="number" name="productStock" class="form-control" value="{{result.productStock}}" step="any" /> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             {{/if}} |             {{/if}} | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="editor" class="col-sm-2 control-label">Product description *</label> |                 <label for="editor" class="col-sm-2 control-label">Product description *</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <textarea id="editor" minlength="5" rows="10" id="frmProductDescription" name="frmProductDescription" class="form-control" required>{{result.productDescription}}</textarea> |                     <textarea id="editor" minlength="5" rows="10" id="productDescription" name="productDescription" class="form-control" required>{{result.productDescription}}</textarea> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label class="col-sm-2 control-label">Permalink</label> |                 <label class="col-sm-2 control-label">Permalink</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <div class="input-group"> |                     <div class="input-group"> | ||||||
|                         <input type="text" class="form-control" name="frmProductPermalink" id="frmProductPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}> |                         <input type="text" class="form-control" name="productPermalink" id="productPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}> | ||||||
|                         <span class="input-group-btn"> |                         <span class="input-group-btn"> | ||||||
|                             <button class="btn btn-success" id="validate_permalink" type="button">Validate</button> |                             <button class="btn btn-success" id="validate_permalink" type="button">Validate</button> | ||||||
|                         </span> |                         </span> | ||||||
|  | @ -60,7 +60,7 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <input type="hidden" id="productOptJson" name="productOptJson" value="{{result.productOptions}}" /> |                 <input type="hidden" id="productOptions" name="productOptions" value="{{stringify result.productOptions}}" /> | ||||||
|                 <label for="editor" class="col-sm-2 control-label">Product options</label> |                 <label for="editor" class="col-sm-2 control-label">Product options</label> | ||||||
|                 <div class="col-lg-10"> |                 <div class="col-lg-10"> | ||||||
|                     <ul class="list-group" id="product_opt_wrapper"> |                     <ul class="list-group" id="product_opt_wrapper"> | ||||||
|  | @ -108,21 +108,21 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductComment" class="col-sm-2 control-label">Allow comment</label> |                 <label for="productComment" class="col-sm-2 control-label">Allow comment</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <div class="checkbox"> |                     <div class="checkbox"> | ||||||
|                         <label> |                         <label> | ||||||
|                             <input class="frmProductComment" type="checkbox" {{checkedState result.productComment}} id="frmProductComment" |                             <input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment" | ||||||
|                                 name="frmProductComment"> |                                 name="productComment"> | ||||||
|                         </label> |                         </label> | ||||||
|                     </div> |                     </div> | ||||||
|                     <p class="help-block">Allow free form comments when adding products to cart</p> |                     <p class="help-block">Allow free form comments when adding products to cart</p> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductTags" class="col-sm-2 control-label">Product tag words</label> |                 <label for="productTags" class="col-sm-2 control-label">Product tag words</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <input type="text" class="form-control" id="frmProductTags" name="frmProductTags" value="{{result.productTags}}"> |                     <input type="text" class="form-control" id="productTags" name="productTags" value="{{result.productTags}}"> | ||||||
|                     <p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p> |                     <p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  | @ -9,24 +9,24 @@ | ||||||
|                 <h2>New product</h2> |                 <h2>New product</h2> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductTitle" class="col-sm-2 control-label">Product title *</label> |                 <label for="productTitle" class="col-sm-2 control-label">Product title *</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <input type="text" id="frmProductTitle" name="frmProductTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/> |                     <input type="text" id="productTitle" name="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductPrice" class="col-sm-2 control-label">Product price *</label> |                 <label for="productPrice" class="col-sm-2 control-label">Product price *</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <div class="input-group"> |                     <div class="input-group"> | ||||||
|                         <span class="input-group-addon">$</span> |                         <span class="input-group-addon">$</span> | ||||||
|                         <input type="number" name="frmProductPrice" class="form-control" step="any" value="{{productPrice}}" required/> |                         <input type="number" name="productPrice" class="form-control" step="any" value="{{productPrice}}" required/> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductPublished" class="col-sm-2 control-label">Status</label> |                 <label for="productPublished" class="col-sm-2 control-label">Status</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <select class="form-control" id="frmProductPublished" name="frmProductPublished"> |                     <select class="form-control" id="productPublished" name="productPublished"> | ||||||
|                         <option value="true" selected>Published</option> |                         <option value="true" selected>Published</option> | ||||||
|                         <option value="false">Draft</option> |                         <option value="false">Draft</option> | ||||||
|                     </select> |                     </select> | ||||||
|  | @ -34,23 +34,23 @@ | ||||||
|             </div> |             </div> | ||||||
|             {{#if config.trackStock}} |             {{#if config.trackStock}} | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductStock" class="col-sm-2 control-label">Stock level</label> |                 <label for="productStock" class="col-sm-2 control-label">Stock level</label> | ||||||
|                 <div class="col-sm-6"> |                 <div class="col-sm-6"> | ||||||
|                     <input type="number" name="frmProductStock" class="form-control" value="{{productStock}}" step="any" /> |                     <input type="number" name="productStock" class="form-control" value="{{productStock}}" step="any" /> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             {{/if}} |             {{/if}} | ||||||
|             <div class="form-group" id="editor-wrapper"> |             <div class="form-group" id="editor-wrapper"> | ||||||
|                 <label for="editor" class="col-sm-2 control-label">Product description *</label> |                 <label for="editor" class="col-sm-2 control-label">Product description *</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <textarea id="editor" minlength="5" rows="10" name="frmProductDescription" class="form-control" required>{{productDescription}}</textarea> |                     <textarea id="editor" minlength="5" rows="10" name="productDescription" class="form-control" required>{{productDescription}}</textarea> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label class="col-sm-2 control-label">Permalink</label> |                 <label class="col-sm-2 control-label">Permalink</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <div class="input-group"> |                     <div class="input-group"> | ||||||
|                         <input type="text" class="form-control" name="frmProductPermalink" id="frmProductPermalink" placeholder="Permalink for the article" value={{productPermalink}}> |                         <input type="text" class="form-control" name="productPermalink" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}> | ||||||
|                         <span class="input-group-btn"> |                         <span class="input-group-btn"> | ||||||
|                             <button class="btn btn-success" id="validate_permalink" type="button">Validate</button> |                             <button class="btn btn-success" id="validate_permalink" type="button">Validate</button> | ||||||
|                         </span> |                         </span> | ||||||
|  | @ -59,7 +59,7 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <input type="hidden" id="productOptJson" name="productOptJson" value="{{result.productOptions}}" /> |                 <input type="hidden" id="productOptions" name="productOptions" value="{{result.productOptions}}" /> | ||||||
|                 <label for="editor" class="col-sm-2 control-label">Product options</label> |                 <label for="editor" class="col-sm-2 control-label">Product options</label> | ||||||
|                 <div class="col-lg-10"> |                 <div class="col-lg-10"> | ||||||
|                     <ul class="list-group" id="product_opt_wrapper"> |                     <ul class="list-group" id="product_opt_wrapper"> | ||||||
|  | @ -107,21 +107,21 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductComment" class="col-sm-2 control-label">Allow comment</label> |                 <label for="productComment" class="col-sm-2 control-label">Allow comment</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <div class="checkbox"> |                     <div class="checkbox"> | ||||||
|                         <label> |                         <label> | ||||||
|                             <input class="frmProductComment" type="checkbox" {{checkedState result.productComment}} id="frmProductComment" |                             <input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment" | ||||||
|                                 name="frmProductComment"> |                                 name="productComment"> | ||||||
|                         </label> |                         </label> | ||||||
|                     </div> |                     </div> | ||||||
|                     <p class="help-block">Allow free form comments when adding products to cart</p> |                     <p class="help-block">Allow free form comments when adding products to cart</p> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="form-group"> |             <div class="form-group"> | ||||||
|                 <label for="frmProductTags" class="col-sm-2 control-label">Product tag words</label> |                 <label for="productTags" class="col-sm-2 control-label">Product tag words</label> | ||||||
|                 <div class="col-sm-10"> |                 <div class="col-sm-10"> | ||||||
|                     <input type="text" class="form-control" id="frmProductTags" name="frmProductTags"> |                     <input type="text" class="form-control" id="productTags" name="productTags"> | ||||||
|                     <p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p> |                     <p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  | @ -12,17 +12,17 @@ | ||||||
|                         {{#ifCond this.optType '==' "select"}} |                         {{#ifCond this.optType '==' "select"}} | ||||||
|                             <strong>{{this.optName}}</strong> |                             <strong>{{this.optName}}</strong> | ||||||
|                             <select name="opt-{{this.optName}}" class="form-control product-opt"> |                             <select name="opt-{{this.optName}}" class="form-control product-opt"> | ||||||
|                                 {{#each this.optOptions}} |                                 {{#each ../this.optOptions}} | ||||||
|                                     <option value="{{this}}">{{this}}</option> |                                     <option value="{{this}}">{{this}}</option> | ||||||
|                                 {{/each}} |                                 {{/each}} | ||||||
|                             </select> |                             </select> | ||||||
|                         {{/ifCond}} |                         {{/ifCond}} | ||||||
|                         {{#ifCond this.optType '==' "radio"}} |                         {{#ifCond this.optType '==' "radio"}} | ||||||
|                             {{#each this.optOptions}} |                             {{#each ../this.optOptions}} | ||||||
|                                 <strong>{{this.optName}}</strong> |                                 <strong>{{this.optName}}</strong> | ||||||
|                                 <div class="radio"> |                                 <div class="radio"> | ||||||
|                                     <label> |                                     <label> | ||||||
|                                         <input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}"> |                                         <input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}"> | ||||||
|                                         {{this}} |                                         {{this}} | ||||||
|                                     </label> |                                     </label> | ||||||
|                                 </div> |                                 </div> | ||||||
|  | @ -31,7 +31,7 @@ | ||||||
|                         {{#ifCond this.optType '==' "checkbox"}} |                         {{#ifCond this.optType '==' "checkbox"}} | ||||||
|                             <div class="checkbox"> |                             <div class="checkbox"> | ||||||
|                                 <label> |                                 <label> | ||||||
|                                     <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong> |                                     <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong> | ||||||
|                                 </label> |                                 </label> | ||||||
|                             </div> |                             </div> | ||||||
|                         {{/ifCond}} |                         {{/ifCond}} | ||||||
|  |  | ||||||
|  | @ -12,17 +12,17 @@ | ||||||
|                     {{#ifCond this.optType '==' "select"}} |                     {{#ifCond this.optType '==' "select"}} | ||||||
|                         <strong>{{this.optName}}</strong> |                         <strong>{{this.optName}}</strong> | ||||||
|                         <select name="opt-{{this.optName}}" class="form-control product-opt"> |                         <select name="opt-{{this.optName}}" class="form-control product-opt"> | ||||||
|                             {{#each this.optOptions}} |                             {{#each ../this.optOptions}} | ||||||
|                                 <option value="{{this}}">{{this}}</option> |                                 <option value="{{this}}">{{this}}</option> | ||||||
|                             {{/each}} |                             {{/each}} | ||||||
|                         </select> |                         </select> | ||||||
|                     {{/ifCond}} |                     {{/ifCond}} | ||||||
|                     {{#ifCond this.optType '==' "radio"}} |                     {{#ifCond this.optType '==' "radio"}} | ||||||
|                         {{#each this.optOptions}} |                         {{#each ../this.optOptions}} | ||||||
|                             <strong>{{this.optName}}</strong> |                             <strong>{{this.optName}}</strong> | ||||||
|                             <div class="radio"> |                             <div class="radio"> | ||||||
|                                 <label> |                                 <label> | ||||||
|                                     <input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}"> |                                     <input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}"> | ||||||
|                                     {{this}} |                                     {{this}} | ||||||
|                                 </label> |                                 </label> | ||||||
|                             </div> |                             </div> | ||||||
|  | @ -31,7 +31,7 @@ | ||||||
|                     {{#ifCond this.optType '==' "checkbox"}} |                     {{#ifCond this.optType '==' "checkbox"}} | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label> |                             <label> | ||||||
|                                 <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong> |                                 <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
|                     {{/ifCond}} |                     {{/ifCond}} | ||||||
|  |  | ||||||
|  | @ -12,17 +12,17 @@ | ||||||
|                     {{#ifCond this.optType '==' "select"}} |                     {{#ifCond this.optType '==' "select"}} | ||||||
|                         <strong>{{this.optName}}</strong> |                         <strong>{{this.optName}}</strong> | ||||||
|                         <select name="opt-{{this.optName}}" class="form-control product-opt"> |                         <select name="opt-{{this.optName}}" class="form-control product-opt"> | ||||||
|                             {{#each this.optOptions}} |                             {{#each ../this.optOptions}} | ||||||
|                                 <option value="{{this}}">{{this}}</option> |                                 <option value="{{this}}">{{this}}</option> | ||||||
|                             {{/each}} |                             {{/each}} | ||||||
|                         </select> |                         </select> | ||||||
|                     {{/ifCond}} |                     {{/ifCond}} | ||||||
|                     {{#ifCond this.optType '==' "radio"}} |                     {{#ifCond this.optType '==' "radio"}} | ||||||
|                         {{#each this.optOptions}} |                         {{#each ../this.optOptions}} | ||||||
|                             <strong>{{this.optName}}</strong> |                             <strong>{{this.optName}}</strong> | ||||||
|                             <div class="radio"> |                             <div class="radio"> | ||||||
|                                 <label> |                                 <label> | ||||||
|                                     <input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}"> |                                     <input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}"> | ||||||
|                                     {{this}} |                                     {{this}} | ||||||
|                                 </label> |                                 </label> | ||||||
|                             </div> |                             </div> | ||||||
|  | @ -31,7 +31,7 @@ | ||||||
|                     {{#ifCond this.optType '==' "checkbox"}} |                     {{#ifCond this.optType '==' "checkbox"}} | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label> |                             <label> | ||||||
|                                 <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong> |                                 <input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
|                     {{/ifCond}} |                     {{/ifCond}} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue