Alan Colon 7 år sedan
förälder
incheckning
8361457774

+ 4 - 1
app/api-service.js

@@ -19,6 +19,9 @@ app.service('api', function($http) {
     list: () => api($http.get(`${apiPrefix}`, opts)),
     list: () => api($http.get(`${apiPrefix}`, opts)),
     create: data => api($http.post(`${apiPrefix}`, data, opts)),
     create: data => api($http.post(`${apiPrefix}`, data, opts)),
     read: id => api($http.get(`${apiPrefix}/${id}`, opts)),
     read: id => api($http.get(`${apiPrefix}/${id}`, opts)),
-    update: (id, data) => api($http.put(`${apiPrefix}/${id}`, data, opts))
+    update: (id, data) => api($http.patch(`${apiPrefix}/${id}`, data, opts)),
+    delete: (id) => api($http.delete(`${apiPrefix}/${id}`, opts)),
+    trash: () => api($http.get(`${apiPrefix}/trash`, opts)),
+    undelete: (id) => api($http.delete(`${apiPrefix}/trash/${id}`, opts))
   })
   })
 })
 })

+ 18 - 0
app/app-index.js

@@ -0,0 +1,18 @@
+const app = require('./app')
+// TODO: Register app specific services
+app.config(($routeProvider, $mdThemingProvider) => {
+  // TODO: App Title
+  window.title = 'My App'
+
+  // TODO: Select a theme
+  const palettes = ['red', 'pink', 'purple', 'deep-purple', 'indigo', 'blue', 'light-blue', 'cyan', 'teal', 'green', 'light-green', 'lime', 'yellow', 'amber', 'orange', 'deep-orange', 'brown', 'grey', 'blue-grey']
+  const randomPalette = () => palettes[Math.floor(Math.random() * palettes.length)]
+  $mdThemingProvider.theme('default')
+    .primaryPalette(randomPalette())
+    .accentPalette(randomPalette())
+    .warnPalette(randomPalette())
+
+  // TODO: App Routes
+  //  $routeProvider.when('/test', {template: '<app-test-page />'})
+
+})

+ 0 - 6
app/app.js

@@ -18,12 +18,6 @@ const app = angular.module('app', ['ngRoute', 'ngMaterial', 'async', 'md.data.ta
 app.config(($routeProvider, $locationProvider, $mdThemingProvider) => {
 app.config(($routeProvider, $locationProvider, $mdThemingProvider) => {
   routes($routeProvider)
   routes($routeProvider)
   $locationProvider.html5Mode(true)
   $locationProvider.html5Mode(true)
-  const palettes = ['red', 'pink', 'purple', 'deep-purple', 'indigo', 'blue', 'light-blue', 'cyan', 'teal', 'green', 'light-green', 'lime', 'yellow', 'amber', 'orange', 'deep-orange', 'brown', 'grey', 'blue-grey']
-  const randomPalette = () => palettes[Math.floor(Math.random() * palettes.length)]
-  $mdThemingProvider.theme('default')
-    .primaryPalette(randomPalette())
-    .accentPalette(randomPalette())
-    .warnPalette(randomPalette())
 })
 })
 
 
 
 

+ 1 - 0
app/assets/app-index.js

@@ -0,0 +1 @@
+// TODO: App specific assets

+ 0 - 0
app/assets/app-style.scss


+ 8 - 4
app/assets/index.js

@@ -1,6 +1,9 @@
-module.exports = {
+const appIndex = require('./app-index')
+
+module.exports = Object.assign({
   style: require('./style.scss'),
   style: require('./style.scss'),
-  genericLogo: require('./generic-logo.svg'),
+  appStyle: require('./app-style.scss'),
+  logo: require('./generic-logo.svg'),
   menuIcon: require('@alancnet/material-design-icons/navigation_ic_menu_48px.svg'),
   menuIcon: require('@alancnet/material-design-icons/navigation_ic_menu_48px.svg'),
   userIcon: require('@alancnet/material-design-icons/social_ic_person_48px.svg'),
   userIcon: require('@alancnet/material-design-icons/social_ic_person_48px.svg'),
   addUserIcon: require('@alancnet/material-design-icons/social_ic_person_add_48px.svg'),
   addUserIcon: require('@alancnet/material-design-icons/social_ic_person_add_48px.svg'),
@@ -8,5 +11,6 @@ module.exports = {
   createIcon: require('@alancnet/material-design-icons/content_ic_add_48px.svg'),
   createIcon: require('@alancnet/material-design-icons/content_ic_add_48px.svg'),
   editIcon: require('@alancnet/material-design-icons/content_ic_create_48px.svg'),
   editIcon: require('@alancnet/material-design-icons/content_ic_create_48px.svg'),
   deleteIcon: require('@alancnet/material-design-icons/action_ic_delete_48px.svg'),
   deleteIcon: require('@alancnet/material-design-icons/action_ic_delete_48px.svg'),
-  saveIcon: require('@alancnet/material-design-icons/content_ic_save_48px.svg')
-}
+  saveIcon: require('@alancnet/material-design-icons/content_ic_save_48px.svg'),
+  undoIcon: require('@alancnet/material-design-icons/content_ic_undo_48px.svg')
+}, appIndex)

+ 1 - 0
app/components/app-index.js

@@ -0,0 +1 @@
+// TODO: App specific components

+ 17 - 22
app/components/crud-pages/details.js

@@ -1,43 +1,37 @@
 const _ = require('lodash')
 const _ = require('lodash')
 const app = require('../../app')
 const app = require('../../app')
-const details = ({
-  typeName,
-  typePlural,
-  camelName,
-  camelPlural,
-  paramName,
-  paramPlural,
-  apiPrefix,
-  appPrefix,
-  columns
-}) => {
+/**
+ * @param {CrudPagesOptions} opts 
+ */
+const details = (opts) => {
   const defaultInput = column => html`
   const defaultInput = column => html`
     <md-input-container>
     <md-input-container>
-      <label>${column.fieldName}</label>
+      <label>${column.titleName}</label>
       <input type="${column.type || 'text'}" ng-model="model.${raw(column.camelName)}" />
       <input type="${column.type || 'text'}" ng-model="model.${raw(column.camelName)}" />
     </md-input-container>
     </md-input-container>
   `
   `
 
 
-  app.component(`app${typeName}DetailsPage`, {
+  app.component(`app${opts.pascalName}DetailsPage`, {
 
 
     template: html`
     template: html`
       <app-user-area>
       <app-user-area>
-        <h1>${typeName}</h1>
+        <h1>{{ctrl.isNew ? 'New ${opts.titleName}' : '${opts.titleName} Details'}}</h1>
         <form name="form" ng-submit="ctrl.submit()">
         <form name="form" ng-submit="ctrl.submit()">
-          ${columns.map(c => c.input || defaultInput(c))}
+          ${opts.columns.map(c => c.input || defaultInput(c))}
 
 
           <div>
           <div>
-            <md-button type="submit">Submit</md-button>
+            <md-button type="submit" class="md-raised md-primary">Submit</md-button>
           </div>
           </div>
 
 
         </form>
         </form>
       </app-user-area>
       </app-user-area>
     `,
     `,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',
-    controller: function(api, $scope, $routeParams, $mdToast) {
-      const crud = api.crud(apiPrefix)
+    controller: function(api, $scope, $routeParams, $mdToast, $location) {
+      this.isNew = $routeParams.id === 'new'
+      const crud = api.crud(opts.apiPrefix)
       let original
       let original
-      if ($routeParams.id === 'new') {
+      if (this.isNew) {
         original = {}
         original = {}
         $scope.model = Object.create(original)
         $scope.model = Object.create(original)
       } else {
       } else {
@@ -49,7 +43,7 @@ const details = ({
 
 
       this.submit = async () => {
       this.submit = async () => {
         try {
         try {
-          if ($routeParams.id === 'new') {
+          if (this.isNew) {
             await crud.create($scope.model)
             await crud.create($scope.model)
           } else {
           } else {
             const obj = {}
             const obj = {}
@@ -60,10 +54,11 @@ const details = ({
             }
             }
             await crud.update(original.id, obj)
             await crud.update(original.id, obj)
           }
           }
-          $mdToast.showSimple(`${typeName} saved.`)
+          $mdToast.showSimple(`${opts.titleName} saved.`)
+          $location.url(opts.appPrefix)
         } catch (err) {
         } catch (err) {
           console.error(err)
           console.error(err)
-          $mdToast.showSimple(`Could not save ${typeName}: ${err.message || err}`)
+          $mdToast.showSimple(`Could not save ${opts.titleName}: ${err.message || err}`)
         }
         }
       }
       }
 
 

+ 68 - 40
app/components/crud-pages/index.js

@@ -1,48 +1,76 @@
 const app = require('../../app')
 const app = require('../../app')
-const { pascal, camel, param } = require('change-case')
+const { pascal, camel, param, title } = require('change-case')
 const plural = require('plural')
 const plural = require('plural')
 const list = require('./list')
 const list = require('./list')
 const details = require('./details')
 const details = require('./details')
+const trash = require('./trash')
 
 
-const crudPages = ({
-  typeName,
-  typePlural,
-  camelName,
-  camelPlural,
-  paramName,
-  paramPlural,
-  apiPrefix,
-  appPrefix,
-  columns
-}) => {
-  if (!typeName) throw new Error('typeName is required')
-  if (typeName !== pascal(typeName)) throw new Error('typeName should be PascalCased')
-  if (!typePlural) typePlural = plural(typeName)
-  if (typePlural !== pascal(typePlural)) throw new Error('typePlural should be PascalCased')
-  if (!camelName) camelName = camel(typeName)
-  if (camelName !== camel(camelName)) throw new Error('camelName should be camelCased')
-  if (!camelPlural) camelPlural = plural(camelName)
-  if (camelPlural !== camel(camelPlural)) throw new Error('camelPlural should be camelCased')
-  if (!paramName) paramName = param(typeName)
-  if (paramName !== param(paramName)) throw new Error('paramName should be param-cased')
-  if (!paramPlural) paramPlural = plural(paramName)
-  if (paramPlural !== param(paramPlural)) throw new Error('paramPlural should be param-cased')
-  if (!apiPrefix) apiPrefix = `/api/${paramPlural}`
-  if (!appPrefix) appPrefix = `/${paramPlural}`
-
-  columns = columns.map(column => Object.assign({}, column, {
-    camelName: column.camelName || camel(column.fieldName)
-  }))
-
-  const listComponentName = `app${typePlural}List`
-  const listComponentTag = `app-${paramName}-list`
-  const listPageComponentName = `app${typePlural}Page`
-  const listPageComponentTag = `app-${paramPlural}-page`
-
-  const args = { typeName, typePlural, camelName, camelPlural, paramName, paramPlural, apiPrefix, appPrefix, columns }
-
-  list(args)
-  details(args)
+/** @define CrudPagesOptions
+ * @property {string} titleName Type name in Title Case. This is used for labels like "New {Title Name}"
+ * @property {string} titlePlural Plural type name in Title Case. This is used for labels like "View All {Title Plurals}"
+ * @property {string} pascalName Type name in PascalCase. This is used for Class definitions like "app{PascalName}Page"
+ * @property {string} pascalPlural Plural type name in PascalCase. This is used for Class definitions like "app{PascalPlural}List"
+ * @property {string} camelName Type name in camelCase. This is used for referencing models, like "model.{camelName}"
+ * @property {string} camelPlural Plural type name in camelCase.
+ * @property {string} paramName Type name in param-case.
+ * @property {string} paramPlural Plural type name in param-case. This is used for urls, like "/api/{param-plural}"
+ * @property {string} apiPrefix API url path prefix. Default: /api/{param-plural}
+ * @property {string} appPrefix APP url path prefix. Default: /{param-plural}
+ * @property {CrudColumnOptions[]} columns
+ */
+
+/** @define CrudColumnOptions
+ * @property {string} titleName Field name in Title Case. This is used for field labels.
+ * @property {string} camelName Field name in camelCase. This is used to reference data in the model.
+ * @property {string} header HTML template for the list table header.
+ * @property {string} cell HTML template for the list table cell
+ * @property {string} type Field type. Can be applied to <input type="{type}" /> or used to determine a renderer.
+ * @property {boolean} inList Default: true. Includes column in list page.
+ */
+
+/**
+ * @param {CrudPagesOptions} opts 
+ */
+const crudPages = (opts) => {
+  if (!opts.pascalName) opts.pascalName = pascal(opts.titleName || opts.camelName || opts.paramName || '')
+  if (!opts.pascalName) throw new Error('pascalName is required')
+  if (opts.pascalName !== pascal(opts.pascalName)) throw new Error('pascalName should be PascalCased')
+  if (!opts.pascalPlural) opts.pascalPlural = plural(opts.pascalName)
+  if (opts.pascalPlural !== pascal(opts.pascalPlural)) throw new Error('pascalPlural should be PascalCased')
+  if (!opts.titleName) opts.titleName = title(opts.pascalName)
+  if (!opts.titlePlural) opts.titlePlural = plural(opts.titleName)
+  if (!opts.camelName) opts.camelName = camel(opts.pascalName)
+  if (opts.camelName !== camel(opts.camelName)) throw new Error('camelName should be camelCased')
+  if (!opts.camelPlural) opts.camelPlural = plural(opts.camelName)
+  if (opts.camelPlural !== camel(opts.camelPlural)) throw new Error('camelPlural should be camelCased')
+  if (!opts.paramName) opts.paramName = param(opts.pascalName)
+  if (opts.paramName !== param(opts.paramName)) throw new Error('paramName should be param-cased')
+  if (!opts.paramPlural) opts.paramPlural = plural(opts.paramName)
+  if (opts.paramPlural !== param(opts.paramPlural)) throw new Error('paramPlural should be param-cased')
+  if (!opts.apiPrefix) opts.apiPrefix = `/api/${opts.paramPlural}`
+  if (!opts.appPrefix) opts.appPrefix = `/${opts.paramPlural}`
+  if (!opts.listComponentName) opts.listComponentName = `app${opts.pascalPlural}List`
+  if (opts.listComponentName !== camel(opts.listComponentName)) throw new Error('listComponentName should be camelCased')
+  if (!opts.listComponentTag) opts.listComponentTag = `app-${opts.paramName}-list`
+  if (opts.listComponentTag !== param(opts.listComponentTag)) throw new Error('listComponentTag should be param-cased')
+  if (!opts.listPageComponentName) opts.listPageComponentName = `app${opts.pascalPlural}Page`
+  if (opts.listPageComponentName !== camel(opts.listPageComponentName)) throw new Error('listPageComponentName should be camelCased')
+  if (!opts.listPageComponentTag) opts.listPageComponentTag = `app-${opts.paramPlural}-page`
+  if (opts.listPageComponentTag !== param(opts.listPageComponentTag)) throw new Error('listPageComponentTag should be param-cased')
+  
+  if (!opts.columns) throw new Error('Columns are required')
+  opts.columns.forEach(col => {
+    if (!col.camelName) col.camelName = camel(opts.titleName)
+    if (!col.camelName) throw new Error('camelName is required')
+    if (col.camelName !== camel(col.camelName)) throw new Error('column.camelName should be camelCased')
+    if (!col.titleName) col.titleName = title(col.camelName)
+    if (col.type === undefined) col.type = 'text'
+    if (col.inList === undefined) col.inList = true
+  })
+
+  list(opts)
+  details(opts)
+  trash(opts)
 }
 }
 
 
 
 

+ 32 - 23
app/components/crud-pages/list.js

@@ -1,54 +1,53 @@
 const app = require('../../app')
 const app = require('../../app')
-const {editIcon, createIcon} = require('../../assets')
-const list = ({
-  typeName,
-  typePlural,
-  camelName,
-  camelPlural,
-  paramName,
-  paramPlural,
-  apiPrefix,
-  appPrefix,
-  columns
-}) => {
-  const defaultHeader = column => html`<th md-column>${column.fieldName}</th>`
-  const defaultCell = column => html`<td md-cell>{{${raw(camelName)}.${raw(column.camelName)}}}</td>`
+const {editIcon, createIcon, deleteIcon} = require('../../assets')
 
 
-  app.component(`app${typePlural}Page`, {
+/**
+ * @param {CrudPagesOptions} opts 
+ */
+const list = (opts) => {
+  const defaultHeader = column => html`<th md-column>${column.titleName}</th>`
+  const defaultCell = column => html`<td md-cell>{{${raw(opts.camelName)}.${raw(column.camelName)}}}</td>`
+
+  app.component(`app${opts.pascalPlural}Page`, {
     template: html`
     template: html`
       <app-user-area>
       <app-user-area>
-        <h1>${typePlural}</h1>
+        <h1>${opts.titlePlural}</h1>
         <md-table-container>
         <md-table-container>
           <table md-table md-row-select md-auto-select  ng-model="ctrl.selected" md-progress="ctrl.promise">
           <table md-table md-row-select md-auto-select  ng-model="ctrl.selected" md-progress="ctrl.promise">
               <thead md-head md-order="query.order" md-on-reorder="ctrl.getRecords">
               <thead md-head md-order="query.order" md-on-reorder="ctrl.getRecords">
                 <tr md-row>
                 <tr md-row>
-                  ${columns.map(c => c.header || defaultHeader(c))}
+                  ${opts.columns.filter(c => c.inList).map(c => c.header || defaultHeader(c))}
                   <th md-column>Actions</th>
                   <th md-column>Actions</th>
                 </tr>
                 </tr>
               </thead>
               </thead>
               <tbody md-body>
               <tbody md-body>
-                <tr md-row md-select="${camelName}" md-select-id="name" md-auto-select ng-repeat="${raw(camelName)} in ctrl.data track by ${raw(camelName)}.id">
-                  ${columns.map(c => c.cell || defaultCell(c))}
+                <tr md-row md-select="${opts.camelName}" md-select-id="name" md-auto-select ng-repeat="${raw(opts.camelName)} in ctrl.data track by ${raw(opts.camelName)}.id">
+                  ${opts.columns.filter(c => c.inList).map(c => c.cell || defaultCell(c))}
                   <td md-cell>
                   <td md-cell>
-                    <md-button ng-href="${appPrefix}/{{${raw(camelName)}.id}}">
+                    <md-button ng-href="${opts.appPrefix}/{{${raw(opts.camelName)}.id}}">
                       <md-icon md-svg-icon="${editIcon}"></md-icon>
                       <md-icon md-svg-icon="${editIcon}"></md-icon>
                       Edit
                       Edit
                     </md-button>
                     </md-button>
+                    <md-button ng-click="ctrl.delete(${raw(opts.camelName)})">
+                      <md-icon md-svg-src="${deleteIcon}"></md-icon>
+                      Delete
+                    </md-button>
+
                   </td>
                   </td>
                 </tr>
                 </tr>
               </tbody>
               </tbody>
             </table>
             </table>
         </md-table-container>
         </md-table-container>
         <div layout="row" layout-align="end">
         <div layout="row" layout-align="end">
-          <md-button class="md-fab" aria-label="Add ${typeName}" ng-href="${appPrefix}/new">
+          <md-button class="md-fab" aria-label="Add ${opts.titleName}" ng-href="${opts.appPrefix}/new">
             <md-icon md-svg-src="${createIcon}"></md-icon>
             <md-icon md-svg-src="${createIcon}"></md-icon>
           </md-button>
           </md-button>
         </div>
         </div>
       </app-user-area>
       </app-user-area>
     `,
     `,
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',
-    controller: function(api) {
-      const crud = api.crud(apiPrefix)
+    controller: function(api, $mdToast) {
+      const crud = api.crud(opts.apiPrefix)
 
 
       this.selected = []
       this.selected = []
       this.data = []
       this.data = []
@@ -59,6 +58,16 @@ const list = ({
         })
         })
       }
       }
 
 
+      this.delete = async (record) => {
+        try {
+          await crud.delete(record.id)
+          $mdToast.showSimple(`${opts.titleName} deleted.`)
+        } catch (err) {
+          console.error(err)
+          $mdToast.showSimple(`Could not delete ${opts.titleName}: ${err.message || err}`)
+        }
+      }
+
       this.getRecords()      
       this.getRecords()      
     }
     }
 
 

+ 0 - 0
app/components/crud-pages/options.js


+ 69 - 0
app/components/crud-pages/trash.js

@@ -0,0 +1,69 @@
+const app = require('../../app')
+const { undoIcon} = require('../../assets')
+
+/**
+ * @param {CrudPagesOptions} opts 
+ */
+const list = (opts) => {
+  const defaultHeader = column => html`<th md-column>${column.titleName}</th>`
+  const defaultCell = column => html`<td md-cell>{{${raw(opts.camelName)}.${raw(column.camelName)}}}</td>`
+
+  app.component(`app${opts.pascalPlural}TrashPage`, {
+    template: html`
+      <app-user-area>
+        <h1>${opts.titlePlural} Trash</h1>
+        <md-table-container>
+          <table md-table md-row-select md-auto-select md-multiple ng-model="ctrl.selected" md-progress="ctrl.promise">
+              <thead md-head md-order="query.order" md-on-reorder="ctrl.getRecords">
+                <tr md-row>
+                  ${opts.columns.filter(c => c.inList).map(c => c.header || defaultHeader(c))}
+                  <th md-column>Actions</th>
+                </tr>
+              </thead>
+              <tbody md-body>
+                <tr md-row md-select="${opts.camelName}" md-select-id="name" md-auto-select ng-repeat="${raw(opts.camelName)} in ctrl.data track by ${raw(opts.camelName)}.id">
+                  ${opts.columns.filter(c => c.inList).map(c => c.cell || defaultCell(c))}
+                  <td md-cell>
+                    <md-button ng-click="ctrl.undelete(${raw(opts.camelName)})">
+                      <md-icon md-svg-icon="${undoIcon}"></md-icon>
+                      Undelete
+                    </md-button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+        </md-table-container>
+      </app-user-area>
+    `,
+    controllerAs: 'ctrl',
+    controller: function(api, $mdToast, $location) {
+      const crud = api.crud(`${opts.apiPrefix}`)
+
+      this.selected = []
+      this.data = []
+
+      this.getRecords = () => {
+        this.promise = crud.trash().then(data => {
+          this.data = data
+        })
+      }
+
+      this.undelete = async rec => {
+        try {
+          await crud.undelete(rec.id)
+          $mdToast.showSimple(`${opts.titleName} undeleted.`)
+          $location.url(`${opts.appPrefix}`)
+        } catch (err) {
+          console.error(err)
+          $mdToast.showSimple(`Could not undelete ${opts.titleName}: ${err.message || err}`)
+        }
+
+      }
+
+      this.getRecords()      
+    }
+
+  })
+}
+
+module.exports = list

+ 0 - 1
app/components/dashboard-page.js

@@ -1,5 +1,4 @@
 const app = require('../app')
 const app = require('../app')
-const { genericLogo } = require('../assets')
 
 
 app.component('appDashboardPage', {
 app.component('appDashboardPage', {
   template: html`
   template: html`

+ 2 - 2
app/components/home-area.js

@@ -1,5 +1,5 @@
 const app = require('../app')
 const app = require('../app')
-const { genericLogo } = require('../assets')
+const { logo } = require('../assets')
 
 
 app.component('appHomeArea', {
 app.component('appHomeArea', {
   transclude: true,
   transclude: true,
@@ -7,7 +7,7 @@ app.component('appHomeArea', {
     <div style="display: flex; align-items: center; justify-content: center; height: 100%; flex-grow: 100;">
     <div style="display: flex; align-items: center; justify-content: center; height: 100%; flex-grow: 100;">
       <div style="flex-basis: auto">
       <div style="flex-basis: auto">
         <div class="logo-container">
         <div class="logo-container">
-          <img class="logo" src="${genericLogo}" />
+          <img class="logo" src="${logo}" />
         </div>
         </div>
         <div class="home-content" ng-transclude>
         <div class="home-content" ng-transclude>
           <!-- content -->
           <!-- content -->

+ 8 - 0
app/components/index.js

@@ -0,0 +1,8 @@
+require('./home-area')
+require('./login-page')
+require('./test-page')
+require('./user-area')
+require('./home-page')
+require('./dashboard-page')
+require('./user-pages')
+require('./app-index')

+ 3 - 11
app/components/user-area.js

@@ -1,5 +1,5 @@
 const app = require('../app')
 const app = require('../app')
-const { genericLogo, menuIcon, userIcon, dashboardIcon } = require('../assets')
+const { logo, menuIcon, userIcon, dashboardIcon } = require('../assets')
 
 
 
 
 app.component('appUserArea', {
 app.component('appUserArea', {
@@ -15,19 +15,11 @@ app.component('appUserArea', {
 
 
         <header>
         <header>
           <div class="logo-container">
           <div class="logo-container">
-            <img class="logo" src="${genericLogo}" />
+            <img class="logo" src="${logo}" />
           </div>          
           </div>          
         </header>
         </header>
 
 
-        <h3>
-          Intelligence
-        </h3>
-        <md-menu-item>
-          <md-button ng-href="/dashboard">
-            <md-icon md-svg-icon="${dashboardIcon}"></md-icon>
-            Dashboard
-          </md-button>
-        </md-menu-item>
+        <app-user-area-nav></app-user-area-nav>
 
 
         <h3>
         <h3>
           Administration
           Administration

+ 9 - 6
app/components/user-pages.js

@@ -1,8 +1,10 @@
 const crudPages = require('./crud-pages')
 const crudPages = require('./crud-pages')
 
 
 crudPages({
 crudPages({
-  typeName: 'User',
-  typePlural: 'Users',
+  titleName: 'User',
+  titlePlural: 'Users',
+  pascalName: 'User',
+  pascalPlural: 'Users',
   camelName: 'user',
   camelName: 'user',
   camelPlural: 'users',
   camelPlural: 'users',
   snakeName: 'user',
   snakeName: 'user',
@@ -10,19 +12,20 @@ crudPages({
   apiPrefix: '/api/users',
   apiPrefix: '/api/users',
   columns: [
   columns: [
     {
     {
-      fieldName: 'Name',
+      titleName: 'Name',
       camelName: 'name',
       camelName: 'name',
       header: html`<th md-column md-order-by="nameToLower"><span>Name</span></th>`,
       header: html`<th md-column md-order-by="nameToLower"><span>Name</span></th>`,
       cell: html`<td md-cell>{{user.name}}</td>`
       cell: html`<td md-cell>{{user.name}}</td>`
     },
     },
     {
     {
-      fieldName: 'Email',
+      titleName: 'Email',
       camelName: 'email'
       camelName: 'email'
     },
     },
     {
     {
-      fieldName: 'Password',
+      titleName: 'Password',
       camelName: 'password',
       camelName: 'password',
-      type: 'password'
+      type: 'password',
+      inList: false
     }
     }
   ]
   ]
 })
 })

+ 3 - 7
app/index.js

@@ -1,8 +1,4 @@
-require('./components/home-area')
-require('./components/login-page')
-require('./components/test-page')
-require('./components/user-area')
-require('./components/home-page')
-require('./components/dashboard-page')
-require('./components/user-pages')
+require('./app')
+require('./components')
 require('./api-service')
 require('./api-service')
+require('./app-index')

+ 1 - 0
app/routes.js

@@ -4,6 +4,7 @@ module.exports = function($routeProvider) {
   $routeProvider.when('/dashboard', {template: '<app-dashboard-page />'})
   $routeProvider.when('/dashboard', {template: '<app-dashboard-page />'})
   $routeProvider.when('/', {template: '<app-home-page />'})
   $routeProvider.when('/', {template: '<app-home-page />'})
   $routeProvider.when('/users', {template: '<app-users-page />'})
   $routeProvider.when('/users', {template: '<app-users-page />'})
+  $routeProvider.when('/users/trash', {template: '<app-users-trash-page />'})
   $routeProvider.when('/users/:id', {template: '<app-user-details-page />'})
   $routeProvider.when('/users/:id', {template: '<app-user-details-page />'})
   $routeProvider.otherwise({template: '<h1>404</h1>'})
   $routeProvider.otherwise({template: '<h1>404</h1>'})
 }
 }

+ 0 - 0
lib/controllers/app-index.js


+ 2 - 2
lib/controllers/auth/login.js

@@ -15,8 +15,8 @@ module.exports = {
         const exp = Math.floor(Date.now()/1000) + config.auth.jwtExpires
         const exp = Math.floor(Date.now()/1000) + config.auth.jwtExpires
         await Session.create({
         await Session.create({
           id: sid,
           id: sid,
-          startTimestamp: Date.now(),
-          endTimestamp: exp
+          startAt: Date.now(),
+          endAt : exp
         })
         })
         const token = JWT.sign({sid, exp}, config.auth.jwtSecret);
         const token = JWT.sign({sid, exp}, config.auth.jwtSecret);
         res.status(200).send({
         res.status(200).send({

+ 20 - 1
lib/controllers/crud-controller.js

@@ -1,3 +1,5 @@
+const { Op } = require('sequelize')
+
 const crudController = ({
 const crudController = ({
   Type
   Type
 }) => ({
 }) => ({
@@ -18,7 +20,24 @@ const crudController = ({
     res.status(200).send(data && data.sanitize ? data.sanitize() : data)
     res.status(200).send(data && data.sanitize ? data.sanitize() : data)
   },
   },
   delete: async (req, res) => {
   delete: async (req, res) => {
-    const data = (await Type.delete({ where: { id: req.params.id } }))
+    const data = (await Type.destroy({ where: { id: req.params.id } }))
+    res.status(204).end()
+  },
+  trash: async (req, res) => {
+    const data = (await Type.findAll({
+      model: Type,
+      paranoid: false,
+      where: {
+        deletedAt: { [Op.ne]: null }
+      }
+    }))
+    res.status(200).send(data && data.sanitize ? data.sanitize() : data)
+  },
+  undelete: async (req, res) => {
+    const data = (await Type.update({ deletedAt: null }, {
+      paranoid: false,
+      where: { id: req.params.id }
+    }))
     res.status(200).send(data && data.sanitize ? data.sanitize() : data)
     res.status(200).send(data && data.sanitize ? data.sanitize() : data)
   }
   }
   // TODO: Create, Read, Update, Delete
   // TODO: Create, Read, Update, Delete

+ 3 - 1
lib/crud-route.js

@@ -29,7 +29,9 @@ module.exports = ({
 
 
   if (controller.list) app.get(`/api/${paramPlural}`, asyncHandler(controller.list))
   if (controller.list) app.get(`/api/${paramPlural}`, asyncHandler(controller.list))
   if (controller.create) app.post(`/api/${paramPlural}`, asyncHandler(controller.create))
   if (controller.create) app.post(`/api/${paramPlural}`, asyncHandler(controller.create))
+  if (controller.trash) app.get(`/api/${paramPlural}/trash`, asyncHandler(controller.trash))
   if (controller.read) app.get(`/api/${paramPlural}/:id`, asyncHandler(controller.read))
   if (controller.read) app.get(`/api/${paramPlural}/:id`, asyncHandler(controller.read))
-  if (controller.update) app.put(`/api/${paramPlural}/:id`, asyncHandler(controller.update))
+  if (controller.update) app.patch(`/api/${paramPlural}/:id`, asyncHandler(controller.update))
   if (controller.delete) app.delete(`/api/${paramPlural}/:id`, asyncHandler(controller.delete))
   if (controller.delete) app.delete(`/api/${paramPlural}/:id`, asyncHandler(controller.delete))
+  if (controller.undelete) app.delete(`/api/${paramPlural}/trash/:id`, asyncHandler(controller.undelete))
 }
 }

+ 1 - 0
lib/database/app-index.js

@@ -0,0 +1 @@
+// TODO: App Specific Models

+ 6 - 49
lib/database/index.js

@@ -1,55 +1,12 @@
-const _ = require('lodash')
-const Sequelize = require('sequelize')
 const config = require('../../config')
 const config = require('../../config')
 const bcrypt = require('bcrypt')
 const bcrypt = require('bcrypt')
+const sequelize = require('./sequelize')
+const User = require('./user')
+const Session = require('./session')
+const appIndex = require('./app-index')
 
 
-const sequelize = new Sequelize(config.sequelize)
-
-const User = sequelize.define('user', {
-  id: {
-    type: Sequelize.UUID,
-    defaultValue: Sequelize.UUIDV1,
-    primaryKey: true
-  },
-  email: {
-    type: Sequelize.STRING,
-    unique: true,
-    validate: {
-      isEmail: true
-    }
-  },
-  name: Sequelize.STRING,
-  password: Sequelize.STRING,
-  resetToken: Sequelize.STRING
-}, {
-  indexes: [
-    {
-      unique: true,
-      fields: ['email']
-    }
-  ],
-  setterMethods: {
-    password(value) {
-      this.setDataValue('password', bcrypt.hashSync(value, config.auth.saltRounds))
-    }
-  }
-})
-
-User.prototype.sanitize = function() {
-  return _.omit(this.dataValues, 'password', 'resetToken')
-}
-
-const Session = sequelize.define('session', {
-  id: {
-    type: Sequelize.UUID,
-    primaryKey: true
-  },
-  startTimestamp: Sequelize.DATE,
-  endTimestamp: Sequelize.DATE
-})
-
-module.exports = {
+module.exports = Object.assign({
   init: () => sequelize.sync(),
   init: () => sequelize.sync(),
   User,
   User,
   Session
   Session
-}
+}, appIndex)

+ 3 - 0
lib/database/sequelize.js

@@ -0,0 +1,3 @@
+const config = require('../../config')
+const Sequelize = require('sequelize')
+module.exports = new Sequelize(config.sequelize)

+ 13 - 0
lib/database/session.js

@@ -0,0 +1,13 @@
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+
+const Session = sequelize.define('session', {
+  id: {
+    type: Sequelize.UUID,
+    primaryKey: true
+  },
+  startAt: Sequelize.DATE,
+  endAt: Sequelize.DATE
+})
+
+module.exports = Session

+ 41 - 0
lib/database/user.js

@@ -0,0 +1,41 @@
+const _ = require('lodash')
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+const config = require('../../config')
+const bcrypt = require('bcrypt')
+
+const User = sequelize.define('user', {
+  id: {
+    type: Sequelize.UUID,
+    defaultValue: Sequelize.UUIDV1,
+    primaryKey: true
+  },
+  email: {
+    type: Sequelize.STRING,
+    unique: true,
+    validate: {
+      isEmail: true
+    }
+  },
+  name: Sequelize.STRING,
+  password: Sequelize.STRING,
+  resetToken: Sequelize.STRING
+}, {
+  paranoid: true,
+  indexes: [
+    {
+      unique: true,
+      fields: ['email']
+    }
+  ],
+  setterMethods: {
+    password(value) {
+      this.setDataValue('password', bcrypt.hashSync(value, config.auth.saltRounds))
+    }
+  }
+})
+User.prototype.sanitize = function() {
+  return _.omit(this.dataValues, 'password', 'resetToken')
+}
+
+module.exports = User

+ 0 - 6
lib/models/login.js

@@ -1,6 +0,0 @@
-const Joi = require('joi')
-
-module.exports = {
-  email     : Joi.string().email().required(), // Required
-  password  : Joi.string().required().min(6)   // minimum length 6 characters
-}