diff --git a/examples/demo/rest-sandbox.http b/examples/demo/rest-sandbox.http new file mode 100644 index 000000000..daf79c29b --- /dev/null +++ b/examples/demo/rest-sandbox.http @@ -0,0 +1,49 @@ +# This is for VSCode `REST Client` plugin. Use it, it rocks +# @host = https://rcm-dev-api.9ci.io +@host = http://localhost:8080 +@base_url = {{host}}/api/ar/customer +# @host = localhost:8080 +@contentType = application/json +# @token = dfasfasdfadfs + + +# @name login +POST {{host}}/api/login +Content-Type: application/json + +{"username":"admin","password":"123F"} + +### +@token = {{login.response.body.$.access_token}} + +### Get Customers### Get Customers +GET {{base_url}}?max=20&page=1&sort=org.calc.totalDue +Authorization: Bearer {{token}} + +### get customers query +GET {{base_url}}?q=wal% + +### post/create and put/update + +# @name post_it +POST {{base_url}} +Content-Type: application/json + +{ + "num": {{$randomInt 100 10000}}, + "name": "Test" +} + +### Get it + +@created_id = {{post_it.response.body.$.id}} + +GET {{base_url}}/{{created_id}} + +### update it + +PUT {{base_url}}/{{created_id}} + +{ + "name": "TestUpdate" +} diff --git a/examples/demo/src/app.config.js b/examples/demo/src/app.config.js index 555805b49..a1464ac64 100644 --- a/examples/demo/src/app.config.js +++ b/examples/demo/src/app.config.js @@ -3,8 +3,9 @@ import _ from 'lodash' import appName from './app.module' import './config.router' import appState from 'angle-grinder/src/tools/AppState' -import {setConfig} from 'angle-grinder/src/tools/AppConfig' -import {setClientConfig} from "angle-grinder/src/dataApi/kyApi"; +import { KyFactory } from "angle-grinder/src/stores/ky"; +KyFactory.enableAuthHeader() +KyFactory.defaults.prefixUrl = configData.base_url const app = angular.module(appName) // export default app.name @@ -63,24 +64,10 @@ app.run(function($rootScope, $state, $stateParams) { appState.sidenav.open = true $rootScope.appState = appState - setClientConfig({ - // configData comes from index.html - to be able to change backend url on fly after build, takes data from `config.js` in root folder of the app - // eslint-disable-next-line no-undef - prefixUrl: configData.base_url - }) + // setClientConfig({ + // // configData comes from index.html - to be able to change backend url on fly after build, takes data from `config.js` in root folder of the app + // // eslint-disable-next-line no-undef + // prefixUrl: configData.base_url + // }) - const conf = { - controls: { - ranges: { - fromField: { - name: '$gt' - }, - toField: { - name: '$lt' - } - } - } - } - - setConfig(conf) }) diff --git a/examples/demo/src/grids/customGridList/list/ListCtrl.js b/examples/demo/src/grids/customGridList/list/ListCtrl.js index 8c1f8cc36..f831dce6c 100644 --- a/examples/demo/src/grids/customGridList/list/ListCtrl.js +++ b/examples/demo/src/grids/customGridList/list/ListCtrl.js @@ -1,7 +1,7 @@ // import controller from './listCtrl' // import template from './list.html' import BaseListCtrl from 'angle-grinder/src/ng/gridz/list/BaseListCtrl' -import restStoreApi from '../../../store/RestStoreApi' +import dataApiFactory from '../../../store/dataApiFactory' import toast from 'angle-grinder/src/tools/toast' export default class ListCtrl extends BaseListCtrl { @@ -17,7 +17,7 @@ export default class ListCtrl extends BaseListCtrl { // static $inject = _.union(super.$inject, ['someService']) constructor(...args) { super(...args) - this.dataApi = restStoreApi.invoice + this.dataApi = dataApiFactory.invoice } async $onInit() { diff --git a/examples/demo/src/grids/localStoreGrid/index.js b/examples/demo/src/grids/localStoreGrid/index.js index 661e49cbe..83db3ca93 100644 --- a/examples/demo/src/grids/localStoreGrid/index.js +++ b/examples/demo/src/grids/localStoreGrid/index.js @@ -7,7 +7,7 @@ const template = `\ export default angular .module(compDemoModule) - .component('basicGridIndex', { + .component('localStoreIndex', { template: template, controller: function() { this.rawHtml = require('./list.html') diff --git a/examples/demo/src/grids/localStoreGrid/list.html b/examples/demo/src/grids/localStoreGrid/list.html index ebbbae105..0a0221bc7 100644 --- a/examples/demo/src/grids/localStoreGrid/list.html +++ b/examples/demo/src/grids/localStoreGrid/list.html @@ -1,11 +1,11 @@
- - +
vm: {{$ctrl.vm | json}}
Selected Rows data: {{$ctrl.selectedRowsData | json}}
diff --git a/examples/demo/src/grids/localStoreGrid/listComp.js b/examples/demo/src/grids/localStoreGrid/listComp.js index 2c3fdf746..1d920e897 100644 --- a/examples/demo/src/grids/localStoreGrid/listComp.js +++ b/examples/demo/src/grids/localStoreGrid/listComp.js @@ -1,13 +1,13 @@ // import controller from './listCtrl' import template from './list.html' -import BaseListCtrl from 'angle-grinder/src/ng/gridz/list/BaseListCtrl' +import ListDataApiCtrl from 'angle-grinder/src/ng/gridz/list-datastore/ListDataApiCtrl' import buildOptions from './listCtrlOptions' -import localStoreApi from '../../store/LocalStoreApi' +import sessionStores from '../../store/sessionServices' import Log from 'angle-grinder/src/utils/Log' import Swal from 'angle-grinder/src/tools/swal' import _ from 'lodash' -class ListCtrl extends BaseListCtrl { +class ListCtrl extends ListDataApiCtrl { isLoaded = false editTemplate = require('./templates/editDialog.html') @@ -15,11 +15,10 @@ class ListCtrl extends BaseListCtrl { constructor(...args) { super(...args) - this.dataApi = localStoreApi.invoice + this.dataApi = sessionStores.invoice } async $onInit() { - // this.dataApi = localStoreApi.invoice this.cfg = buildOptions(this) await this.doConfig(this.cfg) } diff --git a/examples/demo/src/grids/routes.js b/examples/demo/src/grids/routes.js index 70822ee0b..f8b1fa13c 100644 --- a/examples/demo/src/grids/routes.js +++ b/examples/demo/src/grids/routes.js @@ -1,3 +1,5 @@ +import dataApiFactory from '../store/dataApiFactory' + const gridsStates = { name: 'grids', abstract: true, @@ -7,15 +9,15 @@ const gridsStates = { }, children: [ { - name: 'vanilla-agGridList', - data: { title: 'Customer Grid Example' }, - abstract: 'fresh.grid.vanilla-agGridList.list', - url: '/customer', + name: 'agGridDatastore', + data: { title: 'Customer Grid' }, + abstract: 'fresh.grid.agGridDatastore.list', + url: '/customer-ds', children: [{ name: 'list', isMenuItem: false, url: '', - component: 'agGridList' + component: 'agGridDatastore' }, { name: 'Edit Customer', isMenuItem: false, @@ -24,6 +26,11 @@ const gridsStates = { }], resolve: { apiKey: () => 'customer', + dataApi: () => { + let ds = dataApiFactory.customer + console.log("dataApiFactory.customer", ds) + return ds + }, // gridOptions: () => ({multiSort: true}), notification: () => ({ class: 'is-primary is-light', @@ -40,7 +47,7 @@ const gridsStates = { name: 'list', isMenuItem: false, url: '', - component: 'agGridList' + component: 'agGridDatastore' }, { name: 'Edit Customer', isMenuItem: false, @@ -49,6 +56,9 @@ const gridsStates = { }], resolve: { apiKey: () => 'customer', + dataApi: () => { + return dataApiFactory.customer + }, initSearch: () => ({ name: 'Yodo' }), notification: () => ({ class: 'is-primary is-light', @@ -59,10 +69,13 @@ const gridsStates = { { name: 'editOnly-agGridList', url: '/tag', - component: 'agGridList', + component: 'agGridDatastore', data: { title: 'Edit Only agGridList' }, resolve: { apiKey: () => 'tag', + dataApi: () => { + return dataApiFactory.tag + }, notification: () => ({ class: 'is-primary is-light', text: 'Uses ui-router to send rest apiKey to generic agGridList component, configured to only allow editing desc' @@ -80,9 +93,9 @@ const gridsStates = { component: 'basicGridRestIndex' }, { - name: 'basic-grid', - component: 'basicGridIndex', - url: '/foo' + name: 'local-store-grid', + component: 'localStoreIndex', + url: '/localstore' } ] } diff --git a/examples/demo/src/grids/twoGrids/customerList/ListCtrl.js b/examples/demo/src/grids/twoGrids/customerList/ListCtrl.js index 62cdf6841..86a4791c0 100644 --- a/examples/demo/src/grids/twoGrids/customerList/ListCtrl.js +++ b/examples/demo/src/grids/twoGrids/customerList/ListCtrl.js @@ -1,9 +1,9 @@ -import BaseListCtrl from 'angle-grinder/src/ng/gridz/list/BaseListCtrl' -import restStoreApi from '../../../store/RestStoreApi' +import ListDataApiCtrl from 'angle-grinder/src/ng/gridz/list-datastore/ListDataApiCtrl' +import dataApiFactory from '../../../store/dataApiFactory' import _ from 'lodash' -export default class ListCtrl extends BaseListCtrl { - static $inject = _.union(super.$inject, ['dataStoreApi', '$state', 'selectedRow']) +export default class ListCtrl extends ListDataApiCtrl { + static $inject = _.union(super.$inject, ['$state', 'selectedRow']) apiKey = 'customer' eventHandlers = { onSelect: (event, id) => { @@ -13,7 +13,7 @@ export default class ListCtrl extends BaseListCtrl { constructor(...args) { super(...args) - this.dataApi = restStoreApi.customer + this.dataApi = dataApiFactory.customer } async $onInit() { diff --git a/examples/demo/src/grids/twoGrids/customerList/list.html b/examples/demo/src/grids/twoGrids/customerList/list.html index c6d1254a8..e1e7ea849 100644 --- a/examples/demo/src/grids/twoGrids/customerList/list.html +++ b/examples/demo/src/grids/twoGrids/customerList/list.html @@ -1,8 +1,8 @@
- - +
diff --git a/examples/demo/src/grids/twoGrids/invoistList/ListCtrl.js b/examples/demo/src/grids/twoGrids/invoistList/ListCtrl.js index a916fdd59..54afcf178 100644 --- a/examples/demo/src/grids/twoGrids/invoistList/ListCtrl.js +++ b/examples/demo/src/grids/twoGrids/invoistList/ListCtrl.js @@ -1,23 +1,23 @@ // import controller from './listCtrl' // import template from './list.html' -import BaseListCtrl from 'angle-grinder/src/ng/gridz/list/BaseListCtrl' -import restStoreApi from '../../../store/RestStoreApi' +import ListDataApiCtrl from 'angle-grinder/src/ng/gridz/list-datastore/ListDataApiCtrl' +import dataApiFactory from '../../../store/dataApiFactory' import _ from 'lodash' -export default class ListCtrl extends BaseListCtrl { - static $inject = _.union(super.$inject, ['dataStoreApi', '$state', 'selectedRow']) +export default class ListCtrl extends ListDataApiCtrl { + static $inject = _.union(super.$inject, ['$state', 'selectedRow']) apiKey = 'invoice' searchModel= {} - eventHandlers = { - } + eventHandlers = {} // static $inject = _.union(super.$inject, ['someService']) constructor(...args) { super(...args) - this.dataApi = restStoreApi.invoice + this.dataApi = dataApiFactory.invoice } async $onInit() { + this.isConfigured = false this.cfg = {} await this.doConfig() diff --git a/examples/demo/src/grids/twoGrids/invoistList/list.html b/examples/demo/src/grids/twoGrids/invoistList/list.html index f7887ffb0..268d4924c 100644 --- a/examples/demo/src/grids/twoGrids/invoistList/list.html +++ b/examples/demo/src/grids/twoGrids/invoistList/list.html @@ -1,7 +1,7 @@
- - +
diff --git a/examples/demo/src/store/LocalStoreApi.js b/examples/demo/src/store/LocalStoreApi.js deleted file mode 100644 index 45b282a93..000000000 --- a/examples/demo/src/store/LocalStoreApi.js +++ /dev/null @@ -1,27 +0,0 @@ -import SessionStorageApi from 'angle-grinder/src/dataApi/SessionStorageApi' - -// eslint-disable-next-line space-before-blocks -function makeDataApi(name, path){ - return new SessionStorageApi(name, path) -} - -/** main holder for api */ -export class LocalStoreApi{ - - static factory() { - return new LocalStoreApi() - } - - // constructor() { - // } - - get customer(){ return makeDataApi('customers', 'data/Customers.json') } - get batch(){ return makeDataApi('customers', 'data/Batch.json') } - get invoice() { return makeDataApi('invoices', 'data/Invoices.json') } - get tranState() { return makeDataApi('tranStates', 'data/TranStates.json') } - -} - -const _instance = LocalStoreApi.factory() - -export default _instance diff --git a/examples/demo/src/store/RestStoreApi.js b/examples/demo/src/store/RestStoreApi.js deleted file mode 100644 index ab0507498..000000000 --- a/examples/demo/src/store/RestStoreApi.js +++ /dev/null @@ -1,40 +0,0 @@ -import RestDataApi from 'angle-grinder/src/dataApi/RestDataApi' -import kyApi from 'angle-grinder/src/dataApi/kyApi' - -function makeDataApi(endpoint){ - return new RestDataApi(`api/${endpoint}`) -} - -/** main holder for api */ -export class RestStoreApi { - _cached = {} - - static factory() { - return new RestStoreApi() - } - - constructor() { - } - - get customer() { return makeDataApi('customer') } - get batch() { return makeDataApi('batch') } - get invoice() { return makeDataApi('invoice') } - get tranState() { return makeDataApi('tranState') } - get tag() { return makeDataApi('tag') } - - appConfig(configKey) { - return kyApi.ky.get(`api/appConfig/${configKey}`).json() - } - - /** - * checks cache and if not there then does a ky.get - */ - configFromCache(key) { - - } - -} - -const _instance = RestStoreApi.factory() - -export default _instance diff --git a/examples/demo/src/store/dataApiFactory.js b/examples/demo/src/store/dataApiFactory.js new file mode 100644 index 000000000..05d30d329 --- /dev/null +++ b/examples/demo/src/store/dataApiFactory.js @@ -0,0 +1,21 @@ +// import RestDataApi from 'angle-grinder/src/dataApi/RestDataApi' +import RestDataService from 'angle-grinder/src/stores/rest/RestDataService' +import ky from 'angle-grinder/src/stores/ky' + +function makeDataService(key){ + return RestDataService({ key }) +} + +const dataApiFactory = { + get customer() { return makeDataService('customer') }, + get batch() { return makeDataService('batch') }, + get invoice() { return makeDataService('invoice') }, + get tranState() { return makeDataService('tranState') }, + get tag() { return makeDataService('tag') }, + + appConfig(configKey) { + return ky(`appConfig/${configKey}`).json() + } +} + +export default dataApiFactory diff --git a/examples/demo/src/store/index.js b/examples/demo/src/store/index.js index c4ed9ec8e..7f760a5e3 100644 --- a/examples/demo/src/store/index.js +++ b/examples/demo/src/store/index.js @@ -1,10 +1,9 @@ -import { LocalStoreApi } from './LocalStoreApi' -import { RestStoreApi } from './RestStoreApi' +import sessionServices from './sessionServices' +import dataApiFactory from './dataApiFactory' // export module name export default angular.module('ag.demo.api', []) - .service('localStoreApi', LocalStoreApi) - .service('restStoreApi', RestStoreApi) - // this is the default, used in components, change it to RestStore to test that - .service('dataStoreApi', RestStoreApi) + .service('localStoreApi', function() { return sessionServices}) + // this is the default + .service('dataStoreApi', function() { return dataApiFactory}) .name diff --git a/examples/demo/src/store/sessionServices.js b/examples/demo/src/store/sessionServices.js new file mode 100644 index 000000000..e100b8af7 --- /dev/null +++ b/examples/demo/src/store/sessionServices.js @@ -0,0 +1,16 @@ +import SessionDataService from 'angle-grinder/src/stores/local/SessionDataService' + +// eslint-disable-next-line space-before-blocks +function makeDatastore(storageKey, sourceUrl){ + return SessionDataService({ storageKey, sourceUrl }) +} + +const sessionStores = { + get customer(){ return makeDatastore('customers', 'data/Customers.json') }, + get batch(){ return makeDatastore('customers', 'data/Batch.json') }, + get invoice() { return makeDatastore('invoices', 'data/Invoices.json') }, + get tranState() { return makeDatastore('tranStates', 'data/TranStates.json') } +} + +export default sessionStores + diff --git a/examples/demo/src/svelte/Buttons/Buttons.svelte b/examples/demo/src/svelte/Buttons/Buttons.svelte index b3bb2b613..c6ee0efb9 100644 --- a/examples/demo/src/svelte/Buttons/Buttons.svelte +++ b/examples/demo/src/svelte/Buttons/Buttons.svelte @@ -1,5 +1,5 @@ diff --git a/examples/demo/src/svelte/Gridz/Gridz.svelte b/examples/demo/src/svelte/Gridz/Gridz.svelte index d3b73408b..fa5327117 100644 --- a/examples/demo/src/svelte/Gridz/Gridz.svelte +++ b/examples/demo/src/svelte/Gridz/Gridz.svelte @@ -2,8 +2,7 @@ - <% } %> - - - diff --git a/examples/grails-demo/src/AppCtrl.js b/examples/grails-demo/src/AppCtrl.js deleted file mode 100644 index 1417dfa8f..000000000 --- a/examples/grails-demo/src/AppCtrl.js +++ /dev/null @@ -1,106 +0,0 @@ -import appState from 'angle-grinder/src/tools/AppState' - -/** - * Main Application Controller - */ -class AppCtrl { - constructor($rootScope, $scope, $document, $timeout, cfpLoadingBar, $transitions) { - this.$rootScope = $rootScope - this.$scope = $scope - this.layout = appState.layout - - // this.routerTransitionsEvents() - // the ui-router events, see https://stackoverflow.com/a/43553641 - $transitions.onStart({}, function(trans) { - // start loading bar on stateChangeStart - cfpLoadingBar.start() - $scope.horizontalNavbarCollapsed = true - }) - // the ui-router events, see https://stackoverflow.com/a/43553641 - $transitions.onSuccess({}, function(trans) { - // stop loading bar on stateChangeSuccess - $scope.$on('$viewContentLoaded', function(event) { - cfpLoadingBar.complete() - }) - - // scroll top the page on change state - $('#app .main-content').css({ - position: 'relative', - top: 'auto' - }) - - $('footer').show() - - window.scrollTo(0, 0) - }) - - $rootScope.pageTitle = function() { - return appState.pageTitle - } - - // global function to scroll page up - $scope.toTheTop = function() { - $document.scrollTopAnimated(0, 600) - } - - // Function that find the exact height and width of the viewport in a cross-browser way - var viewport = function() { - var e = window; var a = 'inner' - if (!('innerWidth' in window)) { - a = 'client' - e = document.documentElement || document.body - } - return { - width: e[a + 'Width'], - height: e[a + 'Height'] - } - } - - // function that adds information in a scope of the height and width of the page - $scope.getWindowDimensions = function() { - return { - h: viewport().height, - w: viewport().width - } - } - // Detect when window is resized and set some variables - $scope.$watch($scope.getWindowDimensions, function(newValue, oldValue) { - $scope.windowHeight = newValue.h - $scope.windowWidth = newValue.w - - // Desktop - if (newValue.w >= 1024) { - appState.layout.isDektop = true - appState.layout.isSidebarFixed = true - appState.layout.isSidenavFixed = true - } else { - appState.layout.isDektop = false - appState.layout.isSidebarFixed = false - appState.layout.isSidenavFixed = false - } - if (newValue.w >= 992) { - $scope.isLargeDevice = true - } else { - $scope.isLargeDevice = false - } - if (newValue.w < 992) { - $scope.isSmallDevice = true - } else { - $scope.isSmallDevice = false - } - if (newValue.w <= 768) { - $scope.isMobileDevice = true - } else { - $scope.isMobileDevice = false - } - }, true) - - } - - toggleSidenav() { - appState.sidenav.open = !appState.sidenav.open - } -} - -const app = angular.module('app') -app.controller('AppCtrl', AppCtrl) diff --git a/examples/grails-demo/src/app.config.js b/examples/grails-demo/src/app.config.js deleted file mode 100644 index aa4d3296f..000000000 --- a/examples/grails-demo/src/app.config.js +++ /dev/null @@ -1,49 +0,0 @@ -import angular from 'angular' -import _ from 'lodash' -import './app.module' -import './config.router' -import appState from 'angle-grinder/src/tools/AppState' - -const app = angular.module('app') -// export default app.name - -app.run(function($rootScope, $state, $stateParams) { - // Set the ui-router state vars to global root to access them from any scope - $rootScope.$state = $state - appState.$state = $state - $rootScope.$stateParams = $stateParams - - const userInfo = { - id: '123', - name: 'Peter Schiff', - job: 'Bot Wrangler', - picture: 'app/img/user/02.jpg' - } - _.merge(appState.user, userInfo) - - const defaultLayout = { - isNavbarFixed: true, // true if you want to initialize the template with fixed header - isSidenavFixed: true, // true if you want to initialize the template with fixed sidebar - isFooterFixed: false, // true if you want to initialize the template with fixed footer - theme: 'light', // indicate the theme chosen for your project - logo: 'assets/images/logos/yak-white.svg', // relative path of the project logo - logoWidth: 150, - } - - _.merge(appState.layout, defaultLayout) - - const info = { - name: 'Yak Works Template', // name of your project - author: 'YakWorks', // author's name or company name - description: 'Angular Admin Template', // brief description - version: '1.0', // current version - year: ((new Date()).getFullYear()) // automatic current year (for copyright information) - } - _.merge(appState.info, info) - - // appState defaults - appState.sidenav.open = true - - $rootScope.appState = appState -}) - diff --git a/examples/grails-demo/src/app.module.js b/examples/grails-demo/src/app.module.js deleted file mode 100644 index 1bd78c5ff..000000000 --- a/examples/grails-demo/src/app.module.js +++ /dev/null @@ -1,18 +0,0 @@ -import angular from 'angular' -import agModule from 'angle-grinder/src/angle-grinder' - -//app layout items -import freshLayoutModule from './fresh' - -// demo sections -import gridsModule from './grids' - -// store -import dataApiModule from './store' - -export default angular.module('app', [ - agModule, - dataApiModule, - freshLayoutModule, - gridsModule -]).name diff --git a/examples/grails-demo/src/assets/_codeview.scss b/examples/grails-demo/src/assets/_codeview.scss deleted file mode 100644 index 6960a4445..000000000 --- a/examples/grails-demo/src/assets/_codeview.scss +++ /dev/null @@ -1,154 +0,0 @@ -.codeview { - pre code { - white-space: pre; - } - .codeview-title { - // background: lighten($primary, 10%); - background: $grey; - padding-left: 1rem; - padding-right: 1rem; - line-height: 2; - min-height: 0.25rem; - color: $white; - font-size: $size-5; - border-top-left-radius: $radius; - border-top-right-radius: $radius; - } - .highlight { - position: relative; - text-align: left !important; - height: 3.25rem; - .button-container { - position: absolute; - display: inline-flex; - background: transparent; - border-radius: $radius $radius 0 0; - top: 0.25rem; - right: 1.35rem; - padding: 0 0 0 8px; - vertical-align: top; - .button { - display: flex; - align-items: flex-end; - padding: 0; - text-decoration: underline; - &:not(:last-child) { - margin-right: 0.5rem; - } - .icon { - margin-left: -2px; - } - &:hover, - &:focus { - color: $link; - background: transparent; - } - } - } - pre { - padding: 0; - margin: 0; - code { - overflow: hidden; - max-height: 600px; - &.hljs { - background: inherit; - color: inherit; - padding: 1.25rem 1.5rem; - } - } - } - .codeview-showcode, - .codeview-hidecode { - display: flex; - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - align-items: center; - background-color: rgba($white, 0.8); - border: none; - color: $grey; - cursor: pointer; - font-size: 0.75rem; - justify-content: center; - width: 100%; - font-weight: $weight-semibold; - &:hover { - background-color: rgba($warning, 0.8); - } - .icon { - margin-right: 0.5rem; - } - } - .codeview-hidecode { - position: static; - height: 2rem; - } - &.is-expanded { - height: auto; - pre code { - overflow: inherit; - } - } - &.is-collapsed { - border: 1px solid $warning; - } - } - &:not(:last-child) { - margin-bottom: 1.5rem; - } -} - -// our changes here -.codeview { - - // padding-top: 5px; - // padding-right: 5px; - - background-color: #fffdf8; - - code.hljs { - max-height: inherit; - } - - pre { - padding: 0; - margin: 0; - border: none; - background-color: transparent; - max-height: inherit; - } - .nav-tabs > li > a { - border: none; - font-size: 13px; - padding: 5px 10px 0 10px; - text-decoration: underline !important; - } - .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, - .nav-tabs > li.active > a:focus { - //background-color: #2b2b2b; - color: #666; - border: none; - background-color: transparent; - text-decoration: none !important; - } - .nav.nav-tabs { - margin-bottom: 7px; - } - .nav-tabs { - border-bottom: none; - } - .nav { - padding-left: 5px; - } - .hljs { - padding: 1em; - background-color: transparent; - } - - .api-docs { - padding: 15px; - } -} diff --git a/examples/grails-demo/src/assets/_examples.scss b/examples/grails-demo/src/assets/_examples.scss deleted file mode 100644 index 5c452f512..000000000 --- a/examples/grails-demo/src/assets/_examples.scss +++ /dev/null @@ -1,97 +0,0 @@ - -$example-border: $yellow; - -.example-section { - //make room for the little call out - padding-top: 20px; -} -.example { - position: relative; - display: flex; - flex-direction: column; - border: 1px solid $example-border; - border-top-right-radius: $radius; - border-bottom-right-radius: $radius; - border-bottom-left-radius: $radius; - color: rgba(0, 0, 0, 0.7); - background-color: #fff; - .button-container { - position: absolute; - display: inline-flex; - background: $example-border; - border-radius: $radius $radius 0 0; - bottom: 100%; - right: -1px; - padding: 0 0 0 8px; - vertical-align: top; - .button { - display: flex; - align-items: flex-end; - font-size: 11px; - padding: 0; - text-decoration: none; - &:not(:last-child) { - margin-right: 0.5rem; - } - .icon { - margin-left: 0 !important; - } - &:hover, - &:focus { - color: $link; - background: transparent; - } - } - } - &:not(:first-child) { - margin-top: 2rem; - } - &:not(:last-child) { - margin-bottom: 1.5rem; - } - &:before { - background: $example-border; - border-radius: $radius $radius 0 0; - bottom: 100%; - content: "Example"; - display: inline-block; - font-size: 8px; - font-weight: bold; - left: -1px; - padding: 4px 8px; - position: absolute; - text-transform: uppercase; - vertical-align: top; - } - .example-component { - padding: 1.5rem; - } - .codeview { - border-top: 1px solid $example-border; - } - &.is-horizontal { - @include desktop { - flex-direction: row; - .example-component, - .codeview { - width: 50%; - } - .codeview { - display: flex; - flex-direction: column; - border-top: 0; - border-left: 1px solid $example-border; - } - .codeview, - .highlight, - pre, - code { - flex-grow: 1; - } - } - } -} - -hr.is-medium { - margin: 3rem 0; -} diff --git a/examples/grails-demo/src/assets/images/bg.png b/examples/grails-demo/src/assets/images/bg.png deleted file mode 100644 index 01ec99ed1..000000000 Binary files a/examples/grails-demo/src/assets/images/bg.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/images/bg_transparent.png b/examples/grails-demo/src/assets/images/bg_transparent.png deleted file mode 100644 index 2670c7f23..000000000 Binary files a/examples/grails-demo/src/assets/images/bg_transparent.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/images/dark_dots.png b/examples/grails-demo/src/assets/images/dark_dots.png deleted file mode 100644 index d6791d51d..000000000 Binary files a/examples/grails-demo/src/assets/images/dark_dots.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/images/line_detail.png b/examples/grails-demo/src/assets/images/line_detail.png deleted file mode 100644 index ab3a35f33..000000000 Binary files a/examples/grails-demo/src/assets/images/line_detail.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/images/note_dot.png b/examples/grails-demo/src/assets/images/note_dot.png deleted file mode 100644 index 411c44ba3..000000000 Binary files a/examples/grails-demo/src/assets/images/note_dot.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/images/picture.svg b/examples/grails-demo/src/assets/images/picture.svg deleted file mode 100644 index ef7ecc969..000000000 --- a/examples/grails-demo/src/assets/images/picture.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/examples/grails-demo/src/assets/images/user-background.png b/examples/grails-demo/src/assets/images/user-background.png deleted file mode 100644 index 014000ae0..000000000 Binary files a/examples/grails-demo/src/assets/images/user-background.png and /dev/null differ diff --git a/examples/grails-demo/src/assets/styles.scss b/examples/grails-demo/src/assets/styles.scss deleted file mode 100644 index b47d2d9f2..000000000 --- a/examples/grails-demo/src/assets/styles.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import "~angle-grinder/src/styles/init"; - -@import "examples"; -@import "codeview"; - -//@import "partials"; diff --git a/examples/grails-demo/src/config.router.js b/examples/grails-demo/src/config.router.js deleted file mode 100644 index 567b22858..000000000 --- a/examples/grails-demo/src/config.router.js +++ /dev/null @@ -1,21 +0,0 @@ -import angular from 'angular' -import { fresh } from './routerStates.js' -import appState from 'angle-grinder/src/tools/AppState' -import stateHelperInit from 'angle-grinder/src/ng/uirouter/stateHelperInit' -import _ from 'lodash' - -/** - * Config for the router - */ -angular.module('app') - .config(function($stateProvider, $urlRouterProvider, stateHelperProvider) { - const freshRouterStates = _.cloneDeep(fresh) - const freshMenu = _.cloneDeep(fresh) - - $urlRouterProvider.otherwise('/grails/dashboard') - stateHelperProvider.state(freshRouterStates) - - stateHelperInit(freshMenu) - appState.sideMenuConfig = freshMenu - - }) diff --git a/examples/grails-demo/src/dashboards/dashboard-2.html b/examples/grails-demo/src/dashboards/dashboard-2.html deleted file mode 100644 index ff80c254b..000000000 --- a/examples/grails-demo/src/dashboards/dashboard-2.html +++ /dev/null @@ -1,1459 +0,0 @@ - - - - -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
Desktop49,090
Tablet23,130
Mobile138,570
-
-
-
- -
-
-
-
-
Acquisition
-

- 100%All Sessions -

-

- (17,869 of 17,869) -

-
-
-
- -
- -
-
-
-
-
-
-
-
- -
-
-
-
-
Conversions
-

- Ecommerce Conversion Rate -

-

- 1,20% -

-
-
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-

Top 3 Products

- -
-
-
-
-
-

207K

- +8.24 % -
-
- - - - -
-
- -
- -
-
-
-
- +5,04 % -
- Alpha -
-
-
- +2,21 % -
- Omega -
-
-
- +0,68 % -
- Kappa -
-
-
-
- -
-
-
-
-
-

Users

- -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
imageUI Designer - - Peter Clark - Administrator
imageContent Designer - - Nicole Bell - Editor
imageVisual Designer - - Steven Thompson - Author
imageSenior Designer - - Kenneth Ross - Contributor
imageWeb Editor - - Ella Patterson - Editor
-
-
-
-
-
-
- 26% -
- Blog Post -
-
-
- 62% -
- Forum Post -
-
-
- 12% -
- Forum Reply -
-
-
-
- - image - -
- 5 mins ago - Nicole Bell posted on your timeline. -
- 125 Likes - 12 Comments - 25 Shares -
- In placerat feugiat odio a ullamcorper. Aliquam vel justo vel nisl sollicitudin bibendum. Donec a vehicula risus, a pellentesque metus. -
-
- Like -
-
-
-
-
-
-
-
-
-
-
- - -
-
-
-
-
-
- -
-
-
Revenue YTD
-

$159M

- +6.24M+4.08% -
- -
- -
-
-
-
-
-
-
-
-
Expenses YTD
-

$30.6M

- +15.3K+0.05% -
- -
- -
-
-
-
-
-
-
-
-
Real-Time
-
- -
-

{{randomUsers}}

- active users on site -
-
- -
- CPU used -
- -
- RAM used -
-
-
-
-
-
-
-
-
- -
-
-
Total Products
-

5,635

- 89%with active listings -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-

Monthly Statistics

- based on the major browsers -
- - - - - This Month - - - - - - - - - - - - - - - - - - - - - - - - - -
Google Chrome4909
Mozilla Firefox3857
Safari1789
Internet Explorer612
-
-
- - - Last Month - - - - - - - - - - - - - - - - - - - - - - - - - -
Google Chrome5228
Mozilla Firefox2853
Safari1948
Internet Explorer456
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
-
-
-

Social Marketing

- -
-
-
-
-
-
-

- Social Traffic (7 days) -

- -
-
- -
-
-

Facebook(43.98%)

-
-
-
-
-

- Marketing Channels (7 days) -

- -
-
- -
-
-

Display Ads(72.11%)

-
-
-
-
-
- -
-
- -
-
- 544 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Facebook 6 active campaigns - 1,596 -
- - - Twitter 6 active campaigns - 574 -
- - - YouTube 6 active campaigns - 277 -
- - - Spotify 6 active campaigns - 215 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Twitter 9 active campaigns - 1292 -
- - - Facebook 5 active campaigns - 1,097 -
- - - YouTube 6 active campaigns - 256 -
- - - Spotify 6 active campaigns - 210 -
-
-
-
- -
-
-
-
-
-
-

Campaign Options

- -
-
- -
-

- Daily Budget (USD) -

- -

- Total Budget (USD) -

- -

- Locations -

-
-

- Targeted locations: -

    -
  • - Canada -
  • -
  • - United States -
  • -
-

-

- - Edit - - - View location info - -

-
-
-

- Estimated Daily Reach -

-
-

- 5,300 - 14,000 people -

- -

- 0 - of 18,000,000 -

-
-
-

- Ad Set Duration: 31 days -

-
-
-
- -
-
-
-
- - -
-
-
-
-
-

Demographics

-
-
-
Gender
-

- Based on the share of users whose gender we were able to determinate through social profile analysis. -

-
-
-
- - 46% -
-
-
-
- - 54% -
-
-
-
-
Age
-

- Based on the share of users whose age we were able to determinate through social profile analysis. -

- - 18-29 - - - 30-49 - - - 50-64 - - - 65+ - -
-
- -
-
-
-
-
-

Specialization

-
-
-
    -
  • -
    -
    - 2 minutes ago -
    -

    - - Steven - - has completed his account. -

    -
    -
  • -
  • -
    -
    - 12:30 -
    -

    - Staff Meeting -

    -
    -
  • -
  • -
    -
    - 11:11 -
    -

    - Completed new layout. -

    -
    -
  • -
  • -
    -
    - Thu, 12 Jun -
    -

    - Contacted - - Microsoft - - for license upgrades. -

    -
    -
  • -
  • -
    -
    - Tue, 10 Jun -
    -

    - Started development new site -

    -
    -
  • -
  • -
    -
    - Sun, 11 Apr -
    -

    - Lunch with - - Nicole - - . -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
-
- -
-
-
-
-
-

Sales Performance

-
-
-
-

- Key strengths of sales force -

-

10.6m(53%)

-

- Source:CT Sales Skills Audit -

-
-

Sales skills audit

-
- -
- CPU used -
-
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- - - -
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
- -
-
-
- - - -
-
-
-
-
-
- Multi-slot transclusion has been approved -
- An endorsement for AngularJs -
- Approved -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 38%
-
-
-
-
- Male -
- 62%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Command Line Interface has been approved -
- An endorsement for Apple Watch -
- In Review -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Tools and Training has been approved -
- An endorsement for Visual Basic -
- Declined -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
-
-
-

Applications

-
-
- -
- -
-
-
-
- diff --git a/examples/grails-demo/src/dashboards/dashboard-3.html b/examples/grails-demo/src/dashboards/dashboard-3.html deleted file mode 100644 index 25b6f5a74..000000000 --- a/examples/grails-demo/src/dashboards/dashboard-3.html +++ /dev/null @@ -1,1315 +0,0 @@ - - - - -
-
-
-
- -
-
-
Total Visits
-
- 5,635 -
- +11%Previous period -
-
- -

- 89% -

-
-
-
-
-
-
- -
-
-
Bouce Rate
-
- 53 - % -
- -1%Previous period -
-
- -

- 46% -

-
-
-
-
-
-
- -
-
-
Total Conversions
-
- 179 -
- -12%Previous period -
-
- -

- 77% -

-
-
-
-
-
-
- -
-
-
Conversion Rate
-
- 7,830 - $ -
- +11%Previous period -
-
- -

- 80% -

-
-
-
-
-
-
- - -
-
-
-
-
-

Unique Visitors and Transactions

- -
-
-
-
-
-

$ 89,045

- +8.24 % -
-
- - - - -
-
- -
- -
-
-
-
- Sends -
-
- 4,160 -
-
-
-
- Opens -
-
- 1,720 -
-
-
-
- Clicks -
-
- 780 -
-
-
-
- Total Sales -
-
- 12,430 - $ -
-
-
-
-
- -
-
-
-
-
-

Dashboard Users

- -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
imageUI Designer - - Peter Clark - Administrator
imageContent Designer - - Nicole Bell - Editor
imageVisual Designer - - Steven Thompson - Author
imageSenior Designer - - Kenneth Ross - Contributor
imageWeb Editor - - Ella Patterson - Editor
-
-
-
-
-
-
- 26% -
- Blog Post -
-
-
- 62% -
- Forum Post -
-
-
- 12% -
- Forum Reply -
-
-
-
- - image - -
- 5 mins ago - Nicole Bell posted on your timeline. -
- 125 Likes - 12 Comments - 25 Shares -
- In placerat feugiat odio a ullamcorper. Aliquam vel justo vel nisl sollicitudin bibendum. Donec a vehicula risus, a pellentesque metus. -
-
- Like -
-
-
-
-
-
-
-
-
- - -
-
-
-
- -
-
-
Revenue YTD
-

$159M

- +6.24M+4.08% -
- -
- -
-
-
-
-
-
-
-
-
Expenses YTD
-

$30.6M

- +15.3K+0.05% -
- -
- -
-
-
-
-
- -
-
-
- - -
-
-
-
-
-

Social Marketing

- -
-
-
-
-
-
-

- Social Traffic (7 days) -

- -
-
- -
-
-

Facebook(43.98%)

-
-
-
-
-

- Marketing Channels (7 days) -

- -
-
- -
-
-

Display Ads(72.11%)

-
-
-
-
-
- -
-
- -
-
- 544 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Facebook 6 active campaigns - 1,596 -
- - - Twitter 6 active campaigns - 574 -
- - - YouTube 6 active campaigns - 277 -
- - - Spotify 6 active campaigns - 215 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Twitter 9 active campaigns - 1292 -
- - - Facebook 5 active campaigns - 1,097 -
- - - YouTube 6 active campaigns - 256 -
- - - Spotify 6 active campaigns - 210 -
-
-
-
- -
-
-
-
-
-
-

Campaign Options

- -
-
- -
-

- Daily Budget (USD) -

- -

- Total Budget (USD) -

- -

- Locations -

-
-

- Targeted locations: -

    -
  • - Canada -
  • -
  • - United States -
  • -
-

-

- - Edit - - - View location info - -

-
-
-

- Estimated Daily Reach -

-
-

- 5,300 - 14,000 people -

- -

- 0 - of 18,000,000 -

-
-
-

- Ad Set Duration: 31 days -

-
-
-
- -
-
-
-
- - -
-
-
-
-
-

Demographics

-
-
-
Gender
-

- Based on the share of users whose gender we were able to determinate through social profile analysis. -

-
-
-
- - 46% -
-
-
-
- - 54% -
-
-
-
-
Age
-

- Based on the share of users whose age we were able to determinate through social profile analysis. -

- - 18-29 - - - 30-49 - - - 50-64 - - - 65+ - -
-
- -
-
-
-
-
-

Specialization

-
-
-
    -
  • -
    -
    - 2 minutes ago -
    -

    - - Steven - - has completed his account. -

    -
    -
  • -
  • -
    -
    - 12:30 -
    -

    - Staff Meeting -

    -
    -
  • -
  • -
    -
    - 11:11 -
    -

    - Completed new layout. -

    -
    -
  • -
  • -
    -
    - Thu, 12 Jun -
    -

    - Contacted - - Microsoft - - for license upgrades. -

    -
    -
  • -
  • -
    -
    - Tue, 10 Jun -
    -

    - Started development new site -

    -
    -
  • -
  • -
    -
    - Sun, 11 Apr -
    -

    - Lunch with - - Nicole - - . -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
-
- -
-
-
-
-
-

Sales Performance

-
-
-
-

- Key strengths of sales force -

-

10.6m(53%)

-

- Source:CT Sales Skills Audit -

-
-

Sales skills audit

-
- -
- CPU used -
-
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
-
-

Latest posts

-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-

Applications

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Alpha Ksi4909
Gamma3857
Delta Epsilon1789
Lambda Rho612
-
- -
-
-
-
- diff --git a/examples/grails-demo/src/dashboards/dashboard-4.html b/examples/grails-demo/src/dashboards/dashboard-4.html deleted file mode 100644 index 489f7e995..000000000 --- a/examples/grails-demo/src/dashboards/dashboard-4.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - -
-
-
-
-
- -
-
-
- 5,635 -

TOTAL VISITS

- -

- Last Month: +11% -

- -
- -
-
-
-
-
-
-
-
- -

10.6m(53%)

-
-
-
- -
- CPU used -
-

Sales skills audit

-

- Source:CT Sales Skills Audit -

-
-
-
-
-
-
-
- -
-
-
- 3,234 - $ -

CONVERSION RATE

- -

- Last Month: -16% -

-
-
- -
-
-
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
-
-
- - - -
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
-
- - - -
-
-
-
-
-
- Multi-slot transclusion has been approved -
- An endorsement for AngularJs -
- Approved -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 38%
-
-
-
-
- Male -
- 62%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Command Line Interface has been approved -
- An endorsement for Apple Watch -
- In Review -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Tools and Training has been approved -
- An endorsement for Visual Basic -
- Declined -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Angular 2 integration has been approved -
- An endorsement for AngularJs -
- In Review -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
-
-
- -

In Review

- - - - - - - - - - - - - - - - - - - -
Multi-slot transclusion -
- -
Command Line Interface -
- -
Tools and Training -
- -
Angular 2 integration -
- -
-
- 12 -

Proposal

- -

- Last Month: 16 -

- -
- -
-
-
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-

Project List

-

- Keep track of your projects -

-
-
- -
- -
-
-
-
-
-
-
-
- +5,04 % -
- Alpha -
-
-
- +2,21 % -
- Omega -
-
-
- +0,68 % -
- Kappa -
-
- -
- -
-
-
-
-
-
-
-
-
-

Dashboard Users

-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
imageUI Designer - - Peter Clark - Administrator
imageContent Designer - - Nicole Bell - Editor
imageVisual Designer - - Steven Thompson - Author
imageSenior Designer - - Kenneth Ross - Contributor
imageWeb Editor - - Ella Patterson - Editor
-
-
-
-
-
-
-
-
-
-

All Earnings

-
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-
- diff --git a/examples/grails-demo/src/dashboards/dashboard-5.html b/examples/grails-demo/src/dashboards/dashboard-5.html deleted file mode 100644 index 09f166fc3..000000000 --- a/examples/grails-demo/src/dashboards/dashboard-5.html +++ /dev/null @@ -1,1208 +0,0 @@ - - - - -
-
-
-
-
-
- -
-
-
Revenue YTD
-

$159M

- +6.24M+4.08% -
- -
- -
-
-
-
-
-
-
-
-
Expenses YTD
-

$30.6M

- +15.3K+0.05% -
- -
- -
-
-
-
-
-
-
-
-
Real-Time
-
- -
-

{{randomUsers}}

- active users on site -
-
- -
- CPU used -
- -
- RAM used -
-
-
-
-
-
-
-
-
- -
-
-
Total Products
-

5,635

- 89%with active listings -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-

Monthly Statistics

- based on the major browsers -
- - - - - This Month - - - - - - - - - - - - - - - - - - - - - - - - - -
Google Chrome4909
Mozilla Firefox3857
Safari1789
Internet Explorer612
-
-
- - - Last Month - - - - - - - - - - - - - - - - - - - - - - - - - -
Google Chrome5228
Mozilla Firefox2853
Safari1948
Internet Explorer456
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
-
-
-

Social Marketing

- -
-
-
-
-
-
-

- Social Traffic (7 days) -

- -
-
- -
-
-

Facebook(43.98%)

-
-
-
-
-

- Marketing Channels (7 days) -

- -
-
- -
-
-

Display Ads(72.11%)

-
-
-
-
-
- -
-
- -
-
- 544 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Facebook 6 active campaigns - 1,596 -
- - - Twitter 6 active campaigns - 574 -
- - - YouTube 6 active campaigns - 277 -
- - - Spotify 6 active campaigns - 215 -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignClicksActions
- - - Twitter 9 active campaigns - 1292 -
- - - Facebook 5 active campaigns - 1,097 -
- - - YouTube 6 active campaigns - 256 -
- - - Spotify 6 active campaigns - 210 -
-
-
-
- -
-
-
-
-
-
-

Campaign Options

- -
-
- -
-

- Daily Budget (USD) -

- -

- Total Budget (USD) -

- -

- Locations -

-
-

- Targeted locations: -

    -
  • - Canada -
  • -
  • - United States -
  • -
-

-

- - Edit - - - View location info - -

-
-
-

- Estimated Daily Reach -

-
-

- 5,300 - 14,000 people -

- -

- 0 - of 18,000,000 -

-
-
-

- Ad Set Duration: 31 days -

-
-
-
- -
-
-
-
- - -
-
-
-
-
-

Demographics

-
-
-
Gender
-

- Based on the share of users whose gender we were able to determinate through social profile analysis. -

-
-
-
- - 46% -
-
-
-
- - 54% -
-
-
-
-
Age
-

- Based on the share of users whose age we were able to determinate through social profile analysis. -

- - 18-29 - - - 30-49 - - - 50-64 - - - 65+ - -
-
- -
-
-
-
-
-

Specialization

-
-
-
    -
  • -
    -
    - 2 minutes ago -
    -

    - - Steven - - has completed his account. -

    -
    -
  • -
  • -
    -
    - 12:30 -
    -

    - Staff Meeting -

    -
    -
  • -
  • -
    -
    - 11:11 -
    -

    - Completed new layout. -

    -
    -
  • -
  • -
    -
    - Thu, 12 Jun -
    -

    - Contacted - - Microsoft - - for license upgrades. -

    -
    -
  • -
  • -
    -
    - Tue, 10 Jun -
    -

    - Started development new site -

    -
    -
  • -
  • -
    -
    - Sun, 11 Apr -
    -

    - Lunch with - - Nicole - - . -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
  • -
    -
    - Wed, 25 Mar -
    -

    - server Maintenance. -

    -
    -
  • -
-
- -
-
-
-
-
-

Sales Performance

-
-
-
-

- Key strengths of sales force -

-

10.6m(53%)

-

- Source:CT Sales Skills Audit -

-
-

Sales skills audit

-
- -
- CPU used -
-
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
- - - -
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
-
- -
-
-
- - - -
-
-
-
-
-
- Multi-slot transclusion has been approved -
- An endorsement for AngularJs -
- Approved -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 38%
-
-
-
-
- Male -
- 62%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Command Line Interface has been approved -
- An endorsement for Apple Watch -
- In Review -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- Tools and Training has been approved -
- An endorsement for Visual Basic -
- Declined -
-
-
-
- Subscribers -
- 156,987
-
-
-
-
- Female -
- 68%
-
-
-
-
- Male -
- 32%
-
-
-
- -
-
-
-
-
-

Applications

-
-
- -
- -
-
-
-
- diff --git a/examples/grails-demo/src/dashboards/dashboard.html b/examples/grails-demo/src/dashboards/dashboard.html deleted file mode 100644 index f566c5d01..000000000 --- a/examples/grails-demo/src/dashboards/dashboard.html +++ /dev/null @@ -1,715 +0,0 @@ - - - - -
-
-
-
-
-
-
-
-
-

Project List

-

- Keep track of your projects -

-
-
- -
- -
-
-
-
-
-
-
-
- +5,04 % -
- Alpha -
-
-
- +2,21 % -
- Omega -
-
-
- +0,68 % -
- Kappa -
-
- -
- -
-
-
-
-
-
-
-
-
-

Dashboard Users

-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
imageUI Designer - - Peter Clark - Administrator
imageContent Designer - - Nicole Bell - Editor
imageVisual Designer - - Steven Thompson - Author
imageSenior Designer - - Kenneth Ross - Contributor
imageWeb Editor - - Ella Patterson - Editor
-
-
-
-
-
-
-
-
-
-

All Earnings

-
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-
- - -
-
-
-
-
- -
-
- -
-
-
- -
-
-
Expenses YTD
-

$30.6M

- +15.3K+0.05% -
- -
- -
-
-
-
-
-
- -
-
-
Real-Time
-
- -
-

{{randomUsers}}

- active users on site -
-
- -
- CPU used -
- -
- RAM used -
-
-
-
-
-
-
-
-
-
-
-
-

Sales Performance

- -
-
-
-

- Key strengths of sales force -

-

10.6m(53%)

-

- Source:CT Sales Skills Audit -

-
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
-
-

Social Marketing

- -
-
-
-
-
-
-

- Social Traffic (7 days) -

- -
-
- -
-
-

Facebook(43.98%)

-
-
-
-
-

- Social Traffic (30 days) -

- -
-
- -
-
-

Twitter(72.11%)

-
-
-
-
- -
-
-
-
-
-
-

Campaign Options

- -
-
- -
-

- Daily Budget (USD) -

- -

- Total Budget (USD) -

- -
-

- Estimated Daily Reach -

-
-

- 5,300 - 14,000 people -

- -

- 0 - of 18,000,000 -

-
-
-
- -
-
-
-
- - -
-
-
-
-
-

Latest posts

-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-

Comments

-
-
-
- - image - -
- 5 mins ago - Nicole Bell posted on your timeline. -
- 125 Likes - 12 Comments - 25 Shares -
- In placerat feugiat odio a ullamcorper. Aliquam vel justo vel nisl sollicitudin bibendum. Donec a vehicula risus, a pellentesque metus. -
-
- Like -
-
-
-
- - image - -
- 12 mins ago - Steven Thompson posted on your timeline. -
- 111 Likes - 28 Comments - 9 Shares -
- Praesent egestas vehicula arcu, eu tincidunt lacus porta quis. Ut turpis metus. -
-
- Like -
-
-
-
-
-
-
-
- diff --git a/examples/grails-demo/src/dashboards/dashboardCtrl.js b/examples/grails-demo/src/dashboards/dashboardCtrl.js deleted file mode 100644 index 932f7de45..000000000 --- a/examples/grails-demo/src/dashboards/dashboardCtrl.js +++ /dev/null @@ -1,464 +0,0 @@ -'use strict' -/** - * controllers used for the dashboard - */ - -app.controller('ProductsCtrl', ['$scope', - function($scope) { - $scope.labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - $scope.series = ['Alpha', 'Omega', 'Kappa'] - $scope.data = [[656, 594, 806, 817, 568, 557, 408, 843, 642, 1202, 1322, 847], [282, 484, 402, 194, 864, 275, 905, 1025, 123, 1455, 650, 1651], [768, 368, 253, 163, 437, 678, 1239, 1345, 1898, 1766, 1603, 2116]] - $scope.colors = [{ - fillColor: 'rgba(90,135,112,0.6)', - strokeColor: 'rgba(90,135,112,1)', - pointColor: 'rgba(90,135,112,1)' - }, { - fillColor: 'rgba(127,140,141,0.6)', - strokeColor: 'rgba(127,140,141,1)', - pointColor: 'rgba(127,140,141,1)' - }, { - fillColor: 'rgba(148,116,153,0.6)', - strokeColor: 'rgba(148,116,153,1)', - pointColor: 'rgba(148,116,153,1)' - }] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - responsive: true, - scaleFontFamily: "'Helvetica', 'Arial', sans-serif", - scaleFontSize: 11, - scaleFontColor: '#aaa', - scaleShowGridLines: true, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontSize: 12, - scaleGridLineColor: 'rgba(0,0,0,.05)', - scaleGridLineWidth: 1, - bezierCurve: true, - bezierCurveTension: 0.4, - scaleLineColor: 'transparent', - scaleShowVerticalLines: false, - pointDot: false, - pointDotRadius: 2, - pointDotStrokeWidth: 1, - pointHitDetectionRadius: 20, - datasetStroke: true, - tooltipXPadding: 20, - datasetStrokeWidth: 2, - datasetFill: true, - animationEasing: 'easeInOutExpo' - } - }]) -app.controller('SalesCtrl', ['$scope', - function($scope) { - $scope.labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'] - $scope.series = ['First', 'Second'] - $scope.data = [[65, 59, 80, 81, 56, 55, 40], [28, 48, 40, 19, 86, 27, 90]] - $scope.colors = [{ - fillColor: 'rgba(148,116,153,0.7)', - highlightFill: 'rgba(148,116,153,1)' - }, { - fillColor: 'rgba(127,140,141,0.7)', - highlightFill: 'rgba(127,140,141,1)' - }] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - responsive: true, - scaleFontFamily: "'Helvetica', 'Arial', sans-serif", - scaleFontSize: 11, - scaleFontColor: '#aaa', - scaleBeginAtZero: true, - tooltipTitleFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontSize: 12, - scaleShowGridLines: true, - scaleLineColor: 'transparent', - scaleShowVerticalLines: false, - scaleGridLineColor: 'rgba(0,0,0,.05)', - scaleGridLineWidth: 1, - barShowStroke: false, - barStrokeWidth: 2, - barValueSpacing: 5, - barDatasetSpacing: 1 - } - }]) -app.controller('AcquisitionCtrl', ['$scope', - function($scope) { - $scope.labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'] - $scope.series = ['dataset'] - $scope.data = [[65, 59, 80, 81, 56, 55, 40]] - $scope.colors = [{ - fillColor: 'rgba(148,116,153,0.7)', - strokeColor: 'rgba(148,116,153,0)', - highlightFill: 'rgba(148,116,153,1)', - highlightStroke: 'rgba(148,116,153,1)' - }] - // Chart.js Options - $scope.options = { - maintainAspectRatio: false, - showScale: false, - barDatasetSpacing: 0, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - responsive: true, - scaleBeginAtZero: true, - scaleShowGridLines: false, - scaleLineColor: 'transparent', - barShowStroke: false, - barValueSpacing: 5 - } - }]) -app.controller('ConversionsCtrl', ['$scope', - function($scope) { - $scope.labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - $scope.series = ['Transactions', 'Unique Visitors'] - $scope.data = [[65, 59, 80, 81, 56, 55, 40, 84, 64, 120, 132, 87], [172, 175, 193, 194, 161, 175, 153, 190, 175, 231, 234, 250]] - $scope.colors = [{ - fillColor: 'rgba(91,155,209,0.5)', - strokeColor: 'rgba(91,155,209,1)' - }, { - fillColor: 'rgba(91,155,209,0.5)', - strokeColor: 'rgba(91,155,209,0.5)' - }] - - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - showScale: false, - scaleLineWidth: 0, - responsive: true, - scaleFontFamily: "'Helvetica', 'Arial', sans-serif", - scaleFontSize: 11, - scaleFontColor: '#aaa', - scaleShowGridLines: true, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontSize: 12, - scaleGridLineColor: 'rgba(0,0,0,.05)', - scaleGridLineWidth: 1, - bezierCurve: true, - bezierCurveTension: 0.5, - scaleLineColor: 'transparent', - scaleShowVerticalLines: false, - pointDot: false, - pointDotRadius: 4, - pointDotStrokeWidth: 1, - pointHitDetectionRadius: 20, - datasetStroke: true, - datasetStrokeWidth: 2, - datasetFill: true, - animationEasing: 'easeInOutExpo' - } - }]) -app.controller('BarCtrl', ['$scope', - function($scope) { - $scope.labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'i', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] - $scope.series = ['dataset'] - $scope.data = [[65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 80, 81]] - $scope.colors = [{ - fillColor: 'rgba(154,137,181,0.6)', - highlightFill: 'rgba(154,137,181,0.9)' - }] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - showScale: false, - barDatasetSpacing: 0, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - responsive: true, - scaleBeginAtZero: true, - scaleShowGridLines: false, - scaleLineColor: 'transparent', - barShowStroke: false, - barValueSpacing: 5 - } - }]) -app.controller('BarCtrl2', ['$scope', - function($scope) { - $scope.labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'i', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] - $scope.series = ['dataset'] - $scope.data = [[65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 55, 40, 65, 59, 80, 81, 56, 80, 81]] - $scope.colors = [{ - fillColor: 'rgba(255,255,244,0.3)', - strokeColor: 'rgba(255,255,244,0.5)' - }] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - showScale: false, - barDatasetSpacing: 0, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - responsive: true, - scaleBeginAtZero: true, - scaleShowGridLines: false, - scaleLineColor: 'transparent', - barShowStroke: false, - barValueSpacing: 5 - } - }]) -app.controller('LineCtrl', ['$scope', - function($scope) { - $scope.labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - $scope.series = ['dataset'] - $scope.data = [[65, 59, 80, 81, 56, 95, 100]] - $scope.colors = [{ - fillColor: 'rgba(0,0,0,0)', - strokeColor: 'rgba(0,0,0,0.2)' - }] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - maintainAspectRatio: false, - showScale: false, - scaleLineWidth: 0, - responsive: true, - scaleFontFamily: "'Helvetica', 'Arial', sans-serif", - scaleFontSize: 11, - scaleFontColor: '#aaa', - scaleShowGridLines: true, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipTitleFontSize: 12, - scaleGridLineColor: 'rgba(0,0,0,.05)', - scaleGridLineWidth: 1, - bezierCurve: false, - bezierCurveTension: 0.2, - scaleLineColor: 'transparent', - scaleShowVerticalLines: false, - pointDot: true, - pointDotRadius: 4, - pointDotStrokeWidth: 1, - pointHitDetectionRadius: 20, - datasetStroke: true, - datasetStrokeWidth: 2, - datasetFill: true, - animationEasing: 'easeInOutExpo' - } - }]) -app.controller('RandomCtrl', function($scope, $interval) { - $scope.randomUsers = 0 - var interval = 1500 - - $scope.realtime = function() { - var random = $interval(function() { - $scope.randomUsers = Math.floor((Math.random() * 6) + 100) - interval = Math.floor((Math.random() * 5000) + 1000) - $interval.cancel(random) - $scope.realtime() - }, interval) - } - $scope.realtime() -}) -app.controller('KnobCtrl1', function($scope) { - $scope.value = 65 - $scope.options = { - unit: '%', - readOnly: true, - size: 70, - fontSize: '11px', - textColor: 'rgb(154,137,181)', - trackWidth: 5, - barWidth: 10, - trackColor: 'rgba(154,137,181,0.6)', - barColor: 'rgba(154,137,181,0.9)' - } -}) -app.controller('KnobCtrl2', function($scope) { - $scope.value = 330 - $scope.options = { - unit: 'MB', - readOnly: true, - size: 70, - fontSize: '11px', - textColor: 'rgb(154,137,181)', - trackWidth: 5, - barWidth: 10, - trackColor: 'rgba(154,137,181,0.6)', - barColor: 'rgba(154,137,181,0.9)', - max: 1024 - } -}) -app.controller('KnobCtrl3', function($scope) { - $scope.value = 65 - $scope.options = { - unit: '%', - readOnly: true, - size: 70, - fontSize: '11px', - textColor: '#fff', - trackWidth: 5, - barWidth: 10, - trackColor: 'rgba(255,255,255,0.4)', - barColor: '#8773A8' - } -}) -app.controller('KnobCtrl4', function($scope) { - $scope.value = 330 - $scope.options = { - unit: 'MB', - readOnly: true, - size: 70, - fontSize: '11px', - textColor: '#fff', - trackWidth: 5, - barWidth: 10, - trackColor: 'rgba(255,255,255,0.4)', - barColor: '#8773A8', - max: 1024 - } -}) -app.controller('SocialCtrl1', ['$scope', - function($scope) { - $scope.labels = ['Facebook', 'Twitter', 'YouTube', 'Spotify'] - $scope.data = [300, 150, 100, 80] - $scope.colors = ['#6F83B6', '#79BEF1', '#ED5952', '#8BC33E'] - - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - responsive: false, - scaleShowLabelBackdrop: true, - scaleBackdropColor: 'rgba(255,255,255,0.75)', - scaleBeginAtZero: true, - scaleBackdropPaddingY: 2, - scaleBackdropPaddingX: 2, - scaleShowLine: true, - segmentShowStroke: true, - segmentStrokeColor: '#fff', - segmentStrokeWidth: 2, - animationSteps: 100, - animationEasing: 'easeOutBounce', - animateRotate: true, - animateScale: false - } - }]) -app.controller('SocialCtrl2', ['$scope', - function($scope) { - $scope.labels = ['Facebook', 'Twitter', 'YouTube', 'Spotify'] - $scope.data = [180, 210, 97, 60] - $scope.colors = ['#6F83B6', '#79BEF1', '#ED5952', '#8BC33E'] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - responsive: false, - scaleShowLabelBackdrop: true, - scaleBackdropColor: 'rgba(255,255,255,0.75)', - scaleBeginAtZero: true, - scaleBackdropPaddingY: 2, - scaleBackdropPaddingX: 2, - scaleShowLine: true, - segmentShowStroke: true, - segmentStrokeColor: '#fff', - segmentStrokeWidth: 2, - animationSteps: 100, - animationEasing: 'easeOutBounce', - animateRotate: true, - animateScale: false - } - }]) -app.controller('SocialCtrl3', ['$scope', - function($scope) { - $scope.labels = ['Fb', 'YT', 'Tw'] - $scope.data = [300, 50, 100] - $scope.colors = ['#6F83B6', '#79BEF1', '#ED5952'] - - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - responsive: false, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipCornerRadius: 0, - tooltipCaretSize: 2, - segmentShowStroke: true, - segmentStrokeColor: '#fff', - segmentStrokeWidth: 2, - percentageInnerCutout: 50, - animationSteps: 100, - animationEasing: 'easeOutBounce', - animateRotate: true, - animateScale: false - - } - }]) -app.controller('SocialCtrl4', ['$scope', - function($scope) { - $scope.labels = ['Sc', 'Ad'] - $scope.data = [200, 150] - $scope.colors = ['#8BC33E', '#7F8C8D'] - // Chart.js Options - complete list at http://www.chartjs.org/docs/ - $scope.options = { - responsive: false, - tooltipFontSize: 11, - tooltipFontFamily: "'Helvetica', 'Arial', sans-serif", - tooltipCornerRadius: 0, - tooltipCaretSize: 2, - segmentShowStroke: true, - segmentStrokeColor: '#fff', - segmentStrokeWidth: 2, - percentageInnerCutout: 50, - animationSteps: 100, - animationEasing: 'easeOutBounce', - animateRotate: true, - animateScale: false - - } - }]) -app.controller('PerformanceCtrl1', ['$scope', - function($scope) { - $scope.value = 85 - $scope.options = { - size: 125, - unit: '%', - trackWidth: 10, - barWidth: 10, - step: 5, - trackColor: 'rgba(52,152,219,.1)', - barColor: 'rgba(69,204,206,.5)' - } - }]) -app.controller('BudgetCtrl', ['$scope', - function($scope) { - $scope.dailyValue = '25' - $scope.totalValue = '750' - - $scope.dailyOptions = { - from: 1, - to: 100, - step: 1, - dimension: ' $', - className: 'clip-slider', - css: { - background: { - 'background-color': 'silver' - }, - before: { - 'background-color': 'rgb(154,137,181)' - }, // zone before default value - after: { - 'background-color': 'rgb(154,137,181)' - } // zone after default value - } - } - $scope.totalOptions = { - from: 100, - to: 1000, - step: 1, - dimension: ' $', - className: 'clip-slider', - css: { - background: { - 'background-color': 'silver' - }, - before: { - 'background-color': 'rgb(127,140,141)' - }, // zone before default value - after: { - 'background-color': 'rgb(127,140,141)' - } // zone after default value - } - } - }]) diff --git a/examples/grails-demo/src/dashboards/dashyak.html b/examples/grails-demo/src/dashboards/dashyak.html deleted file mode 100644 index 54c4eeadb..000000000 --- a/examples/grails-demo/src/dashboards/dashyak.html +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/examples/grails-demo/src/fresh/appbar/index.html b/examples/grails-demo/src/fresh/appbar/index.html deleted file mode 100644 index a1207f14f..000000000 --- a/examples/grails-demo/src/fresh/appbar/index.html +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/examples/grails-demo/src/fresh/appbar/index.js b/examples/grails-demo/src/fresh/appbar/index.js deleted file mode 100644 index 7354f5205..000000000 --- a/examples/grails-demo/src/fresh/appbar/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import appState from 'angle-grinder/src/tools/AppState' - -class controller { - constructor($element) { - this.$element = $element - this.appState = appState - } - - get title() { - return appState.title - } - -} - -export default angular.module('demo.fresh.appbar', []) - .component('freshAppbar', { - controller, - template: require('./index.html') - }) - .name diff --git a/examples/grails-demo/src/fresh/index.html b/examples/grails-demo/src/fresh/index.html deleted file mode 100644 index bfd3ded3f..000000000 --- a/examples/grails-demo/src/fresh/index.html +++ /dev/null @@ -1,11 +0,0 @@ -
- -
- -
-
- -
-
- - diff --git a/examples/grails-demo/src/fresh/index.js b/examples/grails-demo/src/fresh/index.js deleted file mode 100644 index af50419cc..000000000 --- a/examples/grails-demo/src/fresh/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import template from './index.html' -import appbarModule from './appbar' - - -export default angular - .module('demo.fresh', [appbarModule]) - .component('freshApp', { template }) - .name diff --git a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.css b/examples/grails-demo/src/fresh/sidenav/simple-sidenav.css deleted file mode 100644 index 63260ed32..000000000 --- a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.css +++ /dev/null @@ -1,101 +0,0 @@ -.ass-menu-button { - margin-top: 10px; - margin-left: 5px; - width: 30px; - height: 30px; - cursor: pointer; -} - -.ass-aside-menu { - position: fixed; - z-index: 1000; - display: block; - left: -350px; - top: 0px; - width: 300px; - height: 100vh; - padding: 25px; - background-color: #eee; - color: #000; -} - -.ass-aside-menu-title { - text-transform: uppercase; -} - -.ass-aside-menu-item { - display: block; - cursor: pointer; - text-decoration: none; - text-transform: uppercase; - height: 40px; - border-bottom: 1px solid #bbb; - line-height: 40px; -} - -.ass-aside-menu-item:first-of-type { - margin-top: 20px; -} - -.ass-aside-menu-item:active, .ass-aside-menu-item:visited, .ass-aside-menu-item:link, -.ass-aside-menu-close:active, .ass-aside-menu-close:visited, .ass-aside-menu-close:link { - color: #000; - text-decoration: none; -} - -.ass-aside-menu-item:hover, .ass-aside-menu-close:hover { - color: #aaa; - text-decoration: none; -} - -.ass-aside-menu-icon { - display: inline-block; - margin-right: 10px; -} - -.ass-aside-menu-close { - position: absolute; - display: inline-block; - right: 25px; - top: 15px; - cursor: pointer; -} - -.ass-aside-overlay { - position: fixed; - z-index: 999; - top: 0px; - left: 0px; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.1); -} - -/* annimations */ -.ass-slide-in { - -webkit-animation: slide-in 0.5s forwards; - animation: slide-in 0.5s forwards; -} - -@-webkit-keyframes slide-in { - 100% { left: 0px; } -} - -@keyframes slide-in { - 100% { left: 0px; } -} - -.ass-slide-out { - -webkit-animation: slide-out 0.5s backwards; - animation: slide-out 0.5s backwards; -} - -@-webkit-keyframes slide-out { - 0% { left: 0px; } - 100% { left: -350px; } -} - -@keyframes slide-out { - 0% { left: 0px; } - 100% { left: -350px; } -} diff --git a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.html b/examples/grails-demo/src/fresh/sidenav/simple-sidenav.html deleted file mode 100644 index 818d08f7a..000000000 --- a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.html +++ /dev/null @@ -1,16 +0,0 @@ -
- -
-
- -
-
diff --git a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.js b/examples/grails-demo/src/fresh/sidenav/simple-sidenav.js deleted file mode 100644 index 9b9d2fc8b..000000000 --- a/examples/grails-demo/src/fresh/sidenav/simple-sidenav.js +++ /dev/null @@ -1,32 +0,0 @@ -import './simple-sidenav.css' -// import appState from 'angle-grinder/src/tools/AppState' - -export default angular.module('simple-sidebar', []) - .directive('simpleSidebar', () => ({ - restrict: 'E', - transclude: true, - scope: { - items: '=', - open: '=', - title: '=', - settings: '=' - }, - template: require('./simple-sidenav.html'), - link: function(scope, element, attrs) { - scope.slide - if (scope.open) { - scope.slide = 'in' - } - - scope.openSidebar = function() { - scope.open = true - scope.slide = 'in' - } - - scope.closeSidebar = function() { - scope.open = false - scope.slide = 'out' - } - } - })) - .name diff --git a/examples/grails-demo/src/grids/customGridList/index.js b/examples/grails-demo/src/grids/customGridList/index.js deleted file mode 100644 index 44f397963..000000000 --- a/examples/grails-demo/src/grids/customGridList/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import compDemoModule from './listComp' - -const template = ` - - - -` -export default angular -.module(compDemoModule) - .component('basicGridRestIndex', { - template: template, - controller: function() { - this.rawHtml = require('./list.html') - this.rawJs = require('!raw-loader!./listComp.js').default - } - }).name - diff --git a/examples/grails-demo/src/grids/customGridList/list.html b/examples/grails-demo/src/grids/customGridList/list.html deleted file mode 100644 index e606c3b07..000000000 --- a/examples/grails-demo/src/grids/customGridList/list.html +++ /dev/null @@ -1,13 +0,0 @@ -
- - - - - - -
vm: {{$ctrl.vm | json}}
-
Selected Rows data: {{$ctrl.selectedRowsData | json}}
-
diff --git a/examples/grails-demo/src/grids/customGridList/listComp.js b/examples/grails-demo/src/grids/customGridList/listComp.js deleted file mode 100644 index f5adf2df0..000000000 --- a/examples/grails-demo/src/grids/customGridList/listComp.js +++ /dev/null @@ -1,55 +0,0 @@ -//import controller from './listCtrl' -import template from './list.html' -import BaseListCtrl from 'angle-grinder/src/ng/gridz/list/BaseListCtrl' -import restStoreApi from '../../store/RestStoreApi' -import toast from 'angle-grinder/src/tools/toast' -import _ from 'lodash' - -class ListCtrl extends BaseListCtrl { - apiKey = 'invoice' - - // massUpdateTemplate = require('../basicGrid/templates/bulkUpdateForm.html') - - //static $inject = _.union(super.$inject, ['someService']) - constructor(...args) { - super(...args) - this.dataApi = restStoreApi.invoice - } - - $onInit() { - this.isConfigured = false - // console.log("ListCtrl ", this) - this.cfg = {} - this.doConfig() - } - - fireToolbarAction(btnItem, event) { - super.fireToolbarAction(btnItem, event) - // if btnItem.key is the same name as function then it will be fired - if(btnItem.key === 'showSelected') this.displaySelectedRowsData() - } - - displaySelectedRowsData() { - // console.log("displaySelectedRowsData") - this.selectedRowsData = this.gridCtrl.getSelectedRows() - } - - // these are called because the super.fireToolbarAction will look for same function name - // as the key - import() { - console.log("import") - toast.success('import something') - } - - ptp() { - toast.success('ptp') - } -} - -export default angular - .module('ag.demo.basicRestGridDemo', ['ag.demo.api']) - .component('basicRestGridDemo', { - template: template, - controller: ListCtrl - }) - .name diff --git a/examples/grails-demo/src/grids/index.js b/examples/grails-demo/src/grids/index.js deleted file mode 100644 index 2c225853e..000000000 --- a/examples/grails-demo/src/grids/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import angular from 'angular' -import customGridList from './customGridList' -import store from '../store' - -const gapp = angular.module('demo.gridz', [ - customGridList, - store -]) - -export default gapp.name - diff --git a/examples/grails-demo/src/main.js b/examples/grails-demo/src/main.js deleted file mode 100644 index f77ff26a7..000000000 --- a/examples/grails-demo/src/main.js +++ /dev/null @@ -1,19 +0,0 @@ -// CSS and Sass -import 'angle-grinder/src/styles/vendor.css.js' -import 'angle-grinder/src/styles/all.scss' -import './assets/styles.scss' - -// VENDOR -import '~/vendor' - -// logging turn on debug -import $log from 'angle-grinder/src/utils/Log' - -import './app.config' -import './AppCtrl' - -import './grids' -import {setClientConfig} from 'angle-grinder/src/dataApi/kyApi' - -setClientConfig({prefixUrl: 'http://localhost:8080'}) -$log.debugEnabled(true) diff --git a/examples/grails-demo/src/routerStates.js b/examples/grails-demo/src/routerStates.js deleted file mode 100644 index a45e71c5e..000000000 --- a/examples/grails-demo/src/routerStates.js +++ /dev/null @@ -1,49 +0,0 @@ - -const dashStates = { - name: 'dashboard', - // template: require("./dashboards/dashboard.html"), - template: require('./dashboards/dashyak.html'), - // resolve: loadSequence('d3', 'ui.knob', 'countTo', 'dashboardCtrl'), - data: { - icon: 'mdi mdi-monitor-dashboard' - } -} - -const gridsStates = { - name: 'grids', - abstract: true, - template: '
', - data: { - icon: 'fa fa-table ' - }, - children: [ - { - name: 'customer', - component: 'agGridList', - data: { title: 'Vanilla rest agGridList' }, - resolve: { - apiKey: () => "customer", - notification: () => ({ - class: "is-primary is-light", - text: "Uses ui-router to send rest apiKey to generic agGridList component" - }) - } - }, - { - name: 'override-rest-grid', - data: { title: 'Custom Grid Comp'}, - component: 'basicGridRestIndex' - } - ] -} - -export const fresh = { - name: 'grails', - url: '/grails', - component: 'freshApp', - abstract: true, - children: [dashStates, gridsStates] -} - -export default { fresh } -// export default appRoot diff --git a/examples/grails-demo/src/store/LocalStoreApi.js b/examples/grails-demo/src/store/LocalStoreApi.js deleted file mode 100644 index 00fa56b26..000000000 --- a/examples/grails-demo/src/store/LocalStoreApi.js +++ /dev/null @@ -1,24 +0,0 @@ -import SessionStorageApi from 'angle-grinder/src/dataApi/SessionStorageApi' - -function makeDataApi(name, path){ - return new SessionStorageApi(name, path) -} - -/** main holder for api*/ -export class LocalStoreApi{ - - static factory() { - return new LocalStoreApi() - } - - constructor() { - } - - get customer() { return makeDataApi("customers", "data/Customers.json")} - get invoice() { return makeDataApi("invoices", "data/Invoices.json")} - -} - -const _instance = LocalStoreApi.factory() - -export default _instance diff --git a/examples/grails-demo/src/store/RestStoreApi.js b/examples/grails-demo/src/store/RestStoreApi.js deleted file mode 100644 index e97d10a8f..000000000 --- a/examples/grails-demo/src/store/RestStoreApi.js +++ /dev/null @@ -1,39 +0,0 @@ -import RestDataApi from 'angle-grinder/src/dataApi/RestDataApi' -import ky from 'ky' - -function makeDataApi(endpoint){ - return new RestDataApi(`http://localhost:8080/api/${endpoint}`) -} - -/** main holder for api*/ -export class RestStoreApi { - _cached = {} - - static factory() { - return new RestStoreApi() - } - - constructor() { - } - - get customer() { return makeDataApi("customer")} - get invoice() { return makeDataApi("invoice")} - get tranState() { return makeDataApi("tranState")} - get tag() { return makeDataApi("tag")} - - appConfig(configKey) { - return ky.get(`api/appConfig/${configKey}`).json() - } - - /** - * checks cache and if not there then does a ky.get - */ - configFromCache(key) { - - } - -} - -const _instance = RestStoreApi.factory() - -export default _instance diff --git a/examples/grails-demo/src/store/index.js b/examples/grails-demo/src/store/index.js deleted file mode 100644 index e4e55bbbd..000000000 --- a/examples/grails-demo/src/store/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import {LocalStoreApi} from "./LocalStoreApi"; -import {RestStoreApi} from "./RestStoreApi"; - -// export module name -export default angular.module('ag.demo.api', []) - .service('localStoreApi', LocalStoreApi) - .service('restStoreApi', RestStoreApi) - //this is the default, used in components, change it to RestStore to test that - .service('dataStoreApi', RestStoreApi) - .name diff --git a/jest.config.js b/jest.config.js index 26bbfbe58..3eb7c5767 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - testMatch: [ "**/__tests__/**/*.[jt]s?(x)"], + testMatch: [ "**/__tests__/**/*.spec.[jt]s?(x)"], // testMatch: ['/svelte/__tests__/specs/**/*.spec.js'], transform: { '^.+\\.m?(j|t)s$': 'babel-jest', diff --git a/jsconfig.json b/jsconfig.json index a9f9f7577..316ed2ebd 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,16 +1,31 @@ + { + "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { - "target": "es2017", - "allowSyntheticDefaultImports": false, - "baseUrl": "./", + "moduleResolution": "node", + "baseUrl": ".", + "outDir": "./dummy", + "target": "es6", + "module": "es6", "paths": { - "~/*": ["src/*"], - "angle-grinder/*": ["src/*"], - // "Components/*": ["src/components/*"], - // "Ducks/*": ["src/ducks/*"], - // "Shared/*": ["src/shared/*"], - // "App/*": ["src/*"] - } + "angle-grinder/src/*": ["src/*"], + "angle-grinder/svelte/*": ["svelte/*"] + }, + "checkJs": true, + "allowJs": true, + "skipLibCheck": true, + "sourceMap": true, + "noImplicitAny": false, + "strictNullChecks": false, + "allowSyntheticDefaultImports": true, + "allowUmdGlobalAccess": true }, - "exclude": ["node_modules", "dist"] + "exclude": [ + "node_modules" + ], + "include": [ + "src/**/*", + "svelte/**/*", + "examples/demo/src/**/*", + ] } diff --git a/package.json b/package.json index 96439acd7..2fef14d5e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "angle-grinder", "license": "MIT", - "version": "4.0.40", + "version": "4.0.39", "scripts": { "build": "rimraf dist && yarn build:wip", "transpile": "babel ./examples/demo/mocker --out-dir ./examples/demo/mocker-server", @@ -54,7 +54,7 @@ "dequal": "^2.0.2", "dotenv": "^10.0.0", "esm": "^3.2.25", - "fast-safe-stringify": "^2.0.8", + "fast-safe-stringify": "^2.1.1", "fastclick": "^1.0.6", "feather-icons": "^4.28.0", "font-awesome": "^4.7.0", @@ -62,6 +62,7 @@ "framework7-icons": "^5.0.3", "framework7-svelte": "^6.3.4", "free-jqgrid": "4.13.3", + "fuse.js": "^6.4.6", "highlight.js": "^10.7.2", "jquery": "3.4.1", "jquery-ui": "^1.12.1", @@ -90,6 +91,9 @@ "@babel/preset-typescript": "^7.16.0", "@fullhuman/postcss-purgecss": "^1.3.0", "@testing-library/jest-dom": "^5.14.1", + "@tsconfig/svelte": "^2.0.1", + "@types/node": "^16.11.12", + "@types/webpack-env": "^1.16.3", "angular-mocks": "1.6.10", "autoprefixer": "^9.7.3", "babel-loader": "^8.0.6", diff --git a/src/__tests__/countries.js b/src/__tests__/countries.js new file mode 100644 index 000000000..db8e67c12 --- /dev/null +++ b/src/__tests__/countries.js @@ -0,0 +1,44 @@ +export default [ // Taken from https://gist.github.com/unceus/6501985 + { id: 1, name: 'Åland Islands', code: 'AX' }, + { id: 2, name: 'Austria', code: 'AT' }, + { id: 3, name: 'Azerbaijan', code: 'AZ' }, + { id: 4, name: 'Brazil', code: 'BR' }, + { id: 5, name: 'British Indian Ocean Territory', code: 'IO' }, + { id: 6, name: 'Brunei Darussalam', code: 'BN' }, + { id: 7, name: 'Bulgaria', code: 'BG' }, + { id: 8, name: 'Canada', code: 'CA' }, + { id: 9, name: 'Cape Verde', code: 'CV' }, + { id: 10, name: 'Cayman Islands', code: 'KY' }, + { id: 11, name: 'Christmas Island', code: 'CX' }, + { id: 12, name: 'Cocos (Keeling) Islands', code: 'CC' }, + { id: 13, name: 'Colombia', code: 'CO' }, + { id: 14, name: 'Denmark', code: 'DK' }, + { id: 15, name: 'Djibouti', code: 'DJ' }, + { id: 16, name: 'Dominica', code: 'DM' }, + { id: 17, name: 'Dominican Republic', code: 'DO' }, + { id: 18, name: 'Mali', code: 'ML' }, + { id: 19, name: 'Malta', code: 'MT' }, + { id: 20, name: 'Netherlands', code: 'NL' }, + { id: 21, name: 'New Caledonia', code: 'NC' }, + { id: 22, name: 'Pakistan', code: 'PK' }, + { id: 23, name: 'Palau', code: 'PW' }, + { id: 24, name: 'Palestinian Territory, Occupied', code: 'PS' }, + { id: 25, name: 'Panama', code: 'PA' }, + { id: 26, name: 'Papua New Guinea', code: 'PG' }, + { id: 27, name: 'Saint Helena', code: 'SH' }, + { id: 28, name: 'Saint Kitts and Nevis', code: 'KN' }, + { id: 29, name: 'Swaziland', code: 'SZ' }, + { id: 30, name: 'Sweden', code: 'SE' }, + { id: 31, name: 'Switzerland', code: 'CH' }, + { id: 32, name: 'Uganda', code: 'UG' }, + { id: 33, name: 'Ukraine', code: 'UA' }, + { id: 34, name: 'United Emirates', code: 'AE' }, + { id: 35, name: 'United Kingdom', code: 'GB' }, + { id: 36, name: 'United States', code: 'US' }, + { id: 37, name: 'Uruguay', code: 'UY' }, + { id: 38, name: 'Uzbekistan', code: 'UZ' }, + { id: 39, name: 'Vanuatu', code: 'VU' }, + { id: 40, name: 'Yemen', code: 'YE' }, + { id: 41, name: 'Zambia', code: 'ZM' }, + { id: 42, name: 'Zimbabwe', code: 'ZW' } +] diff --git a/src/dataApi/AppConfigApi.js b/src/dataApi/AppConfigApi.js index e7a845f7c..7c396a3a1 100644 --- a/src/dataApi/AppConfigApi.js +++ b/src/dataApi/AppConfigApi.js @@ -1,29 +1,27 @@ import kyApi from './kyApi' -class LocalCache { - _values = {} - - get(key) { return this._values[key] } - contains(key) { return key in this._values } - remove(key) { delete this._values[key] } - set(key, value) { this._values[key] = value } - values() { return this._values } - getSet(key, value) { - if (!this.contains(key)) { - this.set(key, typeof value === 'function' ? value() : value) +const makeLocalCache = opts => { + const _values = {} + + return { + get(key) { return _values[key] }, + contains(key) { return key in _values }, + remove(key) { delete _values[key] }, + set(key, value) { _values[key] = value }, + values() { return _values }, + getSet(key, value) { + if (!this.contains(key)) { + this.set(key, typeof value === 'function' ? value() : value) + } + return this.get(key) } - return this.get(key) } } -const _cache = new LocalCache() +const _cache = makeLocalCache() /** main holder for api */ export class AppConfigApi { prefixUrl = 'api/appConfig/' - // _cache = new MicroCache() - - // constructor() { - // } // Allows to use custom function to generate config key, for example namespace_key configKeyGenerator = (configKey) => { diff --git a/src/dataApi/DataQuery.js b/src/dataApi/DataQuery.js deleted file mode 100644 index a448edd45..000000000 --- a/src/dataApi/DataQuery.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable no-unused-vars */ -import _ from 'lodash' - -export default class DataQuery { - //the api endpoint key - dataApi = "" - - // the q map or text - q - - //qSearch if both using the map criteria in q and a text fuzzy search - qSearch - - //initSearch is the initialSearch criteria - initSearch - - // when grid is for child or detail data, restrictSearch is what to filter it by, - // for example is showing invoices for customer then restrictSearch might be set to {custId:123} - restrictSearch - - //sort map - sort - - //page info - max = '20' - - page = '1' -} diff --git a/src/dataApi/ItemStore.js b/src/dataApi/ItemStore.js deleted file mode 100644 index 07c8972c1..000000000 --- a/src/dataApi/ItemStore.js +++ /dev/null @@ -1,85 +0,0 @@ -import cloneDeep from 'lodash/cloneDeep' - -/* - * Generic item store that can be extended and used for anything. - * loosely based on the Storage API but with errors - * https://developer.mozilla.org/en-US/docs/Web/API/Storage - */ - -const itemStoreState = function() { - return { - items: [], - activeItem: {}, - errors: [] // expects objects with at least a message [{ message: 'fubar' }] - } -} - -const ItemStore = { - state: itemStoreState(), - - setItems(items) { - this.state.items = items - }, - - addItem(changes) { - this.state.items.push(Object.assign({}, changes)) - return Promise.resolve() - }, - - updateItem(item, changes) { - Object.assign(item, changes) - return Promise.resolve() - }, - - updateAll(changes) { - for(const item of this.state.items) { - this.updateItem(item, changes) - } - }, - - removeItem(item) { - this.state.items.splice(this.state.items.indexOf(item), 1) - return Promise.resolve() - }, - - setActiveItem(item) { - this.state.activeItem = item - }, - //clears the items, active and errors - clear() { - this.items = [] - this.activeItem = {} - this.errors = [] - }, - - updateActiveItem(changes) { - this.updateItem(this.activeItem, changes) - }, - //sets the activeItem and then returns a clone for editing - editActiveItem(item) { - let aitem = (item === false) ? {} : item - this.setActiveItem(aitem) - return cloneDeep(this.state.activeItem) - }, - - setErrors(errors) { - this.state.errors = errors - }, - //clears and then sets a single error message into the errors array - setErrorMessage(message) { - this.state.errors = [{message: message}] - }, - //gets the first error message - getErrorMessage() { - if (this.state.errors[0] != null) { - return this.state.errors[0].message - } - }, - - clearErrors() { - this.state.errors = [{message: ''}] - } -} - -export default ItemStore - diff --git a/src/dataApi/MemDataApi.js b/src/dataApi/MemDataApi.js index d685fb2ee..35985413a 100644 --- a/src/dataApi/MemDataApi.js +++ b/src/dataApi/MemDataApi.js @@ -45,11 +45,9 @@ class MemDataApi { } // query by example object - qbe(items, qbeItem) { + qbe(items, qbeObject) { return items.filter(it => { - return _.isMatchWith(it, qbeItem, (objValue, srcValue) => { - // console.log("objValue", objValue) - // console.log("srcValue", srcValue) + return _.isMatchWith(it, qbeObject, (objValue, srcValue) => { if (_.isString(objValue) && _.isString(srcValue)) { return objValue.toLowerCase().includes(srcValue.toLowerCase()) } @@ -61,8 +59,6 @@ class MemDataApi { // async search(p, flds) { if (!p) p = {} - // console.log("search p", p) - // console.log("search flds", flds) let list = await this.data() const isSearch = p && (p.q || p.qSearch) @@ -74,24 +70,23 @@ class MemDataApi { acc.order.push(sortar[1]) return acc }, { sort: [], order: [] }) - console.log('sortobj', sortobj) list = _.orderBy(list, sortobj.sort, sortobj.order) } - // console.log("search list", list) + if (flds){ list = list.reduce((acc, item) => { acc.push(_.pick(item, flds)) return acc }, []) } - // console.log("search list", list) + const paged = this.pagination(list, p) - // console.log("query paged", paged) + return paged } filter(list, params) { - console.log("filter params", params) + let flist = list // const filters = params.filters ? JSON.parse(params.filters) : null // const q = params.q ? JSON.parse(params.q) : null @@ -108,7 +103,7 @@ class MemDataApi { } else { flist = this.qSearch(list, q) } - // console.log("filter q flist", flist) + } else if(params.qSearch){ flist = this.qSearch(list, params.qSearch) } @@ -116,7 +111,7 @@ class MemDataApi { } async picklist(p) { - // console.log("picklist p", p) + return this.search(p, this.picklistFields) } @@ -168,7 +163,7 @@ class MemDataApi { // let item = findById(id, items) const idx = this.findItemIndex(items, { id: parseInt(id) }) items[idx] = _.merge(items[idx], data) - // console.log('merged item', items[idx]) + updateItems.push(items[idx]) }) this._commit(items) diff --git a/src/dataApi/SessionStorageApi.js b/src/dataApi/SessionStorageApi.js deleted file mode 100644 index ab719be3f..000000000 --- a/src/dataApi/SessionStorageApi.js +++ /dev/null @@ -1,69 +0,0 @@ -// import {guid} from "../utils/util" -import MemDataApi from './MemDataApi' -import ky from 'ky' -// import _ from 'lodash' - -const REST_DELAY = 500 -/** - * This class simulates a RESTful resource, but the API calls fetch data from - * Session Storage instead of an HTTP call. - * - * Once configured, it loads the initial (pristine) data from the URL provided (using HTTP). - * It exposes GET/PUT/POST/DELETE-like API that operates on the data. All the data is also - * stored in Session Storage. If any data is modified in memory, session storage is updated. - * If the browser is refreshed, the SessionStorage object will try to fetch the existing data from - * the session, before falling back to re-fetching the initial data using HTTP. - * - * For an example, please see dataSources.js - */ -export default class SessionStorageApi extends MemDataApi { - /** - * Creates a new SessionStorage object - * - * @param storageKey The session storage key. The data will be stored in browser's session storage under this key. - * @param sourceUrl The url that contains the initial data. - */ - constructor(storageKey, sourceUrl) { - super({}, REST_DELAY) - this._data = undefined - - // For each data object, the _idProp defines which property has that object's unique identifier - this._idProp = 'id' - - // A basic triple-equals equality checker for two values - this._eqFn = (l, r) => l[this._idProp] === r[this._idProp] - - // Services required to implement the fake REST API - this.storageKey = storageKey - this.sourceUrl = sourceUrl - } - - async getData() { - try { - const data = this.checkSession() - if (data) { - console.log(`using ${this.storageKey} in sessionStorage`) - return data - } - } catch (e) { - console.log(`Unable to parse session for ${this.sourceUrl}, retrieving intial data.`, e) - } - const parsed = await ky.get(this.sourceUrl).json() - this._commit(parsed) - const array = JSON.parse(sessionStorage.getItem(this.storageKey)) - return array - } - - checkSession() { - const fromSession = sessionStorage.getItem(this.storageKey) - if (fromSession) { - return JSON.parse(fromSession) - } - } - - /** Saves all the data back to the session storage */ - _commit(data) { - sessionStorage.setItem(this.storageKey, JSON.stringify(data)) - return data - } -} diff --git a/src/dataApi/__tests__/MemDataApi.spec.js b/src/dataApi/__tests__/MemDataApi.spec.js index d04ee6656..1ede8ee22 100644 --- a/src/dataApi/__tests__/MemDataApi.spec.js +++ b/src/dataApi/__tests__/MemDataApi.spec.js @@ -18,22 +18,6 @@ describe('MemDataApi', () => { // console.log("result", result) expect(result.length).toEqual(1) }) - - test('playing with isMatchWith', function() { - - var match = {refnum: '762', amount: 3240.77, customer:{id:7}} - - const hit = _.isMatchWith(data[0], match, (objValue, srcValue) => { - console.log("objValue", objValue) - console.log("srcValue", srcValue) - if(_.isString(objValue) && _.isString(srcValue)){ - return objValue.toLowerCase().includes(srcValue.toLowerCase()) - } - return undefined - }) - console.log("hit", hit) - expect(hit).toBe(true) - }) }) describe('searching', function() { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 000000000..074077735 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,14 @@ +`/// ` +import ng = require('angular') +import jq = require('jquery') +declare global { + var configData; + var angular: typeof ng; + var $: typeof jq; + interface Window { + $: typeof jq; + angular: typeof ng; + } +} + +export { }; diff --git a/src/gridz/GridCtrl.js b/src/gridz/GridCtrl.js index 46f2564cd..cad61eabd 100644 --- a/src/gridz/GridCtrl.js +++ b/src/gridz/GridCtrl.js @@ -424,7 +424,7 @@ export default class GridCtrl { //we use the sortMap that constructed in jq.gridz so remove the sort and order delete p.order; delete p.sort; let sortMap = this.getParam('sortMap') - console.log('sortMap', sortMap) + // console.log('sortMap', sortMap) if(sortMap){ p.sort = sortMap } diff --git a/src/gridz/GridDataApiCtrl.js b/src/gridz/GridDataApiCtrl.js new file mode 100644 index 000000000..efb0d9ff6 --- /dev/null +++ b/src/gridz/GridDataApiCtrl.js @@ -0,0 +1,769 @@ +/* eslint-disable no-unused-vars */ +import { makeLabel } from '../utils/nameUtils' +import { xlsData, csvData } from './excelExport' +import flattenObject from '../utils/flattenObject' +import toast from '../tools/growl' +import _ from 'lodash' +import { subscribe } from 'svelte/internal' + +export default class GridDataApiCtrl { + formatters + dataApi + unsubs = [] + highlightClass = 'ui-state-highlight' + systemColumns = ['cb', '-row_action_col'] + isDense = false + showSearchForm = false + + defaultCtxMenuOptions = { + edit: { + display: 'Edit', + icon: 'far fa-edit' + }, + delete: { + display: 'Delete', + icon: 'far fa-trash-alt' + } + } + + setupGrid(gridWrapper, jqGridElement, gridOptions) { + const opts = gridOptions + opts.loadui = 'block' + this.gridOptions = opts + + const $jqGrid = $(jqGridElement) + this.jqGridEl = $jqGrid + this.$gridWrapper = $(gridWrapper) + + if (!this.gridId && !_.isNil(opts.gridId)) { + this.gridId = opts.gridId + } + $jqGrid.attr('id', this.gridId) + + + let optsToMerge = _.pick(opts, [ + 'showSearchForm', 'dataApi', 'initSearch', 'restrictSearch', 'contextMenuClick' + ]) + _.mergeWith(this, optsToMerge, (obj, optVal) => { + //dont merge val if its null + return optVal === null ? obj : undefined + }) + + // pager ID setup + if (opts.pager !== false) { + const pagerId = `${this.gridId}-pager` + this.$gridWrapper.find('div.gridz-pager').attr('id', pagerId) + opts.pager = pagerId + } + + // set the hasSelected flag events + const onSelect = (rowId, status, event) => { + if (opts.eventHandlers?.onSelect && _.isFunction(opts.eventHandlers.onSelect)) { + opts.eventHandlers.onSelect(rowId, status, event) + } + this.hasSelected = (this.getSelectedRowIds().length > 0) + $jqGrid.trigger('gridz:selectedRows', [this.getSelectedRowIds()]) + } + $jqGrid.on('jqGridSelectRow', onSelect) + $jqGrid.on('jqGridSelectAll', onSelect) + + // if no datatype is passed in then use internal default + if (_.isNil(opts.datatype)) { + opts.datatype = (params) => this.gridLoader(params) + } + + this.setupColModel(opts) + this.setupCtxMenu(opts) + // this.setupDataLoader(gridOptions) + this.setupGridCompleteEvent(this, $jqGrid, opts) + this.setupFormatters(this, $jqGrid, opts) + this.formatters && this.setupCustomFormatters(this, this.formatters, opts) + + console.log("pageViewStore.subscribe") + // adds the listener to the store + const unsubscribe = this.dataApi.pageViewStore.subscribe(data => { + // console.log("dataApi.currentPage") + this.addJSONData(data) + }); + this.unsubs.push(unsubscribe) + } + + //initialize the grid the jquery way + initGridz(){ + // console.log({opt: this.gridOptions}) + this.jqGridEl.gridz(this.gridOptions) + // setupFilterToolBar(options) + } + + // the jqGrid table element + getGridEl() { + return this.jqGridEl + } + + // the wrapper div that has the toolbar and table and footer + getGridWrapper() { + return this.$gridWrapper + } + + getGridId() { + return this.jqGridEl.attr('id') + } + + // Gives the currently selected rows when multiselect is set to true. + // This is a one-dimensional array and the values in the array correspond + // to the selected id's in the grid. + getSelectedRowIds() { + return this.getParam('selarrrow') + } + + getSelectedRowId() { + return this.getParam('selrow') + } + + hasSelectedRowIds() { + return this.getParam('selarrrow').length > 0 + } + + // Gives selected row objects, [{id:1..}, {id:2..}] + getSelectedRows() { + const getRowData = _.bind(this.getRowData, this) + const ids = this.getSelectedRowIds() + return _.map(ids, id => getRowData(id)) + } + + clearSelection() { + return this.jqGridEl.jqGrid('resetSelection') + } + + // Returns an array with data of the requested id = rowid. + // The returned array is of type name:value, where the name is + // a name from colModel and the value from the associated column in that row. + // It returns an empty array if the rowid can not be found. + getRowData(rowId = null) { + return this.jqGridEl.getRowData(rowId) + } + + // Return all rows + getAllRows() { + return this.jqGridEl.getRowData() + } + + // Populates the grid with the given data. + addJSONData(data) { + //FIXME HACK not sure why we need to do this + const jqgrid = this.jqGridEl.get(0) + if(jqgrid.addJSONData && !_.isEmpty(data)){ + jqgrid.addJSONData(data) + } + // fire jquery event + return this.jqGridEl.trigger('gridz:loadComplete', [data]) + } + + // Reloads the grid with the current settings + reload(options) { + return new Promise((resolve) => { + if (options == null) { options = [] } + this.jqGridEl.on( "gridz:loadComplete", function( event, data ) { + resolve(data) + $(this).off(event) + }); + this.jqGridEl.trigger('reloadGrid', options) + }) + } + + // reloads and keeps what was selected + reloadKeepSelected() { + // Save id of the selected row + const selRow = _.cloneDeep(this.getParam('selrow')) + const selRows = _.cloneDeep(this.getParam('selarrrow')) + + const jqGridEl = this.getGridEl() + // Save grid scroll position + jqGridEl.closest('.ui-jqgrid-bdiv').scrollTop() + + const afterGridComplete = () => { + this.clearSelection() + if (this.getParam('multiselect')) { + _.each(selRows, id => jqGridEl.jqGrid('setSelection', id)) + } else { + jqGridEl.jqGrid('setSelection', selRow) + } + jqGridEl.off('jqGridAfterGridComplete', afterGridComplete) + } + // jqGridEl.off('jqGridAfterGridComplete', afterGridComplete).on('jqGridAfterGridComplete', afterGridComplete); + // {current: true} - used for keep multi select + return this.reload([{ current: true }]) + } + + resetSort(sortname = '', sortorder = '') { + const colModel = this.getParam('colModel') + colModel.forEach(column => { + column.lso = (column.name === sortname) || (column.name === 'id') ? sortorder : '' + }) + + this.$gridWrapper.find('span.s-ico').hide() + this.setParam({ sortname, sortorder }) // .trigger('reloadGrid') + this.reload([{ current: true }]) + const column = this.$gridWrapper.find(`#jqgh_${this.gridId}_id`) + const disabledClassName = 'ui-state-disabled' + column.find('.s-ico').css('display', 'inline-block') + if (sortorder === 'asc') { + column.find('.ui-icon-asc').removeClass(disabledClassName) + column.find('.ui-icon-desc').addClass(disabledClassName) + } else { + column.find('.ui-icon-asc').addClass(disabledClassName) + column.find('.ui-icon-desc').removeClass(disabledClassName) + } + } + + // Gets a particular grid parameter + getParam(name) { + return this.jqGridEl.getGridParam(name) + } + + /** + * Sets the given grid parameter, pass overwrite true if blanking out a param + */ + setParam(params, overwrite) { + this.jqGridEl.setGridParam(params, overwrite) + } + + // returns the column model + getColModel() { + return this.getParam('colModel') + // return _.filter(this.getParam('colModel'), gridColumn => { + // return !systemColumns.includes(gridColumn.name) + // }) + } + + // reconfigures columns, expects an objecct with a visible array and hidden array + configColumns(colConfig) { + const colSetup = { newColumnsOrder: [], displayedColumns: [], hiddenColumns: [] } + + this.getColModel().forEach((column, index) => { + if (this.systemColumns.includes(column.name)) { + return colSetup.newColumnsOrder.push(index) + } + }) + + colConfig.visible.forEach(function(column, index) { + colSetup.displayedColumns.push(column.name) + colSetup.newColumnsOrder.push(column.originalId) + }) + + colConfig.hidden.forEach(function(column, index) { + colSetup.hiddenColumns.push(column.name) + colSetup.newColumnsOrder.push(column.originalId) + }) + + const jqGridEl = this.getGridEl() + jqGridEl.remapColumns(colSetup.newColumnsOrder, true) + jqGridEl.jqGrid('showCol', colSetup.displayedColumns) + jqGridEl.jqGrid('hideCol', colSetup.hiddenColumns) + } + + // contextMenuClick = (model, menuItem) => { + //listCtrl can pass the listener + // return this.contextMenuClickAction(model, menuItem) + //return this.listCtrl.fireRowAction(model, menuItem) + // } + + // Updates the values (using the data array) in the row with rowid. + // The syntax of data array is: {name1:value1,name2: value2...} + // where the name is the name of the column as described in the colModel + // and the value is the new value. + updateRow(id, data, emptyMissingCells) { + if (emptyMissingCells == null) { emptyMissingCells = true } + const flatData = flattenObject(data) + + const prevData = this.getRowData(id) + if (!_.isNil(prevData)) { + // retrieve a list of removed keys + let diff = _.difference(Object.keys(prevData), Object.keys(flatData)) + + // filter out restricted (private) columns like `-row_action_col` + const restrictedColumns = key => !key.match(/^-/) + diff = diff.filter(restrictedColumns) + + // set empty values + if (emptyMissingCells) { + for (const key of Array.from(diff)) { flatData[key] = null } + } + } + + this.jqGridEl.setRowData(id, flatData) + this.flashOnSuccess(id) + return this.jqGridEl.trigger('gridz:rowUpdated', [id, data]) + } + + // Inserts a new row with id = rowid containing the data in data (an object) at + // the position specified (first in the table, last in the table or before or after the row specified in srcrowid). + // The syntax of the data object is: {name1:value1,name2: value2...} + // where name is the name of the column as described in the colModel and the value is the value. + addRow(id, data, position) { + if (position == null) { position = 'first' } + this.jqGridEl.addRowData(id, flattenObject(data), position) + this.jqGridEl.trigger('gridz:rowAdded', [id, data]) + return this.flashOnSuccess(id) + } + + // Returns `true` if the grid contains a row with the given id + hasRow(id) { + return !!this.jqGridEl.getInd(id) + } + + // Returns an array of the id's in the current grid view. + // It returns an empty array if no data is available. + getIds() { + return this.jqGridEl.getDataIDs() + } + + // Returns the current page + getCurrentPage() { + return this.getParam('page') + } + + // Returns the total number of records + getTotalRecords() { + return this.getParam('records') + } + + // Returns the number of rows per page + getPageSize() { + return this.getParam('rowNum') + } + + // Returns the total number of pages + getTotalPages() { + return Math.ceil(this.getTotalRecords() / this.getPageSize()) + } + + // return true if the current grid view displays the first page + isFirstPage() { + const page = this.getCurrentPage() + return page === 1 + } + + // return true if the current grid view displays the last page + isLastPage() { + const page = this.getCurrentPage() + return page === this.getTotalPages() + } + + // Loads the previous page + prevPage() { + if (this.isFirstPage()) { return this.lastPage() } + + const page = this.getCurrentPage() + return this.loadPage(page - 1) + } + + // Loads the next page + nextPage() { + if (this.isLastPage()) { return this.firstPage() } + + const page = this.getCurrentPage() + return this.loadPage(page + 1) + } + + // Loads the first page + firstPage() { return this.loadPage(1) } + + // Loads the last page + lastPage() { return this.loadPage(this.getTotalPages()) } + + // Load the specific page + loadPage(page) { + this.setParam({ page }) + return this.reload() + } + + saveRow(id, data) { + if (this.hasRow(id)) { + return this.updateRow(id, data) + } else { + return this.addRow(id, data) + } + } + + // Deletes the row with the id = rowid. + // This operation does not delete data from the server. + removeRow(id) { + return this.flashOnSuccess(id, () => this.jqGridEl.delRowData(id)) + } + + // Sets the grid search filters and triggers a reload + async quickSearch(queryText) { + return this.search(null, queryText) + } + + // Sets the grid search filters and triggers a reload + async search(q, queryText) { + console.log("GridCtrl search called with ", q) + try { + this.isSearching = true + const params = { + page: 1, + search: this.hasSearchFilters(q) + } + this.setParam(params, true) + + let postData = { q } + if (queryText || queryText === '') postData.qSearch = queryText + this.setParam({ postData }) + + //if its empty then manually blank it out + if(_.isEmpty(q)){ + let pp = this.getParam("postData") + pp.q = {} + } + //reload wil end up calling the gridLoader function + await this.reload() + } catch (er) { + //XXX should not swallow errors + console.error('search error', er) + } + } + + hasSearchFilters(filters) { + for (const k in filters) { + const value = filters[k] + if (_.isNil(value)) { continue } + + if (typeof value === 'string') { + if (value.trim() !== '') { return true } + } else { + return true + } + } + return false + } + + /** + * The main loader for the grid. get called internally from pager and sort. + * + * @param {*} p the params to send to search + */ + async gridLoader(p) { + console.log("gridLoader called with ", p) + this.toggleLoading(true) + try { + //we use the sortMap that constructed in jq.gridz so remove the sort and order + delete p.order; delete p.sort; + let sortMap = this.getParam('sortMap') + console.log('sortMap', sortMap) + if(sortMap){ + p.sort = sortMap + } + + // to be able to set default filters on the first load + let q = p.q + if(_.isString(q) && !_.isEmpty(q)){ + if (q.trim().indexOf('{') === 0) { + q = JSON.parse(q) + } else { + q = {'$qSearch': q} + } + } + // when grid is for child or detail data, restrictSearch is what to filter it by, + // for example is showing invoices for customer then restrictSearch might be set to {custId:123} + const restrictSearch = this.restrictSearch || {} + // const initSearch = this.initSearch || {} + // const search = _.merge(initSearch, searchModel || {}) + q = {...q, ...restrictSearch} + + //now if its not empty set it back to p + if(!_.isEmpty(q)){ + p.q = q + } + const data = await this.dataApi.search(p) + // this.addJSONData(data) + } catch (er) { + this.handleError(er) + } finally { + this.toggleLoading(false) + } + } + + handleError(er) { + console.error(er) + toast.error(er) + } + + // Returns `true` if a columnt with the given id is hidden + isColumnHidden(columnId) { + const column = _.find(this.getParam('colModel'), { name: columnId }) + return column?.hidden + } + + // Toggle visibility of a column with the given id + toggleColumn(columnId) { + const showOrHide = this.isColumnHidden(columnId) ? 'showCol' : 'hideCol' + this.jqGridEl.jqGrid(showOrHide, columnId) + return this._triggerResize() + } + + // Returns data uri with xls file content for rows from the current grid view. + getXlsDataUri() { + return xlsData(this.getGridId(), this.getSelectedRowIds()) + } + + // @ts-ignore + xlsExport() { + if (this.getSelectedRowIds().length !== 0) { + // if browser is IE then open new window and show SaveAs dialog, else use dataUri approach + // can this part be deprecated? + if ((window.navigator.userAgent.indexOf('MSIE ') > 0) || + !!window.navigator.userAgent.match(/Trident.*rv\:11\./)) { + let iframe = document.createElement('IFRAME') + iframe.style.display = 'none' + document.body.appendChild(iframe) + // @ts-ignore + let iframeDoc = iframe.contentWindow.document || iframe.contentDocument.document + const csvDta = 'sep=|\r\n' + this.getCsvData() + iframeDoc.open('text/html', 'replace') + iframeDoc.write(csvDta) + iframeDoc.close() + iframe.focus() + return iframeDoc.execCommand('SaveAs', true, 'download.csv') + } else { + const dataUri = this.getXlsDataUri() + const link = document.createElement('a') + link.href = dataUri + link.setAttribute('download', 'download.xls') + document.body.appendChild(link) + const clickev = document.createEvent('MouseEvents') + // initialize the event + clickev.initEvent('click', true, true) + // trigger the event + return link.dispatchEvent(clickev) + } + } + } + + getCsvData() { + return csvData()(this.getGridId(), this.getSelectedRowIds()) + } + + toggleLoading(show = true) { + const loadEl = this.$gridWrapper.find(`#load_${this.getGridId()}`) + const overlay = this.$gridWrapper.find(`#lui_${this.getGridId()}`) + if (show) { + loadEl.show() + overlay.show() + } else { + loadEl.hide() + overlay.hide() + } + return show ? loadEl.show() : loadEl.hide() + } + + // Triggers grid's resize event + // @private + // TODO fix grid resizing issues + // TODO resize after column chooser dialog + _triggerResize() { + return this.jqGridEl.trigger('resize') + } + + // Flash the given row + flashOnSuccess(id, complete) { + if (complete == null) { complete = ()=>{} } + return this._flashRow(id, '#DFF0D8', complete) + } + + // Flash the row with red background + flashOnError(id, complete) { + if (complete == null) { complete = ()=>{} } + return this._flashRow(id, '#FF0000', complete) + } + + _flashRow(id, color, complete) { + if (color == null) { color = '#DFF0D8' } + if (complete == null) { complete = ()=>{} } + const rowEl = $(this.jqGridEl[0].rows.namedItem(id)) + + rowEl.css('background-color', color) + rowEl.delay(250).fadeOut('medium', () => rowEl.css('background-color', '')) + + return rowEl.fadeIn('fast', () => complete()) + } + + addClass(id, clazz, animation) { + if (animation == null) { animation = true } + const rowEl = $(this.jqGridEl[0].rows.namedItem(id)) + + if (!rowEl.hasClass(clazz)) { + if (animation) { + rowEl.delay(250).fadeOut('medium', () => rowEl.addClass(clazz)) + return rowEl.fadeIn('fast', () => {}) + } else { + return rowEl.addClass(clazz) + } + } + } + + removeClass(id, clazz, animation) { + if (animation == null) { animation = true } + const rowEl = $(this.jqGridEl[0].rows.namedItem(id)) + + if (rowEl.hasClass(clazz)) { + if (animation) { + rowEl.delay(250).fadeOut('medium', () => rowEl.removeClass(clazz)) + return rowEl.fadeIn('fast', () => {}) + } else { + return rowEl.removeClass(clazz) + } + } + } + + // FIXME its not clear to me what these are for. The grid seems to works without the highloghtclass workgin + highlightRow(id) { + const rowEl = $(this.jqGridEl[0].rows.namedItem(id)) + if (!rowEl.hasClass(this.highlightClass)) { + return rowEl.addClass(this.highlightClass) + } + } + + unHighlightRow(id) { + const rowEl = $(this.jqGridEl[0].rows.namedItem(id)) + if (rowEl.hasClass(this.highlightClass)) { + return rowEl.removeClass(this.highlightClass) + } + } + + addAdditionalFooter(data) { + const footerRow = this.$gridWrapper.find('tr.footrow') + let newFooterRow + newFooterRow = this.$gridWrapper.find('tr.myfootrow') + if (newFooterRow.length === 0) { + // add second row of the footer if it's not exist + newFooterRow = footerRow.clone() + newFooterRow.addClass('myfootrow ui-widget-content') + newFooterRow.insertAfter(footerRow) + } + // calculate the value for the second footer row + return (() => { + const result = [] + for (const k in data) { + const v = data[k] + const td = newFooterRow.find('[aria-describedby="arTranGrid_' + k + '"' + ']') + if (td.length > 0) { + if (!isNaN(v)) { + result.push(td[0].innerHTML = `
${v}
`) + } else { + result.push(td[0].innerHTML = `
${v}
`) + } + } else { + result.push(undefined) + } + } + return result + })() + } + + //*** gridzInit ****/ + + setupFormatters(gridCtrl, jqGridEl, options) { + // add any events etc for formatters + jqGridEl.on('click', 'a.editActionLink', function(event) { + event.preventDefault() + const id = $(this).parents('tr:first').attr('id') + return gridCtrl.contextMenuClick({ id: id }, { key: 'edit' }) + }) + + jqGridEl.on('click', 'a.gridLink', function(event) { + event.preventDefault() + const id = $(this).parents('tr:first').attr('id') + window.location.href += (window.location.href.endsWith('/') ? '' : '/') + id + }) + } + + setupCustomFormatters(gridCtrl, formatters, options) { + options.colModel.forEach((col, i) => { + if (col.formatter && _.isString(col.formatter) && formatters[col.formatter]) col.formatter = formatters[col.formatter].bind(this) + }) + } + + setupGridCompleteEvent(gridCtrl, jqGridEl, options) { + jqGridEl.on('jqGridAfterGridComplete', function() { + // Add `min` class to remove pading to minimize row height + if (options.minRowHeight || options.denseRows) { + gridCtrl.isDense = true + // return _.each(jqGridEl[0].rows, it => angular.element(it).addClass('min')) + } + if (options.selectFirstRow === true) { + const dataIds = jqGridEl.getDataIDs() + if (dataIds.length > 0) { + jqGridEl.setSelection(dataIds[0], true) + } + } + }) + } + + /** + * adds the action column and formatter. + */ + addCtxMenuIconColumn(opts) { + const actionCol = { + name: 'context_menu_col', + label: ' ', + width: 20, + sortable: false, + search: false, + hidedlg: true, + resizable: false, + fixed: true, // don't auto calc size + formatter: (cellValue, colOptions, rowObject) => { + return `` + } + } + opts.colModel.unshift(actionCol) + } + + setupCtxMenu(opts) { + if (!opts.contextMenu) return + + if (opts.contextMenu === true) { + // use the defaults + this.ctxMenuOptions = this.defaultCtxMenuOptions + this.addCtxMenuIconColumn(opts) + } + } + + setupColModel(options) { + options.colModel.forEach((col, i) => { + if (!col.label) col.label = makeLabel(col.name) + }) + } + + // the query by example toolbar + // FIXME not used anywhere that I can find was called right after jq gridz call. + setupFilterToolBar(options){ + if (options.filterToolbar) { + this.jqGridEl.jqGrid('filterToolbar', { + beforeSearch() { + const postData = this.jqGridEl.jqGrid('getGridParam', 'postData') + const defaultFilters = postData.defaultFilters || postData.filters + // @ts-ignore + const filters = (_.extend(JSON.parse(defaultFilters), (_.pick(postData, (_value, key) => !['page', 'filters', 'max', 'sort', 'order'].includes(key))))) + filters.firstLoad = false + postData.defaultFilters = defaultFilters + postData.filters = filters + } + }) + } + } + + destroy(){ + this.unsubs.forEach(fn => { + fn() + }) + this.jqGridEl.jqGrid('GridDestroy') + } + +} diff --git a/src/gridz/excelExport.js b/src/gridz/excelExport.js index c170a68d5..9e468d0a8 100644 --- a/src/gridz/excelExport.js +++ b/src/gridz/excelExport.js @@ -109,7 +109,8 @@ export function gridData(gridId, selectedRows) { return resultEl.html() } -export function csvData(gridData) { +export function csvData() { + const prepareCsvHeaders = function(data) { const headers = [] const resultEl = $('
') diff --git a/src/ng/components/loading-bar/loading-bar.css b/src/ng/components/loading-bar/loading-bar.css new file mode 100644 index 000000000..3ee06188b --- /dev/null +++ b/src/ng/components/loading-bar/loading-bar.css @@ -0,0 +1,104 @@ + +/* Make clicks pass-through */ +#loading-bar, +#loading-bar-spinner { + pointer-events: none; + -webkit-pointer-events: none; + -webkit-transition: 350ms linear all; + -moz-transition: 350ms linear all; + -o-transition: 350ms linear all; + transition: 350ms linear all; +} + +#loading-bar.ng-enter, +#loading-bar.ng-leave.ng-leave-active, +#loading-bar-spinner.ng-enter, +#loading-bar-spinner.ng-leave.ng-leave-active { + opacity: 0; +} + +#loading-bar.ng-enter.ng-enter-active, +#loading-bar.ng-leave, +#loading-bar-spinner.ng-enter.ng-enter-active, +#loading-bar-spinner.ng-leave { + opacity: 1; +} + +#loading-bar .bar { + -webkit-transition: width 350ms; + -moz-transition: width 350ms; + -o-transition: width 350ms; + transition: width 350ms; + + background: #29d; + position: fixed; + z-index: 10002; + top: 0; + left: 0; + width: 100%; + height: 2px; + border-bottom-right-radius: 1px; + border-top-right-radius: 1px; +} + +/* Fancy blur effect */ +#loading-bar .peg { + position: absolute; + width: 70px; + right: 0; + top: 0; + height: 2px; + opacity: .45; + -moz-box-shadow: #29d 1px 0 6px 1px; + -ms-box-shadow: #29d 1px 0 6px 1px; + -webkit-box-shadow: #29d 1px 0 6px 1px; + box-shadow: #29d 1px 0 6px 1px; + -moz-border-radius: 100%; + -webkit-border-radius: 100%; + border-radius: 100%; +} + +#loading-bar-spinner { + display: block; + position: fixed; + z-index: 10002; + top: 10px; + left: 10px; +} + +#loading-bar-spinner .spinner-icon { + width: 14px; + height: 14px; + + border: solid 2px transparent; + border-top-color: #29d; + border-left-color: #29d; + border-radius: 50%; + + -webkit-animation: loading-bar-spinner 400ms linear infinite; + -moz-animation: loading-bar-spinner 400ms linear infinite; + -ms-animation: loading-bar-spinner 400ms linear infinite; + -o-animation: loading-bar-spinner 400ms linear infinite; + animation: loading-bar-spinner 400ms linear infinite; +} + +@-webkit-keyframes loading-bar-spinner { + 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} +@-moz-keyframes loading-bar-spinner { + 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } +} +@-o-keyframes loading-bar-spinner { + 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } +} +@-ms-keyframes loading-bar-spinner { + 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } +} +@keyframes loading-bar-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/ng/components/loading-bar/loading-bar.js b/src/ng/components/loading-bar/loading-bar.js new file mode 100644 index 000000000..619cac4d4 --- /dev/null +++ b/src/ng/components/loading-bar/loading-bar.js @@ -0,0 +1,335 @@ +/* + * angular-loading-bar + * + * intercepts XHR requests and creates a loading bar. + * Based on the excellent nprogress work by rstacruz (more info in readme) + * + * (c) 2013 Wes Cruver + * License: MIT + */ + + +(function() { + +'use strict'; + +// Alias the loading bar for various backwards compatibilities since the project has matured: +angular.module('angular-loading-bar', ['cfp.loadingBarInterceptor']); +angular.module('chieffancypants.loadingBar', ['cfp.loadingBarInterceptor']); + + +/** + * loadingBarInterceptor service + * + * Registers itself as an Angular interceptor and listens for XHR requests. + */ +angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar']) + .config(['$httpProvider', function($httpProvider) { + + var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', 'cfpLoadingBar', function($q, $cacheFactory, $timeout, $rootScope, $log, cfpLoadingBar) { + + /** + * The total number of requests made + */ + var reqsTotal = 0; + + /** + * The number of requests completed (either successfully or not) + */ + var reqsCompleted = 0; + + /** + * The amount of time spent fetching before showing the loading bar + */ + var latencyThreshold = cfpLoadingBar.latencyThreshold; + + /** + * $timeout handle for latencyThreshold + */ + var startTimeout; + + + /** + * calls cfpLoadingBar.complete() which removes the + * loading bar from the DOM. + */ + function setComplete() { + $timeout.cancel(startTimeout); + cfpLoadingBar.complete(); + reqsCompleted = 0; + reqsTotal = 0; + } + + /** + * Determine if the response has already been cached + * @param {Object} config the config option from the request + * @return {Boolean} retrns true if cached, otherwise false + */ + function isCached(config) { + var cache; + var defaultCache = $cacheFactory.get('$http'); + var defaults = $httpProvider.defaults; + + // Choose the proper cache source. Borrowed from angular: $http service + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { + cache = angular.isObject(config.cache) ? config.cache + : angular.isObject(defaults.cache) ? defaults.cache + : defaultCache; + } + + var cached = cache !== undefined ? + cache.get(config.url) !== undefined : false; + + if (config.cached !== undefined && cached !== config.cached) { + return config.cached; + } + config.cached = cached; + return cached; + } + + + return { + 'request': function(config) { + // Check to make sure this request hasn't already been cached and that + // the requester didn't explicitly ask us to ignore this request: + if (!config.ignoreLoadingBar && !isCached(config)) { + $rootScope.$broadcast('cfpLoadingBar:loading', {url: config.url}); + if (reqsTotal === 0) { + startTimeout = $timeout(function() { + cfpLoadingBar.start(); + }, latencyThreshold); + } + reqsTotal++; + cfpLoadingBar.set(reqsCompleted / reqsTotal); + } + return config; + }, + + 'response': function(response) { + if (!response || !response.config) { + $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); + return response; + } + + if (!response.config.ignoreLoadingBar && !isCached(response.config)) { + reqsCompleted++; + $rootScope.$broadcast('cfpLoadingBar:loaded', {url: response.config.url, result: response}); + if (reqsCompleted >= reqsTotal) { + setComplete(); + } else { + cfpLoadingBar.set(reqsCompleted / reqsTotal); + } + } + return response; + }, + + 'responseError': function(rejection) { + if (!rejection || !rejection.config) { + $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); + return $q.reject(rejection); + } + + if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) { + reqsCompleted++; + $rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url, result: rejection}); + if (reqsCompleted >= reqsTotal) { + setComplete(); + } else { + cfpLoadingBar.set(reqsCompleted / reqsTotal); + } + } + return $q.reject(rejection); + } + }; + }]; + + $httpProvider.interceptors.push(interceptor); + }]); + + +/** + * Loading Bar + * + * This service handles adding and removing the actual element in the DOM. + * Generally, best practices for DOM manipulation is to take place in a + * directive, but because the element itself is injected in the DOM only upon + * XHR requests, and it's likely needed on every view, the best option is to + * use a service. + */ +angular.module('cfp.loadingBar', []) + .provider('cfpLoadingBar', function() { + + this.autoIncrement = true; + this.includeSpinner = true; + this.includeBar = true; + this.latencyThreshold = 100; + this.startSize = 0.02; + this.parentSelector = 'body'; + this.spinnerTemplate = '
'; + this.loadingBarTemplate = '
'; + + this.$get = ['$injector', '$document', '$timeout', '$rootScope', function($injector, $document, $timeout, $rootScope) { + var $animate; + var $parentSelector = this.parentSelector, + loadingBarContainer = angular.element(this.loadingBarTemplate), + loadingBar = loadingBarContainer.find('div').eq(0), + spinner = angular.element(this.spinnerTemplate); + + var incTimeout, + completeTimeout, + started = false, + status = 0; + + var autoIncrement = this.autoIncrement; + var includeSpinner = this.includeSpinner; + var includeBar = this.includeBar; + var startSize = this.startSize; + + /** + * Inserts the loading bar element into the dom, and sets it to 2% + */ + function _start() { + if (!$animate) { + $animate = $injector.get('$animate'); + } + + $timeout.cancel(completeTimeout); + + // do not continually broadcast the started event: + if (started) { + return; + } + + var document = $document[0]; + var parent = document.querySelector ? + document.querySelector($parentSelector) + : $document.find($parentSelector)[0] + ; + + if (! parent) { + parent = document.getElementsByTagName('body')[0]; + } + + var $parent = angular.element(parent); + var $after = parent.lastChild && angular.element(parent.lastChild); + + $rootScope.$broadcast('cfpLoadingBar:started'); + started = true; + + if (includeBar) { + $animate.enter(loadingBarContainer, $parent, $after); + } + + if (includeSpinner) { + $animate.enter(spinner, $parent, loadingBarContainer); + } + + _set(startSize); + } + + /** + * Set the loading bar's width to a certain percent. + * + * @param n any value between 0 and 1 + */ + function _set(n) { + if (!started) { + return; + } + var pct = (n * 100) + '%'; + loadingBar.css('width', pct); + status = n; + + // increment loadingbar to give the illusion that there is always + // progress but make sure to cancel the previous timeouts so we don't + // have multiple incs running at the same time. + if (autoIncrement) { + $timeout.cancel(incTimeout); + incTimeout = $timeout(function() { + _inc(); + }, 250); + } + } + + /** + * Increments the loading bar by a random amount + * but slows down as it progresses + */ + function _inc() { + if (_status() >= 1) { + return; + } + + var rnd = 0; + + // TODO: do this mathmatically instead of through conditions + + var stat = _status(); + if (stat >= 0 && stat < 0.25) { + // Start out between 3 - 6% increments + rnd = (Math.random() * (5 - 3 + 1) + 3) / 100; + } else if (stat >= 0.25 && stat < 0.65) { + // increment between 0 - 3% + rnd = (Math.random() * 3) / 100; + } else if (stat >= 0.65 && stat < 0.9) { + // increment between 0 - 2% + rnd = (Math.random() * 2) / 100; + } else if (stat >= 0.9 && stat < 0.99) { + // finally, increment it .5 % + rnd = 0.005; + } else { + // after 99%, don't increment: + rnd = 0; + } + + var pct = _status() + rnd; + _set(pct); + } + + function _status() { + return status; + } + + function _completeAnimation() { + status = 0; + started = false; + } + + function _complete() { + if (!$animate) { + $animate = $injector.get('$animate'); + } + + $rootScope.$broadcast('cfpLoadingBar:completed'); + _set(1); + + $timeout.cancel(completeTimeout); + + // Attempt to aggregate any start/complete calls within 500ms: + completeTimeout = $timeout(function() { + var promise = $animate.leave(loadingBarContainer, _completeAnimation); + if (promise && promise.then) { + promise.then(_completeAnimation); + } + $animate.leave(spinner); + }, 500); + } + + return { + start : _start, + set : _set, + status : _status, + inc : _inc, + complete : _complete, + autoIncrement : this.autoIncrement, + includeSpinner : this.includeSpinner, + latencyThreshold : this.latencyThreshold, + parentSelector : this.parentSelector, + startSize : this.startSize + }; + + + }]; // + }); // wtf javascript. srsly +})(); // diff --git a/src/ng/controls/ag-amount-range/index.js b/src/ng/controls/ag-amount-range/index.js index 53da1b37c..2c9b9bf88 100644 --- a/src/ng/controls/ag-amount-range/index.js +++ b/src/ng/controls/ag-amount-range/index.js @@ -1,6 +1,6 @@ import AgBaseControl from '../AgBaseControl' -import { getConfig } from '../../../tools/AppConfig' import _ from 'lodash' +import controlsConfig from "../controlsConfig"; class Controller extends AgBaseControl { rangeOptions = {} @@ -19,8 +19,8 @@ class Controller extends AgBaseControl { } $onInit() { - const rangeConfig = getConfig().controls.ranges - _.merge(this.opts, _.merge(rangeConfig, this.rangeOptions)) + + _.merge(this.opts, controlsConfig.controls.ranges, this.rangeOptions) this.placeholderFrom = this.opts.fromField.placeholder this.placeholderTo = this.opts.toField.placeholder this.elementIdFrom = this.elementId + '_from' diff --git a/src/ng/controls/ag-daterange/index.js b/src/ng/controls/ag-daterange/index.js index c46f3bacf..0c01e80e8 100644 --- a/src/ng/controls/ag-daterange/index.js +++ b/src/ng/controls/ag-daterange/index.js @@ -1,10 +1,11 @@ import AgBaseControl from '../AgBaseControl' // import Log from '../../../utils/Log' import _ from 'lodash' -import { getConfig } from "../../../tools/AppConfig"; +import controlsConfig from "../controlsConfig"; class Controller extends AgBaseControl { datepickerOptions = {} + opts = { showOnFocus: true, fromField: { @@ -17,9 +18,9 @@ class Controller extends AgBaseControl { } } + $onInit() { - const rangeConfig = getConfig().controls.ranges - _.merge(this.opts, _.merge(rangeConfig, this.datepickerOptions)) + _.merge(this.opts, controlsConfig.controls.ranges, this.datepickerOptions) this.placeholderFrom = this.opts.fromField.placeholder this.placeholderTo = this.opts.toField.placeholder this.elementIdFrom = this.elementId + '_from' diff --git a/src/ng/controls/controlsConfig.js b/src/ng/controls/controlsConfig.js new file mode 100644 index 000000000..f1903c3e8 --- /dev/null +++ b/src/ng/controls/controlsConfig.js @@ -0,0 +1,15 @@ +//hacked in for now to make ranges work +const conf = { + controls: { + ranges: { + fromField: { + name: '$gt' + }, + toField: { + name: '$lt' + } + } + } +} + +export default conf diff --git a/src/ng/gridz/gridz-datastore.js b/src/ng/gridz/gridz-datastore.js new file mode 100644 index 000000000..e3b9f7031 --- /dev/null +++ b/src/ng/gridz/gridz-datastore.js @@ -0,0 +1,81 @@ +import angular from 'angular' +import grid2Mod from './module' +import GridDataApiCtrl from '../../gridz/GridDataApiCtrl' +// import Log from 'angle-grinder/src/utils/Log' +import _ from 'lodash' + +angular.module(grid2Mod).directive('gridzDatastore', + function($timeout, $parse, $compile) { + 'ngInject'; + const link = function($scope, $el, attrs, gridCtrl) { + const $gridzEl = $el.find('table.gridz') + + gridCtrl.setupGrid($el, $gridzEl, gridCtrl.gridOptions) + + $gridzEl.on('jqGridAfterGridComplete', function() { + // console.log("directive jqGridAfterGridComplete event") + //this compiles the angular html that gets generated by the formatters + $compile($gridzEl)($scope) + }) + + $gridzEl.on( "gridz:selectedRows", function( event, selectedIds ) { + // the toolbar is tied to the has selected, this makes it pick up the changes + $scope.$evalAsync(() => { + console.log("selectedIds", selectedIds) + gridCtrl.hasSelected = gridCtrl.hasSelected + }) + }) + + $scope.$on('$destroy', function() { + gridCtrl.destroy() + }); + + // scope[gridId] will be set to gridCtrl + // FIXME not clear what this is doing + $parse(gridCtrl.gridId).assign($scope, gridCtrl) + + //FIXME is this drill still needed? looks like its for tabs and making sure we dont init early? + if ($el.is(':visible')) { + // Element is visible, initialize the grid now + gridCtrl.initGridz() + } else { + let unregister + // Initialize the grid when the element will be visible + let timeoutPromise = null + return unregister = $scope.$watch(function() { + $timeout.cancel(timeoutPromise) // Cancel previous timeout + // We have to do timeout because of this issue with uib-tab https://github.com/angular-ui/bootstrap/issues/3796 + // Otherwise when tab is clicked and digest cycle ($watch) runs, the element.is(":visible") is still false, and hence grid is never initialized. + timeoutPromise = $timeout(function() { + if (!$gridzEl.is(':visible')) { return } + // initialize the grid on the visible element + gridCtrl.initGridz() + // unregister the watcher to free resources + return unregister() + }, 100, false) // Here false means don't fire new digest cycle, otherwise $watch will be called infinitely. + + return false + }) + } + } + + return { + restrict: 'E', + controller: GridDataApiCtrl, + controllerAs: 'gridCtrl', + bindToController: { + toolbarOptions: '<', + gridId: '@', + gridOptions: '<', + restrictSearch: '<' + }, + template: `\ +
+ +
+
+
`, + link: link + } + } +) diff --git a/src/ng/gridz/gridz-directive.js b/src/ng/gridz/gridz-directive.js index a512e3f24..2613a642f 100644 --- a/src/ng/gridz/gridz-directive.js +++ b/src/ng/gridz/gridz-directive.js @@ -66,11 +66,12 @@ angular.module(grid2Mod).directive('gridz', bindToController: { toolbarOptions: '<', gridId: '@', - gridOptions: '<' + gridOptions: '<', + restrictSearch: '<' }, template: `\
- +
`, diff --git a/src/ng/gridz/index.js b/src/ng/gridz/index.js index 49c44f5cb..3a2b650f9 100644 --- a/src/ng/gridz/index.js +++ b/src/ng/gridz/index.js @@ -4,4 +4,7 @@ import './support' import './gridz-directive' import './list' +import './gridz-datastore' +import './list-datastore' + export default grid2Mod diff --git a/src/ng/gridz/list-datastore/BulkUpdateModalCtrl.js b/src/ng/gridz/list-datastore/BulkUpdateModalCtrl.js new file mode 100644 index 000000000..4d582c3ee --- /dev/null +++ b/src/ng/gridz/list-datastore/BulkUpdateModalCtrl.js @@ -0,0 +1,40 @@ + +export default class BulkUpdateModalCtrl { + /* @ngInject */ + constructor($uibModalInstance, $scope, dataApi, vm, cfg, selectedIds) { + this.modal = $uibModalInstance + this.$scope = $scope + this.dataApi = dataApi + this.vm = vm + this.cfg = cfg + this.selectedIds = selectedIds + } + + async save() { + // call the agForm submit so it brodcasts and shows the errors + const { agForm } = this.$scope + agForm.submit() + if (agForm.form.$invalid || agForm.form.$pristine) return + this.isSaving = true + try { + const params = { ids: this.selectedIds, data: this.vm } + const results = await this.dataApi.bulkUpdate(params) + this.modal.close(results) + } catch (er) { + this.handleError(er) + } finally { + this.isSaving = false + } + } + + cancel() { + // prevents the "Possibly unhandled rejection: cancel" + this.modal.result.catch(() => this.modal.close()) + this.modal.dismiss('cancel') + } + + handleError(er) { + // FIXME handle a graceful way of displayiing errors + console.error(er) + } +} diff --git a/src/ng/gridz/list-datastore/EditModalCtrl.js b/src/ng/gridz/list-datastore/EditModalCtrl.js new file mode 100644 index 000000000..4659fedb5 --- /dev/null +++ b/src/ng/gridz/list-datastore/EditModalCtrl.js @@ -0,0 +1,43 @@ +// import _ from 'lodash' + +// TODO change to https://github.com/likeastore/ngDialog#api +export default class EditModalCtrl { + /* @ngInject */ + constructor($uibModalInstance, $scope, dataApi, vm, cfg, title) { + this.modal = $uibModalInstance + this.$scope = $scope + this.dataApi = dataApi + this.vm = vm + this.cfg = cfg + this.title = title + } + + async save() { + // call the agForm submit so it brodcasts and shows the errors + const { agForm } = this.$scope + agForm.submit() + if (agForm.form.$invalid || agForm.form.$pristine) return + this.isSaving = true + try { + const savedItem = await this.dataApi.save(this.vm) + this.modal.close(savedItem) + } catch (er) { + this.handleError(er) + } finally { + this.isSaving = false + } + } + + cancel() { + // prevents the "Possibly unhandled rejection: cancel" + this.modal.result.catch(() => this.modal.close()) + this.modal.dismiss('cancel') + } + + async handleError(er) { + const { agForm } = this.$scope + // let errors = await er.response.json() + agForm.setServerErrors(er.response) + // console.error("handleError errors", errors) + } +} diff --git a/src/ng/gridz/list-datastore/ListDataApiCtrl.js b/src/ng/gridz/list-datastore/ListDataApiCtrl.js new file mode 100644 index 000000000..ca26dbae7 --- /dev/null +++ b/src/ng/gridz/list-datastore/ListDataApiCtrl.js @@ -0,0 +1,295 @@ +// @ts-nocheck +// import Log from 'angle-grinder/src/utils/Log' +import _ from 'lodash' +import EditModalCtrl from './EditModalCtrl' +import BulkUpdateModalCtrl from './BulkUpdateModalCtrl' +import { argsMerge } from '../../utils/classUtils' +import appConfigApi from '../../../dataApi/AppConfigApi' +import toast from '../../../tools/toast' +import Swal from '../../../tools/swal' + +export default class ListDataApiCtrl { + defaultToolbarOpts = { + selectedButtons: { + bulkUpdate: { icon: 'far fa-edit', tooltip: 'Bulk Update' }, + xlsExport: { icon: 'far fa-file-excel', tooltip: 'Export to Excel' } + }, + leftButtons: { + create: { icon: 'far fa-plus-square', tooltip: 'Create New' } + }, + searchFormButton: { icon: 'mdi-text-box-search-outline', tooltip: 'Show Search Filters Form' } + } + //in some cases we need filters that couldnt be cleared like payment on batch page + permanentFilters = {} + editTemplate = require('./editDialog.html') + bulkUpdateTemplate = require('./bulkUpdateDialog.html') + // searchTemplate = require('./searchForm.html') + + static $inject = ['$scope', '$element', '$uibModal', '$timeout'] + constructor(...args) { + argsMerge(this, args) + } + + async doConfig(cfg) { + if(!cfg){ + let apiCfg = await appConfigApi.getConfig(this.apiKey) + cfg = _.cloneDeep(apiCfg) + console.log("ListDataApiCtrl called appConfigApi.getConfig", apiCfg) + } + + console.log("ListDataApiCtrl datapi", this.dataApi) + const gopts = cfg.gridOptions || {} + console.log("ListDataApiCtrl gopts", gopts) + if (this.eventHandlers) { + gopts.eventHandlers = this.eventHandlers + } + // assign default datatype to grid loader + // gopts.datatype = (params) => this.gridLoader(params) + gopts.dataApi = this.dataApi + + if (!gopts.toolbarOptions) gopts.toolbarOptions = {} + const tbopts = _.merge({}, this.defaultToolbarOpts, gopts.toolbarOptions) + + // setup search form show based on if searchForm is configured + if (cfg.searchForm === undefined) { + gopts.showSearchForm = false + tbopts.searchFormButton.class = 'hidden' + } + + if (cfg.bulkUpdateForm === undefined) { + tbopts.selectedButtons.bulkUpdate.class = 'hidden' + } + + // give toolbar scope + tbopts.scope = () => this.$scope + cfg.toolbarOptions = tbopts + _.defaults(this.cfg, cfg) + + this.isConfigured = true + + //setup some defaults for gridOpts + gopts.contextMenuClick = (model, menuItem) => { + return this.fireRowAction(model, menuItem) + } + gopts.restrictSearch = this.restrictSearch || {} + gopts.initSearch = this.initSearch || {} + } + + get gridCtrl() { return this.$element.find('gridz-datastore').controller('gridz-datastore') } + get editModalCtrl() { return EditModalCtrl } + get bulkUpdateModalCtrl() { return BulkUpdateModalCtrl } + // get searchCtrl() { return SearchCtrl } + + fireRowAction(model, menuItem) { + switch (menuItem.key) { + case 'edit': + return this.edit(model.id) + case 'delete': + return this.delete(model.id) + // default: + // alert( "I don't know such values" ); + } + } + + fireToolbarAction(btnItem, event) { + switch (btnItem.key) { + case 'create': + return this.create() + case 'bulkUpdate': + return this.showBulkUpdate() + case 'xlsExport': + return this.gridCtrl.xlsExport() + case 'delete': + return this.deleteSelected() + default: + if (_.isFunction(this[btnItem.key])) { + return this[btnItem.key](btnItem, event) + } + } + } + + async edit(id) { + this.gridCtrl.toggleLoading(true) + try { + const vm = await this.dataApi.get(id) + this.showEdit('Edit', vm) + } catch (er) { + this.handleError(er) + } finally { + this.gridCtrl.toggleLoading(false) + } + } + + updateFooter(data) { + setTimeout(_ => { + this.gridCtrl.getGridEl().footerData('set', data) + }) + } + + create(model = {}) { + this.showEdit('Create', model) + } + + showEdit(title, model) { + const isUpdate = !!model.id + const modInst = this.$uibModal.open( + this.getEditOptions(this.editTemplate, model, title) + ) + modInst.result + .then(editedVm => { + isUpdate ? this.gridCtrl.updateRow(editedVm.id, editedVm) : this.gridCtrl.addRow(editedVm.id, editedVm) + }) + .catch(() => { + console.log('Modal dismissed at: ' + new Date()) + }) + // , () => { + // console.log('Modal dismissed at: ' + new Date()) + // }) + } + + // modal options for edit + getEditOptions(template, model, title) { + return { + controller: this.editModalCtrl, + controllerAs: 'dlgCtrl', + template: template, + keyboard: false, // do not close the dialog with ESC key + backdrop: 'static', // do not close on click outside of the dialog, + resolve: { + vm: () => model, + dataApi: () => this.dataApi, + cfg: () => this.cfg, + title: () => title + } + // scope: this.$scope + } + } + + showBulkUpdate() { + const modInst = this.$uibModal.open( + this.getBulkUpdateOptions() + ) + modInst.result + .then(res => { + res.data.forEach(row => { + this.gridCtrl.updateRow(row.id, row, false) + }) + }) + .catch(() => { + console.log('Modal dismissed at: ' + new Date()) + }) + } + + getBulkUpdateOptions(model = {}) { + return { + controller: this.bulkUpdateModalCtrl, + controllerAs: 'dlgCtrl', + template: this.bulkUpdateTemplate, + keyboard: false, // do not close the dialog with ESC key + backdrop: 'static', // do not close on click outside of the dialog, + resolve: { + vm: () => model, + dataApi: () => this.dataApi, + cfg: () => this.cfg, + selectedIds: () => this.gridCtrl.getSelectedRowIds() + } + // scope: this.$scope + } + } + + // showBulkUpdate() { + // const modalOpts = { + // template: this.bulkUpdateTpl, + // keyboard: false, // do not close the dialog with ESC key + // backdrop: 'static' // do not close on click outside of the dialog, + // // scope: this.$scope + // } + // // here just for example, does nothing + // this.form = this.$uibModal.open(modalOpts) + // } + + async delete(id) { + try { + await this.dataApi.remove(id) + this.gridCtrl.removeRow(id) + } catch (er) { + this.handleError(er) + } + } + + async deleteSelected() { + const id = this.gridCtrl.getSelectedRowIds()[0] + this.delete(id) + } + + // load results of a query into gridCtrl + // async gridLoader(p) { + // this.gridCtrl.gridLoader(p, this.searchModel) + // } + + async search(filters) { + console.log("ListDataApiCtrl search called with", filters) + try { + this.isSearching = true + await this.gridCtrl?.search(filters) + } catch (er) { + this.handleError(er) + } finally { + this.isSearching = false + } + } + + searchReset(searchForm) { + this.searchModel = this.initSearch || {} + searchForm.reset() + } + + handleError(er) { + console.error(er) + const message = er?.response?.status === 500 ? 'Unexpected error' : null + toast.error(message || er) + } + + swalError(error) { + Swal.fire({ + icon: 'error', + title: error.title, + text: error.message, + showCloseButton: true + }) + } + + handleResults(response) { + if (response.ok) { + toast.success(` ${response.success.join('
')}`, response.defaultMessage) + } else { + toast.error(`${response.failed.join('
')} `, response.defaultMessage) + } + } + + handleAction(action) { + const ids = this.gridCtrl?.getSelectedRowIds() + const run = async (ids) => { + ids.forEach((id) => { + this.gridCtrl.highlightRow(id) + }) + + try { + const result = await action() + if(result.ok){ + toast.success(result.title || 'Action is sucsess') + this.gridCtrl.reload() // todo: should we reload only selected rows? + } else { + this.swalError({title: result.title , message: result?.failed?.join('
') || ''}) + } + } catch (e) { + this.handleError(e) + } finally { + ids.forEach((id) => { + this.gridCtrl.highlightRow(id) + }) + } + } + return run(ids) + } +} diff --git a/src/ng/gridz/list-datastore/ag-grid-datastore.js b/src/ng/gridz/list-datastore/ag-grid-datastore.js new file mode 100644 index 000000000..5860786c9 --- /dev/null +++ b/src/ng/gridz/list-datastore/ag-grid-datastore.js @@ -0,0 +1,53 @@ +// @ts-nocheck +import ListDataApiCtrl from './ListDataApiCtrl' +import union from 'lodash/union' + +const template = ` +
+
+ + {{$ctrl.notification.text}} +
+ + + + +
+` +class ListCtrl extends ListDataApiCtrl { + + $onInit() { + this.isConfigured = false + this.cfg = {} + super.doConfig() + + if (this.restrictSearch) { + this.dataApi.restrictSearch = this.restrictSearch + } + if (this.initSearch) { + //FIXME why do we set the initSearch to cfg? + this.cfg.initSearch = this.initSearch + this.searchModel = { ...this.initSearch, ...this.searchModel } + } + } + + // we need to generate gridId, because if we have 2 grids on a page they will have the same id and 2 pagers will + // be assisgned to the second grid + gridId() { + return this.apiKey?.replace(/[^\w\s]/gi, '_') + 'Grid' + } +} + +export default { + bindings: { + apiKey: '<', // used for gridId and to get the config + dataApi: '<', + notification: '<', + initSearch: '<', + restrictSearch: '<' + }, + template: template, + controller: ListCtrl +} diff --git a/src/ng/gridz/list-datastore/bulkUpdateDialog.html b/src/ng/gridz/list-datastore/bulkUpdateDialog.html new file mode 100644 index 000000000..93e3b093f --- /dev/null +++ b/src/ng/gridz/list-datastore/bulkUpdateDialog.html @@ -0,0 +1,16 @@ + diff --git a/src/ng/gridz/list-datastore/editDialog.html b/src/ng/gridz/list-datastore/editDialog.html new file mode 100644 index 000000000..e23dc0721 --- /dev/null +++ b/src/ng/gridz/list-datastore/editDialog.html @@ -0,0 +1,18 @@ + diff --git a/src/ng/gridz/list-datastore/index.js b/src/ng/gridz/list-datastore/index.js new file mode 100644 index 000000000..584870d5c --- /dev/null +++ b/src/ng/gridz/list-datastore/index.js @@ -0,0 +1,5 @@ +import gridMod from '../module' +import agGridDatastore from './ag-grid-datastore' + +angular.module(gridMod) + .component('agGridDatastore', agGridDatastore) diff --git a/src/ng/gridz/list/ag-grid-list.js b/src/ng/gridz/list/ag-grid-list.js index 19f3ae3a3..33ade347e 100644 --- a/src/ng/gridz/list/ag-grid-list.js +++ b/src/ng/gridz/list/ag-grid-list.js @@ -1,5 +1,4 @@ import BaseListCtrl from './BaseListCtrl' -// import restStoreApi from '../../store/RestStoreApi' import _ from 'lodash' const template = ` @@ -46,7 +45,8 @@ export default { bindings: { apiKey: '<', notification: '<', - initSearch: '<' + initSearch: '<', + restrictSearch: '<' }, template: template, controller: ListCtrl diff --git a/src/ng/gridz/toolbar/gridz-options-dropdown/index.js b/src/ng/gridz/toolbar/gridz-options-dropdown/index.js index b7fa6aa5f..d2c603cee 100644 --- a/src/ng/gridz/toolbar/gridz-options-dropdown/index.js +++ b/src/ng/gridz/toolbar/gridz-options-dropdown/index.js @@ -30,12 +30,10 @@ export default () => ({ restrict: 'E', // replace: true, controllerAs: 'optsCtrl', - require: { - gridCtrl: '^gridz' - }, bindToController: { menuItems: '<', - menuClick: '=' + menuClick: '=', + gridCtrl: '<' }, template: require('./dropdown.html'), controller: Controller diff --git a/src/ng/gridz/toolbar/gridz-toolbar/index.js b/src/ng/gridz/toolbar/gridz-toolbar/index.js index a65b2e0d0..572cba7e8 100644 --- a/src/ng/gridz/toolbar/gridz-toolbar/index.js +++ b/src/ng/gridz/toolbar/gridz-toolbar/index.js @@ -3,13 +3,11 @@ import _ from 'lodash' export default () => ({ restrict: 'E', - require: { - gridCtrl: '^gridz' - }, template: require('./toolbar.html'), bindToController: true, scope: { - options: '<' + options: '<', + gridCtrl: '<' }, controller: Controller, controllerAs: 'tbCtrl' diff --git a/src/ng/gridz/toolbar/gridz-toolbar/toolbar.html b/src/ng/gridz/toolbar/gridz-toolbar/toolbar.html index 7f0bd1afc..cdc836269 100644 --- a/src/ng/gridz/toolbar/gridz-toolbar/toolbar.html +++ b/src/ng/gridz/toolbar/gridz-toolbar/toolbar.html @@ -25,5 +25,5 @@ - + diff --git a/src/ng/gridz/toolbar/tb-button/index.js b/src/ng/gridz/toolbar/tb-button/index.js index bedcc32d4..c156d84f4 100644 --- a/src/ng/gridz/toolbar/tb-button/index.js +++ b/src/ng/gridz/toolbar/tb-button/index.js @@ -27,13 +27,13 @@ export default () => ({ restrict: 'E', // replace: true, require: { - gridCtrl: '^gridz', tbCtrl: '^gridzToolbar' }, template: require('./tbButton.html'), bindToController: { opts: '<', - isLoading: '<' + isLoading: '<', + gridCtrl: '<' }, controller: Controller, controllerAs: 'tbBtnCtrl' diff --git a/src/ng/utils/ngSvelteShim.js b/src/ng/utils/ngSvelteShim.js index 56f7f75d0..7d44d745e 100644 --- a/src/ng/utils/ngSvelteShim.js +++ b/src/ng/utils/ngSvelteShim.js @@ -18,10 +18,10 @@ * * @param Component * @param events - * @returns {controller} + * @returns the controller class */ export default function(Component, events) { - const controller = class { + const ctrl = class { constructor($scope, $element) { this.$element = $element this.initialProps = {} @@ -40,8 +40,8 @@ export default function(Component, events) { const angularBinding = events[svelteEvent] this.component.$on(svelteEvent, ({ detail }) => { - console.log("svelteEvent", svelteEvent) - console.log("detail", detail) + //console.log("svelteEvent", svelteEvent) + //console.log("detail", detail) this[angularBinding](detail) }) }, this) @@ -71,7 +71,7 @@ export default function(Component, events) { } } - controller.$inject = ['$scope', '$element'] + ctrl.$inject = ['$scope', '$element'] - return controller + return ctrl } diff --git a/src/stores/DataManager.js b/src/stores/DataManager.js new file mode 100644 index 000000000..d50df9f84 --- /dev/null +++ b/src/stores/DataManager.js @@ -0,0 +1,122 @@ +/* eslint-disable no-unused-vars */ +import _ from 'lodash' +import appConfigApi from './AppConfigApi' +import {defaultToolbarOpts} from './dataManagerDefaults' + +/** + * Template object for an entity. In a crud or crud like scenario there is one base entity. + * A customer for instance. We can query for a list and display in a table. + * We can show and or edit one of those customers. + * The customer will have + * + */ +export default class DataManager { + /** + * The uri key. ex: ar/customer + */ + apiKey = "" + + /** + * The datastore instance + */ + dataApi + + /** + * The data, ui and grid configs + * Has everything needed to setup the main ui for search and toolbar + */ + config = {} + + /** + * The q mango criteria. ex: {name: "yo*"} + */ + q = {} + + /** + * The fuzzy search string. Can use both this and q criteria + */ + qSearch = "" + + //initSearch is the initialSearch criteria + /** + * if a default initialization for q is desired then set this + */ + initSearch = {} + + /** + * when grid is for child or detail data, restrictSearch is what to filter it by, + * for example is showing invoices for customer then restrictSearch might be set to {custId:123} + */ + restrictSearch = {} + + /** + * sort object, with key being the field to sort and val being asc or desc + * example: {'name': asc} or {'location.city':'desc', 'name': 'asc'} + */ + sort = {} + + /** + * The number of items to return + */ + maxLength = 20 + + /** + * The requested page number + */ + currentPage = 1 + // + itemsTotal + + // the result of the api query + data + + /** + * if there were errors during request then this will be the problem + */ + problem + + + async doConfig(cfg) { + if(!cfg){ + let apiCfg = await appConfigApi.getConfig(this.apiKey) + cfg = _.cloneDeep(apiCfg) + } + this.config = cfg + + const gridOpts = cfg.gridOptions + if (this.eventHandlers) { + gridOpts.eventHandlers = this.eventHandlers + } + // assign default datatype to grid loader + // gopts.datatype = (params) => this.gridLoader(params) + gridOpts.dataApi = this.dataApi + gridOpts.dataManager = this + + if (!gridOpts.toolbarOptions) gridOpts.toolbarOptions = {} + const gridToolbarOpts = _.merge({}, defaultToolbarOpts, gridOpts.toolbarOptions) + + // setup search form show based on if searchForm is configured + if (cfg.searchForm === undefined) { + gridOpts.showSearchForm = false + gridToolbarOpts.searchFormButton.class = 'hidden' + } + + if (cfg.bulkUpdateForm === undefined) { + gridToolbarOpts.selectedButtons.bulkUpdate.class = 'hidden' + } + + // put the toolbarOptions at the root level + cfg.toolbarOptions = tbopts + _.merge(this.config, cfg) + + //flag that we can react to and only do ui when true + this.isConfigured = true + + //setup some defaults for gridOpts + gridOpts.contextMenuClick = (model, menuItem) => { + return this.fireRowAction(model, menuItem) + } + gridOpts.restrictSearch = this.restrictSearch + gridOpts.initSearch = this.initSearch + } +} diff --git a/src/stores/__tests__/MemDatastore.spec.js b/src/stores/__tests__/MemDatastore.spec.js new file mode 100644 index 000000000..90e68e2bf --- /dev/null +++ b/src/stores/__tests__/MemDatastore.spec.js @@ -0,0 +1,101 @@ +/* eslint-disable */ +import _ from 'lodash' +import MemDataService from '../local/MemDataService' +import { findSomeDeep } from '../../utils/finders' +import countryData from '../../__tests__/countries' +import {expect as x} from '@jest/globals' + +describe('MemDataService', () => { + + const ds = MemDataService({ initData: countryData }) + + describe('MemDataService internals', () => { + test('findSomeDeep country united', () => { + x( findSomeDeep(countryData, 'united').length ).toEqual(3) + }) + + test('qbe', async () => { + const data = await ds.stores.getMasterData() + const result = ds.qbe(data, {code: 'US'}) + x(result.length).toEqual(1) + }) + + test('qSearch', async () => { + const data = await ds.stores.getMasterData() + const s = ds.qSearch(data, 'united') + x( s.length ).toEqual(3) + }) + + test('filter', async () => { + const data = await ds.stores.getMasterData() + const s = ds.filter(data, {q: 'united'}) + x( s.length ).toEqual(3) + }) + + test('filter q object', async () => { + let data = await ds.stores.getMasterData() + let s = ds.filter(data, {q: {code:'US'}}) + x( s.length ).toEqual(1) + + s = ds.filter(data, {q: {name:'united'}}) + x( s.length ).toEqual(3) + }) + }) + + describe('MemDataService search', () => { + test('search nothing', async function() { + const params = { + max: 20, + page: 1, + sort: "code" + } + await ds.search(params) + let viewData = await ds.stores.getData() + let pager = await ds.stores.getPageView() + + x(viewData.length).toEqual(42) + x(pager.data.length).toEqual(20) + + x(pager.page).toEqual(1) + x(pager.records).toEqual(42) + x(pager.total).toEqual(3) + }) + + test('search simple q text', async function() { + const params = { + q: "united", + } + await ds.search(params) + let viewData = await ds.stores.getData() + let pager = await ds.stores.getPageView() + + x(viewData.length).toEqual(3) + x(pager.data.length).toEqual(3) + + x(pager.page).toEqual(1) + x(pager.records).toEqual(3) + x(pager.total).toEqual(1) + }) + + test('qSearch param', async function() { + const params = { + qSearch: "united" + } + let items = await ds.search(params) + + x(items.length).toEqual(3) + + }) + + test('picklist paged', async function() { + const result = await ds.picklist() + //console.log("result", result) + expect(result.length).toEqual(42) + // expect(result.data.length).toEqual(20) + }) + + }) + +}) + + diff --git a/src/stores/crudQueryModel.js b/src/stores/crudQueryModel.js new file mode 100644 index 000000000..a75c8acca --- /dev/null +++ b/src/stores/crudQueryModel.js @@ -0,0 +1,121 @@ +import { findIndexById } from '../utils/finders' +import { crudQueryStores } from './crudQueryStores'; +import mix from '../utils/mix-it-with'; +/** @typedef {import('svelte/store').Writable<{}>} Writable */ + +const not_implemented = "not implemented" + +/** + * Adds the stores property for the writable and subscriable stores + */ +export const withSubStores = (ds) => { + + const {initData = [], ident = 'id'} = ds + + const stores = crudQueryStores() + if(initData) stores.setMasterData(initData) + + return mix(ds).with({ + stores, ident, + + //if paging this is the pager info with data + get pageViewStore(){ + return ds.stores.pageViewStore + }, + + //the viewable or filtered data + get dataStore(){ + return ds.stores.dataStore + }, + }) +} + +/** + * Contract for reading and query + * + * @param {object} ds the base object to mix into + * @returns + */ +export const queryModel = (ds) => { + + const core = withSubStores(ds) + + return mix(ds).with({ + ...core, + + async search(params){ throw Error(not_implemented) }, + + /** + * Fuzzy text search. + * @param {string} searchKey + * @returns the filtered items + */ + async qSearch(text){ throw Error(not_implemented) }, + + async picklist(params){ throw Error(not_implemented) }, + + /** + * Returns a promise for the item with the given identifier + */ + async get(id){ throw Error(not_implemented) }, + + findById(list, id){ + const idx = ds.findIndexById(list, id) + return idx === -1 ? false : list[idx] + }, + + findIndexById(list, id){ + return findIndexById({ list, id , ident: core.ident }) + } + + }) +} + +/** + * constract for (c)create, (u)update, (d)delete + * as well as bulk + */ +export const cudModel = (ds) => { + + const {ident = 'id'} = ds + + return mix(ds).with({ + + /** + * Create if no id prop or Update if it has id prop + * Returns a promise to save the item. + * It delegates to update() or create() if the object has or does not have an identifier set + */ + async save(item){ + return item[ident] ? ds.update(item) : ds.create(item) + }, + + async create(item){ }, + + /** Returns a promise to save (PUT) an existing item. */ + async update(item){ throw Error(not_implemented) }, + + /** + * Returns a promise to remove (DELETE) an item. + */ + async remove(id){ throw Error(not_implemented) }, + + /** + * Bulk updates items + */ + async bulkUpdate({ids, data}){ throw Error(not_implemented) }, + + }) //end mix + +} + +/** + * combination queryModel and cudModel + */ +export const crudQueryModel = (ds = {}) => { + + return mix(ds).with(queryModel, cudModel) + +} + +// export default datastoreStores diff --git a/src/stores/crudQueryStores.js b/src/stores/crudQueryStores.js new file mode 100644 index 000000000..0964871c9 --- /dev/null +++ b/src/stores/crudQueryStores.js @@ -0,0 +1,122 @@ +import { get, writable } from 'svelte/store'; +import mix from '../utils/mix-it-with'; +import { findIndexById } from '../utils/finders' +/** @typedef {import('svelte/store').Writable<{}>} Writable */ + +/** + * The base datastore composed of the the stores for current item, page + */ +export const crudQueryStores = (stores = {}) => { + + let itemStore = writable({}) + + let pageViewStore = writable({}) + + let queryStore = writable({}) + + let masterDataStore = writable([]) + + let dataStore = writable([]) + + return mix(stores).with({ + + /** + * the store for the current item + * @type {Writable} + */ + get itemStore(){ + return itemStore + }, + + setItem(currentItem){ + itemStore.set(currentItem) + }, + + /** + * The data cache. will be the entire data when not rest. + * not sync as it should already be loaded before this is called + * @type {Writable} + */ + get masterDataStore(){ + return masterDataStore + }, + + /** + * sets the data in the store + */ + setMasterData(data){ + return masterDataStore.set(data) + }, + + /** + * the actual data list from the store + * @return {object} + */ + getMasterData(){ + return get(masterDataStore) + }, + + /** + * the current or visible array of data after filter. + * on init this would be equal to whats in the masterData + * @type {Writable} + */ + get dataStore(){ + return dataStore + }, + + /** + * the current array of filtered of viewable data + * @type {object} + */ + getData(){ + return get(dataStore) + }, + + /** + * sets the viewable data in the store + */ + setData(data){ + return dataStore.set(data) + }, + + /** + * the current page view of the data {data:[...], page: , records: , total: } + * @type {Writable} + */ + get pageViewStore(){ + return pageViewStore + }, + + /** + * the page data + * @type {object} + */ + getPageView(){ + return get(pageViewStore) + }, + + /** + * sets the page data into the store + */ + setPageView(pageData){ + return pageViewStore.set(pageData) + }, + + /** + * the query parameters with the q search {max:int, page:int, sort:string, q:{} , qSearch:string} + * @type {Writable} + */ + get queryStore(){ + return queryStore + }, + + /** + * sets the page data into the store + */ + setQuery(qdata){ + return queryStore.set(qdata) + }, + + }) //end mix +} diff --git a/src/stores/dataManagerDefaults.js b/src/stores/dataManagerDefaults.js new file mode 100644 index 000000000..b13e6c2cb --- /dev/null +++ b/src/stores/dataManagerDefaults.js @@ -0,0 +1,10 @@ +export const defaultToolbarOpts = { + selectedButtons: { + bulkUpdate: { icon: 'far fa-edit', tooltip: 'Bulk Update' }, + xlsExport: { icon: 'far fa-file-excel', tooltip: 'Export to Excel' } + }, + leftButtons: { + create: { icon: 'far fa-plus-square', tooltip: 'Create New' } + }, + searchFormButton: { icon: 'mdi-text-box-search-outline', tooltip: 'Show Search Filters Form' } +} diff --git a/src/stores/ky.js b/src/stores/ky.js new file mode 100644 index 000000000..3672a2014 --- /dev/null +++ b/src/stores/ky.js @@ -0,0 +1,137 @@ +import {default as KY} from 'ky' +import { ensurePrefix } from '../utils/ensure' +/** + * @typedef {import('ky/distribution/types/ky').KyInstance} KyInstance + * @typedef {import('ky').ResponsePromise} ResponsePromise + */ + + +// subscriber's handlers, allows to add subs without needing to create a new ky +let subs = { + before: [], + after: [] +} + +const defaults = { + prefixUrl: '/api', + hooks: { + beforeRequest: [ + request => { subs.before.forEach(sub => sub(request)) } + ], + afterResponse: [ + (_request, _options, response) => { subs.after.forEach(sub => sub(_request, _options, response)) } + ] + } +} + +export const KyFactory = { + defaults, + + + /** + * the current ky instance + * @type {KyInstance} + */ + get ky(){ return this._KyInstance}, + + /** + * builds the default ky with any extra overide options passed in + * shouldn't need to do this more than once really, and then KySupport.ky can be used + * + * @param {Object} opts extra options + * @returns {KyInstance} the instance + */ + build(opts){ + this._KyInstance = KY.create({...this.defaults, ...opts}) + return this._KyInstance + }, + + /** + * can subscribe to ky hooks and not need to recreate a ky to have them fired + * Used for loading indicators and logging + * + * @param {string} type before or after + * @param {function} handler funtion, see ky for what it can expect depending on the before or after + * @returns {function} a function to call to unsub to avoid mem leaks + */ + subscribe(type, handler) { + subs[type] = [...subs[type], handler] // add handler to the array of subscribers + return () => subs[type] = subs[type].filter(sub => sub !== handler) // return unsubscribe function + }, + + /** + * opinionated way to add Bearer token header. add a beforeRequest sub to add the header + */ + enableAuthHeader() { + const authHandler = request => { + if (request.url.indexOf('login') > -1) return + request.headers.set('Authorization', 'Bearer ' + localStorage.getItem('bearer')) + } + this.subscribe('before', authHandler) + } +} + +KyFactory.build() +export default KyFactory.ky; + +/** + * object uses ky to make fetch rest calls tied to a specific endpoint. + * If ky baseUrl is http://foo.9ci.io/api and key passed in is go/bar then + * all the verb calls will be against http://foo.9ci.io/api/go/bar. + * The 'path' is an action param, a term for the controller method or id on the server. AKA "rpc" or ID. + * - the path is just appended to the base endpoint url. + * - also where we put the id. so /api/go/bar/{id} is /api/go/bar/{path} + * + * @param {string} key uri to be appended to the baseUrl in ky. + * @returns the kyFetch object helper + */ +export const kyFetch = key => { + + /** + * pass method into args. can also pass 'path' and it will be appended to key. + * path is where to put the {id} path param. so to get /api/customer/1, pass 1 into the path + * + * @param {{ method: string, path: string }} param0 the method is the rest verb, path is the postfix to append + * @returns {ResponsePromise} the json result + */ + function kyFetch({method = 'get', path = '', ...opts}){ + path = ensurePrefix(path, '/') + return KyFactory.ky(`${key}${path}`, {...opts, method}) + } + + return { + fetch: kyFetch, + /** + * the ky instance + * @type {KyInstance} + */ + get ky(){ return KyFactory.ky}, + + async get(opts){ + //get is default, we dont need to pass it in + return kyFetch(opts).json() + }, + + async getById(id, opts){ + //get is default, we dont need to pass it in + return kyFetch({ path: id , ...opts}).json() + }, + + async post(opts){ + return kyFetch({...opts, method: 'post'}).json() + }, + + async put(opts){ + return kyFetch({...opts, method: 'put'}).json() + }, + + async patch(opts){ + return kyFetch({...opts, method: 'patch'}).json() + }, + + async delete(opts){ + return kyFetch({...opts, method: 'delete'}).json() + } + + } +} diff --git a/src/stores/local/MemDataService.js b/src/stores/local/MemDataService.js new file mode 100644 index 000000000..6a16a64da --- /dev/null +++ b/src/stores/local/MemDataService.js @@ -0,0 +1,223 @@ +/* eslint-disable no-unused-vars, eqeqeq */ +import _ from 'lodash' +import { findSomeDeep, qbe, findIndexById } from '../../utils/finders' +import { isString } from '../../utils/inspect' +import mix from '../../utils/mix-it-with' +import {crudQueryModel} from '../crudQueryModel' + +/** + * Local memory based data service + */ +const MemDataService = ({ + mockDelay = 0, + picklistFields = ['id', 'name'], + initData = [], + ident = 'id' +} = {}) => { + + const ds = { + initData, + ident + } + + ds.delay = (ms) => { + ms = ms || mockDelay + return new Promise(resolve => setTimeout(resolve, ms)) + } + + ds.search = async (params = {}) => { + console.log("MemDataService search", params) + await ds.delay() + + let {q, qSearch, sort, order} = params + + let items = ds.stores.getMasterData() + + const isSearch = q || qSearch + + if (isSearch) items = ds.filter(items, {q, qSearch}) + + if (sort) { + let sortobj = sort + //will be string if coming from query param or object if used locally + // if(isString(sort)){ + // sortobj = sort.split(',').reduce((acc, item) => { + // const sortar = item.trim().split(':') + // acc.sort.push(sortar[0]) + // acc.order.push(sortar[1]) + // return acc + // }, { sort: [], order: [] }) + // } + Object.keys(sort).forEach( prop => { + items = _.orderBy(items, prop, sort[prop]) + }) + //since the sort keys can be nested then need to cylce over and do them one by one + // items = _.orderBy(items, sortobj.sort, sortobj.order) + } + ds.stores.setData(items) + let { page, max} = params + ds.paginate({data: items, page, max}) + return items + } + + ds.qbe = (items, qbeObject) => { + return qbe(items, qbeObject) + } + + /** + * Fuzzy search. + * @param {*} searchKey + * @returns the filtered items + */ + ds.qSearch = (items, searchKey) => { + let foundItems = findSomeDeep(items, searchKey) + // console.log("qSearch foundItems items", foundItems) + return foundItems + } + + /** + * filters down the items using qSearch or qbe + * @returns the filtered items + */ + ds.filter = (items, {q, qSearch}) => { + // console.log("filter params", params) + let filtered = items + + if (q) { + if (isString(q)){ + if (q.trim().startsWith('{')){ + q = JSON.parse(q) + } else { + //its a string for qSearch + qSearch = q + // since its string null out q now so only qSearch gets run in a bit + q = null + } + } else if(q['$qSearch']) { + qSearch = q['$qSearch'] + delete q['$qSearch'] + } + } + //if q has something at this point + if(q){ + filtered = ds.qbe(items, q) + } + // if it also has text qSearch then + if(qSearch){ + filtered = ds.qSearch(filtered, qSearch) + // console.log("qSearch filtered items", filtered) + } + + return filtered + } + + ds.picklist = async (p) => { + + let items = await ds.search(p) + let flds = picklistFields + + items = items.reduce((acc, item) => { + acc.push(_.pick(item, flds)) + return acc + }, []) + return items + } + + /** Returns a promise for the item with the given identifier */ + ds.get = async (id) => { + const items = await ds.stores.getMasterData() + const item = items.find(item => item.id === id) + return item + } + + /** + * Create if no id prop or Update if it has id prop + * Returns a promise to save the item. + * It delegates to put() or post() if the object has or does not have an identifier set + */ + ds.save = async (item) => { + return item[ident] ? ds.update(item) : ds.create(item) + } + + /** + * Returns a promise to save (POST) a new item. + * The item's identifier is auto-assigned. + */ + ds.create = async (item) => { + ds.stores.masterDataStore.update( data => { + item.id = _.max(data.map(it => it.id)) + 1 + data.push(item) + return data + }) + return item + }, + + /** Returns a promise to save (PUT) an existing item. */ + ds.update = async (item) => { + ds.stores.masterDataStore.update( data => { + const idx = ds.findIndexById(data, item[ident]) + data[idx] = item + return data + }) + return item + } + + /** + * Returns a promise to remove (DELETE) an item. + */ + ds.remove = async (id) => { + ds.stores.masterDataStore.update( data => { + // const intId = parseInt(id) + const idx = findIndexById({ list:data, id }) + //splice delete + data.splice(idx, 1); + return data + }) + } + + ds.bulkUpdate = async (muItem) => { + const { data: updateData, ids } = muItem + const updateItems = [] + ds.stores.masterDataStore.update( data => { + ids.forEach(id => { + let item = ds.findById(data, id) + item = _.merge(item, updateData) + // console.log('merged item', items[idx]) + updateItems.push(item) + }) + return data + }) + return { data: updateItems } + } + + ds.findById = (data, id) => { + const idx = ds.findIndexById(data, id) + return idx === -1 ? false : data[idx] + } + + ds.findIndexById = (data, id) => { + return findIndexById({ list: data, id , ident}) + } + + ds.paginate = async ({ data, page = 1, max = 20}) => { + let newPage = { + data: data.slice((page - 1) * max, page * max), + page: page, + records: data.length, + total: Math.floor(data.length / max) + (data.length % max === 0 ? 0 : 1) + } + ds.stores.setPageView(newPage) + return newPage + } + + ds.countTotals = async (field) => { + const items = await ds.stores.getMasterData() + return { [field]: items.reduce((sum, item) => sum + item.amount, 0) } + } + + return mix(ds).it(MemDataService).with( + crudQueryModel + ) +} + +export default MemDataService diff --git a/src/stores/local/SessionDataService.js b/src/stores/local/SessionDataService.js new file mode 100644 index 000000000..9f44cf5f3 --- /dev/null +++ b/src/stores/local/SessionDataService.js @@ -0,0 +1,62 @@ +// import {guid} from "../utils/util" +import MemDataService from './MemDataService' +import ky from 'ky' //simple ky to bypass so we can load a file +import stringify from '../../utils/stringify'; +import { isEmpty } from '../../utils/inspect'; +import mix from '../../utils/mix-it-with'; + +/** + * SessionStorage based data service + */ +const SessionDataService = (opts) => { + + let { sourceUrl, storageKey, mockDelay = 500 } = opts + + let memDs = MemDataService({...opts, mockDelay}) + + let sessionDs = { + + getSessionData() { + const fromSession = sessionStorage.getItem(storageKey) + if (fromSession) { + return JSON.parse(fromSession) + } + } + } + + //overrides to load data on first search + sessionDs.search = async (params = {}) => { + console.log("SessionDataService search", params) + await sessionDs.init() + return memDs.search(params) + } + + //override the getData to pull it from the rest + sessionDs.init = async () =>{ + // try { + // let data = sessionDs.checkSession() + let dataCache = sessionDs.stores.getMasterData() + //if dataCache is populated then its been init already + if (isEmpty(dataCache)) { + console.log(`using ${storageKey} in sessionStorage`) + // let sessionCache = sessionDs.getSessionData() + // if(!sessionCache){ + // //pull it from the uri key + // const sessionCache = await ky.get(sourceUrl).json() + // sessionStorage.setItem(storageKey, stringify(sessionCache)) + // } + const sessionCache = await ky.get(sourceUrl).json() + sessionStorage.setItem(storageKey, stringify(sessionCache)) + sessionDs.stores.setMasterData(sessionCache) + return sessionCache + } + // } catch (e) { + // console.log(`Unable to parse session for ${sourceUrl}, retrieving intial data.`, e) + // } + } + + return mix(sessionDs).with(memDs) + +} + +export default SessionDataService diff --git a/src/stores/rest/RestDataService.js b/src/stores/rest/RestDataService.js new file mode 100644 index 000000000..254f05af3 --- /dev/null +++ b/src/stores/rest/RestDataService.js @@ -0,0 +1,28 @@ +import { kyFetch } from '../ky' +import mix from '../../utils/mix-it-with'; +import { restPicklist } from './restPicklist'; +import { restQuery } from './restQuery'; +import { restSave } from './restSave'; +import { crudQueryModel } from '../crudQueryModel' + +/** + * A common wrapper around RESTful resource + */ +export const RestDataService = ({ key, ...args }) => { + let api = kyFetch(key) + + let ds = { + ...args, + api, + key + } + + return mix(ds).it(RestDataService).with( + restSave({ api }), + restQuery({ api }), + restPicklist({ api }), + crudQueryModel + ) +} + +export default RestDataService diff --git a/src/stores/rest/restPicklist.js b/src/stores/rest/restPicklist.js new file mode 100644 index 000000000..db56405a9 --- /dev/null +++ b/src/stores/rest/restPicklist.js @@ -0,0 +1,21 @@ +import mix from '../../utils/mix-it-with'; + +/** + * + * @param {*} param0 pass in the api to use + * @returns the function returned take the main object that will get merged into + */ +export const restPicklist = ({ api }) => obj => { + + return mix(obj).with({ + + async picklist(params) { + let cleanParams = obj.setupSearchParams(params) + const o = { path: 'picklist', searchParams: cleanParams } + const data = await api.get(o) + return data + } + + }) + +} diff --git a/src/stores/rest/restQuery.js b/src/stores/rest/restQuery.js new file mode 100644 index 000000000..d8d263ba3 --- /dev/null +++ b/src/stores/rest/restQuery.js @@ -0,0 +1,49 @@ +import prune from '../../utils/prune'; +import stringify from '../../utils/stringify'; +import mix from '../../utils/mix-it-with'; + +export const restQuery = ({ api }) => ds => { + + /** + * when grid is for child or detail data, restrictSearch is what to filter it by, + * for example is showing invoices for customer then restrictSearch might be set to {custId:123} + */ + let { restrictSearch } = ds + + + return mix(ds).with({ + + async get(id) { + const item = await api.getById(id) + ds.stores.setItem(item) + return item + }, + + /** + * adds searchParams, which are the query params ( the part after the ? ) + * and then calls the get. if the params has a q property and its a string then + * @param {*} params + */ + async search(params) { + let searchParams = ds.setupSearchParams(params) + const page = await api.get({ searchParams }) + ds.stores.setPageView(page) + return page + }, + + // prunes params and stringifies the q param if exists + setupSearchParams(params){ + let pruned = prune(params) + let { q, sort } = pruned + if(restrictSearch) q = {...q, ...restrictSearch} + //save it before we stringify + ds.stores.setQuery({...pruned, q, sort}) + + if(q) pruned.q = stringify(q) + //stringify sort and remove the quotes and brackets + if(sort) pruned.sort = stringify(sort).replace(/{|}|"/g, '') + return pruned + } + }) + +} diff --git a/src/stores/rest/restSave.js b/src/stores/rest/restSave.js new file mode 100644 index 000000000..692fb1dc3 --- /dev/null +++ b/src/stores/rest/restSave.js @@ -0,0 +1,33 @@ +import mix from '../../utils/mix-it-with'; + +export const restSave = ({ api }) => ds => { + + const { ident } = ds + + return mix(ds).with({ + + async create(item) { + const newItem = await api.post({ json: item }) + return newItem + }, + + async update(item) { + const id = item[ident] + const newItem = await api.put({ path: id, json: item }) + return newItem + }, + + /** Returns a promise to remove (DELETE) an item. */ + async remove(id) { + await api.delete({ path: id }) + return true + }, + + async bulkUpdate(muItem) { + const results = await api.post({path: 'bulkUpdate', json: muItem }) + return results + } + + }) + +} diff --git a/src/stores/storeSupport.js b/src/stores/storeSupport.js new file mode 100644 index 000000000..d3a5328cb --- /dev/null +++ b/src/stores/storeSupport.js @@ -0,0 +1,28 @@ +/** + * simple implementation of the store contract from svelte so we dont need to + * depend on svelte for some our base datastores + * + * @param {*} initial_value the inital value for the store + * @returns the writable store + */ +export const writable = (initial_value = 0) => { + + let value = initial_value // content of the store + let subs = [] // subscriber's handlers + + const subscribe = (handler) => { + subs = [...subs, handler] // add handler to the array of subscribers + handler(value) // call handler with current value + return () => subs = subs.filter(sub => sub !== handler) // return unsubscribe function + } + + const set = (new_value) => { + if (value === new_value) return // same value, exit + value = new_value // update value + subs.forEach(sub => sub(value)) // update subscribers + } + + const update = (update_fn) => set(update_fn(value)) // update function + + return { subscribe, set, update } // store contract +} diff --git a/src/styles/framework7/framework7.less b/src/styles/framework7/framework7.less index a133cb3be..8dc832454 100644 --- a/src/styles/framework7/framework7.less +++ b/src/styles/framework7/framework7.less @@ -47,6 +47,7 @@ @import '~framework7/components/block/block.less'; @import '~framework7/components/list/list.less'; @import '~framework7/components/badge/badge.less'; +@import '~framework7/components/chip/chip.less'; // @import './components/button/button.less'; @import '~framework7/components/touch-ripple/touch-ripple.less'; @import '~framework7/components/icon/icon.less'; diff --git a/src/tools/AppState.js b/src/tools/AppState.js index fd6999cd1..a2fdaef38 100644 --- a/src/tools/AppState.js +++ b/src/tools/AppState.js @@ -5,6 +5,8 @@ // import _ from 'lodash' class AppState { + // api url + apiUrl // the ui-router state instance $state @@ -36,8 +38,6 @@ class AppState { open: true } - layout = {} - static factory() { return new AppState() } @@ -46,6 +46,13 @@ class AppState { this._debug = false } + /** + * go to to the page key + */ + go(pageKey){ + this.$state.go(pageKey) + } + /** * the title of the current active state page/component */ @@ -76,6 +83,7 @@ class AppState { }; return check })() + } const _instance = AppState.factory() diff --git a/src/tools/globalLoader/index.js b/src/tools/globalLoader/index.js index 9f711785f..64ae840cc 100644 --- a/src/tools/globalLoader/index.js +++ b/src/tools/globalLoader/index.js @@ -1,3 +1,5 @@ +import { KyFactory } from "../../stores/ky"; + // Global object for loader could be replaced with other lib that doesnt rely on angular lib const globalLoader = { getLoaderService() { @@ -20,4 +22,7 @@ const globalLoader = { } } - export default globalLoader \ No newline at end of file +KyFactory.subscribe('before', () => globalLoader.start()) +KyFactory.subscribe('after', () => globalLoader.complete()) + +export default globalLoader diff --git a/src/utils/__tests__/finders.spec.js b/src/utils/__tests__/finders.spec.js new file mode 100644 index 000000000..3ca41b8c1 --- /dev/null +++ b/src/utils/__tests__/finders.spec.js @@ -0,0 +1,44 @@ +import { hasSomeDeep, findSomeDeep } from '../finders' +import {expect as x} from '@jest/globals' +import countryData from '../../__tests__/countries' + +const sobj = { + a: 'foos', + b: 123, + c:{ + nest: 'bars', + val: 234 + } +} +const sobj2 = { a: 'foo'} + +const testList = [sobj, sobj2] + +describe('hasSomeDeep', () => { + + test('first level', () => { + x(hasSomeDeep(sobj, 'foo')).toBe(true) + x(hasSomeDeep(sobj, 'foos')).toBe(true) + x(hasSomeDeep(sobj, 'oos')).toBe(true) + x(hasSomeDeep(sobj, 123)).toBe(true) + }) + + test('deep', () => { + x(hasSomeDeep(sobj, 'bars')).toBe(true) + x(hasSomeDeep(sobj, 'ars')).toBe(true) + x(hasSomeDeep(sobj, 234)).toBe(true) + }) +}) + +describe('findSomeDeep', () => { + + test('findSomeDeep', () => { + x( findSomeDeep(testList, 'foo').length ).toEqual(2) + x( findSomeDeep(testList, 'foos').length ).toEqual(1) + }) + + test('findSomeDeep country united', () => { + x( findSomeDeep(countryData, 'united').length ).toEqual(3) + }) + +}) diff --git a/src/utils/__tests__/inspect.spec.js b/src/utils/__tests__/inspect.spec.js new file mode 100644 index 000000000..b9a7867de --- /dev/null +++ b/src/utils/__tests__/inspect.spec.js @@ -0,0 +1,26 @@ +/* eslint-disable */ +import {isString, isPlainObject} from '../inspect'; +import {expect as x} from '@jest/globals' + +describe('isPlainObject', () => { + it('should return `true` if the object is created by the `Object` constructor.', () => { + x(isPlainObject(Object.create({}))).toBe(true) + x(isPlainObject(Object.create(Object.prototype))).toBe(true) + x(isPlainObject({foo: 'bar'})).toBe(true) + x(isPlainObject({})).toBe(true) + x(isPlainObject(Object.create(null))).toBe(true) + }); + + it('should return `false` if the object is not created by the `Object` constructor.', () => { + function Foo() {this.abc = {};}; + + x( isPlainObject(/foo/) ).toBe(false) + x( isPlainObject(function() {}) ).toBe(false) + x( isPlainObject(1) ).toBe(false) + x( isPlainObject(['foo', 'bar']) ).toBe(false) + x( isPlainObject([]) ).toBe(false) + x( isPlainObject(null) ).toBe(false) + x( isPlainObject(new Foo) ).toBe(false) + }); + +}); diff --git a/src/utils/__tests__/mix-it-with-piping.spec.js b/src/utils/__tests__/mix-it-with-piping.spec.js new file mode 100644 index 000000000..1f425dcc9 --- /dev/null +++ b/src/utils/__tests__/mix-it-with-piping.spec.js @@ -0,0 +1,230 @@ +// @ts-nocheck +/* eslint-disable */ +import pipe from '../pipe' +import mix, {mixConstructor} from '../mix-it-with' +import {expect as x} from '@jest/globals' + +// Set up some functional mixins +/** + * pass in the obj to add the flyable function to + */ +const makeFlyable = obj => { + let isFlying = false; + // showing a few different ways to skin the cat + const fly = () => { + isFlying = true + return obj + } + + function land() { + isFlying = false + return obj + } + + return Object.assign(obj, { + fly, land, + fooFly: false, + isFlying: () => isFlying + }) +} + +// const withBattery = ({ capacity }) => o => { +// let percentCharged = 100 +// return { +// ...o, +// getCapacity: () => capacity +// }; +// }; + +/** + * adds battery to the obj + * keep the charge private and exposes with getters + */ +const withBattery = ({ battery , ...obj}) => { + let percentCharged = 100 + //pull the capicity out or default to 1100 + let {capacity = 1100, ...restBattery} = battery || {} + + obj.battery = { + ...restBattery, + draw (percent) { + const remaining = percentCharged - percent + percentCharged = remaining > 0 ? remaining : 0 + return this + }, + //example of couple options for getters + get charge(){ return percentCharged}, + get capacity(){ return capacity } + } + return obj +}; + +// const Drone = ({ capacity = '9' } = {}) => mix(Drone).pipe( +// withFlying, +// withFixedBattery({ capacity }) +// ).factory({}) + +const Drone = function(opts){ + opts = { name:"Base Drone Model" , ...opts} + + let main = (o) => ({ + ...o, + fooFly: true, //test that it overrides fooFly in makeFlyable + hasRemote: true + }) + + return mix(opts).it(Drone).with( + makeFlyable, + withBattery, + main + ) +} + +/** + * adds rotors to a drone and 'overrides' the fly and land + * + * @param {Drone} drone + * @returns the object with the features added + */ +const rotors = drone => { + let rotating = false + let {fly: superFly, land: superLand} = drone + return Object.assign(drone, { + fly(){ + superFly() + rotating = true + return this + }, + //overrides land so make rotating + land () { + superLand() + rotating = false + return this + }, + isRotating: () => rotating + }) +} + +const Copiter = (opts) => { + opts = { name: "Copiter Model", ...opts } + + return mix(opts).it(Copiter).with( + Drone, + rotors + ) +} + +// Copiter.of = Copiter + +const copter = Copiter() + +console.log(` + can fly: ${ copter.fly().isFlying() === true } + can land: ${ copter.land().isFlying() === false } + battery capacity: ${ copter.battery.capacity } + battery status: ${ copter.battery.draw(50).charge }% + battery drained: ${ copter.battery.draw(75).charge }% remaining +`) + +// const copter = Copiter() + +// const bigCopter = Copiter({capacity: '20000mAH'}) + + +describe('mix drone', () => { + test('Drone using simple pipe', () => { + + const DroneUsingPipe = () => pipe( + makeFlyable, + withBattery, + mixConstructor(DroneUsingPipe) + )({}) + + const myDrone = DroneUsingPipe() + + // let ctor = myDrone.constructor + // x(ctor).toEqual(DroneUsingPipe) + // x(ctor.of).toEqual(DroneUsingPipe) + }) + + test('mixer drone', () => { + + let myDrone = Drone() + + x(myDrone.fooFly).toEqual(true) + x(myDrone.name).toEqual('Base Drone Model') + x(myDrone.battery.capacity).toEqual(1100) + + myDrone = Drone({name: 'mini drone', battery:{capacity: 20}}) + x(myDrone.name).toEqual('mini drone') + x( myDrone.battery.capacity ).toBe(20) + + //cant set it + expect(() => { myDrone.battery.capacity = '99'}).toThrow() + + //not flying yet + x( myDrone.isFlying() ).toBe(false) + myDrone.fly() + //fly and check status + x( myDrone.isFlying() ).toBe(true) + //land and check status as method chaining is enabled + x( myDrone.land().isFlying() ).toBe(false) + + x( myDrone.battery.draw(90).charge ).toBe(10) + x( myDrone.battery.draw(10).charge ).toBe(0) + + // let ctor = myDrone.constructor + // x(ctor).toEqual(Drone) + // x(ctor.of).toEqual(Drone) + + myDrone = Drone() + let { battery } = myDrone + console.log(` + can fly: ${ myDrone.fly().isFlying() === true } + can land: ${ myDrone.land().isFlying() === false } + battery capacity: ${ battery.capacity } + battery status: ${ battery.draw(50).charge }% + battery drained: ${ battery.draw(75).charge }% remaining + `) + + }) + + test('check copter overrides', () => { + + let copter = Copiter() + x(copter.name).toEqual('Copiter Model') + + copter = Copiter({name: 'Custom'}) + x(copter.name).toEqual('Custom') + + //not flying yet + x( copter.isFlying() ).toBe(false) + copter.fly() + //fly and check status + x( copter.isFlying() ).toBe(true) + //rotors should be spinning too + x( copter.isRotating() ).toBe(true) + + //land and check status as method chaining is enabled + x( copter.land().isFlying() ).toBe(false) + //rotors should not be spinning now + x( copter.isRotating() ).toBe(false) + + //it should be a Copter + // let ctor = copter.constructor + // x(ctor).toEqual(Copiter) + x(Copiter.of).toEqual(Copiter) + + console.log(` + can fly: ${ copter.fly().isFlying() === true } + can land: ${ copter.land().isFlying() === false } + battery capacity: ${ copter.battery.capacity } + battery status: ${ copter.battery.draw(50).charge }% + battery drained: ${ copter.battery.draw(75).charge }% remaining + `) + + }) + +}) + + diff --git a/src/utils/__tests__/mix-it-with.spec.js b/src/utils/__tests__/mix-it-with.spec.js new file mode 100644 index 000000000..2dbae978c --- /dev/null +++ b/src/utils/__tests__/mix-it-with.spec.js @@ -0,0 +1,23 @@ +// @ts-nocheck +/* eslint-disable */ +import mix from '../mix-it-with' +import {expect as x} from '@jest/globals' + +describe('mix with objects', () => { + test('should merge objects or functions', () => { + + const master = { a:1 } + + const obj1 = { a:2, b:2 } + const obj2 = { a:3, b:3 } + + const mixed = mix(master).with(obj1, obj2) + + x(mixed).toEqual(master) + x(mixed).toEqual({a:1, b:2}) + + }) + +}) + + diff --git a/src/utils/__tests__/mixer.spec.js b/src/utils/__tests__/mixer.spec.js new file mode 100644 index 000000000..072d18f4d --- /dev/null +++ b/src/utils/__tests__/mixer.spec.js @@ -0,0 +1,228 @@ +// @ts-nocheck +/* eslint-disable */ +import pipe from '../pipe' +import mix, {mixConstructor} from '../mixer' +import {expect as x} from '@jest/globals' + +// Set up some functional mixins +/** + * pass in the obj to add the flyable function to + */ +const makeFlyable = obj => { + let isFlying = false; + // showing a few different ways to skin the cat + const fly = () => { + isFlying = true + return obj + } + + function land() { + isFlying = false + return obj + } + + return Object.assign(obj, { + fly, land, + fooFly: false, + isFlying: () => isFlying + }) +} + +// const withBattery = ({ capacity }) => o => { +// let percentCharged = 100 +// return { +// ...o, +// getCapacity: () => capacity +// }; +// }; + +/** + * adds battery to the obj + * keep the charge private and exposes with getters + */ +const withBattery = ({ battery , ...obj}) => { + let percentCharged = 100 + //pull the capicity out or default to 1100 + let {capacity = 1100, ...restBattery} = battery || {} + + obj.battery = { + ...restBattery, + draw (percent) { + const remaining = percentCharged - percent + percentCharged = remaining > 0 ? remaining : 0 + return this + }, + //example of couple options for getters + get charge(){ return percentCharged}, + get capacity(){ return capacity } + } + return obj +}; + +// const Drone = ({ capacity = '9' } = {}) => mix(Drone).pipe( +// withFlying, +// withFixedBattery({ capacity }) +// ).factory({}) + +const Drone = function(opts){ + opts = { name:"Base Drone Model" , ...opts} + + let main = (o) => ({ + ...o, + fooFly: true, //test that it overrides fooFly in makeFlyable + hasRemote: true + }) + + return mix(Drone).it(opts).with( + makeFlyable, + withBattery, + main + ) +} + +/** + * adds rotors to a drone and 'overrides' the fly and land + * + * @param {Drone} drone + * @returns the object with the features added + */ +const rotors = drone => { + let rotating = false + let {fly: superFly, land: superLand} = drone + return Object.assign(drone, { + fly(){ + superFly() + rotating = true + return this + }, + //overrides land so make rotating + land () { + superLand() + rotating = false + return this + }, + isRotating: () => rotating + }) +} + +const Copiter = (opts) => { + let name = "Copiter Model" + + return mix(Copiter).it({ name, ...opts }).with( + Drone, + rotors + ) +} + +const copter = Copiter() + +console.log(` + can fly: ${ copter.fly().isFlying() === true } + can land: ${ copter.land().isFlying() === false } + battery capacity: ${ copter.battery.capacity } + battery status: ${ copter.battery.draw(50).charge }% + battery drained: ${ copter.battery.draw(75).charge }% remaining +`) + +// const copter = Copiter() + +// const bigCopter = Copiter({capacity: '20000mAH'}) + + +describe('mix drone', () => { + test('Drone using simple pipe', () => { + + const DroneUsingPipe = () => pipe( + makeFlyable, + withBattery, + mixConstructor(DroneUsingPipe) + )({}) + + const myDrone = DroneUsingPipe() + + // let ctor = myDrone.constructor + // x(ctor).toEqual(DroneUsingPipe) + // x(ctor.of).toEqual(DroneUsingPipe) + }) + + test('mixer drone', () => { + + let myDrone = Drone() + + x(myDrone.fooFly).toEqual(true) + x(myDrone.name).toEqual('Base Drone Model') + x(myDrone.battery.capacity).toEqual(1100) + + myDrone = Drone({name: 'mini drone', battery:{capacity: 20}}) + x(myDrone.name).toEqual('mini drone') + x( myDrone.battery.capacity ).toBe(20) + + //cant set it + expect(() => { myDrone.battery.capacity = '99'}).toThrow() + + //not flying yet + x( myDrone.isFlying() ).toBe(false) + myDrone.fly() + //fly and check status + x( myDrone.isFlying() ).toBe(true) + //land and check status as method chaining is enabled + x( myDrone.land().isFlying() ).toBe(false) + + x( myDrone.battery.draw(90).charge ).toBe(10) + x( myDrone.battery.draw(10).charge ).toBe(0) + + // let ctor = myDrone.constructor + // x(ctor).toEqual(Drone) + // x(ctor.of).toEqual(Drone) + + myDrone = Drone() + let { battery } = myDrone + console.log(` + can fly: ${ myDrone.fly().isFlying() === true } + can land: ${ myDrone.land().isFlying() === false } + battery capacity: ${ battery.capacity } + battery status: ${ battery.draw(50).charge }% + battery drained: ${ battery.draw(75).charge }% remaining + `) + + }) + + test('check copter overrides', () => { + + let copter = Copiter() + x(copter.name).toEqual('Copiter Model') + + copter = Copiter({name: 'Custom'}) + x(copter.name).toEqual('Custom') + + //not flying yet + x( copter.isFlying() ).toBe(false) + copter.fly() + //fly and check status + x( copter.isFlying() ).toBe(true) + //rotors should be spinning too + x( copter.isRotating() ).toBe(true) + + //land and check status as method chaining is enabled + x( copter.land().isFlying() ).toBe(false) + //rotors should not be spinning now + x( copter.isRotating() ).toBe(false) + + //it should be a Copter + // let ctor = copter.constructor + // x(ctor).toEqual(Copiter) + x(Copiter.of).toEqual(Copiter) + + console.log(` + can fly: ${ copter.fly().isFlying() === true } + can land: ${ copter.land().isFlying() === false } + battery capacity: ${ copter.battery.capacity } + battery status: ${ copter.battery.draw(50).charge }% + battery drained: ${ copter.battery.draw(75).charge }% remaining + `) + + }) + +}) + + diff --git a/src/utils/__tests__/mixwith.spec.js b/src/utils/__tests__/mixwith.spec.js new file mode 100644 index 000000000..485c1332a --- /dev/null +++ b/src/utils/__tests__/mixwith.spec.js @@ -0,0 +1,286 @@ + +import { + apply, + isApplicationOf, + wrap, + unwrap, + hasMixin, + Mixin, + BareMixin, + Cache, + DeDupe, + HasInstance, + mix, +} from '../mixClass'; + +describe('mixwith.js', () => { + + describe('apply() and isApplicationOf()', () => { + + test('apply() applies a mixin function', () => { + const M = (s) => class extends s { + test() { + return true; + } + }; + class Test extends apply(Object, M) {} + const i = new Test(); + expect(i.test()).toBe(true); + }); + + test('isApplication() returns true for a mixin applied by apply()', () => { + const M = (s) => class extends s {}; + expect(isApplicationOf(apply(Object, M).prototype, M)).toBe(true); + }); + + test('isApplication() works with wrapped mixins', () => { + const M = (s) => class extends s {}; + const WrappedM = wrap(M, (superclass) => apply(superclass, M)); + expect(isApplicationOf(WrappedM(Object).prototype, WrappedM)).toBe(true); + }); + + test('isApplication() returns false when it should', () => { + const M = (s) => class extends s {}; + const X = (s) => class extends s {}; + expect(isApplicationOf(apply(Object, M).prototype, X)).toBe(false); + }); + + }); + + describe('hasMixin()', () => { + test('hasMixin() returns true for a mixin applied by apply()', () => { + const M = (s) => class extends s {}; + expect(hasMixin(apply(Object, M).prototype, M)).toBe(true); + }); + + }); + + describe('wrap() and unwrap()', () => { + + test('wrap() sets the prototype', () => { + const f = (x) => x*x; + f.test = true; + const wrapper = (x) => f(x); + wrap(f, wrapper); + expect(wrapper.test).toBe(true); + expect(Object.getPrototypeOf(wrapper)).toEqual(f); + }); + + test('unwrap() returns the wrapped function', () => { + const f = (x) => x*x; + const wrapper = (x) => f(x); + wrap(f, wrapper); + expect(unwrap(wrapper)).toEqual(f); + }); + + }); + + describe('BareMixin', () => { + + test('mixin application is on prototype chain', () => { + const M = BareMixin((s) => class extends s {}); + class C extends M(Object) {} + const i = new C(); + expect(hasMixin(i, M)).toBe(true) + }); + + test('methods on mixin are present', () => { + const M = BareMixin((s) => class extends s { + foo() { return 'foo'; } + }); + class C extends M(Object) {} + const i = new C(); + expect(i.foo()).toEqual('foo'); + }); + + test('methods on superclass are present', () => { + const M = BareMixin((s) => class extends s {}); + class S { + foo() { return 'foo'; } + } + class C extends M(S) {} + const i = new C(); + expect(i.foo()).toEqual('foo'); + }); + + test('methods on subclass are present', () => { + const M = BareMixin((s) => class extends s {}); + class C extends M(Object) { + foo() { return 'foo'; } + } + const i = new C(); + expect(i.foo()).toEqual('foo'); + }); + + test('methods on mixin override superclass', () => { + const M = BareMixin((s) => class extends s { + foo() { return 'bar'; } + }); + class S { + foo() { return 'foo'; } + } + class C extends M(S) {} + const i = new C(); + expect(i.foo()).toEqual('bar'); + }); + + test('methods on mixin can call super', () => { + const M = BareMixin((s) => class extends s { + foo() { return super.foo(); } + }); + class S { + foo() { return 'superfoo'; } + } + class C extends M(S) {} + const i = new C(); + expect(i.foo()).toEqual('superfoo'); + }); + + test('methods on subclass override superclass', () => { + const M = BareMixin((s) => class extends s {}); + class S { + foo() { return 'superfoo'; } + } + class C extends M(S) { + foo() { return 'subfoo'; } + } + const i = new C(); + + expect(i.foo()).toEqual('subfoo'); + }); + + test('methods on subclass override mixin', () => { + const M = BareMixin((s) => class extends s { + foo() { return 'mixinfoo'; } + }); + class S {} + class C extends M(S) { + foo() { return 'subfoo'; } + } + const i = new C(); + + expect(i.foo()).toEqual('subfoo'); + }); + + test('methods on subclass can call super to superclass', () => { + const M = BareMixin((s) => class extends s {}); + class S { + foo() { return 'superfoo'; } + } + class C extends M(S) { + foo() { return super.foo(); } + } + const i = new C(); + + expect(i.foo()).toEqual('superfoo'); + }); + + }); + + describe('DeDupe', () => { + + test('applies the mixin the first time', () => { + const M = DeDupe(BareMixin((superclass) => class extends superclass {})); + class C extends M(Object) {} + const i = new C(); + + expect(hasMixin(i, M)).toBe(true); + }); + + test('does\'n apply the mixin the second time', () => { + let applicationCount = 0; + const M = DeDupe(BareMixin((superclass) => { + applicationCount++; + return class extends superclass {}; + })); + class C extends M(M(Object)) {} + const i = new C(); + + expect(hasMixin(i, M)).toBe(true); + expect(applicationCount).toEqual(1); + }); + + }); + + describe('HasInstance', () => { + + let hasNativeHasInstance = false; + + beforeAll(() => { + // Enable the @@hasInstance patch in mixwith.HasInstance + if (!Symbol.hasInstance) { + Symbol.hasInstance = Symbol('hasInstance'); + } + + class Check { + static [Symbol.hasInstance](o) { return true; } + } + hasNativeHasInstance = 1 instanceof Check; + }); + + // test('subclasses implement mixins', () => { + // const M = HasInstance((s) => class extends s {}); + // class C extends M(Object) {} + // const i = new C(); + + // if (hasNativeHasInstance) { + // assert.instanceOf(i, C); + // } else { + // assert.isTrue(C[Symbol.hasInstance](i)); + // } + // }); + + }); + + describe('mix().with()', () => { + + test('applies mixins in order', () => { + const M1 = BareMixin((s) => class extends s {}); + const M2 = BareMixin((s) => class extends s {}); + class S {} + class C extends mix(S).with(M1, M2) {} + const i = new C(); + + expect(hasMixin(i, M1)).toBe(true); + expect(hasMixin(i, M2)).toBe(true); + + expect(isApplicationOf(i.__proto__.__proto__, M2)).toBe(true); + expect(isApplicationOf(i.__proto__.__proto__.__proto__, M1)).toBe(true); + expect(S.prototype).toEqual(i.__proto__.__proto__.__proto__.__proto__); + }); + + test('mix() can omit the superclass', () => { + const M = BareMixin((s) => class extends s { + static staticMixinMethod() { + return 42; + } + + foo() { + return 'foo'; + } + }); + class C extends mix().with(M) { + static staticClassMethod() { + return 7; + } + + bar() { + return 'bar'; + } + } + let i = new C(); + + expect(hasMixin(i, M)).toBe(true); + expect(isApplicationOf(i.__proto__.__proto__, M)).toBe(true); + + expect(i.foo()).toEqual('foo') + expect(i.bar()).toEqual('bar') + + expect(C.staticMixinMethod()).toEqual(42) + expect(C.staticClassMethod()).toEqual(7) + + }); + + }); + +}); diff --git a/src/utils/__tests__/pipe.spec.js b/src/utils/__tests__/pipe.spec.js new file mode 100644 index 000000000..7b0353f4d --- /dev/null +++ b/src/utils/__tests__/pipe.spec.js @@ -0,0 +1,18 @@ +/* eslint-disable */ +import pipe from '../pipe' + +describe('Sandbox', () => { + test('findSomeDeep country united', () => { + let getName = (person) => person.name + let uppercase = (string) => string.toUpperCase() + let get6Chars = (string) => string.substring(0, 6) + + let piped = pipe(getName, uppercase, get6Chars) + + expect( piped({ name: 'Buckethead' }) ).toEqual('BUCKET') + + }) + +}) + + diff --git a/src/utils/__tests__/sandbox.spec.js b/src/utils/__tests__/sandbox.spec.js new file mode 100644 index 000000000..171dfa60a --- /dev/null +++ b/src/utils/__tests__/sandbox.spec.js @@ -0,0 +1,17 @@ +import {expect as x} from '@jest/globals' + +describe('sandbox', () => { + test('testing simple spread defaults', () => { + + const setDefaults = ({ foo = 'bar', ...args}) => ({ ...args, foo }) + + let defArgs = setDefaults({a:'x'}) + x( defArgs ).toEqual({a:'x', foo:'bar'}) + + defArgs = setDefaults({a:'x', foo:'buzz'}) + x( defArgs ).toEqual({a:'x', foo:'buzz'}) + }) + +}) + + diff --git a/src/utils/__tests__/truthy.spec.js b/src/utils/__tests__/truthy.spec.js index edf89c171..217dfffc7 100644 --- a/src/utils/__tests__/truthy.spec.js +++ b/src/utils/__tests__/truthy.spec.js @@ -1,4 +1,4 @@ -import { isTruthy, isFalsy, falsyCheck, truthyCheck } from '../truthy' +import { isTruthy, isFalsy, falsyCheck, truthyCheck } from '../inspect' describe('isFalsy', () => { diff --git a/src/utils/dash.js b/src/utils/dash.js index b5528b521..2e858b32d 100644 --- a/src/utils/dash.js +++ b/src/utils/dash.js @@ -9,25 +9,29 @@ export { default as isEmpty } from 'lodash/isEmpty'; export { default as isFunction } from 'lodash/isFunction'; export { default as isDate } from 'lodash/isDate'; export { default as isEqual } from 'lodash/isEqual'; -export { default as isMatchWith } from 'lodash/isMatchWith'; export { default as set } from 'lodash/set'; export { default as get } from 'lodash/get'; export { default as difference } from 'lodash/difference'; export { default as merge } from 'lodash/merge'; +export { default as _defaults } from 'lodash/defaults'; export { default as extend } from 'lodash/extend'; export { default as max } from 'lodash/max'; export { default as forEach } from 'lodash/forEach'; export { default as find } from 'lodash/find'; export { default as upperFirst } from 'lodash/upperFirst'; +export { default as toString } from 'lodash/toString'; export { default as split } from 'lodash/split'; export { default as orderBy } from 'lodash/orderBy'; export { default as pick } from 'lodash/pick'; export { default as remove } from 'lodash/remove'; export { default as cloneDeep } from 'lodash/cloneDeep'; +export { default as isMatchWith } from 'lodash/isMatchWith'; +export { default as isMatch } from 'lodash/isMatch'; + //these should not really be needed export { default as map } from 'lodash/map'; export { default as each } from 'lodash/each'; diff --git a/src/utils/ensure.js b/src/utils/ensure.js new file mode 100644 index 000000000..e1965809a --- /dev/null +++ b/src/utils/ensure.js @@ -0,0 +1,25 @@ +/** + * function to 'ensure', or make sure of something and try to correct it + */ + +/** + * make sure the text starts with the prefix + * @param {*} text will get converted to string if its something else + * @param {string} prefix that chars to prepend + * @returns the text if its already prefixed or the new prefixed string + */ +export function ensurePrefix(text, prefix){ + if(text && !(`${text}`.startsWith(prefix)) ) text = `${prefix}${text}` + return text +} + +/** + * make sure the text ends with the postfix + * @param {*} text will get converted to string if its something else + * @param {string} postfix that chars to prepend + * @returns the text if its already postfixed or the new postfix string + */ + export function ensurePostfix(text, postfix){ + if(text && !(`${text}`.endsWith(postfix)) ) text = `${text}${postfix}` + return text +} diff --git a/src/utils/finders.js b/src/utils/finders.js new file mode 100644 index 000000000..2caf59fc8 --- /dev/null +++ b/src/utils/finders.js @@ -0,0 +1,65 @@ +import { isPlainObject, isString } from './inspect' +import {toString, isMatchWith} from './dash' + +/** + * searches array for id key match + * + * @param {{list: array, id: object, ident?: string}} param0 should have data and id, can also pass in idField if identity is other than 'id' + * @returns {object} the found item in array + */ +export const findIndexById = ({ list, id, ident = 'id'}) => { + const idx = list.findIndex((item) => item[ident] === id) + // if (idx === -1) throw Error(`${id} not found`) + return idx +} + +/** + * deep checks if property includes the string or if not string === the searchkey + * + * @param {*} obj the object to look into + * @param {*} searchKey the searchKey + * @returns true or false + */ +export const hasSomeDeep = (obj, searchKey) => { + return Object.keys(obj).some(key => { + const val = obj[key] + if (isPlainObject(val)) { + return hasSomeDeep(val, searchKey) + } else { + if (isString(val)) { + return val.toLowerCase().includes(toString(searchKey).toLowerCase()) + } else { + return val == searchKey + } + } + }) +} + +/** + * hasSomeDeep to filter array + * + * @param {*} arr the array to filter + * @param {*} searchKey the searchKey + * @returns the filtered array of matches + */ +export const findSomeDeep = (arr, searchKey) => { + return arr.filter(obj => hasSomeDeep(obj, searchKey)) +} + +/** + * query by example, uses lodash isMatch + * @param {*} data + * @param {*} qbe + */ +export function qbe(data, qbe) { + let filteredItems = data.filter(it => { + return isMatchWith(it, qbe, (objValue, searchVal) => { + //overrides so string does an includes instead of equality check + if (isString(objValue)) { + return objValue.toLowerCase().includes(toString(searchVal).toLowerCase()) + } + return undefined // return undefined puts it through the built in lodash match + }) + }) + return filteredItems +} diff --git a/src/utils/inspect.js b/src/utils/inspect.js new file mode 100644 index 000000000..19b3278ec --- /dev/null +++ b/src/utils/inspect.js @@ -0,0 +1,28 @@ + +export { default as isEmpty } from 'lodash/isEmpty'; + +export function isString(value) { + return (typeof value === 'string' || value instanceof String) +} + +export function isObject(value) { + return Object.prototype.toString.call(value) === '[object Object]' +} +export function isPlainObject(o) { + if (isObject(o) === false) return false; + + // If has modified constructor + let ctor = o.constructor; + if (ctor === undefined) return true; + + // If has modified prototype + let prot = ctor.prototype; + if (isObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) return false; + + return true; +} + +export * from './truthy' diff --git a/src/utils/mix-it-with.js b/src/utils/mix-it-with.js new file mode 100644 index 000000000..3cfdf2f78 --- /dev/null +++ b/src/utils/mix-it-with.js @@ -0,0 +1,130 @@ +import pipe from './pipe' +import {_defaults, isFunction} from './dash' + +/** + * @typedef {object} Factory + * @property {object} of self reference so can be used like Car.of + */ + +/** + * @typedef {object} Mixer + * @property {object} target the target object that will be merged to. + * @property {function} ctor the constructor function + * @property {function(object): object} factory the piped function of functions + * @property {function(function): Mixer} it the constructor function + * @property {function(object): Mixer} pipe the functions for the pipe + * @property {function(...any): object} with the functions for the pipe + * @property {function(object): object} extend adds props but does not overwrite + */ + + +/** + * Adds the of 'static' method to the constructor function and + * adds it self to the __proto__ constructor. + * The returned function expects an object arg that will be merged in to the final + * object. + * + * @param {Factory|function} constructor + * @returns + */ + export const mixConstructor = constructor => o => { + constructor.of = constructor + // o.__proto__.constructor = constructor + return o +} + +/** + * mix builder to functionaly compose objects. + * It expects the functions to be factory functions that accept and object + * + * @example + * mix(target).it(Drone).with( + * makeFlyable, + * withBattery + * ) + * + * mix().with(withBattery, Object.freeze) + * + * mix(target).of(Drone).merge(simpleObj, simpleObj) + * + * @param {object} target plain object to be used as the base, can be undefined and will default to a new + * @param sources + * @returns {Mixer} - the mixer instance + */ +const mixer = ( target = {}, ...sources ) => { + + /** @type Mixer */ + let o = { } + + //defaults to an empty target object + o.target = target + + /** + * Fluent interface for setting the constuctor, see example + * @param {function} constructor factory function, used to pas through to mixConstructor + * @returns this mixer instance + */ + o.it = function(constructor) { + o.ctor = constructor + // mixConstructor(o.ctor) + return o + } + + /** + * pipe the functions together into the factory property but dont call it + * 'with' function calls this and also calls the resulting function with the target args + * + * @param {...function} funcs + * @returns {object} this mixer for fluent calls + */ + o.pipe = function(...funcs) { + if(o.ctor) funcs.push(mixConstructor(o.ctor)) + //pipes the factory functions and returns the final function + o.factory = pipeObject(...funcs) + // o.factory = pipe(...funcs) + return o + // return pipe(...funcs, mixConstructor(constructor)) + } + + /** + * Build -> pipe the functions together and call the result with whats in the target + * + * @param {...function} funcs + * @returns {object} the built object + */ + o.with = function(...funcs) { + //combines the functions using a default empty object as starting point + // @ts-ignore + return o.pipe(...funcs).factory(o.target) + } + + /** + * Build -> if no factory functions are piped and want to merge simple objects. + * Modifies the target in place as does not copy or clone. + * Think of it more like a class extends where the supers are whats being extended. + * If the target has the property of function then it keeps it. if not then its assigns in and will use the supers. + * shallow assign to the target object for properties that resolve to undefined on target. + * super source objects are applied from left to right, so the first one is what wins. + * Once a property is set, additional values of the same property are ignored. + * Uses the lodash _defaults + */ + o.extend = function(...supers ) { + return _defaults(o.target, ...supers) + } + + return o +} + +export const pipeObject = (...fns) => initialObj => { + return fns.reduce((accumFn, fnOrObj) => { + if(isFunction(fnOrObj)){ + return fnOrObj(accumFn) + } else { + const objAsFunc = (obj) => _defaults(obj, fnOrObj) + //assume its an object and merge it with _defaults + return objAsFunc(accumFn) + } + }, initialObj); +} + +export default mixer diff --git a/src/utils/mixClass.js b/src/utils/mixClass.js new file mode 100644 index 000000000..69a446cad --- /dev/null +++ b/src/utils/mixClass.js @@ -0,0 +1,249 @@ +'use strict'; +//see https://github.com/justinfagnani/mixwith.js +// used by apply() and isApplicationOf() +const _appliedMixin = '__mixwith_appliedMixin'; + +/** + * A function that returns a subclass of its argument. + * + * @example + * const M = (superclass) => class extends superclass { + * getMessage() { + * return "Hello"; + * } + * } + * + * @typedef {Function} MixinFunction + * @param {Function} superclass + * @return {Function} A subclass of `superclass` + */ + +/** + * Applies `mixin` to `superclass`. + * + * `apply` stores a reference from the mixin application to the unwrapped mixin + * to make `isApplicationOf` and `hasMixin` work. + * + * This function is usefull for mixin wrappers that want to automatically enable + * {@link hasMixin} support. + * + * @example + * const Applier = (mixin) => wrap(mixin, (superclass) => apply(superclass, mixin)); + * + * // M now works with `hasMixin` and `isApplicationOf` + * const M = Applier((superclass) => class extends superclass {}); + * + * class C extends M(Object) {} + * let i = new C(); + * hasMixin(i, M); // true + * + * @function + * @param {Function} superclass A class or constructor function + * @param {MixinFunction} mixin The mixin to apply + * @return {Function} A subclass of `superclass` produced by `mixin` + */ +export const apply = (superclass, mixin) => { + let application = mixin(superclass); + application.prototype[_appliedMixin] = unwrap(mixin); + return application; +}; + +/** + * Returns `true` iff `proto` is a prototype created by the application of + * `mixin` to a superclass. + * + * `isApplicationOf` works by checking that `proto` has a reference to `mixin` + * as created by `apply`. + * + * @function + * @param {Object} proto A prototype object created by {@link apply}. + * @param {MixinFunction} mixin A mixin function used with {@link apply}. + * @return {boolean} whether `proto` is a prototype created by the application of + * `mixin` to a superclass + */ +export const isApplicationOf = (proto, mixin) => + proto.hasOwnProperty(_appliedMixin) && proto[_appliedMixin] === unwrap(mixin); + +/** + * Returns `true` iff `o` has an application of `mixin` on its prototype + * chain. + * + * @function + * @param {Object} o An object + * @param {MixinFunction} mixin A mixin applied with {@link apply} + * @return {boolean} whether `o` has an application of `mixin` on its prototype + * chain + */ +export const hasMixin = (o, mixin) => { + while (o != null) { + if (isApplicationOf(o, mixin)) return true; + o = Object.getPrototypeOf(o); + } + return false; +} + + +// used by wrap() and unwrap() +const _wrappedMixin = '__mixwith_wrappedMixin'; + +/** + * Sets up the function `mixin` to be wrapped by the function `wrapper`, while + * allowing properties on `mixin` to be available via `wrapper`, and allowing + * `wrapper` to be unwrapped to get to the original function. + * + * `wrap` does two things: + * 1. Sets the prototype of `mixin` to `wrapper` so that properties set on + * `mixin` inherited by `wrapper`. + * 2. Sets a special property on `mixin` that points back to `mixin` so that + * it can be retreived from `wrapper` + * + * @function + * @param {MixinFunction} mixin A mixin function + * @param {MixinFunction} wrapper A function that wraps {@link mixin} + * @return {MixinFunction} `wrapper` + */ +export const wrap = (mixin, wrapper) => { + Object.setPrototypeOf(wrapper, mixin); + if (!mixin[_wrappedMixin]) { + mixin[_wrappedMixin] = mixin; + } + return wrapper; +}; + +/** + * Unwraps the function `wrapper` to return the original function wrapped by + * one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped + * function. + * + * @function + * @param {MixinFunction} wrapper A wrapped mixin produced by {@link wrap} + * @return {MixinFunction} The originally wrapped mixin + */ +export const unwrap = (wrapper) => wrapper[_wrappedMixin] || wrapper; + +const _cachedApplications = '__mixwith_cachedApplications'; + +/** + * Decorates `mixin` so that it caches its applications. When applied multiple + * times to the same superclass, `mixin` will only create one subclass, memoize + * it and return it for each application. + * + * Note: If `mixin` somehow stores properties its classes constructor (static + * properties), or on its classes prototype, it will be shared across all + * applications of `mixin` to a super class. It's reccomended that `mixin` only + * access instance state. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap with caching behavior + * @return {MixinFunction} a new mixin function + */ +export const Cached = (mixin) => wrap(mixin, (superclass) => { + // Get or create a symbol used to look up a previous application of mixin + // to the class. This symbol is unique per mixin definition, so a class will have N + // applicationRefs if it has had N mixins applied to it. A mixin will have + // exactly one _cachedApplicationRef used to store its applications. + + let cachedApplications = superclass[_cachedApplications]; + if (!cachedApplications) { + cachedApplications = superclass[_cachedApplications] = new Map(); + } + + let application = cachedApplications.get(mixin); + if (!application) { + application = mixin(superclass); + cachedApplications.set(mixin, application); + } + + return application; +}); + +/** + * Decorates `mixin` so that it only applies if it's not already on the + * prototype chain. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap with deduplication behavior + * @return {MixinFunction} a new mixin function + */ +export const DeDupe = (mixin) => wrap(mixin, (superclass) => + (hasMixin(superclass.prototype, mixin)) + ? superclass + : mixin(superclass)); + +/** + * Adds [Symbol.hasInstance] (ES2015 custom instanceof support) to `mixin`. + * + * @function + * @param {MixinFunction} mixin The mixin to add [Symbol.hasInstance] to + * @return {MixinFunction} the given mixin function + */ +export const HasInstance = (mixin) => { + if (Symbol && Symbol.hasInstance && !mixin[Symbol.hasInstance]) { + Object.defineProperty(mixin, Symbol.hasInstance, { + value(o) { + return hasMixin(o, mixin); + }, + }); + } + return mixin; +}; + +/** + * A basic mixin decorator that applies the mixin with {@link apply} so that it + * can be used with {@link isApplicationOf}, {@link hasMixin} and the other + * mixin decorator functions. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap + * @return {MixinFunction} a new mixin function + */ +export const BareMixin = (mixin) => wrap(mixin, (s) => apply(s, mixin)); + +/** + * Decorates a mixin function to add deduplication, application caching and + * instanceof support. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap + * @return {MixinFunction} a new mixin function + */ +export const Mixin = (mixin) => DeDupe(Cached(BareMixin(mixin))); + +/** + * A fluent interface to apply a list of mixins to a superclass. + * + * ```javascript + * class X extends mix(Object).with(A, B, C) {} + * ``` + * + * The mixins are applied in order to the superclass, so the prototype chain + * will be: X->C'->B'->A'->Object. + * + * This is purely a convenience function. The above example is equivalent to: + * + * ```javascript + * class X extends C(B(A(Object))) {} + * ``` + * + * @function + * @param {Function} [superclass=Object] + * @return {MixinBuilder} + */ +export const mix = (superclass) => new MixinBuilder(superclass); + +class MixinBuilder { + + constructor(superclass) { + this.superclass = superclass || class {}; + } + + /** + * Applies `mixins` in order to the superclass given to `mix()`. + * + * @param {Array.} mixins + * @return {Function} a subclass of `superclass` with `mixins` applied + */ + with(...mixins) { + return mixins.reduce((c, m) => m(c), this.superclass); + } +} diff --git a/src/utils/mixer.js b/src/utils/mixer.js new file mode 100644 index 000000000..02b1a0036 --- /dev/null +++ b/src/utils/mixer.js @@ -0,0 +1,112 @@ +import pipe from './pipe' + +/** + * @typedef {object} Factory + * @property {object} of self reference so can be used like Car.of + */ + +/** + * @typedef {object} Mixer + * @property {object} target the target object that will be merged to. + * @property {function(object): object} factory + * @property {function(object): Mixer} it the initial props + * @property {function(object): Mixer} pipe the initial props + * @property {function(...any): object} with the initial props + * @property {function(object): object} merge the initial props + * @property {function(object): object} freeze the initial props + */ + + +/** + * Adds the of 'static' method to the constructor function and + * adds it self to the __proto__ constructor. + * The returned function expects an object arg that will be merged in to the final + * object. + * + * @param {Factory|function} constructor + * @returns + */ + export const mixConstructor = constructor => o => { + constructor.of = constructor + // o.__proto__.constructor = constructor + return o +} + +/** + * mix builder to functionaly compose objects. + * It expects the functions to be factory functions that accept and object + * + * @example + * mix(Drone).it(target).with( + * makeFlyable, + * withBattery + * ) + * + * mix(target).of(Drone).freeze().with( + * makeFlyable, + * withBattery + * ) + * + * mix(target).of(Drone).build() + * + * @param {Factory|function} constructor factory function, used to pas through to mixConstructor + * @returns {Mixer} - the mixer object + */ +const mixer = constructor => { + + /** @type Mixer */ + let o = { } + + //defaults empty target object + o.target = {} + + + /** + * Fluent interface for setting the target object + * @param {object} object + * @returns the mixer instance + */ + o.it = function(object) { + o.target = object + return o + } + + /** + * pipe the functions together into the factory property but dont call it + * 'with' function calls this and also calls the resulting function with the target args + * + * @param {...function} funcs + * @returns {object} this mixer for fluent calls + */ + o.pipe = function(...funcs) { + + //pipes the factory functions and returns the final function + o.factory = pipe(...funcs, mixConstructor(constructor)) + // o.factory = pipe(...funcs) + return o + // return pipe(...funcs, mixConstructor(constructor)) + } + + /** + * pipe the functions together and call the result with whats in the target + * + * @param {...function} funcs + * @returns {object} the build object + */ + o.with = function(...funcs) { + //combines the functions using a default empty object as starting point + return o.pipe(...funcs).factory(o.target) + } + + /** + * Call + * @returns the Object.freeze(target) + */ + o.freeze = function() { + return Object.freeze(o.merge(opts)) + } + + return o +} + +export default mixer diff --git a/src/utils/pipe.js b/src/utils/pipe.js new file mode 100644 index 000000000..2083347f3 --- /dev/null +++ b/src/utils/pipe.js @@ -0,0 +1,27 @@ +/** + * Usage for composing function and factories. Pipes results from function to function + * Creates a function that returns the result of invoking the given functions, + * where each successive invocation is supplied the return value of the previous. + * + * Same as https://lodash.com/docs/#flow but in one line + * see https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1 + * and https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/ + * + * instead of doing something like this uppercase(getName({ name: 'Foo' })) == 'FOO' + * you could do the following + * let getUpperCaseName = pipe(getName, uppercase) //creates new function that pipes + * getUpperCaseName({ name: 'Foo' }) == 'FOO + * + * + * @param {...function} fns + * @returns the new composed Function + */ +export const pipe = (...fns) => args => fns.reduce((accumFn, fn) => fn(accumFn), args); +// const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); + +/** + * same as pipe but starts from right and moves left + */ +export const pipeRight = (...fns) => args => fns.reduceRight((accumFn, fn) => fn(accumFn), args); + +export default pipe diff --git a/src/utils/stringify.js b/src/utils/stringify.js new file mode 100644 index 000000000..78ce7184b --- /dev/null +++ b/src/utils/stringify.js @@ -0,0 +1,10 @@ +import safeStringify from 'fast-safe-stringify' +import { isObject } from './inspect' + +/** + * see https://github.com/davidmarkclements/fast-safe-stringify + * stringify if its an object, otherwise return itself + */ +export default function stringify(obj, replacer, spacer, options){ + return isObject(obj) ? safeStringify(obj, replacer, spacer, options) : obj +} diff --git a/src/vendor.js b/src/vendor.js index 473b0ad3c..38bc7df84 100644 --- a/src/vendor.js +++ b/src/vendor.js @@ -13,7 +13,36 @@ require('bootstrap-sass/assets/javascripts/bootstrap/dropdown') // require('../legacy/bootstrapx-clickover/bootstrapx-clickover') +//jqgrid +// require('free-jqgrid/js/grid.base') +// require('free-jqgrid/js/grid.common') +// require('free-jqgrid/js/grid.custom') +// require('free-jqgrid/js/grid.jqueryui.js') +// require('free-jqgrid/js/jqdnr') +// require('free-jqgrid/js/jqmodal') +// require('free-jqgrid/js/jquery.fmatter.js') + require('free-jqgrid/js/jquery.jqgrid.src') +// includes the following, above is attempt to skinny it down +// "js/grid.base.js", +// "js/grid.celledit.js", +// "js/grid.common.js", +// "js/grid.custom.js", +// "js/grid.filter.js", +// "js/jsonxml.js", +// "js/grid.formedit.js", +// "js/grid.grouping.js", +// "js/grid.import.js", +// "js/grid.inlinedit.js", +// "js/grid.jqueryui.js", +// "js/grid.pivot.js", +// "js/grid.subgrid.js", +// "js/grid.tbltogrid.js", +// "js/grid.treegrid.js", +// "js/jqdnr.js", +// "js/jqmodal.js", +// "js/jquery.fmatter.js" + require('Select2/select2') // require('moment') diff --git a/svelte/framework7-components.js b/svelte/framework7-components.js index fccb1e09d..d21d69ab8 100644 --- a/svelte/framework7-components.js +++ b/svelte/framework7-components.js @@ -14,6 +14,7 @@ export { default as CardContent } from 'framework7-svelte/esm/svelte/card-conten export { default as CardFooter } from 'framework7-svelte/esm/svelte/card-footer.svelte'; export { default as CardHeader } from 'framework7-svelte/esm/svelte/card-header.svelte'; export { default as Card } from 'framework7-svelte/esm/svelte/card.svelte'; +export { default as Chip } from 'framework7-svelte/esm/svelte/chip.svelte'; export { default as ListButton } from 'framework7-svelte/esm/svelte/list-button.svelte'; export { default as ListGroup } from 'framework7-svelte/esm/svelte/list-group.svelte'; diff --git a/tests/unit/angleGrinder/select2/uiSelect2Spec.js b/tests/unit/angleGrinder/select2/uiSelect2Spec.js index 66270dafb..f7904ad91 100644 --- a/tests/unit/angleGrinder/select2/uiSelect2Spec.js +++ b/tests/unit/angleGrinder/select2/uiSelect2Spec.js @@ -27,7 +27,7 @@ angular.module(uiselect2).directive('injectTransformers', [ function () { angular.module(uiselect2).service('dataStoreApi', function() {}) /*global describe, beforeEach, module, inject, it, spyOn, expect, $ */ -describe('uiSelect2', function () { +describe('uiSelect2', function() { 'use strict'; var scope, $compile, options, $timeout; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index f6a16a2e5..000000000 --- a/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "~/*": ["./src/*"], - "#/*": ["/"] - }, - "allowJs": true, - "outDir": "./dist/", - "sourceMap": true, - "noImplicitAny": false, - "module": "es6", - "target": "es5", - "allowSyntheticDefaultImports": true - } -} diff --git a/yarn.lock b/yarn.lock index 8d921154e..ab26bd77f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1716,6 +1716,11 @@ resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/svelte@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-2.0.1.tgz#0e8d7caa693e9b2afce5e622c0475bb0fd89c12c" + integrity sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A== + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" @@ -1891,6 +1896,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== +"@types/node@^16.11.12": + version "16.11.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" + integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" @@ -1953,6 +1963,11 @@ dependencies: "@types/jest" "*" +"@types/webpack-env@^1.16.3": + version "1.16.3" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" + integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz" @@ -5615,10 +5630,10 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz" - integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fastclick@^1.0.6: version "1.0.6" @@ -6014,6 +6029,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +fuse.js@^6.4.6: + version "6.4.6" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.6.tgz#62f216c110e5aa22486aff20be7896d19a059b79" + integrity sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"