'
48 | ]
49 | }, {
50 | text: 'Headcount',
51 | dataIndex: 'headcount',
52 | flex: 1,
53 | cell: {
54 | encodeHtml: false
55 | },
56 | tpl: [
57 | '
',
58 | '{headcount:plural("employee")}',
59 | ''
60 | ]
61 | }],
62 |
63 | listeners: {
64 | childdoubletap: 'onChildActivate'
65 | }
66 | }]
67 | });
68 |
--------------------------------------------------------------------------------
/client/app/view/tablet/office/BrowseController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.office.BrowseController', {
2 | extend: 'App.view.office.BrowseController',
3 | alias: 'controller.tablet-officebrowse',
4 |
5 | onCreate: function() {
6 | // The creation form can be accessed either by clicking the "create" button (dialog)
7 | // or via the #office/create url (page) - default config matches the "page" view.
8 | // Note that this dialog will be destroyed on close.
9 | Ext.create({
10 | xtype: 'officecreate',
11 | record: Ext.create('App.model.Office'),
12 | centered: true,
13 | floated: true,
14 | modal: true,
15 | ui: 'dialog',
16 | toolbar: {
17 | docked: 'bottom'
18 | }
19 | }).show();
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/client/app/view/tablet/office/BrowseToolbar.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.office.BrowseToolbar', {
2 | extend: 'App.view.widgets.BrowseToolbar',
3 | // xtype: 'officebrowsetoolbar', -- set by profile
4 |
5 | items: {
6 | countries: {
7 | xtype: 'combobox',
8 | valueField: 'value',
9 | displayField: 'label',
10 | placeholder: 'All Country',
11 | queryMode: 'local',
12 | weight: 10,
13 | bind: {
14 | selection: '{filters.country}',
15 | store: '{countries}'
16 | }
17 | },
18 | create: {
19 | xtype: 'button',
20 | iconCls: 'x-fa fa-plus',
21 | handler: 'onCreate',
22 | text: 'Create',
23 | weight: 50
24 | }
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/client/app/view/tablet/organization/Browse.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.organization.Browse', {
2 | extend: 'App.view.organization.Browse',
3 | // xtype: 'organizationbrowse', -- set by profile
4 |
5 | requires: [
6 | 'Ext.plugin.ListPaging'
7 | ],
8 |
9 | controller: 'tablet-organizationbrowse',
10 |
11 | tbar: {
12 | xtype: 'organizationbrowsetoolbar'
13 | },
14 |
15 | items: [{
16 | xtype: 'grid',
17 | emptyText: 'No organization was found to match your search',
18 | bind: '{organizations}',
19 | ui: 'listing',
20 |
21 | selectable: {
22 | disabled: true
23 | },
24 |
25 | plugins: [{
26 | type: 'listpaging',
27 | autoPaging: true
28 | }],
29 |
30 | columns: [{
31 | text: 'Name',
32 | dataIndex: 'name',
33 | flex: 2,
34 | cell: {
35 | encodeHtml: false
36 | },
37 | tpl: '
{name}'
38 | }, {
39 | text: 'Manager',
40 | dataIndex: 'manager.lastname',
41 | flex: 2,
42 | cell: {
43 | encodeHtml: false
44 | },
45 | tpl: [
46 | '
',
47 | '',
50 | '',
51 | '
{office.name}, ',
52 | '{office.city} ({office.country})',
53 | '
',
54 | ''
55 | ]
56 | }, {
57 | text: 'Headcount',
58 | dataIndex: 'headcount',
59 | flex: 1,
60 | cell: {
61 | encodeHtml: false
62 | },
63 | tpl: [
64 | '
',
65 | '{headcount:plural("employee")}',
66 | ''
67 | ]
68 | }],
69 |
70 | listeners: {
71 | childdoubletap: 'onChildActivate'
72 | }
73 | }]
74 | });
75 |
--------------------------------------------------------------------------------
/client/app/view/tablet/organization/BrowseController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.organization.BrowseController', {
2 | extend: 'App.view.organization.BrowseController',
3 | alias: 'controller.tablet-organizationbrowse',
4 |
5 | onCreate: function() {
6 | // The creation form can be accessed either by clicking the "create" button (dialog)
7 | // or via the #organization/create url (page) - default config matches the "page"
8 | // view. Note that this dialog will be destroyed on close.
9 | Ext.create({
10 | xtype: 'organizationcreate',
11 | record: Ext.create('App.model.Organization'),
12 | centered: true,
13 | floated: true,
14 | modal: true,
15 | ui: 'dialog',
16 | toolbar: {
17 | docked: 'bottom'
18 | }
19 | }).show();
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/client/app/view/tablet/organization/BrowseToolbar.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.organization.BrowseToolbar', {
2 | extend: 'App.view.widgets.BrowseToolbar',
3 | // xtype: 'organizationbrowsetoolbar', -- set by profile
4 |
5 | items: {
6 | managers: {
7 | xtype: 'combobox',
8 | valueField: 'value',
9 | displayField: 'label',
10 | placeholder: 'All Managers',
11 | queryMode: 'local',
12 | weight: 10,
13 | bind: {
14 | selection: '{filters.manager}',
15 | store: '{managers}'
16 | }
17 | },
18 | create: {
19 | xtype: 'button',
20 | iconCls: 'x-fa fa-plus',
21 | handler: 'onCreate',
22 | text: 'Create',
23 | weight: 50
24 | }
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/client/app/view/tablet/person/Browse.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.person.Browse', {
2 | extend: 'App.view.person.Browse',
3 | // xtype: 'personbrowse', -- set by profile
4 |
5 | requires: [
6 | 'Ext.plugin.ListPaging'
7 | ],
8 |
9 | controller: 'tablet-personbrowse',
10 |
11 | tbar: {
12 | xtype: 'personbrowsetoolbar'
13 | },
14 |
15 | items: [{
16 | xtype: 'grid',
17 | emptyText: 'No employee was found to match your search',
18 | bind: '{people}',
19 | ui: 'listing',
20 |
21 | selectable: {
22 | disabled: true
23 | },
24 |
25 | plugins: [{
26 | type: 'listpaging',
27 | autoPaging: true
28 | }],
29 |
30 | columnMenu: {
31 | items: {
32 | groupByThis: false,
33 | showInGroups: false
34 | }
35 | },
36 |
37 | columns: [{
38 | dataIndex: 'picture',
39 | menuDisabled: true,
40 | hideable: false,
41 | sortable: false,
42 | align: 'center',
43 | width: 58,
44 | cell: {
45 | encodeHtml: false
46 | },
47 | tpl: '
'
48 | }, {
49 | text: 'Name / Title',
50 | dataIndex: 'lastname',
51 | flex: 1,
52 | cell: {
53 | encodeHtml: false
54 | },
55 | tpl: [
56 | '
{fullname}',
57 | '
{title}
'
58 | ]
59 | }, {
60 | text: 'Organization',
61 | dataIndex: 'organization.name',
62 | flex: 1,
63 | cell: {
64 | encodeHtml: false
65 | },
66 | tpl: [
67 | '
',
68 | '{name}',
69 | '',
72 | ''
73 | ]
74 | }, {
75 | text: 'Office',
76 | dataIndex: 'office.name',
77 | flex: 1,
78 | cell: {
79 | encodeHtml: false
80 | },
81 | tpl: [
82 | '
',
83 | '{name}',
84 | '{city}, {country}
',
85 | ''
86 | ]
87 | }, {
88 | sortable: false,
89 | dataIndex: 'email',
90 | text: 'Email/Phone',
91 | flex: 1,
92 | cell: {
93 | encodeHtml: false
94 | },
95 | tpl: [
96 | '
{email}
',
97 | '
{phone}
'
98 | ]
99 | }],
100 |
101 | listeners: {
102 | childdoubletap: 'onChildActivate'
103 | }
104 | }]
105 | });
106 |
--------------------------------------------------------------------------------
/client/app/view/tablet/person/BrowseController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.person.BrowseController', {
2 | extend: 'App.view.person.BrowseController',
3 | alias: 'controller.tablet-personbrowse',
4 |
5 | onCreate: function() {
6 | // The creation form can be accessed either by clicking the "create" button (dialog)
7 | // or via the #person/create url (page) - default config matches the "page" view.
8 | // Note that this dialog will be destroyed on close.
9 | Ext.create({
10 | xtype: 'personcreate',
11 | record: Ext.create('App.model.Person'),
12 | centered: true,
13 | floated: true,
14 | modal: true,
15 | ui: 'dialog',
16 | toolbar: {
17 | docked: 'bottom'
18 | }
19 | }).show();
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/client/app/view/tablet/person/BrowseToolbar.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.tablet.person.BrowseToolbar', {
2 | extend: 'App.view.widgets.BrowseToolbar',
3 | // xtype: 'personbrowsetoolbar', -- set by profile
4 |
5 | items: {
6 | organizations: {
7 | xtype: 'combobox',
8 | valueField: 'value',
9 | displayField: 'label',
10 | placeholder: 'All Organizations',
11 | queryMode: 'local',
12 | weight: 11,
13 | bind: {
14 | selection: '{filters.organization}',
15 | store: '{organizations}'
16 | }
17 | },
18 | offices: {
19 | xtype: 'combobox',
20 | valueField: 'value',
21 | displayField: 'label',
22 | placeholder: 'All Offices',
23 | queryMode: 'local',
24 | weight: 10,
25 | bind: {
26 | selection: '{filters.office}',
27 | store: '{offices}'
28 | }
29 | },
30 | create: {
31 | xtype: 'button',
32 | iconCls: 'x-fa fa-plus',
33 | handler: 'onCreate',
34 | text: 'Create',
35 | weight: 50
36 | }
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/client/app/view/viewport/ViewportModel.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.viewport.ViewportModel', {
2 | extend: 'Ext.app.ViewModel',
3 | alias: 'viewmodel.viewport',
4 |
5 | data: {
6 | user: null
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Browse.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.Browse', {
2 | extend: 'Ext.Panel',
3 | xtype: 'browse',
4 |
5 | config: {
6 | route: null,
7 | store: null,
8 | fields: {
9 | search: {
10 | property: '#search',
11 | defaultValue: null
12 | }
13 | }
14 | },
15 |
16 | eventedConfig: {
17 | /**
18 | * Make the config trigger an event on change to allow the controller to monitor it.
19 | * https://www.sencha.com/blog/using-sencha-ext-config/
20 | */
21 | route: null,
22 | store: null
23 | },
24 |
25 | controller: 'browse',
26 | viewModel: {
27 | data: {
28 | filters: null
29 | }
30 | },
31 |
32 | layout: 'fit',
33 |
34 | reset: function() {
35 | this.fireEvent('reset');
36 | return this;
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/client/app/view/widgets/BrowseController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.BrowseController', {
2 | extend: 'Ext.app.ViewController',
3 | alias: 'controller.browse',
4 |
5 | control: {
6 | '#': {
7 | routechange: 'onRouteChange',
8 | storechange: 'onStoreChange'
9 | }
10 | },
11 |
12 | initViewModel: function(vm) {
13 | vm.bind(
14 | { bindTo: '{filters}', deep: true },
15 | Ext.Function.createBuffered(function() {
16 | if (!this.destroyed) {
17 | // The view might have been destroyed (e.g. user deauthentication)
18 | this.updateFilters()
19 | }
20 | }, 500, this, {}));
21 | },
22 |
23 | updateFilters: function(reload) {
24 | var view = this.getView(),
25 | store = view.getStore(),
26 | collection = store && store.getFilters(),
27 | filters = this.getViewModel().get('filters'),
28 | fields = view.getFields(),
29 | dirty = !!reload,
30 | item, value;
31 |
32 | if (!collection) {
33 | return;
34 | }
35 |
36 | Ext.Object.each(fields, function(key, field) {
37 | value = filters[key];
38 | if (value && value.isModel) {
39 | value = value.get('value');
40 | }
41 |
42 | key = field.property || key;
43 | item = collection.get(key);
44 | if ((item && item.getValue()) == value) {
45 | return;
46 | }
47 |
48 | dirty = true;
49 | if (value == null) {
50 | store.removeFilter(key, true);
51 | } else {
52 | store.filter(key, value, true);
53 | }
54 | });
55 |
56 | if (dirty) {
57 | store.removeAll();
58 | store.load();
59 | }
60 | },
61 |
62 | onRouteChange: function(view, route) {
63 | var me = this,
64 | vm = me.getViewModel(),
65 | regex = /([^\/]+)\/([^\/]+)/g,
66 | fields = me.getView().getFields() || {},
67 | filters = {},
68 | field, value;
69 |
70 | Ext.Object.each(fields, function(key, value) {
71 | filters[key] = value.defaultValue || null
72 | });
73 |
74 | while (match = regex.exec(route)) {
75 | field = match[1];
76 | value = match[2];
77 | if (Ext.isDefined(filters[field])) {
78 | filters[field] = field !== 'search'?
79 | Ext.create(App.model.Filter, { value: value }) :
80 | value;
81 | }
82 | }
83 |
84 | vm.set('filters', filters);
85 | me.updateFilters();
86 | },
87 |
88 | onStoreChange: function() {
89 | this.updateFilters(true);
90 | },
91 |
92 | onChildActivate: function(dataview, location) {
93 | var record = location.record;
94 | if (record) {
95 | this.redirectTo(record);
96 | }
97 | },
98 |
99 | onEditAction: function(list, data) {
100 | this.redirectTo(data.record.toEditUrl());
101 | },
102 |
103 | onRefreshTap: function() {
104 | var store = this.getView().getStore();
105 | if (store) {
106 | store.reload();
107 | }
108 | },
109 |
110 | onClearFiltersTap: function() {
111 | this.getViewModel().set('filters', {});
112 | }
113 | });
114 |
--------------------------------------------------------------------------------
/client/app/view/widgets/BrowseToolbar.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.BrowseToolbar', {
2 | extend: 'Ext.Toolbar',
3 | xtype: 'personbrowsetoolbar',
4 |
5 | cls: 'browse-toolbar',
6 | weighted: true,
7 | ui: 'tools',
8 |
9 | defaults: {
10 | ui: 'action'
11 | },
12 |
13 | items: {
14 | search: {
15 | xtype: 'searchfield',
16 | reference: 'search',
17 | placeholder: 'Search',
18 | userCls: 'expandable',
19 | bind: '{filters.search}',
20 | weight: 0
21 | },
22 | refresh: {
23 | iconCls: 'x-fa fa-refresh',
24 | handler: 'onRefreshTap',
25 | tooltip: 'Refresh',
26 | weight: 30
27 | },
28 | clear: {
29 | iconCls: 'x-fa fa-undo',
30 | handler: 'onClearFiltersTap',
31 | tooltip: 'Clear Filters',
32 | weight: 20
33 | }
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/client/app/view/widgets/HistoryPanel.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.HistoryPanel', {
2 | extend: 'Ext.Panel',
3 | xtype: 'historypanel',
4 |
5 | config: {
6 | store: null
7 | },
8 |
9 | cls: 'historypanel',
10 | defaultBindProperty: 'store',
11 | referenceHolder: true,
12 | title: 'Recent Activity',
13 |
14 | header: {
15 | items: {
16 | showall: {
17 | xtype: 'button',
18 | reference: 'showallbutton',
19 | tooltip: 'Show all activity',
20 | handler: 'onHistoryAllTap',
21 | iconCls: 'x-fa fa-history',
22 | ui: 'block'
23 | }
24 | }
25 | },
26 |
27 | items: [{
28 | xtype: 'historyview',
29 | reference: 'historyview',
30 | displayField: 'subject',
31 | emptyText: 'No activity was found',
32 | selectable: {
33 | disabled: true
34 | }
35 | }],
36 |
37 | initialize: function() {
38 | var me = this;
39 | me.callParent(arguments);
40 | me.relayEvents(me.lookup('historyview'), ['childtap']);
41 | },
42 |
43 | applyStore: function(value) {
44 | return value? Ext.getStore(value) : null;
45 | },
46 |
47 | updateStore: function(curr, prev) {
48 | var listeners = {
49 | datachanged: 'updateButtonState',
50 | scope: this
51 | };
52 |
53 | if (prev && prev.isStore) {
54 | prev.un(listeners);
55 | }
56 | if (curr && curr.isStore) {
57 | curr.on(listeners);
58 | }
59 |
60 | this.lookup('historyview').setStore(curr);
61 | this.updateButtonState(curr);
62 | },
63 |
64 | updateButtonState: function(store) {
65 | this.lookup('showallbutton').setDisabled(!store || !store.getCount());
66 | }
67 | });
68 |
--------------------------------------------------------------------------------
/client/app/view/widgets/HistoryView.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.HistoryView', {
2 | extend: 'Ext.dataview.DataView',
3 | xtype: 'historyview',
4 |
5 | config: {
6 | displayField: 'recipient.fullname'
7 | },
8 |
9 | cls: 'historyview',
10 | ui: 'history light',
11 | emptyText: 'No history',
12 | deferEmptyText: false,
13 | minHeight: 80,
14 | inline: true,
15 |
16 |
17 | updateDisplayField: function(value) {
18 | this.setItemTpl([
19 | '
',
20 | '
',
21 | '
',
22 | '
',
23 | '
',
24 | '
',
25 | '
{', value, '}
',
26 | '
{created:date(\'F j, Y\')}
',
27 | '
',
28 | '
'
29 | ]);
30 | },
31 |
32 | itemCls: 'history-item'
33 | });
34 |
--------------------------------------------------------------------------------
/client/app/view/widgets/HistoryView.scss:
--------------------------------------------------------------------------------
1 | $historyitem-badge-size: dynamic(22px);
2 |
3 | @include dataview-ui(
4 | $ui: 'history',
5 | $background-color: $neutral-light-color,
6 | $item-background-color: $neutral-light-color,
7 | $item-padding: 5px 15px,
8 | $item-padding-big: 5px 15px
9 | );
10 |
11 | .history-visual {
12 | white-space: nowrap;
13 |
14 | .action, .picture {
15 | display: inline-block;
16 | position: relative;
17 | vertical-align: middle;
18 | }
19 |
20 | .action {
21 | border-radius: 50%;
22 | font-size: 14px;
23 | height: $historyitem-badge-size;
24 | width: $historyitem-badge-size;
25 | line-height: $historyitem-badge-size;
26 | text-align: center;
27 | z-index: 1;
28 | }
29 |
30 | .picture {
31 | margin-left: -$historyitem-badge-size*0.4;
32 | z-index: 0;
33 | }
34 | }
35 |
36 | .historyview {
37 | .history-item {
38 | cursor: pointer;
39 |
40 | .tablet-profile & {
41 | width: 100%;
42 |
43 | @media screen and (max-width: 600px) {
44 | width: 50%;
45 | }
46 | @media screen and (max-width: 400px) {
47 | width: 100%;
48 | }
49 | }
50 |
51 | .phone-profile.x-portrait & {
52 | width: 100%;
53 | }
54 |
55 | .phone-profile.x-landscape & {
56 | width: 50%;
57 | }
58 | }
59 |
60 | .history-item-wrapper {
61 | align-items: center;
62 | display: flex;
63 | flex-direction: row;
64 | }
65 |
66 | .history-details {
67 | padding: 8px;
68 | line-height: 1.4;
69 | flex: 1;
70 | width: 0;
71 |
72 | > * {
73 | @include ellipsis;
74 | }
75 |
76 | .display {
77 | display: block;
78 | font-size: 15px;
79 | font-weight: 600;
80 | }
81 |
82 | .date {
83 | font-size: 13px;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/client/app/view/widgets/MapView.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.MapView', {
2 | extend: 'Ext.ux.google.Map',
3 | xtype: 'mapview',
4 |
5 | cls: 'mapview',
6 |
7 | markerTemplate: {
8 | title: '{name}',
9 | animation: 'DROP',
10 | position: {
11 | lat: '{location.latitude}',
12 | lng: '{location.longitude}'
13 | }
14 | },
15 |
16 | // https://developers.google.com/maps/documentation/javascript/reference#MapOptions
17 | mapOptions: {
18 | disableDoubleClickZoom: true,
19 | disableDefaultUI: true,
20 | scrollwheel: false,
21 | zoom: 8,
22 |
23 | styles: [{
24 | featureType: "all",
25 | elementType: "all",
26 | stylers: [
27 | { visibility: "simplified" }
28 | ]
29 | }, {
30 | featureType: "administrative",
31 | elementType: "all",
32 | stylers: [
33 | { visibility: "on"},
34 | { lightness: 33 }
35 | ]
36 | }, {
37 | featureType: "landscape",
38 | elementType: "all",
39 | stylers: [
40 | { color: "#eaeaea" }
41 | ]
42 | }, {
43 | featureType: "poi.park",
44 | elementType: "geometry",
45 | stylers: [
46 | { color: "#c5dac6" }
47 | ]
48 | }, {
49 | featureType: "poi.park",
50 | elementType: "labels",
51 | stylers: [
52 | { visibility: "off" }
53 | ]
54 | }, {
55 | featureType: "road",
56 | elementType: "geometry",
57 | stylers: [
58 | { hue: "#bbc0c4" },
59 | { saturation: -93 },
60 | { lightness: 20 }
61 | ]
62 | }, {
63 | featureType: "water",
64 | elementType: "all",
65 | stylers: [
66 | { visibility: "on" },
67 | { color: "#acbcc9" }
68 | ]
69 | }]
70 | },
71 |
72 | updateMarkers: function(current, previous) {
73 | var me = this,
74 | listeners = {
75 | refresh: 'onStoreRefresh',
76 | scope: me
77 | };
78 |
79 | me.callParent(arguments);
80 |
81 | if (previous) {
82 | previous.un(listeners);
83 | }
84 |
85 | if (current) {
86 | current.on(listeners);
87 | me.onStoreRefresh(current);
88 | }
89 | },
90 |
91 | onStoreRefresh: function(store) {
92 | var records = store.getRange();
93 | if (records.length === 1) {
94 | this.setMapCenter(records[0]);
95 | } else {
96 | this.fitMarkersInView(records);
97 | }
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/client/app/view/widgets/MapView.scss:
--------------------------------------------------------------------------------
1 | .mapview {
2 | height: 192px;
3 | }
4 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Show.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.Show', {
2 | extend: 'Ext.Panel',
3 |
4 | controller: {
5 | type: 'wizard'
6 | },
7 |
8 | viewModel: {
9 | data: {
10 | record: null
11 | }
12 | },
13 |
14 | eventedConfig: {
15 | /**
16 | * Make the config trigger an event on change to allow the controller to monitor it.
17 | * https://www.sencha.com/blog/using-sencha-ext-config/
18 | */
19 | record: null
20 | },
21 |
22 | platformConfig: {
23 | phone: {
24 | header: {
25 | items: {
26 | edit: {
27 | xtype: 'button',
28 | iconCls: 'x-fa fa-pencil',
29 | handler: 'onEditTap',
30 | weight: 10
31 | }
32 | }
33 | }
34 | },
35 |
36 | '!phone': {
37 | header: {
38 | hidden: true
39 | }
40 | }
41 | },
42 |
43 | scrollable: {
44 | y: 'scroll'
45 | },
46 |
47 | weighted: true,
48 |
49 | defaults: {
50 | userCls: 'page-constrained'
51 | },
52 |
53 | items: {
54 | header: {
55 | xtype: 'showheader',
56 | weight: -10
57 | },
58 |
59 | content: {
60 | weighted: true,
61 | userCls: [
62 | 'page-constrained',
63 | 'blocks'
64 | ],
65 |
66 | defaults: {
67 | userCls: 'blocks-column',
68 | weighted: true,
69 |
70 | defaults: {
71 | ui: 'block'
72 | }
73 | },
74 |
75 | items: {
76 | left: {
77 | weighted: true
78 | },
79 |
80 | right: {
81 | weighted: true,
82 |
83 | items: {
84 | history: {
85 | xtype: 'historypanel',
86 | bind: '{history}',
87 | ui: 'block',
88 | listeners: {
89 | 'childtap': 'onHistoryChildTap'
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 | });
98 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Show.scss:
--------------------------------------------------------------------------------
1 | $show-header-padding: dynamic(8px);
2 | $show-header-spacing: dynamic(16px);
3 | $show-header-picture-size: dynamic(150px);
4 |
--------------------------------------------------------------------------------
/client/app/view/widgets/ShowController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.ShowController', {
2 | extend: 'Ext.app.ViewController',
3 | alias: 'controller.show',
4 |
5 | control: {
6 | '#': {
7 | recordchange: 'onRecordChange'
8 | }
9 | },
10 |
11 | getRecord: function() {
12 | return this.getViewModel().get('record');
13 | },
14 |
15 | onRecordChange: function(view, record) {
16 | this.getViewModel().set('record', record);
17 |
18 | // Scroll to the top of the view but make sure that the view is still
19 | // valid since the record is reset to null when the view is destroyed.
20 | if (!view.destroying && !view.destroyed) {
21 | view.getScrollable().scrollTo(null, 0, true);
22 | }
23 | },
24 |
25 | onEditTap: function() {
26 | this.redirectTo(this.getRecord().toEditUrl());
27 | },
28 |
29 | onPeopleChildTap: function(view, location) {
30 | var record = location.record;
31 | if (record) {
32 | this.redirectTo(record);
33 | }
34 | },
35 |
36 | onHistoryChildTap: function(view, location) {
37 | var record = location.record;
38 | if (record) {
39 | this.redirectTo(record.getRecipient());
40 | }
41 | },
42 |
43 | onHistoryAllTap: function() {
44 | this.redirectTo('history/recipient/' + this.getRecord().getId());
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/client/app/view/widgets/ShowHeader.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.ShowHeader', {
2 | extend: 'Ext.Container',
3 | xtype: 'showheader',
4 |
5 | cls: 'show-header',
6 | weighted: true,
7 |
8 | layout: {
9 | type: 'hbox',
10 | align: 'end'
11 | },
12 |
13 | items: {
14 | title: {
15 | xtype: 'component',
16 | userCls: 'header-title',
17 | flex: 1,
18 | bind: {
19 | record: '{record}'
20 | }
21 | },
22 |
23 | edit: {
24 | xtype: 'button',
25 | iconCls: 'x-fa fa-pencil',
26 | handler: 'onEditTap',
27 | text: 'Edit',
28 | weight: 10,
29 | ui: 'flat',
30 |
31 | platformConfig: {
32 | phone: {
33 | hidden: true
34 | }
35 | }
36 | }
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/client/app/view/widgets/ShowHeader.scss:
--------------------------------------------------------------------------------
1 | .show-header {
2 | @include single-box-shadow;
3 | min-height: 128px;
4 | position: relative;
5 | z-index: 1;
6 |
7 | > .x-body-el {
8 | padding: 16px 24px;
9 | }
10 |
11 | .header-title {
12 | line-height: 1.4;
13 | padding: 6px;
14 |
15 | .icon {
16 | border-right: 1px solid;
17 | color: $base-color;
18 | float: left;
19 | font-size: 38px;
20 | margin-right: 8px;
21 | padding-right: 8px;
22 | }
23 |
24 | .name,
25 | .desc {
26 | white-space: nowrap;
27 | }
28 |
29 | .name {
30 | color: $base-color;
31 | font-size: 24px;
32 | font-weight: 400;
33 | }
34 |
35 | .desc {
36 | font-size: 16px;
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Sidebar.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.Sidebar', {
2 | extend: 'Ext.Container',
3 | xtype: 'sidebar',
4 |
5 | config: {
6 | expanded: false
7 | },
8 |
9 | classCls: 'sidebar',
10 |
11 | initialize: function() {
12 | var me = this;
13 |
14 | me.callParent();
15 |
16 | me.el.insertFirst({
17 | cls: me.getBaseCls() + '-mask',
18 | tag: 'div'
19 | }).on({
20 | tap: 'onMaskTap',
21 | scope: me
22 | });
23 | },
24 |
25 | updateExpanded: function(value) {
26 | this.toggleCls('expanded', value);
27 | },
28 |
29 | updateMode: function(curr, prev) {
30 | this.replaceCls(prev, curr);
31 | },
32 |
33 | toggleExpanded: function() {
34 | this.setExpanded(!this.getExpanded());
35 | },
36 |
37 | onMaskTap: function(ev) {
38 | this.setExpanded(false);
39 | ev.preventDefault();
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Sidebar.scss:
--------------------------------------------------------------------------------
1 | $sidebar-background-color: dynamic($neutral-dark-color);
2 | $sidebar-color: dynamic($neutral-light-color);
3 | $sidebar-font-size: dynamic(14px);
4 | $sidebar-font-size-big: dynamic(14px);
5 | $sidebar-item-padding: dynamic(16px);
6 | $sidebar-item-padding-big: dynamic(18px);
7 | $sidebar-icon-size: dynamic(21px);
8 | $sidebar-icon-size-big: dynamic(21px);
9 | $sidebar-icon-horizontal-spacing: dynamic($sidebar-item-padding);
10 | $sidebar-icon-horizontal-spacing-big: dynamic($sidebar-item-padding-big);
11 | $sidebar-slide-width: dynamic(75vw);
12 | $sidebar-micro-width: dynamic($sidebar-icon-size + $sidebar-item-padding*2);
13 | $sidebar-micro-expanded-width: dynamic(256px);
14 |
15 | .sidebar {
16 | $sidebar-picture-size: $sidebar-icon-size*1.6;
17 | $sidebar-picture-size-big: $sidebar-icon-size-big*1.8;
18 | $sidebar-picture-spacing: (2*$sidebar-item-padding + $sidebar-icon-size - $sidebar-picture-size)/2;
19 | $sidebar-picture-spacing-big: (2*$sidebar-item-padding-big + $sidebar-icon-size-big - $sidebar-picture-size-big)/2;
20 |
21 | background-color: $sidebar-background-color;
22 | color: $sidebar-color;
23 | overflow: visible;
24 |
25 | > .x-dock {
26 | overflow: visible;
27 | }
28 |
29 | @include button-ui(
30 | $ui: 'large',
31 | $icon-horizontal-spacing: $sidebar-icon-horizontal-spacing,
32 | $icon-horizontal-spacing-big: $sidebar-icon-horizontal-spacing-big
33 | );
34 |
35 | @include button-ui(
36 | $ui: 'picture',
37 | $icon-horizontal-spacing: $sidebar-picture-spacing,
38 | $icon-horizontal-spacing-big: $sidebar-picture-spacing-big,
39 | $icon-size: $sidebar-picture-size,
40 | $icon-size-big: $sidebar-picture-size-big,
41 | $icon-font-size: $sidebar-picture-size,
42 | $icon-font-size-big: $sidebar-picture-size-big,
43 | $padding: $sidebar-picture-spacing,
44 | $padding-big: $sidebar-picture-spacing-big
45 | );
46 | }
47 |
48 | .sidebar-body-el {
49 | z-index: 2;
50 | }
51 |
52 | .sidebar-micro {
53 | width: $sidebar-micro-width;
54 | }
55 |
56 | .sidebar-micro-body-el {
57 | @include transition-property(width);
58 | @include transition-duration(0.25s);
59 | width: $sidebar-micro-width;
60 |
61 | > div {
62 | width: $sidebar-micro-expanded-width;
63 | }
64 |
65 | .expanded > .x-dock > & {
66 | width: $sidebar-micro-expanded-width;
67 | }
68 | }
69 |
70 | .sidebar-slide {
71 | width: 0;
72 | }
73 |
74 | .sidebar-slide-body-el {
75 | @include transition-property(margin-left);
76 | @include transition-duration(0.25s);
77 | margin-top: $sidebar-icon-size-big + $sidebar-item-padding-big * 2;
78 | margin-left: -$sidebar-slide-width;
79 | width: $sidebar-slide-width;
80 |
81 | .expanded > & {
82 | margin-left: 0;
83 | }
84 | }
85 |
86 | .sidebar-mask {
87 | background: black;
88 | display: none;
89 | opacity: .005;
90 | position: fixed;
91 | top: 0;
92 | left: 0;
93 | bottom: 0;
94 | right: 0;
95 | z-index: 1;
96 |
97 | .sidebar.expanded > & {
98 | display: block;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/client/app/view/widgets/Wizard.scss:
--------------------------------------------------------------------------------
1 | .wizard-screen {
2 | align-items: center;
3 |
4 | > .x-body-el {
5 | max-width: $form-max-width;
6 | width: 100%;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/client/app/view/widgets/WizardController.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.view.widgets.WizardController', {
2 | extend: 'Ext.app.ViewController',
3 | alias: 'controller.wizard',
4 |
5 | requires: [
6 | 'Ext.History'
7 | ],
8 |
9 | getItemCount: function(tabs) {
10 | return tabs.getInnerItems().length;
11 | },
12 |
13 | getActiveIndex: function(tabs) {
14 | return tabs.getInnerItems().indexOf(tabs.getActiveItem());
15 | },
16 |
17 | advance: function(increment) {
18 | var me = this,
19 | tabs = me.lookup('tabs'),
20 | index = me.getActiveIndex(tabs),
21 | count = me.getItemCount(tabs),
22 | next = index + increment;
23 |
24 | tabs.setActiveItem(Math.max(0, Math.min(count-1, next)));
25 | },
26 |
27 | resync: function() {
28 | var me = this,
29 | vm = me.getViewModel(),
30 | tabs = me.lookup('tabs'),
31 | prev = me.lookup('prev'),
32 | next = me.lookup('next'),
33 | index = me.getActiveIndex(tabs),
34 | count = me.getItemCount(tabs),
35 | single = count < 2;
36 |
37 | tabs.getTabBar().setHidden(single);
38 | prev.setDisabled(index <= 0).setHidden(single);
39 | next.setDisabled(index == -1 || index >= count-1).setHidden(single);
40 | },
41 |
42 | finalize: function() {
43 | var view = this.getView();
44 | if (view.getFloated()) {
45 | view.close();
46 | } else {
47 | Ext.History.back();
48 | }
49 | },
50 |
51 | onSubmitTap: function() {
52 | var me = this,
53 | form = me.getView(),
54 | record = me.getViewModel().get('record');
55 |
56 | if (!form.validate()) {
57 | return;
58 | }
59 |
60 | if (!record.isDirty()) {
61 | me.finalize();
62 | return;
63 | }
64 |
65 | form.setMasked({ xtype: 'loadmask' });
66 | form.clearErrors();
67 | record.save({
68 | callback: function(result, operation) {
69 | form.setMasked(false);
70 | if (!App.util.Errors.process(operation, form)) {
71 | me.finalize();
72 | }
73 | }
74 | });
75 | },
76 |
77 | onCancelTap: function() {
78 | this.finalize();
79 | },
80 |
81 | onPrevTap: function() {
82 | this.advance(-1);
83 | },
84 |
85 | onNextTap: function() {
86 | this.advance(1);
87 | },
88 |
89 | onScreenAdd: function() {
90 | this.resync();
91 | },
92 |
93 | onScreenRemove: function(tabs) {
94 | if (!tabs.destroying) {
95 | this.resync();
96 | }
97 | },
98 |
99 | onScreenActivate: function(tabs) {
100 | // This event is triggered when the view is being destroyed!
101 | if (!tabs.destroying) {
102 | this.resync();
103 | }
104 | }
105 | });
106 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Coworkee | Ext JS Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/overrides/util/Format.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Exposes new template modifiers (e.g. '{birthday:dateDiff(date, "y")}', etc.)
3 | */
4 | Ext.define('App.overrides.util.Format', {
5 | override: 'Ext.util.Format',
6 |
7 | dateDiff: function(v0, v1, unit) {
8 | var seconds, name, diff;
9 |
10 | if (!unit || unit == 'auto') {
11 | seconds = Math.floor((+v1 - v0)/1000);
12 | unit =
13 | seconds < 1 ? 'ms' : // 1 second
14 | seconds < 60 ? 's' : // 1 minute
15 | seconds < 3600 ? 'mi' : // 60 minutes
16 | seconds < 86400 ? 'h' : // 24 hours
17 | seconds < 604800 ? 'd' : // 7 days
18 | seconds < 2419200 ? 'w' : // 4 weeks
19 | seconds < 31622400 ? 'mo' : // 366 days
20 | 'y';
21 | }
22 |
23 | switch (unit) {
24 | case 'ms': name = 'millisecond'; break;
25 | case 's': name = 'second'; break;
26 | case 'mi': name = 'minute'; break;
27 | case 'h': name = 'hour'; break;
28 | case 'd': name = 'day'; break;
29 | case 'w': name = 'week'; break;
30 | case 'mo': name = 'month'; break;
31 | case 'y': name = 'year'; break;
32 | default:
33 | }
34 |
35 | diff = Ext.Date.diff(v0, v1, unit);
36 | return Ext.util.Format.plural(diff, name);
37 | },
38 |
39 | actionIconCls: function(type) {
40 | switch (type) {
41 | case 'profile': type = 'user'; break;
42 | case 'email': type = 'envelope'; break;
43 | default:
44 | }
45 |
46 | return 'x-fa fa-' + type;
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coworkee",
3 | "product": "ext",
4 | "version": "0.0.1",
5 | "description": "Coworkee Demo Application",
6 | "scripts": {
7 | "clean": "rimraf build",
8 | "start": "webpack-dev-server --env.environment=development",
9 | "production": "npm run clean && webpack --env.environment=production --env.treeshake=true"
10 | },
11 | "extbuild": {
12 | "defaultenvironment": "development",
13 | "defaultverbose": "no"
14 | },
15 | "dependencies": {
16 | "@sencha/ext-modern": "^7.0.0",
17 | "@sencha/ext-google": "^7.0.0",
18 | "@sencha/ext-modern-theme-material": "^7.0.0",
19 | "@sencha/ext-modern-theme-triton": "^7.0.0",
20 | "@sencha/ext": "^7.0.0",
21 | "escape-string-regexp": "^1.0.5",
22 | "express": "^4.14.0",
23 | "minimist": "^1.2.0",
24 | "sw-precache": "^4.1.0"
25 | },
26 | "devDependencies": {
27 | "gulp": "^3.9.1",
28 | "@sencha/ext-webpack-plugin": "~7.0.0",
29 | "command-line-args": "^5.0.2",
30 | "cross-env": "^5.2.0",
31 | "portfinder": "^1.0.18",
32 | "react": "16.6.3",
33 | "react-hot-loader": "4.3.12",
34 | "html-webpack-plugin": "^3.2.0",
35 | "webpack": "^4.21.0",
36 | "webpack-cli": "^3.1.2",
37 | "webpack-dev-server": "^3.1.9"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": ""
42 | },
43 | "keywords": [
44 | "Ext JS",
45 | "Sencha",
46 | "HTML5"
47 | ],
48 | "author": "Sencha, Inc.",
49 | "license": "ISC",
50 | "bugs": {
51 | "url": "https://github.com/"
52 | },
53 | "homepage": "http://www.sencha.com"
54 | }
55 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/overrides/LoadMask.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.overrides.LoadMask', {
2 | override: 'Ext.LoadMask',
3 |
4 | config: {
5 | message: ''
6 | }
7 | });
8 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/overrides/dataview/listswiper/ListSwiper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Since all list swiping gestures of the app should provide the same user experiences, let's
3 | * define some common config in this override (e.g. "stepper" behavior, direction lock, etc.)
4 | */
5 | Ext.define('App.override.dataview.listswiper.ListSwiper', {
6 | override: 'Ext.dataview.listswiper.ListSwiper',
7 |
8 | config: {
9 | directionLock: false,
10 |
11 | widget: {
12 | xtype: 'listswiperstepper',
13 | undo: {
14 | iconCls: 'x-fa fa-undo'
15 | }
16 | }
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/overrides/field/Field.js:
--------------------------------------------------------------------------------
1 | Ext.define('App.override.field.Field', {
2 | override: 'Ext.field.Field',
3 |
4 | config: {
5 | requiredMessage: 'This field is required',
6 |
7 | labelTextAlign: 'right',
8 |
9 | errorTip: {
10 | anchor: true,
11 | align: 'l-r?',
12 | ui: 'tooltip invalid'
13 | }
14 | },
15 |
16 | platformConfig: {
17 | phone: {
18 | errorTarget: 'under'
19 | }
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/overrides/init.js:
--------------------------------------------------------------------------------
1 | Ext.namespace('Ext.theme.is')['coworkee'] = true;
2 | Ext.theme.name = 'coworkee';
3 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/etc/all.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is is imported by virtue of "sass.etc" in "app.json".
3 | */
4 | @function contrasted($color, $percent: 50%) {
5 | @if $percent < 0 {
6 | $dark: lighten($color, -$percent);
7 | $light: darken($color, -$percent);
8 | } @else {
9 | $dark: darken($color, $percent);
10 | $light: lighten($color, $percent);
11 | }
12 |
13 | @return if(brightness($color) > 50, $dark, $light);
14 | }
15 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/Button.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains Ext.Button visual themes (ui)
3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Button.html#sass-mixins
4 | */
5 |
6 | .x-button .x-text-el {
7 | .title {
8 | text-transform: uppercase;
9 | }
10 | .value {
11 | opacity: 0.5;
12 | font-size: 13px;
13 | }
14 | }
15 |
16 | @include button-ui(
17 | $ui: 'block',
18 | $border-radius: 0,
19 | $border-width: 0,
20 | $icon-size: 28px,
21 | $icon-size-big: 28px,
22 | $icon-font-size: 20px,
23 | $icon-font-size-big: 20px,
24 | $font-size: 18px,
25 | $font-size-big: 18px,
26 | $font-weight: bold,
27 | $line-height: 1.4,
28 | $line-height-big: 1.4,
29 | $icon-only-padding: 16px,
30 | $icon-only-padding-big: 16px,
31 | $padding: 16px,
32 | $padding-big: 16px
33 | );
34 |
35 | @include button-ui(
36 | $ui: 'action',
37 | $border-radius: 0,
38 | $border-width: 0
39 | );
40 |
41 | @include button-ui(
42 | $ui: 'large',
43 | $font-size: 14px,
44 | $font-size-big: 14px,
45 | $icon-size: 21px,
46 | $icon-size-big: 21px,
47 | $icon-font-size: 21px,
48 | $icon-font-size-big: 21px,
49 | $icon-only-padding: 16px,
50 | $icon-only-padding-big: 18px,
51 | $padding: 16px,
52 | $padding-big: 18px
53 | );
54 |
55 | @include button-ui(
56 | $ui: 'flat',
57 | $background-color: rgba(white, 0),
58 | $color: $neutral-medium-dark-color,
59 | $pressed-color: $base-color
60 | );
61 |
62 | @include button-ui(
63 | $ui: 'dark',
64 | $color: $neutral-light-color,
65 | $pressed-color: $neutral-light-color,
66 | $background-color: $neutral-dark-color,
67 | $pressed-background-color: $base-color
68 | );
69 |
70 | @include button-ui(
71 | $ui: 'segmented',
72 | $border-color: transparent,
73 | $border-style: solid,
74 | $border-width: 0 0 2px 0,
75 | $hovered-border-color: $neutral-light-color,
76 | $pressed-border-color: $base-color,
77 | $icon-only-padding: $tab-padding,
78 | $icon-only-padding-big: $tab-padding-big,
79 | $padding: $tab-padding,
80 | $padding-big: $tab-padding-big
81 | );
82 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/Mask.scss:
--------------------------------------------------------------------------------
1 | .x-mask {
2 | &.x-has-message {
3 | .x-loading-spinner-outer {
4 | height: auto;
5 | }
6 | }
7 |
8 | &.x-loading-mask {
9 | .x-mask-inner {
10 | background-color: transparent;
11 | }
12 | }
13 | }
14 |
15 | @include st-loading-spinner(26px, $loading-spinner-color, 4px, 2px);
16 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/Panel.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains Ext.Panel/Header/Title visual themes (ui)
3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Panel.html#sass-mixins
4 | */
5 |
6 | @include panel-ui(
7 | $ui: 'block',
8 | $body-background-color: $neutral-light-color,
9 | $header-background-color: $neutral-light-color,
10 | $header-color: $neutral-medium-dark-color,
11 | $header-icon-size: 34px,
12 | $header-icon-size-big: 38px,
13 | $header-icon-font-size: 18px,
14 | $header-icon-font-size-big: 20px,
15 | $header-font-size: 18px,
16 | $header-font-size-big: 20px,
17 | $header-line-height: 1.2,
18 | $header-line-height-big: 1.2
19 | );
20 |
21 | .x-paneltitle-block {
22 | .x-icon-el {
23 | background-color: $base-color;
24 | color: contrasted($base-light-color, -25%);
25 | border-radius: 50%;
26 | }
27 | }
28 |
29 | @include panel-ui(
30 | $ui: 'dialog',
31 | $body-background-color: $background-color,
32 | $header-background-color: $background-color,
33 | $header-border-width: 0,
34 | $header-icon-size: 24px,
35 | $header-icon-size-big: 24px,
36 | $header-font-size: 18px,
37 | $header-font-size-big: 18px,
38 | $header-line-height: 2,
39 | $header-line-height-big: 2,
40 | $header-title-padding: 16px 20px,
41 | $header-title-padding-big: 20px 24px
42 | );
43 |
44 | @include panel-ui(
45 | $ui: 'invalid',
46 | $anchor-margin: 0,
47 | $anchor-height: 8px,
48 | $anchor-width: 12px,
49 | $border-color: $invalid-base-color,
50 | $border-width: 1px,
51 | $border-style: none,
52 | $body-background-color: $invalid-base-color,
53 | $body-border-style: none,
54 | $body-border-width: 0,
55 | $body-color: $invalid-text-color
56 | );
57 |
58 | @include panel-ui(
59 | $ui: 'flat',
60 | $body-background-color: transparent,
61 | $header-background-color: transparent
62 | );
63 |
64 | @include panel-ui(
65 | $ui: 'dark',
66 | $header-background-color: $neutral-dark-color,
67 | $header-color: $neutral-light-color
68 | );
69 |
70 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/Toolbar.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains Ext.Toolbar visual themes (ui)
3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Toolbar.html#sass-mixins
4 | */
5 |
6 | @include toolbar-ui(
7 | $ui: 'tools',
8 | $background-color: $neutral-light-color,
9 | $box-shadow: 0 4px 2px -3px rgba(black, 0.2) inset
10 | );
11 |
12 | @include toolbar-ui(
13 | $ui: 'flat',
14 | $background-color: transparent
15 | );
16 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/dataview/DataView.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains Ext.dataview.DataView visual themes (ui)
3 | * http://docs.sencha.com/extjs/latest/modern/Ext.dataview.DataView.html#sass-mixins
4 | */
5 |
6 | @include dataview-ui(
7 | $ui: 'light',
8 | $item-color: $neutral-dark-color,
9 | $item-hovered-color: contrasted($neutral-dark-color, -5%),
10 | $item-selected-color: contrasted($neutral-dark-color, -20%),
11 | $item-pressed-color: contrasted($neutral-dark-color, -10%),
12 | $item-background-color: $neutral-light-color,
13 | $item-alt-background-color: contrasted($neutral-light-color, -5%),
14 | $item-hovered-background-color: contrasted($neutral-light-color, 5%),
15 | $item-selected-background-color: $base-color,
16 | $item-pressed-background-color: contrasted($neutral-light-color, 10%)
17 | );
18 |
19 | @include dataview-ui(
20 | $ui: 'dark',
21 | $background-color: $neutral-dark-color,
22 | $item-color: $neutral-light-color,
23 | $item-hovered-color: contrasted($neutral-light-color, -5%),
24 | $item-selected-color: contrasted($neutral-light-color, -20%),
25 | $item-pressed-color: contrasted($neutral-light-color, -10%),
26 | $item-background-color: $neutral-dark-color,
27 | $item-alt-background-color: contrasted($neutral-dark-color, -5%),
28 | $item-hovered-background-color: contrasted($neutral-dark-color, 5%),
29 | $item-selected-background-color: $base-color,
30 | $item-pressed-background-color: contrasted($neutral-dark-color, 15%)
31 | );
32 |
33 | @include dataview-ui(
34 | $ui: 'large',
35 | $item-font-weight: 400,
36 | $item-font-size: 14px,
37 | $item-font-size-big: 14px,
38 | $item-padding: 16px,
39 | $item-padding-big: 18px
40 | );
41 |
42 | @include dataview-ui(
43 | $ui: 'thumbnails',
44 | $background-color: $neutral-light-color
45 | );
46 |
47 | .x-dataview-thumbnails {
48 | .x-dataview-item {
49 | .thumbnail {
50 | @include background-size('cover');
51 | cursor: pointer;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/dataview/ListItem.scss:
--------------------------------------------------------------------------------
1 | .x-listitem-listing {
2 | .item-caption,
3 | .item-title,
4 | .item-stats,
5 | .item-info {
6 | @include ellipsis;
7 |
8 | > .x-fa {
9 | @include opacity(0.75);
10 | font-size: 14px;
11 | }
12 | }
13 |
14 | .item-title {
15 | font-weight: bold;
16 | }
17 |
18 | .item-caption {
19 | font-size: 12px;
20 | }
21 |
22 | .item-stats {
23 | font-size: 11px;
24 | }
25 |
26 | .item-details {
27 | padding: 0 8px;
28 | }
29 |
30 | .x-big & {
31 | .item-title {
32 | font-size: 14px;
33 | }
34 | .item-caption {
35 | font-size: 13px;
36 | }
37 | .item-stats {
38 | font-size: 12px;
39 | }
40 | }
41 | }
42 |
43 | .x-listitem-listing-inner-el {
44 | width: 100%;
45 |
46 | > .x-innerhtml {
47 | display: flex;
48 | flex: 1;
49 | flex-direction: row;
50 | align-items: center;
51 | }
52 |
53 | .item-details {
54 | flex: 1 1 0px;
55 | width: 0;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/dataview/listswiper/Stepper.scss:
--------------------------------------------------------------------------------
1 | .x-listswiperstepper {
2 | .x-text {
3 | line-height: 1.4;
4 | }
5 |
6 | .subject {
7 | @include transition-property(height);
8 | @include transition-duration(0.25s);
9 | @include ellipsis;
10 |
11 | display: block;
12 | font-size: 12px;
13 | height: 0;
14 | }
15 |
16 | &.x-active {
17 | .subject {
18 | height: 18px;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/field/Search.scss:
--------------------------------------------------------------------------------
1 | .x-searchfield {
2 | background-color: $textfield-input-background-color;
3 |
4 | &.expandable {
5 | .x-input-el {
6 | padding-left: 0;
7 | }
8 |
9 | .x-body-wrap-el {
10 | @include transition-property(width);
11 | @include transition-duration(0.5s);
12 | width: 180px;
13 | }
14 |
15 | &.x-empty:not(.x-focused) {
16 | .x-body-wrap-el {
17 | width: 92px;
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/grid/column/Column.scss:
--------------------------------------------------------------------------------
1 | .x-gridcolumn {
2 | .x-title-el {
3 | @include single-text-shadow();
4 | cursor: pointer;
5 | }
6 |
7 | .x-trigger-el {
8 | cursor: pointer;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/packages/local/coworkee/sass/src/tab/Panel.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains Ext.tab.Panel visual themes (ui)
3 | * http://docs.sencha.com/extjs/latest/modern/Ext.tab.Bar.html#sass-mixins
4 | * http://docs.sencha.com/extjs/latest/modern/Ext.tab.Tab.html#sass-mixins
5 | */
6 |
7 | @include tabbar-ui(
8 | $ui: 'flat',
9 | $background-color: transparent,
10 | $border-color: mix($neutral-light-color, $neutral-highlight-color, 50%),
11 | $border-style: solid,
12 | $border-width: 0 0 1px 0,
13 | $horizontal-spacing: 0
14 | );
15 |
16 | @include tab-ui(
17 | $ui: 'flat',
18 | $active-background-color: transparent,
19 | $active-border-color: $base-color,
20 | $background-color: transparent,
21 | $border-color: transparent,
22 | $border-style: solid,
23 | $border-width: 0 0 3px 0,
24 | $border-radius: 0,
25 | $border-radius-big: 0,
26 | $color: $neutral-medium-dark-color,
27 | $hovered-border-color: $neutral-light-color,
28 | $focused-border-color: $neutral-highlight-color,
29 | $focused-color: $neutral-dark-color,
30 | $focused-outline-style: none
31 | );
32 |
--------------------------------------------------------------------------------
/client/resources/images/auth-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/client/resources/images/auth-background.jpg
--------------------------------------------------------------------------------
/client/resources/images/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/client/resources/images/loading.png
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const ExtWebpackPlugin = require('@sencha/ext-webpack-plugin');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const webpack = require('webpack');
5 | const portfinder = require('portfinder');
6 |
7 | module.exports = async function (env) {
8 | var browserprofile
9 | var watchprofile
10 | var buildenvironment = env.environment || process.env.npm_package_extbuild_defaultenvironment
11 | if (buildenvironment == 'production') {
12 | browserprofile = false
13 | watchprofile = 'no'
14 | }
15 | else {
16 | if (env.browser == undefined) {env.browser = 'yes'}
17 | browserprofile = env.browser || 'yes'
18 | watchprofile = env.watch || 'yes'
19 | }
20 | const isProd = buildenvironment === 'production'
21 | var buildprofile = env.profile || process.env.npm_package_extbuild_defaultprofile
22 | var buildenvironment = env.environment || process.env.npm_package_extbuild_defaultenvironment
23 | var buildverbose = env.verbose || process.env.npm_package_extbuild_defaultverbose
24 | if (buildprofile == 'all') { buildprofile = '' }
25 | if (env.treeshake == undefined) {env.treeshake = 'no'}
26 | var treeshake = env.treeshake ? env.treeshake : 'no'
27 | var basehref = env.basehref || '/'
28 | var mode = isProd ? 'production': 'development'
29 |
30 | portfinder.basePort = (env && env.port) || 1962;
31 | return portfinder.getPortPromise().then(port => {
32 | const nodeEnv = env && env.prod ? 'production' : 'development'
33 | const isProd = nodeEnv === 'production'
34 | const plugins = [
35 | new HtmlWebpackPlugin({
36 | template: 'index.html',
37 | hash: true,
38 | inject: "body"
39 | }),
40 | new ExtWebpackPlugin({
41 | framework: 'extjs',
42 | port: port,
43 | emit: 'yes',
44 | browser: 'no',
45 | treeshake: treeshake,
46 | watch: watchprofile,
47 | profile: buildprofile,
48 | environment: buildenvironment,
49 | verbose: buildverbose
50 | })
51 | ]
52 | return {
53 | performance: { hints: false },
54 | mode: mode,
55 | devtool: (mode === 'development') ? 'inline-source-map' : false,
56 | context: path.join(__dirname, './'),
57 | entry: {
58 | main: "./app.js"
59 | },
60 | output: {
61 | path: path.resolve(__dirname, './'),
62 | filename: '[name].js'
63 | },
64 | module: {
65 | rules: [
66 | {
67 | test: /.js$/,
68 | exclude: /node_modules/
69 | }
70 | ]
71 | },
72 | plugins: plugins,
73 | devServer: {
74 | contentBase: './',
75 | historyApiFallback: true,
76 | host: '0.0.0.0',
77 | hot: false,
78 | port,
79 | disableHostCheck: false,
80 | compress: isProd,
81 | inline: !isProd,
82 | stats: {
83 | entrypoints: false,
84 | assets: false,
85 | children: false,
86 | chunks: false,
87 | hash: false,
88 | modules: false,
89 | publicPath: false,
90 | timings: false,
91 | version: false,
92 | warnings: false,
93 | colors: {
94 | green: '[32m'
95 | }
96 | }
97 | }
98 | }
99 | });
100 | }
101 |
--------------------------------------------------------------------------------
/client/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [],
3 | "frameworks": {
4 | "ext": "node_modules/@sencha/ext"
5 | },
6 | "build": {
7 | "dir": "${workspace.dir}/build"
8 | },
9 | "packages": {
10 | "dir": "${workspace.dir}/packages/local,${workspace.dir}/packages,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name},${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-treegrid,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-base,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-ios,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-material,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-aria,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neutral,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-classic,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-gray,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-crisp,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-crisp-touch,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neptune,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neptune-touch,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-triton,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-graphite,${workspace.dir}/node_modules/@sencha/ext-calendar,${workspace.dir}/node_modules/@sencha/ext-charts,${workspace.dir}/node_modules/@sencha/ext-d3,${workspace.dir}/node_modules/@sencha/ext-exporter,${workspace.dir}/node_modules/@sencha/ext-pivot,${workspace.dir}/node_modules/@sencha/ext-pivot-d3,${workspace.dir}/node_modules/@sencha/ext-ux,${workspace.dir}/node_modules/@sencha/ext-google",
11 | "extract": "${workspace.dir}/packages/remote"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/server/api/actions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var helpers = require('../utils/helpers.js');
4 | var session = require('../utils/session.js');
5 | var errors = require('../utils/errors.js');
6 | var models = require('../models');
7 |
8 | var Service = {
9 | list: function(params, callback, sid, req) {
10 | session.verify(req).then(function(session) {
11 | return models.Action.scope('nested').findAndCount(
12 | helpers.sequelizify(params, models.Action, {
13 | where: { person_id: session.user.get('id') }
14 | }));
15 | }).then(function(results) {
16 | callback(null, {
17 | total: results.count,
18 | data: results.rows
19 | });
20 | }).catch(function(err) {
21 | callback(err);
22 | });
23 | },
24 |
25 | insert: function(params, callback, sid, req) {
26 | session.verify(req).then(function(session) {
27 | return models.Person.lookup(params.recipient_id).then(function(person) {
28 | var subject = models.Action.subject(params.type, person);
29 | if (subject === null) {
30 | throw errors.types.invalidParams({
31 | path: 'type', message: 'Invalid action type'
32 | });
33 | }
34 |
35 | return session.user.createAction({
36 | recipient_id: params.recipient_id,
37 | type: params.type,
38 | subject: subject
39 | });
40 | });
41 | }).then(function(row) {
42 | callback(null, {
43 | data: row
44 | });
45 | }).catch(function(err) {
46 | callback(err);
47 | });
48 | },
49 |
50 | update: function(params, callback, sid, req) {
51 | session.verify(req).then(function() {
52 | // NOTE(SB): the direct proxy requires methods for all CRUD actions
53 | throw errors.types.notImplemented();
54 | }).catch(function(err) {
55 | callback(err);
56 | });
57 | },
58 |
59 | remove: function(params, callback, sid, req) {
60 | session.verify(req).then(function() {
61 | var ids = helpers.idsFromParams(params);
62 | if (ids.length === 0) {
63 | throw errors.types.invalidParams({
64 | path: 'id', message: 'Missing required parameter: id',
65 | });
66 | }
67 |
68 | return models.Action.destroy({
69 | where: {
70 | id: { $in: ids }
71 | }
72 | });
73 | }).then(function() {
74 | callback(null);
75 | }).catch(function(err) {
76 | callback(err);
77 | });
78 | },
79 |
80 | filters: function(params, callback, sid, req) {
81 | session.verify(req).then(function(session) {
82 | return helpers.fetchFilters(params, models.Action, {
83 | where: { person_id: session.user.get('id') }
84 | });
85 | }).then(function(results) {
86 | callback(null, {
87 | data: results
88 | });
89 | }).catch(function(err) {
90 | callback(err);
91 | });
92 | }
93 | }
94 |
95 | module.exports = Service;
96 |
--------------------------------------------------------------------------------
/server/api/auth.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var session = require('../utils/session.js');
4 | var errors = require('../utils/errors.js');
5 |
6 | var Service = {
7 | /**
8 | * @param {String} params.username The user username or email.
9 | * @param {String} params.password The user (hashed) password.
10 | */
11 | login: function(params, callback, sid, req, res) {
12 | session.initiate(params.username, params.password, res).then(function(data) {
13 | callback(null, data);
14 | }).catch(function(err) {
15 | callback(err);
16 | });
17 | },
18 |
19 | logout: function(params, callback, sid, req) {
20 | session.verify(req).then(function() {
21 | callback(null, true);
22 | }).catch(function(err) {
23 | callback(err);
24 | });
25 | },
26 |
27 | /**
28 | * Returns the currently authenticated user.
29 | */
30 | user: function(params, callback, sid, req) {
31 | session.verify(req).then(function(session) {
32 | callback(null, session.user);
33 | }).catch(function(err) {
34 | callback(err);
35 | });
36 | }
37 | };
38 |
39 | module.exports = Service;
40 |
--------------------------------------------------------------------------------
/server/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('server:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/server/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "cors": {
3 | "enabled": false
4 | },
5 |
6 | "cron": {
7 | "reset": "0 * * * *"
8 | },
9 |
10 | "direct": {
11 | "rootNamespace": "Server",
12 | "apiName": "API",
13 | "apiUrl": "/api",
14 | "classRouteUrl": "/api",
15 | "classPath": "api",
16 | "server": "localhost",
17 | "port": "3000",
18 | "protocol": "http",
19 | "timeout": 30000,
20 | "cacheAPI": false,
21 | "relativeUrl": true,
22 | "appendRequestResponseObjects": true,
23 | "enableProcessors": false,
24 | "enableMetadata": true,
25 | "responseHelper": true
26 | },
27 |
28 | "database": {
29 | "dialect": "sqlite",
30 | "storage": ".//data.db",
31 | "logging": false,
32 | "define": {
33 | "createdAt": "created",
34 | "updatedAt": "updated",
35 | "deletedAt": "deleted",
36 | "underscored": true
37 | }
38 | },
39 |
40 | "session": {
41 | "secret": "62P59nE68F38q0q2wvHho58oR38aY7U9",
42 | "duration": 86400,
43 | "readonly": false
44 | },
45 |
46 | "client": {
47 | "path": "../client"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/server/data/Offices.json:
--------------------------------------------------------------------------------
1 | [{"id":"2725949a-a1a5-45f8-ab29-4605629f9b49","name":"Fairfield","address":"200 Manufacturers Plaza","postcode":null,"city":"Energodar","region":null,"country":"Ukraine","location":{"latitude":47.49865,"longitude":34.6574}},
2 | {"id":"74190840-82d6-4c0c-9e3e-715c389fc66d","name":"Lukken","address":"66855 Bashford Lane","postcode":null,"city":"Novogireyevo","region":null,"country":"Russia","location":{"latitude":55.75378,"longitude":37.81885}},
3 | {"id":"73954bfa-4c30-4c3f-8ac7-cc1e4fd99266","name":"Pepper Wood","address":"98510 Rutledge Court","postcode":null,"city":"Pamiątkowo","region":null,"country":"Poland","location":{"latitude":52.55334,"longitude":16.68094}},
4 | {"id":"38905470-accb-4905-8091-8418f6ea8ed9","name":"Dryden","address":"26241 Mosinee Terrace","postcode":"87090 CEDEX 9","city":"Limoges","region":"Limousin","country":"France","location":{"latitude":45.8315,"longitude":1.2578}},
5 | {"id":"96edb1c4-04fe-463b-b8d0-346226a7b60e","name":"Welch","address":"3 Corscot Drive","postcode":null,"city":"Marystown","region":"Newfoundland and Labrador","country":"Canada","location":{"latitude":47.16663,"longitude":-55.14829}},
6 | {"id":"f9a3451b-39ce-4d23-84c6-2dce8e18e84b","name":"Northfield","address":"39149 Carberry Avenue","postcode":null,"city":"Achanizo","region":null,"country":"Peru","location":{"latitude":-15.80611,"longitude":-73.96694}},
7 | {"id":"fcab7f8d-0d89-43b0-a11a-12415a4bb9c1","name":"Dottie","address":"714 Armistice Alley","postcode":null,"city":"El Mida","region":null,"country":"Tunisia","location":{"latitude":36.72556,"longitude":10.85528}},
8 | {"id":"dbcc6a21-5563-4792-95c3-bfdc446bc17f","name":"Little Fleur","address":"6739 Veith Junction","postcode":null,"city":"Sidomukti","region":null,"country":"Indonesia","location":{"latitude":-8.2051,"longitude":113.8279}},
9 | {"id":"716f14d9-268b-4c3b-8d45-37b22971179e","name":"Prairieview","address":"5 3rd Court","postcode":"0162","city":"Oslo","region":"Oslo","country":"Norway","location":{"latitude":59.9127,"longitude":10.7461}},
10 | {"id":"1f5e2af7-d959-4ff1-8a97-b88bbd51a40f","name":"Forest Dale","address":"4057 Miller Road","postcode":"85732","city":"Tucson","region":"Arizona","country":"United States","location":{"latitude":32.0848,"longitude":-110.7122}},
11 | {"id": "3f06f7a7-dc80-4987-81dd-9cd807008ee5","name":"Manitowish","address":"9 Grim Center","postcode":"987-2224","city":"Yokosuka","region":null,"country":"Japan","location":{"latitude":35.28361,"longitude":139.66722}}]
12 |
--------------------------------------------------------------------------------
/server/data/Organizations.json:
--------------------------------------------------------------------------------
1 | [{"id":"9f2cea7a-2147-4a3f-aebf-f0956591c6e8","name":"Services"},
2 | {"id":"026d3aba-4193-442d-97fb-3da46c95bc5d","name":"Marketing"},
3 | {"id":"3b23e955-dea8-4fd9-858b-0515a690ad6c","name":"Human Resources"},
4 | {"id":"469eda61-b88e-4dcb-99cd-aab5e45c1fe8","name":"Financial"},
5 | {"id":"26c446ac-21ea-4fd5-954b-2cc6c71f539f","name":"Sales"},
6 | {"id":"c145efd4-1127-41dd-95e0-c3d5b96207fd","name":"Operational"},
7 | {"id":"f467e5f1-fb74-40bd-9499-83fcad1b51b3","name":"Customer Service"},
8 | {"id":"4e10c31c-dbb5-48e7-9f05-fc9b04cd9c7c","name":"Management"},
9 | {"id":"f8379d72-0a57-4c25-86c8-e9761e4b8086","name":"Engineering"},
10 | {"id":"82dc9f3c-35b4-4813-9e82-98da195b0500","name":"Strategy"}]
11 |
--------------------------------------------------------------------------------
/server/models/action.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function (sequelize, DataTypes) {
4 | var Model = sequelize.define("Action", {
5 | id: {
6 | type: DataTypes.UUID,
7 | defaultValue: DataTypes.UUIDV4,
8 | allowNull: false,
9 | primaryKey: true,
10 | validate: {
11 | isUUID: 4
12 | }
13 | },
14 | type: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | searchable: true,
18 | validate: {
19 | notEmpty: true
20 | }
21 | },
22 | subject: {
23 | type: DataTypes.STRING,
24 | searchable: true,
25 | validate: {
26 | notEmpty: true
27 | }
28 | }
29 | });
30 |
31 | Model.associate = function (models) {
32 | Model.belongsTo(models.Person, { as: 'recipient', constraints: false });
33 | Model.belongsTo(models.Person, {as: 'actions' });
34 | Model.addScope('nested', {
35 | include: [{
36 | model: models.Person,
37 | as: 'recipient',
38 | include: [{
39 | model: models.Office,
40 | as: 'office'
41 | }, {
42 | model: models.Organization,
43 | as: 'organization'
44 | }]
45 | }]
46 | });
47 | };
48 |
49 | Model.subject = function (action, recipient) {
50 | switch (action) {
51 | case 'phone':
52 | var extension = recipient.get('extension');
53 | return recipient.get('phone') + (extension ? ':' + extension : '');
54 | case 'profile':
55 | return recipient.get('username');
56 | case 'email':
57 | case 'linkedin':
58 | case 'skype':
59 | return recipient.get(action);
60 | default:
61 | return null;
62 | }
63 | };
64 |
65 | return Model;
66 | };
67 |
--------------------------------------------------------------------------------
/server/models/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file collects all the models from the models directory and associates them if needed.
3 | */
4 |
5 | "use strict";
6 |
7 | var fs = require("fs");
8 | var path = require("path");
9 | var Sequelize = require("sequelize");
10 | var env = process.env.NODE_ENV || "development";
11 | var config = require(path.join(__dirname, '..', 'utils', 'config')).database;
12 | var sequelize = new Sequelize(config.database, config.username, config.password, config);
13 | var db = {};
14 |
15 | fs.readdirSync(__dirname)
16 | .filter(function(file) {
17 | return (file.indexOf(".") !== 0) && (file !== "index.js");
18 | })
19 | .forEach(function(file) {
20 | var model = sequelize.import(path.join(__dirname, file));
21 | db[model.name] = model;
22 | });
23 |
24 | Object.keys(db).forEach(function(modelName) {
25 | if ("associate" in db[modelName]) {
26 | db[modelName].associate(db);
27 | }
28 | });
29 |
30 | db.sequelize = sequelize;
31 | db.Sequelize = Sequelize;
32 |
33 | module.exports = db;
34 |
--------------------------------------------------------------------------------
/server/models/office.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function(sequelize, DataTypes) {
4 | var Model = sequelize.define("Office", {
5 | id: {
6 | type: DataTypes.UUID,
7 | defaultValue: DataTypes.UUIDV4,
8 | allowNull: false,
9 | primaryKey: true,
10 | validate: {
11 | isUUID: 4
12 | }
13 | },
14 | name: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | searchable: true,
18 | unique: {
19 | msg: 'An office with this name already exists.'
20 | },
21 | validate: {
22 | notEmpty: true
23 | }
24 | },
25 | address: {
26 | type: DataTypes.STRING,
27 | allowNull: false,
28 | searchable: true,
29 | validate: {
30 | notEmpty: true
31 | }
32 | },
33 | postcode: {
34 | type: DataTypes.STRING,
35 | allowNull: true,
36 | searchable: true
37 | },
38 | region: {
39 | type: DataTypes.STRING,
40 | allowNull: true,
41 | searchable: true
42 | },
43 | city: {
44 | type: DataTypes.STRING,
45 | allowNull: false,
46 | searchable: true,
47 | validate: {
48 | notEmpty: true
49 | }
50 | },
51 | country: {
52 | type: DataTypes.STRING,
53 | allowNull: false,
54 | searchable: true,
55 | validate: {
56 | notEmpty: true
57 | }
58 | },
59 | location: {
60 | type: DataTypes.TEXT,
61 | allowNull: false,
62 | get: function () {
63 | return JSON.parse(this.getDataValue('location'));
64 | },
65 | set: function (value) {
66 | return this.setDataValue('location', JSON.stringify(value));
67 | }
68 | }
69 | });
70 |
71 | Model.associate = function(models) {
72 | Model.hasMany(models.Person, { as: 'members' });
73 |
74 | // http://stackoverflow.com/a/37817966
75 | Model.addScope('nested', {
76 | attributes: {
77 | include: [[sequelize.literal('(SELECT COUNT(*) FROM People WHERE People.office_id = Office.id)'), 'headcount']]
78 | }
79 | });
80 | };
81 |
82 | return Model;
83 | };
84 |
--------------------------------------------------------------------------------
/server/models/organization.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function(sequelize, DataTypes) {
4 | var Model = sequelize.define("Organization", {
5 | id: {
6 | type: DataTypes.UUID,
7 | defaultValue: DataTypes.UUIDV4,
8 | allowNull: false,
9 | primaryKey: true,
10 | validate: {
11 | isUUID: 4
12 | }
13 | },
14 | name: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | searchable: true,
18 | unique: {
19 | msg: 'An organization with this name already exists.'
20 | },
21 | validate: {
22 | notEmpty: true
23 | }
24 | }
25 | });
26 |
27 | Model.associate = function(models) {
28 | Model.hasMany(models.Person, { as: 'members' });
29 | Model.belongsTo(models.Person, { as: 'manager', constraints: false });
30 |
31 | // http://stackoverflow.com/a/37817966
32 | Model.addScope('nested', {
33 | attributes: {
34 | include: [[sequelize.literal('(SELECT COUNT(*) FROM People WHERE People.organization_id = Organization.id)'), 'headcount']]
35 | },
36 | include: [{
37 | model: models.Person,
38 | as: 'manager',
39 | include: [{
40 | model: models.Office,
41 | as: 'office'
42 | }]
43 | }]
44 | });
45 | };
46 |
47 | return Model;
48 | };
49 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "0.0.2",
4 | "private": true,
5 | "scripts": {
6 | "start": "nodemon ./bin/www"
7 | },
8 | "dependencies": {
9 | "array-unique": "~0.3.2",
10 | "body-parser": "~1.18.3",
11 | "console-stamp": "~0.2.7",
12 | "cookie-parser": "~1.4.3",
13 | "cors": "~2.8.5",
14 | "debug": "~4.1.0",
15 | "deepmerge": "~2.2.1",
16 | "express": "~4.16.4",
17 | "extdirect": "~2.0.5",
18 | "jade": "~1.11.0",
19 | "jsonwebtoken": "~8.4.0",
20 | "latinize": "~0.4.0",
21 | "morgan": "~1.9.1",
22 | "node-cron": "~2.0.3",
23 | "sequelize": "~4.41.2",
24 | "serve-favicon": "~2.5.0",
25 | "sqlite3": "~4.0.4",
26 | "yargs": "~12.0.5"
27 | },
28 | "devDependencies": {
29 | "nodemon": "^1.18.6"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/public/api/portraits/men/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/0.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/1.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/10.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/11.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/12.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/13.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/14.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/15.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/16.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/17.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/18.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/19.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/2.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/20.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/21.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/22.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/23.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/24.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/24.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/25.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/25.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/26.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/26.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/3.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/4.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/5.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/6.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/7.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/8.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/9.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/0.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/1.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/10.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/11.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/12.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/13.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/14.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/15.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/16.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/17.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/18.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/19.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/2.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/20.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/21.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/22.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/23.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/3.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/4.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/5.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/6.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/7.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/8.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/9.jpg
--------------------------------------------------------------------------------
/server/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/server/utils/config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var merge = require('deepmerge');
4 | var config = require('../config.json');
5 |
6 | // Override main config (config.json) with potential local config (config.local.json): that's
7 | // useful when deploying the app on a server with different server url and port (Ext.Direct).
8 | try {
9 | config = merge(config, require('../config.local.json'));
10 | } catch (e) {
11 | }
12 |
13 | module.exports = config;
14 |
--------------------------------------------------------------------------------
/server/utils/errors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * http://www.jsonrpc.org/specification#error_object
3 | */
4 |
5 | "use strict";
6 |
7 | var extend = require('util')._extend;
8 |
9 | var codes = {
10 | INVALID_REQUEST: -32600,
11 | UNAUTHORIZED: -32099,
12 | AUTH_TOKEN_EXPIRED: -32098,
13 | AUTH_TOKEN_INVALID: -32097,
14 | READONLY_SESSION: -32096,
15 | INVALID_PARAMS: -32001,
16 | NOT_IMPLEMENTED: -32000
17 | };
18 |
19 | function generate(message, error, code, data) {
20 | return {
21 | // Ext.Direct expects the error (object or string) to be store in data.message
22 | message: extend(extend({}, data), {
23 | message: message || 'Invalid Request',
24 | name: error || 'InvalidRequest',
25 | code: code == null? codes.INVALID_REQUEST : code
26 | })
27 | };
28 | };
29 |
30 | var types = {
31 | unauthorized: function(data) {
32 | return generate(
33 | 'User is not authorized to perform this action',
34 | 'Unauthorized',
35 | codes.UNAUTHORIZED,
36 | data
37 | );
38 | },
39 |
40 | authTokenExpired: function(data) {
41 | return generate(
42 | 'Your session has expired, please login again',
43 | 'AuthTokenExpired',
44 | codes.AUTH_TOKEN_EXPIRED,
45 | data
46 | );
47 | },
48 |
49 | authTokenInvalid: function(data) {
50 | return generate(
51 | 'Your session is no longer valid, please login again',
52 | 'AuthTokenInvalid',
53 | codes.AUTH_TOKEN_INVALID,
54 | data
55 | );
56 | },
57 |
58 | notImplemented: function(data) {
59 | return generate(
60 | 'Not implemented',
61 | 'NotImplemented',
62 | codes.NOT_IMPLEMENTED,
63 | data
64 | );
65 | },
66 |
67 | invalidParams: function(data) {
68 | if (!Array.isArray(data)) {
69 | data = [data];
70 | }
71 |
72 | return generate(
73 | 'Invalid parameters',
74 | 'InvalidParameters',
75 | codes.INVALID_PARAMS,
76 | { errors: data }
77 | );
78 | },
79 |
80 | readonly: function(data) {
81 | return generate(
82 | 'Read-only session, data not updated',
83 | 'ReadOnlySession',
84 | codes.READONLY_SESSION,
85 | data
86 | );
87 | }
88 | };
89 |
90 | module.exports = {
91 | codes: codes,
92 |
93 | types: types,
94 |
95 | generate: generate,
96 |
97 | parse: function(error) {
98 | switch (error.name) {
99 | case 'SequelizeValidationError':
100 | case 'SequelizeUniqueConstraintError':
101 | return types.invalidParams(error.errors.map(function(error) {
102 | return { message: error.message, path: error.path };
103 | }));
104 | default:
105 | return error;
106 | }
107 | },
108 |
109 | fromJwtError: function(data) {
110 | // https://github.com/auth0/node-jsonwebtoken#errors--codes
111 | if (data.name === 'TokenExpiredError') {
112 | return types.authTokenExpired(data);
113 | } else if (data.name === 'JsonWebTokenError') {
114 | return types.authTokenInvalid(data);
115 | } else {
116 | return types.unauthorized(data);
117 | }
118 | }
119 | };
120 |
--------------------------------------------------------------------------------
/server/utils/session.js:
--------------------------------------------------------------------------------
1 | /**
2 | * https://jwt.io/introduction/
3 | */
4 |
5 | "use strict";
6 |
7 | var errors = require('./errors');
8 | var config = require('./config');
9 | var models = require('../models');
10 | var jwt = require('jsonwebtoken');
11 |
12 | module.exports = {
13 |
14 | readonly: config.session.readonly,
15 |
16 | initiate: function(username, password, res) {
17 | return models.Person.scope('nested').findOne({
18 | where: {
19 | password: password,
20 | $or: [
21 | { username: username },
22 | { email: username }
23 | ]
24 | }
25 | }).then(function(user) {
26 | if (!user) {
27 | throw errors.types.invalidParams({
28 | path: 'username', message: 'Invalid username and/or password'
29 | });
30 | }
31 |
32 | var duration = config.session.duration;
33 | var expires = new Date(Date.now() + duration*1000);
34 | var token = jwt.sign(
35 | { user_id: user.get('id') },
36 | config.session.secret,
37 | { expiresIn: duration });
38 |
39 | return {
40 | user: user,
41 | token: token,
42 | expires: expires
43 | };
44 | });
45 | },
46 |
47 | verify: function(request) {
48 | return new Promise(function(resolve, reject) {
49 | // https://jwt.io/introduction/#how-do-json-web-tokens-work-
50 | var header = request.headers && request.headers.authorization;
51 | var matches = header? /^Bearer (\S+)$/.exec(header) : null;
52 | var token = matches && matches[1];
53 |
54 | if (!token) {
55 | return reject(errors.types.unauthorized('No authorization token was found'));
56 | }
57 |
58 | jwt.verify(token, config.session.secret, function(err, decoded) {
59 | if (err) {
60 | return reject(errors.fromJwtError(err));
61 | }
62 |
63 | models.Person.scope('nested').findOne({
64 | where: {
65 | id: decoded.user_id
66 | }
67 | }).then(function(user) {
68 | if (!user) {
69 | throw errors.types.authTokenInvalid();
70 | }
71 |
72 | resolve({
73 | user: user,
74 | token: token,
75 | expires: new Date(decoded.exp)
76 | });
77 | }).catch(function(err) {
78 | reject(err);
79 | });
80 | });
81 | });
82 | }
83 | };
84 |
--------------------------------------------------------------------------------
/server/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/server/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------