'
50 | ]
51 | }, {
52 | text: 'Headcount',
53 | dataIndex: 'headcount',
54 | flex: 1,
55 | cell: {
56 | encodeHtml: false
57 | },
58 | tpl: [
59 | '
',
60 | '{headcount:plural("employee")}',
61 | ''
62 | ]
63 | }],
64 |
65 | listeners: {
66 | childdoubletap: 'onChildActivate'
67 | }
68 | }]
69 | });
70 |
--------------------------------------------------------------------------------
/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 | collapsible: false,
21 | variableHeights: true,
22 |
23 | selectable: {
24 | disabled: true
25 | },
26 |
27 | plugins: [{
28 | type: 'listpaging',
29 | autoPaging: true
30 | }],
31 |
32 | columns: [{
33 | text: 'Name',
34 | dataIndex: 'name',
35 | flex: 2,
36 | cell: {
37 | encodeHtml: false
38 | },
39 | tpl: '
{name}'
40 | }, {
41 | text: 'Manager',
42 | dataIndex: 'manager.lastname',
43 | flex: 2,
44 | cell: {
45 | encodeHtml: false
46 | },
47 | tpl: [
48 | '
',
49 | '',
52 | '',
53 | '
{office.name}, ',
54 | '{office.city} ({office.country})',
55 | '
',
56 | ''
57 | ]
58 | }, {
59 | text: 'Headcount',
60 | dataIndex: 'headcount',
61 | flex: 1,
62 | cell: {
63 | encodeHtml: false
64 | },
65 | tpl: [
66 | '
',
67 | '{headcount:plural("employee")}',
68 | ''
69 | ]
70 | }],
71 |
72 | listeners: {
73 | childdoubletap: 'onChildActivate'
74 | }
75 | }]
76 | });
77 |
--------------------------------------------------------------------------------
/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 | collapsible: false,
21 | variableHeights: true,
22 |
23 | selectable: {
24 | disabled: true
25 | },
26 |
27 | plugins: [{
28 | type: 'listpaging',
29 | autoPaging: true
30 | }],
31 |
32 | columnMenu: {
33 | items: {
34 | groupByThis: false,
35 | showInGroups: false
36 | }
37 | },
38 |
39 | columns: [{
40 | dataIndex: 'picture',
41 | menuDisabled: true,
42 | hideable: false,
43 | sortable: false,
44 | align: 'center',
45 | width: 58,
46 | cell: {
47 | encodeHtml: false
48 | },
49 | tpl: '
'
50 | }, {
51 | text: 'Name / Title',
52 | dataIndex: 'lastname',
53 | flex: 1,
54 | cell: {
55 | encodeHtml: false
56 | },
57 | tpl: [
58 | '
{fullname}',
59 | '
{title}
'
60 | ]
61 | }, {
62 | text: 'Organization',
63 | dataIndex: 'organization.name',
64 | flex: 1,
65 | cell: {
66 | encodeHtml: false
67 | },
68 | tpl: [
69 | '
',
70 | '{name}',
71 | '',
74 | ''
75 | ]
76 | }, {
77 | text: 'Office',
78 | dataIndex: 'office.name',
79 | flex: 1,
80 | cell: {
81 | encodeHtml: false
82 | },
83 | tpl: [
84 | '
',
85 | '{name}',
86 | '{city}, {country}
',
87 | ''
88 | ]
89 | }, {
90 | sortable: false,
91 | dataIndex: 'email',
92 | text: 'Email/Phone',
93 | flex: 1,
94 | cell: {
95 | encodeHtml: false
96 | },
97 | tpl: [
98 | '
{email}
',
99 | '
{phone}
'
100 | ]
101 | }],
102 |
103 | listeners: {
104 | childdoubletap: 'onChildActivate'
105 | }
106 | }]
107 | });
108 |
--------------------------------------------------------------------------------
/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 (Ext.isEmpty(value)) {
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: 'fas fa-sync',
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: 'far fa-pen',
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: 'far fa-pen',
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/extBackup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "product": "ext",
4 | "version": "1.0.0-0",
5 | "description": "",
6 | "repository": {
7 | "type": "git",
8 | "url": ""
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "",
13 | "bugs": {
14 | "url": ""
15 | },
16 | "homepage": "",
17 | "scripts": {
18 | "start": "npm run dev",
19 | "clean": "rimraf build",
20 | "dev": "webpack-dev-server --env browser=yes --env verbose=no",
21 | "build": "npm run clean && cross-env webpack --env environment=production --env treeshake=yes",
22 | "build:testing": "npm run clean && cross-env webpack --env treeshake=yes --env cmdopts=--testing"
23 | },
24 | "dependencies": {
25 | "@sencha/ext": "~7.5.1",
26 | "@sencha/ext-font-awesome": "^7.5.1",
27 | "@sencha/ext-google": "^7.5.1",
28 | "@sencha/ext-modern": "~7.5.1",
29 | "@sencha/ext-modern-theme-triton": "^7.5.1"
30 | },
31 | "devDependencies": {
32 | "@babel/cli": "^7.5.5",
33 | "@babel/core": "^7.5.5",
34 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
35 | "@babel/plugin-transform-async-to-generator": "^7.5.1",
36 | "@babel/plugin-transform-regenerator": "^7.4.5",
37 | "@babel/preset-env": "^7.5.5",
38 | "@sencha/ext-webpack-plugin": "~7.5.1",
39 | "babel-loader": "^8.0.6",
40 | "babel-plugin-add-module-exports": "^1.0.2",
41 | "cross-env": "^5.2.0",
42 | "lodash.find": "^4.6.0",
43 | "portfinder": "^1.0.21",
44 | "replace": "^1.1.1",
45 | "rimraf": "^3.0.2",
46 | "webpack": "^5.55.1",
47 | "webpack-cli": "^4.8.0",
48 | "webpack-dev-server": "^4.2.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Coworkee | Ext JS Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | //this file exists so the webpack build process will succeed
2 | Ext._find = require('lodash.find');
--------------------------------------------------------------------------------
/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 ((type === 'skype' || type === 'linkedin') ? 'x-fab' : 'x-fa') + ' fa-' + type;
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "product": "ext",
4 | "version": "1.0.0-0",
5 | "description": "",
6 | "repository": {
7 | "type": "git",
8 | "url": ""
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "",
13 | "bugs": {
14 | "url": ""
15 | },
16 | "homepage": "",
17 | "scripts": {
18 | "start": "npm run dev",
19 | "clean": "rimraf build",
20 | "dev": "webpack-dev-server --env.browser=yes --env.verbose=no",
21 | "build": "npm run clean && cross-env webpack --env.environment=production --env.treeshake=yes"
22 | },
23 | "dependencies": {
24 | "@sencha/ext": "~7.6.0",
25 | "@sencha/ext-font-awesome": "^7.6.0",
26 | "@sencha/ext-google": "^7.6.0",
27 | "@sencha/ext-modern": "~7.6.0",
28 | "@sencha/ext-modern-theme-triton": "^7.6.0"
29 | },
30 | "devDependencies": {
31 | "@sencha/ext-webpack-plugin": "~7.6.0",
32 | "cross-env": "^5.2.0",
33 | "portfinder": "^1.0.21",
34 | "webpack": "~4.39.2",
35 | "webpack-cli": "~3.3.6",
36 | "webpack-dev-server": "~3.8.0",
37 | "@babel/cli": "^7.5.5",
38 | "@babel/core": "^7.5.5",
39 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
40 | "@babel/plugin-transform-async-to-generator": "^7.5.0",
41 | "@babel/plugin-transform-regenerator": "^7.4.5",
42 | "@babel/preset-env": "^7.5.5",
43 | "babel-plugin-add-module-exports": "^1.0.2",
44 | "babel-loader": "^8.0.6",
45 | "lodash.find": "^4.6.0",
46 | "replace": "^1.1.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/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/9160ea41bcb108ee85a4d77fade8d0c674aceee2/client/resources/images/auth-background.jpg
--------------------------------------------------------------------------------
/client/resources/images/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/client/resources/images/loading.png
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | //this file exists so the webpack build process will succeed
--------------------------------------------------------------------------------
/client/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | * An object containing key value pair framework descriptors.
4 | *
5 | * The value can be a string or an object containing at least one of "dir" or "pkg",
6 | * where "dir" can be a filesystem path to the framework sources and "pkg" can be a
7 | * package name. For example:
8 | *
9 | * "frameworks": {
10 | *
11 | * "ext-x": "/absolute/path/to/ext",
12 | * "ext-y": {
13 | * "source": "../relative/path/to/ext",
14 | * "path": "ext"
15 | * },
16 | * "ext-z": {
17 | * "package": "ext@n.n.n",
18 | * "path": "ext-n.n.n"
19 | * },
20 | * "touch": "touch"
21 | * }
22 | *
23 | */
24 | "frameworks": {
25 | "ext": "node_modules/@sencha/ext"
26 | },
27 | /**
28 | * This is the folder for build outputs in the workspace.
29 | */
30 | "build": {
31 | "dir": "${workspace.dir}/build"
32 | },
33 | /**
34 | * These configs determine where packages are generated and extracted to (when downloaded).
35 | */
36 | "packages": {
37 | /**
38 | * This folder contains all local packages.
39 | * If a comma-separated string is used as value the first path will be used as the path to generate new packages.
40 | */
41 | "dir": "${workspace.dir}/packages/local,${workspace.dir}/packages,${framework.dir}/../ext-google",
42 | /**
43 | * This folder contains all extracted (remote) packages.
44 | */
45 | "extract": "${workspace.dir}/packages/remote"
46 | }
47 | }
--------------------------------------------------------------------------------
/server/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/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 Op = Sequelize.Op;
11 | var env = process.env.NODE_ENV || "development";
12 | var config = require(path.join(__dirname, '..', 'utils', 'config')).database;
13 | config = config || {};
14 |
15 | config.operatorsAliases = {
16 | $eq: Op.eq,
17 | $ne: Op.ne,
18 | $gte: Op.gte,
19 | $gt: Op.gt,
20 | $lte: Op.lte,
21 | $lt: Op.lt,
22 | $not: Op.not,
23 | $in: Op.in,
24 | $notIn: Op.notIn,
25 | $is: Op.is,
26 | $like: Op.like,
27 | $notLike: Op.notLike,
28 | $iLike: Op.iLike,
29 | $notILike: Op.notILike,
30 | $regexp: Op.regexp,
31 | $notRegexp: Op.notRegexp,
32 | $iRegexp: Op.iRegexp,
33 | $notIRegexp: Op.notIRegexp,
34 | $between: Op.between,
35 | $notBetween: Op.notBetween,
36 | $overlap: Op.overlap,
37 | $contains: Op.contains,
38 | $contained: Op.contained,
39 | $adjacent: Op.adjacent,
40 | $strictLeft: Op.strictLeft,
41 | $strictRight: Op.strictRight,
42 | $noExtendRight: Op.noExtendRight,
43 | $noExtendLeft: Op.noExtendLeft,
44 | $and: Op.and,
45 | $or: Op.or,
46 | $any: Op.any,
47 | $all: Op.all,
48 | $values: Op.values,
49 | $col: Op.col
50 | };
51 | var sequelize = new Sequelize(config.database, config.username, config.password, config);
52 | var db = {};
53 |
54 | fs.readdirSync(__dirname)
55 | .filter(function(file) {
56 | return (file.indexOf(".") !== 0) && (file !== "index.js");
57 | })
58 | .forEach(function(file) {
59 | var model = sequelize.import(path.join(__dirname, file));
60 | db[model.name] = model;
61 | });
62 |
63 | Object.keys(db).forEach(function(modelName) {
64 | if ("associate" in db[modelName]) {
65 | db[modelName].associate(db);
66 | }
67 | });
68 |
69 | db.sequelize = sequelize;
70 | db.Sequelize = Sequelize;
71 |
72 | module.exports = db;
73 |
--------------------------------------------------------------------------------
/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.19.0",
11 | "console-stamp": "~0.2.7",
12 | "cookie-parser": "~1.4.4",
13 | "cors": "~2.8.5",
14 | "debug": "~4.1.1",
15 | "deepmerge": "~4.2.1",
16 | "express": "~4.17.1",
17 | "extdirect": "~2.0.5",
18 | "pug": "~2.0.3",
19 | "jsonwebtoken": "~8.5.1",
20 | "latinize": "~0.4.0",
21 | "morgan": "~1.9.1",
22 | "node-cron": "~2.0.3",
23 | "sequelize": "~5.21.1",
24 | "serve-favicon": "~2.5.0",
25 | "sqlite3": "~4.1.0",
26 | "yargs": "~14.2.0"
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/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/0.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/1.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/10.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/11.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/12.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/13.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/14.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/15.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/16.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/17.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/18.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/19.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/2.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/20.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/21.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/22.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/23.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/24.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/24.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/25.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/25.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/26.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/26.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/3.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/4.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/5.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/6.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/7.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/8.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/men/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/men/9.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/0.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/1.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/10.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/11.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/12.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/13.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/14.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/15.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/16.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/17.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/18.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/19.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/2.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/20.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/21.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/22.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/23.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/3.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/4.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/5.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/6.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/7.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/server/public/api/portraits/women/8.jpg
--------------------------------------------------------------------------------
/server/public/api/portraits/women/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee/9160ea41bcb108ee85a4d77fade8d0c674aceee2/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/data.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var models = require("../models");
4 | var sequelize = models.sequelize;
5 | var Promise = models.Sequelize.Promise;
6 |
7 | function pick(items, index) {
8 | var count = items.length;
9 | if (index === undefined) {
10 | index = Math.floor(Math.random() * count);
11 | }
12 |
13 | return items[index % count];
14 | }
15 |
16 | module.exports = {
17 | reset: function() {
18 | console.info('Populating database with example data...');
19 | return sequelize.transaction(function(t) {
20 | return sequelize.sync({ force: true, transaction: t }).then(function () {
21 | return Promise.all([
22 | models.Action.destroy({ truncate: true, transaction: t }),
23 | models.Office.destroy({ truncate: true, transaction: t }),
24 | models.Organization.destroy({ truncate: true, transaction: t }),
25 | models.Person.destroy({ truncate: true, transaction: t })
26 | ]);
27 | }).then(function() {
28 | return Promise.all([
29 | models.Office.bulkCreate(require('../data/Offices.json'), { transaction: t }),
30 | models.Organization.bulkCreate(require('../data/Organizations.json'), { transaction: t }),
31 | Promise.map(require('../data/People.json'), function(data) {
32 | return models.Person.create(data, { include: [{ model: models.Action, as: 'actions' }], transaction: t });
33 | })
34 | ]);
35 | });
36 | }).then(function() {
37 | return sequelize.transaction(function(t) {
38 | return Promise.all([
39 | models.Action.findAll(),
40 | models.Person.findAll(),
41 | models.Office.findAll(),
42 | models.Organization.findAll()
43 | ]).spread(function(actions, persons, offices, organizations) {
44 | return Promise.all([
45 | // associate Person (manager) -> Organization
46 | Promise.map(organizations, function(organization) {
47 | return organization.setManager(pick(persons), { transaction: t });
48 | }),
49 | // associate Person -> Organization
50 | Promise.map(persons, function(person, index) {
51 | return person.setOrganization(pick(organizations), { transaction: t });
52 | }),
53 | // associate Person -> Office
54 | Promise.map(persons, function(person, index) {
55 | return person.setOffice(pick(offices), { transaction: t });
56 | }),
57 | // associate Action -> Person (recipient)
58 | Promise.map(actions, function(action) {
59 | var recipient = pick(persons);
60 | action.subject = models.Action.subject(action.type, recipient);
61 | return Promise.all([
62 | action.setRecipient(recipient, { transaction: t }),
63 | action.save({ transaction: t })
64 | ]);
65 | })
66 | ]);
67 | });
68 | });
69 | }).then(function() {
70 | console.info('Populating database: DONE');
71 | });
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/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 | var Op = models.Sequelize.Op;
12 |
13 |
14 | module.exports = {
15 |
16 | readonly: config.session.readonly,
17 |
18 | initiate: function(username, password, res) {
19 | return models.Person.scope('nested').findOne({
20 | where: {
21 | password: password,
22 | $or: [
23 | { username: username },
24 | { email: username }
25 | ]
26 | }
27 | }).then(function(user) {
28 | if (!user) {
29 | throw errors.types.invalidParams({
30 | path: 'username', message: 'Invalid username and/or password'
31 | });
32 | }
33 |
34 | var duration = config.session.duration;
35 | var expires = new Date(Date.now() + duration*1000);
36 | var token = jwt.sign(
37 | { user_id: user.get('id') },
38 | config.session.secret,
39 | { expiresIn: duration });
40 |
41 | return {
42 | user: user,
43 | token: token,
44 | expires: expires
45 | };
46 | });
47 | },
48 |
49 | verify: function(request) {
50 | return new Promise(function(resolve, reject) {
51 | // https://jwt.io/introduction/#how-do-json-web-tokens-work-
52 | var header = request.headers && request.headers.authorization;
53 | var matches = header? /^Bearer (\S+)$/.exec(header) : null;
54 | var token = matches && matches[1];
55 |
56 | if (!token) {
57 | return reject(errors.types.unauthorized('No authorization token was found'));
58 | }
59 |
60 | jwt.verify(token, config.session.secret, function(err, decoded) {
61 | if (err) {
62 | return reject(errors.fromJwtError(err));
63 | }
64 |
65 | models.Person.scope('nested').findOne({
66 | where: {
67 | id: decoded.user_id
68 | }
69 | }).then(function(user) {
70 | if (!user) {
71 | throw errors.types.authTokenInvalid();
72 | }
73 |
74 | resolve({
75 | user: user,
76 | token: token,
77 | expires: new Date(decoded.exp)
78 | });
79 | }).catch(function(err) {
80 | reject(err);
81 | });
82 | });
83 | });
84 | }
85 | };
86 |
--------------------------------------------------------------------------------
/server/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/server/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------