Formatting labels
Change-Id: I131f27f2f6fcd5cd76f4fbc13c632f7cd1aa17d0
diff --git a/package.json b/package.json
index ff66a45..ef2db62 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"angular-ui-router": "1.0.0-beta.1",
"jquery": "^3.1.1",
"lodash": "^4.17.2",
+ "pluralize": "^3.1.0",
"rxjs": "^5.0.1",
"socket.io-client": "^1.7.2"
},
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index d93e627..2a9603c 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -7,6 +7,7 @@
import {RuntimeStates} from './services/runtime-states';
import {NavigationService} from './services/navigation';
import {PageTitle} from './services/page-title';
+import {ConfigHelpers} from './services/helpers/config.helpers';
export const xosCore = 'xosCore';
@@ -16,6 +17,7 @@
.provider('RuntimeStates', RuntimeStates)
.service('NavigationService', NavigationService)
.service('PageTitle', PageTitle)
+ .service('ConfigHelpers', ConfigHelpers)
.component('xosHeader', xosHeader)
.component('xosFooter', xosFooter)
.component('xosNav', xosNav)
diff --git a/src/app/core/services/helpers/config.helpers.spec.ts b/src/app/core/services/helpers/config.helpers.spec.ts
new file mode 100644
index 0000000..44260d8
--- /dev/null
+++ b/src/app/core/services/helpers/config.helpers.spec.ts
@@ -0,0 +1,63 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+
+import {IXosConfigHelpersService} from './config.helpers';
+import {xosCore} from '../../index';
+
+let service: IXosConfigHelpersService;
+describe('The ConfigHelpers service', () => {
+
+ beforeEach(angular.mock.module(xosCore));
+
+ beforeEach(angular.mock.inject((
+ ConfigHelpers: IXosConfigHelpersService,
+ ) => {
+ service = ConfigHelpers;
+ }));
+
+ describe('The pluralize function', () => {
+ it('should pluralize string', () => {
+ expect(service.pluralize('test')).toEqual('tests');
+ expect(service.pluralize('test', 1)).toEqual('test');
+ expect(service.pluralize('xos')).toEqual('xosses');
+ expect(service.pluralize('slice')).toEqual('slices');
+ });
+
+ it('should preprend count to string', () => {
+ expect(service.pluralize('test', 6, true)).toEqual('6 tests');
+ expect(service.pluralize('test', 1, true)).toEqual('1 test');
+ });
+ });
+
+ describe('the label formatter', () => {
+ it('should format a camel case string', () => {
+ expect(service.toLabel('camelCase')).toEqual('Camel case');
+ });
+
+ it('should format a snake case string', () => {
+ expect(service.toLabel('snake_case')).toEqual('Snake case');
+ });
+
+ it('should format a kebab case string', () => {
+ expect(service.toLabel('kebab-case')).toEqual('Kebab case');
+ });
+
+ it('should set plural', () => {
+ expect(service.toLabel('kebab-case', true)).toEqual('Kebab cases');
+ });
+
+ it('should format an array of strings', () => {
+ let strings: string[] = ['camelCase', 'snake_case', 'kebab-case'];
+ let labels = ['Camel case', 'Snake case', 'Kebab case'];
+ expect(service.toLabel(strings)).toEqual(labels);
+ });
+
+ it('should set plural on an array of strings', () => {
+ let strings: string[] = ['camelCase', 'snake_case', 'kebab-case'];
+ let labels = ['Camel cases', 'Snake cases', 'Kebab cases'];
+ expect(service.toLabel(strings, true)).toEqual(labels);
+ });
+ });
+
+});
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
new file mode 100644
index 0000000..44d3cdc
--- /dev/null
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -0,0 +1,107 @@
+import * as _ from 'lodash';
+import * as pluralize from 'pluralize';
+import {IXosTableColumn} from '../../table/table';
+
+export interface IXosModelDefsField {
+ name: string;
+ type: string;
+}
+
+export interface IXosConfigHelpersService {
+ modeldefToTableCfg(fields: IXosModelDefsField[]): any[]; // TODO use a proper interface
+ pluralize(string: string, quantity?: number, count?: boolean): string;
+ toLabel(string: string, pluralize?: boolean): string;
+}
+
+export class ConfigHelpers {
+
+ constructor() {
+ pluralize.addIrregularRule('xos', 'xosses');
+ pluralize.addPluralRule(/slice$/i, 'slices');
+ }
+
+ pluralize(string: string, quantity?: number, count?: boolean): string {
+ return pluralize(string, quantity, count);
+ }
+
+ toLabel(string: string, pluralize?: boolean): string {
+
+ if (angular.isArray(string)) {
+ return _.map(string, s => {
+ return this.toLabel(s, pluralize);
+ });
+ }
+
+ if (pluralize) {
+ string = this.pluralize(string);
+ }
+
+ string = this.fromCamelCase(string);
+ string = this.fromSnakeCase(string);
+ string = this.fromKebabCase(string);
+
+ return this.capitalizeFirst(string);
+ }
+
+ modeldefToTableCfg(fields: IXosModelDefsField[]): IXosTableColumn[] {
+ const excluded_fields = [
+ 'created',
+ 'updated',
+ 'enacted',
+ 'policed',
+ 'backend_register',
+ 'deleted',
+ 'write_protect',
+ 'lazy_blocked',
+ 'no_sync',
+ 'no_policy',
+ 'omf_friendly',
+ 'enabled'
+ ];
+ const cfg = _.map(fields, (f) => {
+ if (excluded_fields.indexOf(f.name) > -1) {
+ return;
+ }
+ const col: IXosTableColumn = {
+ label: this.toLabel(f.name),
+ prop: f.name
+ };
+
+ if (f.name === 'backend_status') {
+ col.type = 'icon';
+ col.formatter = (item) => {
+ if (item.backend_status.indexOf('1') > -1) {
+ return 'check';
+ }
+ if (item.backend_status.indexOf('2') > -1) {
+ return 'exclamation-circle';
+ }
+ if (item.backend_status.indexOf('0') > -1) {
+ return 'clock-o';
+ }
+ };
+ }
+ return col;
+ })
+ .filter(v => angular.isDefined(v));
+
+ return cfg;
+ };
+
+ private fromCamelCase(string: string): string {
+ return string.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join(' ');
+ }
+
+ private fromSnakeCase(string: string): string {
+ return string.split('_').join(' ').trim();
+ }
+
+ private fromKebabCase(string: string): string {
+ return string.split('-').join(' ').trim();
+ }
+
+ private capitalizeFirst(string: string): string {
+ return string.slice(0, 1).toUpperCase() + string.slice(1);
+ }
+}
+
diff --git a/src/app/core/table/table.html b/src/app/core/table/table.html
index 58a46e0..f425c0b 100644
--- a/src/app/core/table/table.html
+++ b/src/app/core/table/table.html
@@ -15,10 +15,10 @@
{{col.label}}
<span ng-if="vm.config.order">
<a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">
- <i class="glyphicon glyphicon-chevron-up"></i>
+ <i class="fa fa-chevron-up"></i>
</a>
<a href="" ng-click="vm.orderBy = col.prop; vm.reverse = true">
- <i class="glyphicon glyphicon-chevron-down"></i>
+ <i class="fa fa-chevron-down"></i>
</a>
</span>
</th>
@@ -51,8 +51,8 @@
<td ng-repeat="col in vm.columns">
<span ng-if="!col.type || col.type === 'text'">{{item[col.prop]}}</span>
<span ng-if="col.type === 'boolean'">
- <i class="glyphicon"
- ng-class="{'glyphicon-ok': item[col.prop], 'glyphicon-remove': !item[col.prop]}">
+ <i class="fa"
+ ng-class="{'fa-ok': item[col.prop], 'fa-remove': !item[col.prop]}">
</i>
</span>
<span ng-if="col.type === 'date'">
@@ -73,7 +73,7 @@
{{col.formatter(item)}}
</span>
<span ng-if="col.type === 'icon'">
- <i class="glyphicon glyphicon-{{col.formatter(item)}}">
+ <i class="fa fa-{{col.formatter(item)}}">
</i>
</span>
</td>
@@ -83,7 +83,7 @@
ng-click="action.cb(item)"
title="{{action.label}}">
<i
- class="glyphicon glyphicon-{{action.icon}}"
+ class="fa fa-{{action.icon}}"
style="color: {{action.color}};"></i>
</a>
</td>
diff --git a/src/app/core/table/table.ts b/src/app/core/table/table.ts
index 1d40092..77067fa 100644
--- a/src/app/core/table/table.ts
+++ b/src/app/core/table/table.ts
@@ -4,6 +4,23 @@
import './table.scss';
import * as _ from 'lodash';
+enum EXosTableColType {
+ 'boolean',
+ 'array',
+ 'object',
+ 'custom',
+ 'date' ,
+ 'icon'
+}
+
+export interface IXosTableColumn {
+ label: string;
+ prop: string;
+ type?: string; // understand why enum does not work
+ formatter?(item: any): string;
+ link?(item: any): string;
+}
+
interface IXosTableCgfOrder {
reverse: boolean;
field: string;
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index 48798df..797685b 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -57,7 +57,7 @@
WebSocket = _WebSocket_;
// ModelRest will call the backend
- httpBackend.expectGET(`${AppConfig.apiEndpoint}/core/tests`)
+ httpBackend.expectGET(`${AppConfig.apiEndpoint}/core/samples`)
.respond(queryData);
}));
@@ -67,7 +67,7 @@
it('the first event should be the resource response', (done) => {
let event = 0;
- service.query('test')
+ service.query('sample')
.subscribe(collection => {
event++;
if (event === 2) {
@@ -83,7 +83,7 @@
describe('when a web-socket event is received for that model', () => {
it('should update the collection', (done) => {
let event = 0;
- service.query('test')
+ service.query('sample')
.subscribe(
collection => {
event++;
@@ -102,7 +102,7 @@
);
window.setTimeout(() => {
WebSocket.next({
- model: 'test',
+ model: 'sample',
msg: {
changed_fields: ['id'],
object: {id: 3, name: 'baz'},
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index d306f9d..e33d7cd 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -33,7 +33,7 @@
}
private loadInitialData(model: string) {
- const endpoint = `/core/${model.toLowerCase()}s/`;
+ const endpoint = `/core/${model.toLowerCase()}s`; // NOTE check is pluralize is better
this.ModelRest.getResource(endpoint).query().$promise
.then(
res => {
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 4663b59..d1e4d0e 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -1,12 +1,13 @@
import {IXosTableCfg} from '../../core/table/table';
import {IModelStoreService} from '../../datasources/stores/model.store';
+import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
export interface IXosCrudData {
model: string;
xosTableCfg: IXosTableCfg;
}
class CrudController {
- static $inject = ['$state', '$scope', 'ModelStore'];
+ static $inject = ['$state', '$scope', 'ModelStore', 'ConfigHelpers'];
public data: IXosCrudData;
public tableCfg: IXosTableCfg;
@@ -16,17 +17,19 @@
constructor(
private $state: angular.ui.IStateService,
private $scope: angular.IScope,
- private store: IModelStoreService
+ private store: IModelStoreService,
+ private ConfigHelpers: IXosConfigHelpersService
) {
this.data = this.$state.current.data;
this.tableCfg = this.data.xosTableCfg;
- this.title = this.data.model;
+ this.title = this.ConfigHelpers.pluralize(this.data.model);
this.store.query(this.data.model)
.subscribe(
(event) => {
// NOTE Observable mess with $digest cycles, we need to schedule the expression later
$scope.$evalAsync(() => {
+ this.title = this.ConfigHelpers.pluralize(this.data.model, event.length);
this.tableData = event;
});
}
diff --git a/src/index.ts b/src/index.ts
index 77955b3..7021f86 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -23,40 +23,12 @@
import * as _ from 'lodash';
import {IXosNavigationService} from './app/core/services/navigation';
import {IXosPageTitleService} from './app/core/services/page-title';
+import {IXosConfigHelpersService} from './app/core/services/helpers/config.helpers';
export interface IXosState extends angular.ui.IState {
data: IXosCrudData;
};
-const modeldefToTableCfg = (fields: {name: string, type: string}[]): any[] => {
- const excluded_fields = [
- 'created',
- 'updated',
- 'enacted',
- 'policed',
- 'backend_register',
- 'deleted',
- 'write_protect',
- 'lazy_blocked',
- 'no_sync',
- 'no_policy',
- 'omf_friendly',
- 'enabled'
- ];
- const cfg = _.map(fields, (f) => {
- if (excluded_fields.indexOf(f.name) > -1) {
- return;
- }
- return {
- label: `${f.name}`, // TODO confert name to label
- prop: f.name
- };
- })
- .filter(v => angular.isDefined(v));
-
- return cfg;
-};
-
angular
.module('app', [xosCore, xosDataSources, xosViews, 'ui.router', 'ngResource'])
.config(routesConfig)
@@ -69,25 +41,27 @@
ModelDefs: IModeldefsService,
RuntimeStates: IRuntimeStatesService,
NavigationService: IXosNavigationService,
+ ConfigHelpers: IXosConfigHelpersService,
PageTitle: IXosPageTitleService
) => {
// Dinamically add a core states
ModelDefs.get()
.then((models: IModeldef[]) => {
+ // TODO move in a separate service and test
_.forEach(models, (m: IModeldef) => {
const state: IXosState = {
parent: 'xos',
- url: `${m.name.toLowerCase()}s`, // TODO use https://github.com/blakeembrey/pluralize
+ url: ConfigHelpers.pluralize(m.name.toLowerCase()),
component: 'xosCrud',
data: {
model: m.name,
xosTableCfg: {
- columns: modeldefToTableCfg(m.fields)
+ columns: ConfigHelpers.modeldefToTableCfg(m.fields)
}
}
};
- RuntimeStates.addState(`${m.name.toLowerCase()}s`, state);
- NavigationService.add({label: `${m.name}s`, url: `${m.name.toLowerCase()}s`});
+ RuntimeStates.addState(ConfigHelpers.pluralize(m.name.toLowerCase()), state);
+ NavigationService.add({label: ConfigHelpers.pluralize(m.name), url: ConfigHelpers.pluralize(m.name.toLowerCase())});
});
});
});
diff --git a/src/interceptors.ts b/src/interceptors.ts
index 4db7b9c..53fd7df 100644
--- a/src/interceptors.ts
+++ b/src/interceptors.ts
@@ -45,6 +45,7 @@
return {
request: (req) => {
if (req.url.indexOf('.html') === -1) {
+ // NOTE this may fail if there are already query params
req.url += '?no_hyperlinks=1';
}
return req;
diff --git a/src/routes.ts b/src/routes.ts
index 35723c5..c6de435 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -7,6 +7,10 @@
$locationProvider.html5Mode(false).hashPrefix('');
$urlRouterProvider.otherwise('/');
+ // TODO onload redirect to correct URL
+ // routes are created asynchronously so by default any time you reload
+ // you end up in /
+
$stateProvider
.state('xos', {
abstract: true,