TimeTrex Community Edition v16.2.0
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
class HtmlTemplates {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally insert variable or html. Can be used in two ways.
|
||||
* Either 1) pass html, and if so it will use the html if the field is true.
|
||||
* Or, 2) if the field is a value (and truthy), and html is undefined/not provided, then return the field value.
|
||||
* Instead of 2), you can also reference a variable directly, but risk outputting 'undefined' into the output html.
|
||||
* @param field Either a data value evaluating as truthy, or a Boolean.
|
||||
* @param html Optional html field to use if field is Boolean.
|
||||
* @returns {string|*}
|
||||
*/
|
||||
outputif( field, html ) { // function to be called outputif, or printif
|
||||
if ( field ) {
|
||||
return html ? html : field;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Conditionally output options passed to the view controller constructor. Wrapper for outputif to handle multiple options.
|
||||
* @param options
|
||||
* @returns {string}
|
||||
*/
|
||||
outputOptions( options ) {
|
||||
let output = [];
|
||||
for ( var i = 0; i < options.length; i++ ) {
|
||||
let result = this.outputif( options[i].option, options[i].html );
|
||||
if ( result ) {
|
||||
output.push( result );
|
||||
}
|
||||
}
|
||||
return output.length > 0 ? '{ ' + output.join( ', ' ) + ' }' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* PascalCase to snake_case
|
||||
*/
|
||||
convertPascalCase2SnakeCase( string ) {
|
||||
// all lowercase separated by _
|
||||
return string.split( /(?=[A-Z][a-z])/ ).join( '_' ).toLowerCase(); // Fix: [A-Z][a-z] is needed to not split on all caps like ROEView. But this wont handle single caps at the end though.
|
||||
}
|
||||
|
||||
/**
|
||||
* PascalCase to kebab-case
|
||||
*/
|
||||
convertPascalCase2KebabCase( string ) {
|
||||
// all lowercase separated by -
|
||||
return string.split( /(?=[A-Z][a-z])/ ).join( '-' ).toLowerCase(); // Fix: [A-Z][a-z] is needed to not split on all caps like ROEView. But this wont handle single caps at the end though.
|
||||
}
|
||||
|
||||
getTemplateTypeFromFilename( filename ) {
|
||||
var type;
|
||||
|
||||
switch ( true ) { // known as 'overloaded switch'
|
||||
// The following views will use the new templating logic. Order of these statements is important, first rule to match is used.
|
||||
case this.checkViewClassForInlineHtmlbyFilename( filename ) !== false: // If success, it will return a String with the html.
|
||||
// If a view class contains a static html_template value, then use this as an override instead of any specific type template matched by filename.
|
||||
type = TemplateType.INLINE_HTML;
|
||||
break;
|
||||
case filename.indexOf( 'Sub' ) === 0: // Checks 'Sub' at start of filename. Must come before ReportView, otherwise it will conflict for SubSavedReportView.html
|
||||
type = TemplateType.SUB_VIEW;
|
||||
break;
|
||||
case filename.indexOf( 'EditView.html' ) !== -1: // Must come before List Views, otherwise it will match for those due to both ending in View.html
|
||||
type = TemplateType.EDIT_VIEW;
|
||||
break;
|
||||
case filename.indexOf( 'ReportView.html' ) !== -1 && filename.includes( 'Saved' ) === false: // Must come before List Views, and after 'Sub' otherwise reports will be loaded as list views. However make sure SavedReport is loaded as a list view.
|
||||
type = TemplateType.REPORT_VIEW;
|
||||
break;
|
||||
case filename.indexOf( 'View.html' ) !== -1: // Must come more or less last, otherwise it will conflict with other files containing View.html, like EditView.html and ReportView.html
|
||||
type = TemplateType.LIST_VIEW;
|
||||
break;
|
||||
default:
|
||||
// If no template types are matched, treat as legacy html.
|
||||
type = TemplateType.LEGACY_HTML; // This results in the relevant html file being loaded via the network. The new tab parsing logic may still run!
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
//Certain views do not fall under the rules of 'getTemplateTypeFromFilename()' and require special handling.
|
||||
//This can be removed once we switch how views are loaded and can apply these rules directly on the view itself.
|
||||
getTemplateOptionsFromViewId( view_id ) {
|
||||
let options = {
|
||||
view_id: view_id,
|
||||
// Remember, sub_view_mode is not included here as not yet available here; view controller has not yet been loaded. Sub View Mode will be determined by template_type using the file name data. Will be applied as an option in HtmlTemplates.getGenericListViewHtml
|
||||
};
|
||||
|
||||
//The following tax reports require an additional tab.
|
||||
let reports_require_form_setup = ['RemittanceSummaryReport', 'T4SummaryReport', 'T4ASummaryReport', 'Form941Report', 'Form940Report', 'Form1099NecReport', 'FormW2Report', 'AffordableCareReport', 'USStateUnemploymentReport'];
|
||||
if ( reports_require_form_setup.includes( view_id ) ) {
|
||||
options.report_form_setup = true;
|
||||
}
|
||||
|
||||
let sub_view_require_warning_message = [`UserDateTotal`];
|
||||
if ( sub_view_require_warning_message.includes( view_id ) ) {
|
||||
options.sub_view_warning_message = true;
|
||||
}
|
||||
|
||||
//Issue #3158 - If these controllers are cached then side effects can occur.
|
||||
//Such as permission denied alerts after opening login view on Invoice -> Client view.
|
||||
let view_do_not_cache_controller = [`LoginUserWizard`, 'LoginUser', 'FindAvailableWizard', 'FindAvailable'];
|
||||
if ( view_do_not_cache_controller.includes( view_id ) ) {
|
||||
options.do_not_cache_controller = true;
|
||||
}
|
||||
|
||||
//Audit log is both a TemplateType.INLINE_HTML and a TemplateType.SUB_VIEW under the current HTML2JS system.
|
||||
//Once we switch to loading ViewControllers before HTML this special case can be removed.
|
||||
if( view_id === 'Log' ) {
|
||||
options.is_sub_view = true;
|
||||
}
|
||||
|
||||
//Views extending BaseTreeViewController have slightly different requirments such as not show total number div.
|
||||
//Once we switch to loading ViewControllers before the HTML this can be conditional by checking if the controller has tree_mode set to true.
|
||||
let base_tree_views = ['JobGroup', 'JobItemGroup', 'PunchTagGroup', 'UserGroup', 'DocumentGroup', 'KPIGroup', 'ClientGroup', 'ProductGroup'];
|
||||
if ( base_tree_views.includes( view_id ) ) {
|
||||
options.base_tree_view = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
checkViewClassForInlineHtmlbyFilename( filename ) {
|
||||
// Lets see if this view class contains a html_template override, which should precede any type definitions.
|
||||
let check_class = window[ filename.replace(/\.html$/,'') + 'Controller' ];
|
||||
if( check_class !== undefined && typeof check_class.html_template === 'string' ) {
|
||||
return check_class.html_template;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TemplateType} type
|
||||
* @param {Object} options
|
||||
* @param {Function} [onResult]
|
||||
*/
|
||||
getTemplate( type, options = {}, onResult ) {
|
||||
|
||||
var html_template;
|
||||
|
||||
switch ( type ) {
|
||||
case TemplateType.LIST_VIEW:
|
||||
html_template = this.getGenericListViewHtml( options );
|
||||
break;
|
||||
case TemplateType.SUB_VIEW:
|
||||
options.is_sub_view = true; // Force this to be true, as it's a sub_view after all. (This switch data is based off html filename request)
|
||||
html_template = this.getGenericListViewHtml( options );
|
||||
break;
|
||||
case TemplateType.EDIT_VIEW:
|
||||
// code block
|
||||
html_template = HtmlTemplatesGlobal.genericEditView( options );
|
||||
break;
|
||||
case TemplateType.REPORT_VIEW:
|
||||
// code block
|
||||
html_template = HtmlTemplatesGlobal.genericReportEditView( options );
|
||||
break;
|
||||
case TemplateType.INLINE_HTML:
|
||||
html_template = this.getViewScriptTagHtml( options ) + this.checkViewClassForInlineHtmlbyFilename( options.filename );
|
||||
break;
|
||||
default:
|
||||
Debug.Error( 'HTML2JS: Error occured getting template. No matches for ' + options.view_id, 'HtmlTemplates.js', 'HtmlTemplates.js', 'getTemplate', 1 );
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If callback onResult exists, call the function, else return html.
|
||||
if ( onResult ) {
|
||||
onResult( html_template ); // should we put this outside the switch? Depends how similar the other switch statements are.
|
||||
} else {
|
||||
return html_template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This also handles subview script tags, if options.is_sub_view is true.
|
||||
* @param options
|
||||
* @returns {string}
|
||||
*/
|
||||
getGenericListViewHtml( options = {} ) {
|
||||
Debug.Text( 'HTML2JS: Template retrieved for ' + options.view_id, 'HtmlTemplates.js', 'HtmlTemplates.js', 'getGenericListViewHtml', 10 );
|
||||
// Prepend the <script> tag to the html template, so it can be executed when inserted into the DOM later on in the onResult.
|
||||
return this.getViewScriptTagHtml( options ) + HtmlTemplatesGlobal.genericListView( options );
|
||||
}
|
||||
|
||||
getViewScriptTagHtml( options = {} ) {
|
||||
|
||||
/*
|
||||
* Note: In the legacy html load, the view controller would get instantiated via the <script> tag at the top of the html file, when this got loaded into the DOM.
|
||||
* This would have happened in the onResult() function, one of such is BaseViewController.loadView -> doNext().
|
||||
* Rather than insert a check there to trigger the view controller, or prepend the script tag, we can also try to just do that AFTER the onResult.
|
||||
* We should not do it before, as there is still cleanup code run by IndexController.removeCurrentView
|
||||
*
|
||||
* However, for now currently the best option is to pre-append the script tag info, because the onResult function won't always be from BaseViewController.loadView
|
||||
* */
|
||||
|
||||
let class_name = options.view_id + 'ViewController';
|
||||
let html_view_script_tag = `<!-- JS2HTML ${this.outputif( options.is_sub_view, 'SUB_VIEW ' )}--><script type="text/javascript">
|
||||
var ${this.outputif( options.is_sub_view, 'sub_' )}${this.convertPascalCase2SnakeCase( options.view_id )}_view_controller = new ${class_name}(${this.outputOptions( [
|
||||
{ option: options.is_sub_view, html: 'sub_view_mode: true' },
|
||||
{ option: options.do_not_cache_controller, html: 'can_cache_controller: false' },
|
||||
] )});
|
||||
</script>`;
|
||||
|
||||
// Prepend the <script> tag to the html template, so it can be executed when inserted into the DOM later on in the onResult.
|
||||
// html_template = html_view_script_tag + html_template;
|
||||
//
|
||||
// debugger; // A good debug point when you get errors around view controller instances not defined. But could also mean parent controller needs manual html insertion into the tab_model, as the subview doesnt have the right columns in the generic template, thus the container (to which the html initializing the controller) is attached, does not exist.
|
||||
return html_view_script_tag;
|
||||
}
|
||||
|
||||
genericListView( options = {} ) {
|
||||
|
||||
// Prepare the required data for the html template
|
||||
let container_id = this.convertPascalCase2SnakeCase( options.view_id ) + '_view_container'; // E.g. branch_view_container. all lowercase separated by _ (Later in code, an id is added at the end of the string, like '_318') - Fix: [A-Z][a-z] is needed to not split on all caps like ROEView. But this wont handle single caps at the end though. // Also in most view.el
|
||||
let container_class = this.convertPascalCase2KebabCase( options.view_id ) + '-view'; // E.g. branch-view. all lowercase separated by -
|
||||
let view_template = `
|
||||
<div class="view js-generic-list-view ${this.outputif( container_class )}${this.outputif( options.is_sub_view, ' sub-view' )}" id="${this.outputif( container_id )}">
|
||||
${this.outputif( options.sub_view_warning_message, '<span class="warning-message"></span>' )}
|
||||
<div class="clear-both-div"></div>
|
||||
${this.genericListGrid( options )}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return view_template;
|
||||
}
|
||||
|
||||
genericListGrid( options = {} ) {
|
||||
// let sub_view = options.is_sub_view;
|
||||
let grid_template = `
|
||||
<div class="grid-div js-generic-grid">
|
||||
${this.outputif( !options.is_sub_view && !options.base_tree_view, '<div class="total-number-div"><span class="total-number-span"></span></div>' )}
|
||||
<div class="grid-top-border"></div>
|
||||
|
||||
${this.outputif( options.is_sub_view, '<div class="sub-grid-view-div">' )}
|
||||
<table id="grid"></table>
|
||||
${this.outputif( options.is_sub_view, '</div>' )}
|
||||
|
||||
<div class="bottom-div">
|
||||
<div class="grid-bottom-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return grid_template;
|
||||
}
|
||||
|
||||
genericEditView( options = {} ) {
|
||||
// Currently just for User Title Edit.
|
||||
// viewId: UserTitle
|
||||
// fileName: UserTitleEditView.html
|
||||
// Prepare the required data for the html template
|
||||
let tab_bar_id = this.convertPascalCase2SnakeCase( options.view_id ) + '_edit_view_tab_bar'; // e.g. user_title_edit_view_tab_bar - all lowercase separated by _ - Fix: [A-Z][a-z] is needed to not split on all caps like ROEView. But this wont handle single caps at the end though.
|
||||
let edit_view_class = options.view_id + 'EditView'; // e.g. UserTitleEditView
|
||||
let edit_view_template = `
|
||||
<div class="js-generic-edit-view edit-view ${this.outputif( edit_view_class )}">
|
||||
<div class="edit-view-tab-bar" id="${this.outputif( tab_bar_id )}">
|
||||
<div class="navigation-div" style="display: none">
|
||||
<span class="navigation-label"></span>
|
||||
<img class="left-click arrow"/>
|
||||
<div class="navigation-widget-div"></div>
|
||||
<img class="right-click arrow"/>
|
||||
</div>
|
||||
<span class="close-icon">x</span>
|
||||
<ul class="edit-view-tab-bar-label"></ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return edit_view_template;
|
||||
}
|
||||
|
||||
genericTab( options = {} ) {
|
||||
let is_multi_column = options.is_multi_column;
|
||||
let show_permission_div = options.show_permission_div;
|
||||
let is_sub = options.is_sub_view || false;
|
||||
let tab_id = options.tab_id;
|
||||
|
||||
let save_continue_sub_view = `
|
||||
<div class="save-and-continue-div">
|
||||
<span class="message"></span>
|
||||
<div class="save-and-continue-button-div">
|
||||
<button class="tt-button p-button p-component" type="button">
|
||||
<span class="icon"></span>
|
||||
<span class="p-button-label"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
let permission_div = `
|
||||
<div class="save-and-continue-div permission-defined-div">
|
||||
<span class="message permission-message"></span>
|
||||
</div>`;
|
||||
let template = `
|
||||
<div id="${tab_id}" class="html2js_flag edit-view-tab-outside${this.outputif( is_sub, '-sub-view' )}">
|
||||
<div class="edit-view-tab" id="${tab_id}_content_div">
|
||||
<div class="first-column${this.outputif( is_sub, '-sub-view' )}${this.outputif( !is_multi_column, ' full-width-column' )}"></div>
|
||||
${this.outputif( is_multi_column, '<div class="second-column"></div>' )}
|
||||
|
||||
${this.outputif( is_sub, save_continue_sub_view )}
|
||||
${this.outputif( show_permission_div, permission_div )}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
auditTab( options = {} ) {
|
||||
let template = `
|
||||
<div id="tab_audit" class="html2js_flag_audit edit-view-tab-outside-sub-view">
|
||||
<div class="edit-view-tab" id="tab_audit_content_div">
|
||||
<div class="first-column-sub-view"></div>
|
||||
<div class="save-and-continue-div">
|
||||
<span class="message"></span>
|
||||
<div class="save-and-continue-button-div">
|
||||
<button class="tt-button p-button p-component" type="button">
|
||||
<span class="icon"></span>
|
||||
<span class="p-button-label"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
genericReportEditView( options = {} ) {
|
||||
// Some examples of the differences. Interestingly, all checked templates so far all had the main div class set to active-shift-report-view.
|
||||
// ActiveShiftReportView.html : view_id=ActiveShiftReport : id=active_shift_report_view_tab_bar
|
||||
// UserSummaryReportView.html : id=user_summary_report_view_tab_bar
|
||||
// PayStubTransactionSummaryReportView.html : id=pay_stub_summary_report_view_tab_bar
|
||||
|
||||
let tab_bar_id = this.convertPascalCase2SnakeCase( options.view_id ) + '_view_tab_bar';
|
||||
let edit_view_template = `
|
||||
<div class="js-generic-report-edit-view edit-view active-shift-report-view">
|
||||
<div class="edit-view-tab-bar" id="${this.outputif( tab_bar_id )}">
|
||||
<div class="navigation-div" style="display: none">
|
||||
<span class="navigation-label"></span>
|
||||
<img class="left-click arrow"/>
|
||||
<div class="navigation-widget-div"></div>
|
||||
<img class="right-click arrow"/>
|
||||
</div>
|
||||
<span class="close-icon">x</span>
|
||||
<ul class="edit-view-tab-bar-label">
|
||||
<li><a ref="tab_report" href="#tab_report"></a></li>
|
||||
<li><a ref="tab_setup" href="#tab_setup"></a></li>
|
||||
<li><a ref="tab_chart" href="#tab_chart"></a></li>
|
||||
${this.outputif( options.report_form_setup || ( options.view_id && options.view_id === 'PayrollExportReport' ), '<li><a ref="tab_form_setup" href="#tab_form_setup"></a></li>' )}
|
||||
<li><a ref="tab_custom_columns" href="#tab_custom_columns"></a></li>
|
||||
<li><a ref="tab_saved_reports" href="#tab_saved_reports"></a></li>
|
||||
</ul>
|
||||
<div id="tab_report" class="edit-view-tab-outside">
|
||||
<div class="edit-view-tab" id="tab_report_content_div">
|
||||
<div class="first-column full-width-column"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab_setup" class="edit-view-tab-outside">
|
||||
<div class="edit-view-tab" id="tab_setup_content_div">
|
||||
<div class="first-column full-width-column"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab_chart" class="edit-view-tab-outside">
|
||||
<div class="edit-view-tab" id="tab_chart_content_div">
|
||||
<div class="first-column full-width-column"></div>
|
||||
<div class="save-and-continue-div permission-defined-div">
|
||||
<span class="message permission-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.outputif( options.report_form_setup, `
|
||||
<div id="tab_form_setup" class="edit-view-tab-outside">
|
||||
<div class="edit-view-tab" id="tab_form_setup_content_div">
|
||||
<div class="first-column full-width-column"></div>
|
||||
</div>
|
||||
</div>`
|
||||
)}
|
||||
${this.outputif( options.view_id && options.view_id === 'PayrollExportReport', `
|
||||
<div id="tab_form_setup" class="edit-view-tab-outside">
|
||||
<div class="edit-view-tab" id="tab_form_setup_content_div">
|
||||
<div class="first-row first-column full-width-column">
|
||||
</div>
|
||||
<div class="inside-editor-div full-width-column">
|
||||
<div class="grid-div">
|
||||
<table id="export_grid"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
)}
|
||||
<div id="tab_custom_columns" class="edit-view-tab-outside-sub-view">
|
||||
<div class="edit-view-tab" id="tab_custom_columns_content_div">
|
||||
<div class="first-column-sub-view"></div>
|
||||
<div class="save-and-continue-div">
|
||||
<span class="message"></span>
|
||||
<div class="save-and-continue-button-div">
|
||||
<button class="tt-button p-button p-component" type="button">
|
||||
<span class="icon"></span>
|
||||
<span class="p-button-label"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-and-continue-div permission-defined-div">
|
||||
<span class="message permission-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab_saved_reports" class="edit-view-tab-outside-sub-view">
|
||||
<div class="edit-view-tab" id="tab_saved_reports_content_div">
|
||||
<div class="first-column-sub-view"></div>
|
||||
<div class="save-and-continue-div">
|
||||
<span class="message"></span>
|
||||
<div class="save-and-continue-button-div">
|
||||
<button class="tt-button p-button p-component" type="button">
|
||||
<span class="icon"></span>
|
||||
<span class="p-button-label"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return edit_view_template;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// This might still work, but put on hold as we chose to focus on the list, edit and sub views and tabs
|
||||
// getEditViewFormItem( options = {} ) {
|
||||
// let sub_view = options.is_sub_view || false;
|
||||
// let template = `
|
||||
// <div class="edit-view-form-item-div">
|
||||
// <div class="edit-view-form-item-${ sub_view ? 'sub-' : ''}label-div"><span class="edit-view-form-item-label"></span></div>
|
||||
// <div class="edit-view-form-item-input-div"></div>
|
||||
// </div>
|
||||
// `;
|
||||
//
|
||||
// return template;
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Readonly<{string, symbol}>}
|
||||
*/
|
||||
const TemplateType = Object.freeze( {
|
||||
LIST_VIEW: Symbol( 'LIST_VIEW'),
|
||||
SUB_VIEW: Symbol( 'SUB_VIEW'),
|
||||
EDIT_VIEW: Symbol( 'EDIT_VIEW'),
|
||||
REPORT_VIEW: Symbol( 'REPORT_VIEW'),
|
||||
INLINE_HTML: Symbol( 'INLINE_HTML'),
|
||||
LEGACY_HTML: Symbol( 'LEGACY_HTML'),
|
||||
} );
|
||||
|
||||
const HtmlTemplatesGlobal = new HtmlTemplates();
|
||||
window.TT_HTML_G = HtmlTemplatesGlobal; // TODO: Temp for dev, remove after all done.
|
||||
export {
|
||||
HtmlTemplatesGlobal,
|
||||
// HtmlTemplates as HtmlTemplatesClass, // Not yet used as we are sharing the one global instance across scripts at the moment.
|
||||
TemplateType,
|
||||
};
|
||||
@@ -0,0 +1,657 @@
|
||||
import firebase from 'firebase/app';
|
||||
import 'firebase/messaging';
|
||||
import { TTAPI } from '@/services/TimeTrexClientAPI';
|
||||
import 'bootstrap';
|
||||
import TTEventBus from '@/services/TTEventBus';
|
||||
|
||||
class NotificationConsumer {
|
||||
|
||||
constructor() {
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyB9tM0QYb1D3JF07RqpeG-14ADGhezGRws",
|
||||
authDomain: "timetrex-app.firebaseapp.com",
|
||||
databaseURL: "https://timetrex-app.firebaseio.com",
|
||||
projectId: "timetrex-app",
|
||||
storageBucket: "timetrex-app.appspot.com",
|
||||
messagingSenderId: "462133047262",
|
||||
appId: "1:462133047262:web:1705b6bfca364bcd99b74f"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp( firebaseConfig );
|
||||
this.browser_supported = firebase.messaging.isSupported();
|
||||
this.messaging = firebase.messaging.isSupported() ? firebase.messaging() : null;
|
||||
this.user_notification_device_token_api = TTAPI.APINotificationDeviceToken;
|
||||
this.notification_api = TTAPI.APINotification;
|
||||
this.user_preference_api = TTAPI.APIUserPreference;
|
||||
this.notification_holder = document.querySelector( '#notification-holder' );
|
||||
this.notification_duration = 120000;
|
||||
this.token = '';
|
||||
this.notification_total = 0;
|
||||
this.notification_on_screen_total = 0; // How many notification pop ups are currently on screen.
|
||||
this.create_event_listeners = true;
|
||||
this.notification_sound = null;
|
||||
this.sound_timer = null;
|
||||
this.title_timer = null;
|
||||
this.previous_page_title = document.title;
|
||||
this.pending_events = [];
|
||||
this.event_bus = new TTEventBus({ view_id: 'notification_consumer' });
|
||||
}
|
||||
|
||||
setupUser( request_permission, refresh_token ) {
|
||||
if ( refresh_token === true ) {
|
||||
this.deleteNotificationDeniedCookie();
|
||||
}
|
||||
|
||||
if ( this.isBrowserSupported( refresh_token ) === false ) {
|
||||
if ( refresh_token === true ) {
|
||||
TAlertManager.showAlert( $.i18n._( 'Sorry, this is browser does not support push notifications.' ), $.i18n._( 'Push Notifications' ) );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//If impersonating other users do not show notification permission request pop ups or notifications.
|
||||
var alternate_session_data = getCookie( 'AlternateSessionData' );
|
||||
if ( alternate_session_data && !refresh_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//User has either repeatedly denied permissions or has disabled notifications for this browser in user preferences.
|
||||
if ( this.getNotificationDeniedCookie().asked_count >= 2 && refresh_token === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( LocalCacheData.getLoginUser() && APIGlobal.pre_login_data.production === true && APIGlobal.pre_login_data.demo_mode !== true && APIGlobal.pre_login_data.sandbox !== true ) {
|
||||
this.notification_duration = parseInt( LocalCacheData.getLoginUserPreference().notification_duration ) * 1000;
|
||||
|
||||
if ( Notification.permission === 'granted' && LocalCacheData.getLoginUserPreference().notification_status_id !== 2 ) {
|
||||
this.registerWorkerAndGetToken( true );
|
||||
this.createNotificationListeners();
|
||||
if ( refresh_token === true ) {
|
||||
// Provide feedback to user when they click refresh push notifications button.
|
||||
TAlertManager.showAlert( $.i18n._( 'Push Notifications Enabled' ), $.i18n._( 'Push Notifications' ) );
|
||||
}
|
||||
} else if ( request_permission && Notification.permission === 'default' ) {
|
||||
//Do not show mobile browsers notification permission alert unless they manually refresh notifications in My Account -> Preferences.
|
||||
if ( APIGlobal.pre_login_data.user_agent_data.is_mobile === true && refresh_token === false ) {
|
||||
return false;
|
||||
}
|
||||
this.showNotificationPermissionAlert();
|
||||
} else if ( refresh_token === true ) {
|
||||
this.showNotificationPermissionAlert();
|
||||
}
|
||||
} else {
|
||||
// Else user has declined permissions.
|
||||
if ( refresh_token === true ) {
|
||||
TAlertManager.showAlert( $.i18n._( 'Sorry, push notifications are disabled on this server.' ), $.i18n._( 'Push Notifications' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register our service worker and vapidKey.
|
||||
// Safari and Firefox require this to trigger on a user action and not just randomly ask.
|
||||
registerWorkerAndGetToken( send_token ) {
|
||||
try {
|
||||
navigator.serviceWorker.register( './services/firebase-messaging-sw.js' )
|
||||
.then( ( registration ) => {
|
||||
this.messaging.getToken( {
|
||||
serviceWorkerRegistration: registration,
|
||||
vapidKey: 'BAIFamGLNE689DChvdL8bWrvgiPFUMGzPwBrxuDiKQNTzpbQu-VZ3urH3SdIOSQ4DUYAOmeTrhmGTNQaNdtW-2I'
|
||||
} ).then( ( currentToken ) => {
|
||||
if ( currentToken ) {
|
||||
this.token = currentToken;
|
||||
if ( send_token ) {
|
||||
this.sendDeviceToken( this.token );
|
||||
}
|
||||
|
||||
//If permission alert exists, remove it.
|
||||
if ( $( '.modal-alert' ).length ) {
|
||||
$( '.modal-alert' ).remove();
|
||||
Global.setUIReady();
|
||||
this.sendAnalytics( 'allow-confirm' ); //Only trigger this when the employee is actually asked to allow permissions. Not everytime the service worker is registered.
|
||||
}
|
||||
} else {
|
||||
//request permission window happens
|
||||
}
|
||||
} ).catch( ( err ) => {
|
||||
// unexpected error
|
||||
} );
|
||||
} );
|
||||
} catch ( err ) {
|
||||
Debug.Text( 'Error attempting to register firebase service workers and push notification service: ' + err.message, 'NotificationConsumer.js', 'NotificationConsumer', 'registerWorkerAndGetToken', 9 );
|
||||
}
|
||||
}
|
||||
|
||||
isBrowserSupported( show_message ) {
|
||||
if ( window.location.protocol !== 'https:' ) {
|
||||
Debug.Text( 'Not on a HTTPS connection. Push Notifications disabled.', 'NotificationConsumer.js', 'NotificationConsumer', 'checkBrowserSupported', 9 );
|
||||
if ( show_message === true ) {
|
||||
// Provide feedback to user why push notifications are not working if they clicked to refresh push notifications in My Account -> Preferences.
|
||||
TAlertManager.showAlert( $.i18n._( 'Push Notification are only available on HTTPS connections.' ), $.i18n._( 'Push Notifications' ) );
|
||||
}
|
||||
return false;
|
||||
} else if ( this.browser_supported === false ) {
|
||||
Debug.Text( 'User on an unsupported browser.', 'NotificationConsumer.js', 'NotificationConsumer', 'checkBrowserSupported', 9 );
|
||||
if ( show_message === true ) {
|
||||
// Provide feedback to user why push notifications are not working if they clicked to refresh push notifications in My Account -> Preferences.
|
||||
TAlertManager.showAlert( $.i18n._( 'Push Notification are not supported on this browser.' ), $.i18n._( 'Push Notifications' ) );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
sendDeviceToken( device_token ) {
|
||||
this.user_notification_device_token_api.checkAndSetNotificationDeviceToken( device_token, 100, {
|
||||
onResult: function( res ) {
|
||||
let result = res.getResult();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
deleteToken() {
|
||||
var $this = this;
|
||||
if ( this.token ) {
|
||||
this.messaging.deleteToken().then( ( result ) => {
|
||||
let data = {};
|
||||
data.device_token = this.token;
|
||||
this.user_notification_device_token_api.deleteNotificationDeviceToken( [data], {
|
||||
onResult: function( res ) {
|
||||
$this.token = '';
|
||||
var result = res.getResult();
|
||||
if ( result ) {
|
||||
Debug.Text( 'Successfully deleted device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 );
|
||||
} else {
|
||||
Debug.Text( 'Failed to delete device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
} ).catch( ( err ) => {
|
||||
Debug.Text( 'ERROR: While attempting to delete notification device token.', 'NotificationConsumer.js', 'NotificationConsumer', 'deleteToken', 9 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllTokens() {
|
||||
//This deletion path does necessarily mean the current session has a device token, but will attempt to delete all device tokens for the current user.
|
||||
var $this = this;
|
||||
this.user_notification_device_token_api.deleteAllNotificationDeviceTokens( {
|
||||
onResult: function( res ) {
|
||||
$this.token = '';
|
||||
var result = res.getResult();
|
||||
if ( result ) {
|
||||
Debug.Text( 'Successfully deleted all device tokens.', 'NotificationConsumer.js', 'NotificationConsumer', 'DeleteAllDeviceTokens', 9 );
|
||||
} else {
|
||||
Debug.Text( 'Failed to delete all device tokens, none may exist.', 'NotificationConsumer.js', 'NotificationConsumer', 'DeleteAllDeviceTokens', 9 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
createNotificationListeners() {
|
||||
if ( this.create_event_listeners === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $this = this;
|
||||
this.create_event_listeners = false;
|
||||
|
||||
// Handles foreground, background and background notification-clicked events.
|
||||
navigator.serviceWorker.addEventListener( 'message', payload => {
|
||||
this.handlePushNotificationEvent( payload.data.messageType === 'push-received', payload.data );
|
||||
} );
|
||||
|
||||
this.notification_holder.addEventListener( 'click', event => {
|
||||
var element = event.target;
|
||||
if ( element.className === 'notification-close' ) {
|
||||
// Ignored notifications are not marked as read.
|
||||
this.removeNotification( element.parentNode.parentNode.parentNode );
|
||||
} else if ( element.className === 'notification-link' ) {
|
||||
// Mark notification as read when user clicks "view details" and is sent to the notificwtion link.
|
||||
this.setNotificationAsRead( element.id );
|
||||
this.removeNotification( element.parentNode.parentNode );
|
||||
|
||||
//If notification has an open_view event attached, trigger that event and then delete it.
|
||||
for ( var i = this.pending_events.length - 1; i >= 0; i-- ) {
|
||||
if ( this.pending_events[i].id === element.id && this.pending_events[i].event === 'open_view' ) {
|
||||
this.openViewLinkedToNotification( this.pending_events[i].event_data );
|
||||
event.preventDefault(); //Stop default href from being followed.
|
||||
this.pending_events.splice( i, 1 ); //Delete event from pending events.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
window.addEventListener( 'focus', function( event ) {
|
||||
$this.cancelTimers();
|
||||
}, false );
|
||||
}
|
||||
|
||||
handlePushNotificationEvent( foreground, payload ) {
|
||||
let timetrex_data = JSON.parse( payload.data.timetrex );
|
||||
|
||||
Debug.Arr( payload, 'Push notification received.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 );
|
||||
|
||||
this.event_bus.emit( 'tt_topbar', 'profile_pending_counts', { //When push notification received update all "My Profile" badges.
|
||||
object_types: []
|
||||
} );
|
||||
|
||||
// If on foreground displays the notification.
|
||||
if ( payload.messageType !== 'notification-clicked' && timetrex_data.user_id !== undefined && LocalCacheData.getLoginUser() && LocalCacheData.getLoginUser().id === timetrex_data.user_id && payload.notification && payload.notification.title ) {
|
||||
// Increment total on bell everytime a new notification comes in.
|
||||
this.updateBell( true, this.notification_total + 1 );
|
||||
this.showNotification( payload.notification.title, payload.notification.body, payload.notification.click_action, timetrex_data.id, timetrex_data.priority, payload.data.link_target ? payload.data.link_target : '' );
|
||||
Debug.Text( 'Showing Notification on UI as user_id matches and notification was not a system click.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 );
|
||||
}
|
||||
|
||||
if ( payload.messageType === 'notification-clicked' ) {
|
||||
Debug.Text( 'Notification was clicked on from desktop.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 );
|
||||
|
||||
// Set notification as read as we about to redirect the user to the notification.
|
||||
this.setNotificationAsRead( timetrex_data.id );
|
||||
// If the notification toast is still on screen remove it.
|
||||
this.removeNotificationById( timetrex_data.id );
|
||||
|
||||
// User clicked desktop notification, redirect them directly to the notification if a click_action was given.
|
||||
if ( payload.notification.click_action !== undefined && payload.notification.click_action !== '' ) {
|
||||
window.location = payload.notification.click_action;
|
||||
} else {
|
||||
window.location = Global.getBaseURL() + '#!m=Notification';
|
||||
}
|
||||
} else {
|
||||
//Handle background events if any are in the payload.
|
||||
if ( timetrex_data.event !== undefined && timetrex_data.event.length > 0 ) {
|
||||
this.handleBackgroundEvent( timetrex_data );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleBackgroundEvent( timetrex_data ) {
|
||||
// Handles timetrex specific data of notification payload.
|
||||
Debug.Text( 'Background action was supplied in the push notification.', 'NotificationConsumer.js', 'NotificationConsumer', 'handlePushNotificationEvent', 9 );
|
||||
for ( let i = 0; i < timetrex_data.event.length; i++ ) {
|
||||
switch ( timetrex_data.event[i].type ) {
|
||||
case 'clean_cache':
|
||||
LocalCacheData.cleanNecessaryCache();
|
||||
break;
|
||||
case 'open_view':
|
||||
//Only triggered if user clicks the notification.
|
||||
this.pending_events.push( {
|
||||
id: timetrex_data.id,
|
||||
event: timetrex_data.event[i].type,
|
||||
event_data: timetrex_data.event[i]
|
||||
} );
|
||||
break;
|
||||
case 'open_view_immediate':
|
||||
this.openViewLinkedToNotification( timetrex_data.event[i] );
|
||||
break;
|
||||
case 'redirect':
|
||||
if ( timetrex_data.event[i].ask === 1 ) {
|
||||
TAlertManager.showConfirmAlert( $.i18n._( timetrex_data.event[i].text ), $.i18n._( 'Redirect Confirmation' ), ( flag ) => {
|
||||
if ( flag === true ) {
|
||||
if ( timetrex_data.event[i].target && timetrex_data.event[i].target === '_blank' ) {
|
||||
window.open(
|
||||
timetrex_data.event[i].link,
|
||||
'_blank'
|
||||
);
|
||||
} else {
|
||||
window.location = timetrex_data.event[i].link;
|
||||
}
|
||||
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
if ( timetrex_data.event[i].target && timetrex_data.event[i].target === '_blank' ) {
|
||||
window.open(
|
||||
timetrex_data.event[i].link,
|
||||
'_blank'
|
||||
);
|
||||
} else {
|
||||
window.location = timetrex_data.event[i].link;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'refresh_job_queue':
|
||||
this.event_bus.emit( 'tt_topbar', 'toggle_job_queue_spinner', {
|
||||
//Boolean events for job queue spinner.
|
||||
show: timetrex_data.event[i].show, //Show the job queue spinner
|
||||
get_job_data: timetrex_data.event[i].get_job_data, //Update job queue panel data
|
||||
check_completed: timetrex_data.event[i].check_completed //Check if job queue is completed and hide the job queue spinner if no pending tasks.
|
||||
} );
|
||||
|
||||
//Update TimeSheet is user is on it.
|
||||
if ( LocalCacheData.current_open_primary_controller && LocalCacheData.current_open_primary_controller.viewId === 'TimeSheet' ) {
|
||||
LocalCacheData.current_open_primary_controller.search();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openViewLinkedToNotification( event_data ) {
|
||||
//Open a view with the option of pre-filling fields.
|
||||
LocalCacheData.setAutoFillData( event_data.data );
|
||||
// This is taking them to listview in some cases so a on a onAdd/onEdit click can be clicked afterwards.
|
||||
IndexViewController.goToViewByViewLabel( event_data.view_name );
|
||||
|
||||
// Ignore edit only views that don't have list views.
|
||||
if ( event_data.view_name !== 'InOut' && event_data.view_name !== 'Contact Information' ) {
|
||||
// Need to add the promise before onTabShow is called where it's originally intended to be added otherwise the below wait is not triggered.
|
||||
TTPromise.add( 'BaseViewController', 'onTabShow' );
|
||||
TTPromise.wait( 'BaseViewController', 'onTabShow', function() {
|
||||
if ( event_data.action === 'add' ) {
|
||||
LocalCacheData.current_open_primary_controller.onAddClick();
|
||||
} else if ( event_data.action === 'edit' ) {
|
||||
LocalCacheData.current_open_primary_controller.onEditClick( event_data.view_id );
|
||||
} else if ( event_data.action === 'view' ) {
|
||||
LocalCacheData.current_open_primary_controller.onViewClick( event_data.view_id );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
setNotificationDeniedCookie( cookie_value ) {
|
||||
cookie_value.asked_count++;
|
||||
cookie_value.last_asked = new Date().getTime();
|
||||
|
||||
setCookie( 'disable_push_notification_ask', JSON.stringify( cookie_value ), 10000, APIGlobal.pre_login_data.cookie_base_url );
|
||||
}
|
||||
|
||||
deleteNotificationDeniedCookie() {
|
||||
deleteCookie( 'disable_push_notification_ask' );
|
||||
}
|
||||
|
||||
getNotificationDeniedCookie() {
|
||||
if ( getCookie( 'disable_push_notification_ask' ) ) {
|
||||
return JSON.parse( getCookie( 'disable_push_notification_ask' ) );
|
||||
}
|
||||
|
||||
var cookie_value = {};
|
||||
cookie_value.asked_count = 0;
|
||||
cookie_value.last_asked = 0;
|
||||
|
||||
return cookie_value;
|
||||
}
|
||||
|
||||
showNotificationPermissionAlert() {
|
||||
//Only ever ask twice to enable notification permissions for this browser.
|
||||
//If we have only asked once before and it has been 180 days since then, ask again.
|
||||
var notification_denied_cookie = this.getNotificationDeniedCookie();
|
||||
if ( notification_denied_cookie.asked_count > 1 || notification_denied_cookie.last_asked + ( 180 * 24 * 60 * 60 * 1000 ) > new Date().getTime() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TAlertManager.showModalAlert( 'push_notification', 'ask', ( flag ) => {
|
||||
if ( flag === true ) {
|
||||
this.showPermissionHelp();
|
||||
this.setUserPreferencePushNotification( 1 );
|
||||
this.sendAnalytics( 'allow' );
|
||||
} else {
|
||||
this.setUserPreferencePushNotification( 0 );
|
||||
this.sendAnalytics( 'deny' );
|
||||
this.setNotificationDeniedCookie( notification_denied_cookie );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
showPermissionHelp() {
|
||||
this.registerWorkerAndGetToken( true );
|
||||
this.createNotificationListeners();
|
||||
|
||||
TAlertManager.showModalAlert( 'push_notification', 'wait_for_permission', ( flag ) => {
|
||||
if ( flag === true ) {
|
||||
TAlertManager.showModalAlert( 'push_notification', 'help_text', '' );
|
||||
this.showArrowToEnablePushNotifications();
|
||||
this.sendAnalytics( 'unsure' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
showArrowToEnablePushNotifications() {
|
||||
const arrow = $( '<div class="permission-arrow">' +
|
||||
'<img style="display: block; margin-left: auto; margin-right: auto;" src="' + Global.getRealImagePath( 'images/notification-arrow.svg' ) + '" width="150" height="150!">' +
|
||||
'</div>' );
|
||||
|
||||
$( '.modal-alert' ).append( arrow );
|
||||
}
|
||||
|
||||
setUserPreferencePushNotification( status ) {
|
||||
if ( LocalCacheData.getLoginUser() && LocalCacheData.getLoginUserPreference() ) {
|
||||
var data = {};
|
||||
|
||||
data.user_id = LocalCacheData.getLoginUser().id;
|
||||
data.id = LocalCacheData.getLoginUserPreference().id;
|
||||
data.browser_permission_ask_date = Math.round( new Date().getTime() / 1000 );
|
||||
//If user agrees set notifications to enabled. Else only set last browser_permission_ask_date.
|
||||
if ( status === 1 ) {
|
||||
data.notification_status_id = status;
|
||||
}
|
||||
|
||||
this.user_preference_api.setUserPreference( data, {
|
||||
onResult: function( res ) {
|
||||
let result = res.getResult();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
sendAnalytics( choice ) {
|
||||
Global.sendAnalyticsEvent( 'push_notifications', 'click', 'click:push_notifications:' + choice );
|
||||
}
|
||||
|
||||
getUnreadNotifications() {
|
||||
this.notification_api.getUnreadNotifications( {
|
||||
onResult: ( result ) => {
|
||||
this.notification_total = parseInt( result.getResult() );
|
||||
this.updateBell( false, this.notification_total );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
getSystemNotifications( target ) {
|
||||
this.notification_api.getSystemNotification( target, {
|
||||
onResult: ( result ) => {
|
||||
var new_system_notifications = parseInt( result.getResult() );
|
||||
|
||||
if ( new_system_notifications > 0 ) {
|
||||
this.updateBell( true, this.notification_total + new_system_notifications );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
setNotificationAsRead( id ) {
|
||||
this.notification_api.setNotificationStatus( [id], 20, {
|
||||
onResult: ( result ) => {
|
||||
this.updateBell( true, this.notification_total - 1 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
updateBell( manual, amount ) {
|
||||
if ( manual ) {
|
||||
this.notification_total = amount;
|
||||
}
|
||||
|
||||
this.event_bus.emit( 'tt_topbar', 'notification_bell', {
|
||||
notification_count: this.notification_total
|
||||
});
|
||||
}
|
||||
|
||||
playSound() {
|
||||
// Only load notification sound when we first need it. Then reuse from then on.
|
||||
if ( this.notification_sound === null ) {
|
||||
this.notification_sound = new Audio();
|
||||
this.notification_sound.src = Global.getBaseURL( '../' ) + 'sounds/notification.mp3';
|
||||
this.notification_sound.load();
|
||||
}
|
||||
|
||||
const playPromise = this.notification_sound.play();
|
||||
if ( playPromise !== undefined ) { //Older browsers play() does not return anything.
|
||||
playPromise.then( () => {
|
||||
//Notification audio is playing.
|
||||
} )
|
||||
.catch( error => {
|
||||
console.log( error );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
startTimers( notification ) {
|
||||
var $this = this;
|
||||
if ( document.hasFocus() === false && this.sound_timer === null ) {
|
||||
//Change page title and repeat notififation sound if page does not have focus and no timer is already set.
|
||||
this.sound_timer = setInterval( function() {
|
||||
if ( document.hasFocus() ) {
|
||||
//If tab is in focus cancel the timer.
|
||||
$this.cancelTimers();
|
||||
} else {
|
||||
$this.playSound();
|
||||
}
|
||||
}, 5000 );
|
||||
|
||||
this.title_timer = setInterval( function() {
|
||||
if ( document.hasFocus() ) {
|
||||
//If tab is in focus cancel the timer.
|
||||
$this.cancelTimers();
|
||||
} else {
|
||||
if ( document.title === '!!!!!!!!!!!!!' ) {
|
||||
document.title = $.i18n._( 'NOTICE!' );
|
||||
} else {
|
||||
document.title = '!!!!!!!!!!!!!';
|
||||
}
|
||||
}
|
||||
}, 2000 );
|
||||
}
|
||||
}
|
||||
|
||||
cancelTimers() {
|
||||
if ( this.sound_timer !== null ) {
|
||||
clearInterval( this.sound_timer );
|
||||
this.sound_timer = null;
|
||||
}
|
||||
|
||||
if ( this.title_timer !== null ) {
|
||||
document.title = this.previous_page_title;
|
||||
clearInterval( this.title_timer );
|
||||
this.title_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
showNotification( title, body, url, notification_id, priority, target ) {
|
||||
if ( priority == 10 ) {
|
||||
//User is not notified nor receives toast for low priority notifications.
|
||||
return false;
|
||||
}
|
||||
|
||||
//All notifications other than low play notification sound.
|
||||
this.playSound();
|
||||
|
||||
// To stop infinite stacking notifications on screen we only show up to 5 at a time.
|
||||
// Allow high and critical priority notifications with priority 1 or 2 through.
|
||||
if ( this.notification_on_screen_total >= 5 && priority > 2 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notification = document.createElement( 'div' );
|
||||
notification.className = 'toast show toast-spacing';
|
||||
notification.style = 'width: 22rem; background-color: hsla(0, 0%, 100%, 1) !important; margin-bottom: 0.4rem !important;';
|
||||
|
||||
const notification_header = document.createElement( 'div' );
|
||||
notification_header.className = 'toast-header';
|
||||
|
||||
const notification_title = document.createElement( 'strong' );
|
||||
notification_title.className = 'mr-auto';
|
||||
notification_title.textContent = title;
|
||||
|
||||
const notification_close_button = document.createElement( 'button' );
|
||||
notification_close_button.className = 'ml-2 mb-1 close';
|
||||
notification_close_button.setAttribute( 'aria-label', 'close' );
|
||||
notification_close_button.innerHTML = '<span class="notification-close" aria-hidden="true">×</span>';
|
||||
|
||||
const notification_body = document.createElement( 'div' );
|
||||
notification_body.className = 'toast-body';
|
||||
|
||||
const notification_body_text = document.createElement( 'p' );
|
||||
notification_body_text.textContent = body;
|
||||
|
||||
const notification_body_link = document.createElement( 'a' );
|
||||
notification_body_link.textContent = 'View Details';
|
||||
notification_body_link.id = notification_id;
|
||||
notification_body_link.className = 'notification-link';
|
||||
if ( url !== undefined && url !== '' ) {
|
||||
notification_body_link.href = url;
|
||||
} else {
|
||||
notification_body_link.href = Global.getBaseURL() + '#!m=Notification';
|
||||
}
|
||||
|
||||
if ( target === '_blank' ) {
|
||||
notification_body_link.target = '_blank';
|
||||
}
|
||||
|
||||
notification_header.appendChild( notification_title );
|
||||
notification_header.appendChild( notification_close_button );
|
||||
notification.appendChild( notification_header );
|
||||
|
||||
notification_body.appendChild( notification_body_text );
|
||||
notification_body.appendChild( notification_body_link );
|
||||
notification.appendChild( notification_body );
|
||||
|
||||
this.notification_holder.appendChild( notification );
|
||||
|
||||
this.notification_on_screen_total++;
|
||||
|
||||
if ( priority == 2 ) {
|
||||
//High priority notifications flash border twice around notification.
|
||||
notification.className += ' notification-outline-repeat';
|
||||
} else if ( priority == 1 ) {
|
||||
//Critical priority notification repeat the notification sound, change document title and continuously flash border around toast.
|
||||
this.startTimers( notification );
|
||||
notification.className += ' notification-outline-infinite';
|
||||
}
|
||||
|
||||
if ( this.notification_duration !== 0 && priority != 1 ) {
|
||||
// User notification preferences with 0 delay or critical notifications with priority 1 are never automatically removed.
|
||||
setTimeout( () => {
|
||||
this.removeNotification( notification );
|
||||
}, this.notification_duration );
|
||||
}
|
||||
}
|
||||
|
||||
removeNotification( notification ) {
|
||||
if ( notification ) {
|
||||
this.notification_on_screen_total--;
|
||||
notification.remove();
|
||||
}
|
||||
}
|
||||
|
||||
removeNotificationById( id ) {
|
||||
var notification_link = document.getElementById( id );
|
||||
if ( notification_link ) {
|
||||
this.removeNotification( notification_link.parentNode.parentNode );
|
||||
}
|
||||
}
|
||||
|
||||
removeAllNotifications() {
|
||||
var notifications = document.querySelectorAll( ".toast" );
|
||||
for ( var i = 0; i < notifications.length; i++ ) {
|
||||
this.removeNotification( notifications[i] );
|
||||
}
|
||||
this.cancelTimers();
|
||||
}
|
||||
|
||||
detectBrowserNeedsExtraPermission() {
|
||||
// Some browsers by default block push notification permission so we need to detect them to show user a different prompt
|
||||
if ( Global.getBrowserVendor() === 'Edge' ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const NotificationConsumerObj = new NotificationConsumer();
|
||||
@@ -0,0 +1,716 @@
|
||||
import { ResponseObject } from '@/model/ResponseObject';
|
||||
import { TTUUID } from '@/global/TTUUID';
|
||||
import { APIReturnHandler } from '@/model/APIReturnHandler';
|
||||
|
||||
export class ServiceCaller extends Backbone.Model {
|
||||
constructor() {
|
||||
$.xhrPool = [];
|
||||
super();
|
||||
}
|
||||
|
||||
getMessageId() {
|
||||
if ( this.message_id ) {
|
||||
return this.message_id
|
||||
} else {
|
||||
this.setMessageId( TTUUID.generateUUID() );
|
||||
return this.message_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setMessageId( value ) {
|
||||
this.message_id = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getIsIdempotent() {
|
||||
if ( this.is_idempotent ) {
|
||||
return this.is_idempotent
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsIdempotent( value ) {
|
||||
this.is_idempotent = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
argumentsHandler() {
|
||||
var className = arguments[0];
|
||||
var function_name = arguments[1];
|
||||
var apiArgsAndResponseObject = arguments[2];
|
||||
var lastApiArgsAndResponseObject = arguments[2][( apiArgsAndResponseObject.length - 1 )];
|
||||
var apiArgs = {};
|
||||
var responseObject;
|
||||
var len;
|
||||
|
||||
if ( Global.isSet( lastApiArgsAndResponseObject.onResult ) || Global.isSet( lastApiArgsAndResponseObject.async ) ) {
|
||||
len = ( apiArgsAndResponseObject.length - 1 );
|
||||
|
||||
responseObject = new ResponseObject( lastApiArgsAndResponseObject );
|
||||
|
||||
} else {
|
||||
len = apiArgsAndResponseObject.length;
|
||||
responseObject = null;
|
||||
}
|
||||
|
||||
for ( var i = 0; i < len; i++ ) {
|
||||
apiArgs[i] = apiArgsAndResponseObject[i];
|
||||
|
||||
if ( i === 0 && len === 1 &&
|
||||
Global.isSet( apiArgs[i] ) &&
|
||||
Global.isSet( apiArgs[i].second_parameter ) ) {
|
||||
apiArgs[1] = apiArgs[i].second_parameter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.call( className, function_name, responseObject, apiArgs );
|
||||
}
|
||||
|
||||
getOptionsCacheKey( api_args, key ) {
|
||||
|
||||
$.each( api_args, function( index, value ) {
|
||||
|
||||
if ( $.type( value ) === 'object' ) {
|
||||
key = key + '_' + JSON.stringify( value );
|
||||
} else {
|
||||
key = key + '_' + value;
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return key;
|
||||
|
||||
}
|
||||
|
||||
repeatAPICall( className, function_name, apiArgs, responseObject ) {
|
||||
let params = Object.values( JSON.parse( apiArgs.json ) );
|
||||
TTAPI[className][function_name]( ...params, responseObject.attributes );
|
||||
}
|
||||
|
||||
uploadFile( form_data, paramaters, responseObj ) {
|
||||
var message_id = this.getMessageId();
|
||||
ProgressBar.showProgressBar( message_id );
|
||||
|
||||
//On IE 9
|
||||
if ( typeof FormData == 'undefined' ) {
|
||||
form_data.attr( 'method', 'POST' );
|
||||
form_data.attr( 'action', ServiceCaller.getURLByObjectType( 'upload' ) + '?' + paramaters + '&' + Global.getSessionIDKey() + '=' + LocalCacheData.getSessionID() );
|
||||
form_data.attr( 'enctype', 'multipart/form-data' );
|
||||
|
||||
ProgressBar.changeProgressBarMessage( 'File Uploading' );
|
||||
form_data.ajaxForm().ajaxSubmit( {
|
||||
success: function( result ) {
|
||||
if ( result && result.toString().toLocaleLowerCase() !== 'true' ) {
|
||||
TAlertManager.showAlert( result );
|
||||
}
|
||||
ProgressBar.removeProgressBar();
|
||||
if ( responseObj.onResult ) {
|
||||
responseObj.onResult( result );
|
||||
}
|
||||
|
||||
}
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
ProgressBar.changeProgressBarMessage( 'File Uploading' );
|
||||
$.ajax( {
|
||||
url: ServiceCaller.getURLByObjectType( 'upload' ) + '?' + paramaters + '&' + Global.getSessionIDKey() + '=' + LocalCacheData.getSessionID(), //Server script to process data
|
||||
headers: {
|
||||
//Handle CSRF tokens and related headers here.
|
||||
'X-Client-ID': 'Browser-TimeTrex',
|
||||
'X-CSRF-Token': getCookie( 'CSRF-Token' ),
|
||||
},
|
||||
type: 'POST',
|
||||
|
||||
// xhr: function() { // Custom XMLHttpRequest
|
||||
// var myXhr = $.ajaxSettings.xhr();
|
||||
// if ( myXhr.upload ) { // Check if upload property exists
|
||||
// myXhr.upload.addEventListener( 'progress', progressHandlingFunction, false ); // For handling the progress of the upload
|
||||
// }
|
||||
//
|
||||
// function progressHandlingFunction() {
|
||||
// }
|
||||
//
|
||||
// return myXhr;
|
||||
//
|
||||
// },
|
||||
|
||||
success: function( result ) {
|
||||
if ( result && result.toString().toLocaleLowerCase() !== 'true' ) {
|
||||
TAlertManager.showAlert( result );
|
||||
}
|
||||
|
||||
if ( responseObj.onResult ) {
|
||||
responseObj.onResult( result );
|
||||
}
|
||||
|
||||
ProgressBar.removeProgressBar();
|
||||
},
|
||||
// Form data
|
||||
data: form_data,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
prettyPrintAPIArguments( apiArgs ) {
|
||||
if ( apiArgs && apiArgs.json ) {
|
||||
var retval = [];
|
||||
var args = JSON.parse( apiArgs.json );
|
||||
for ( var property_name in args ) {
|
||||
var arg = args[property_name];
|
||||
retval.push( JSON.stringify( arg, null, 2 ) ); //Pretty print JSON
|
||||
}
|
||||
|
||||
return retval.join( ', ' );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getCache( cache_key, responseObject, function_name, apiArgs ) {
|
||||
let result = LocalCacheData.result_cache[cache_key];
|
||||
//Debug.Arr(result, 'Response from cached result. Key: '+cache_key, 'ServiceCaller.js', 'ServiceCaller', 'call', 10);
|
||||
|
||||
let apiReturnHandler = new APIReturnHandler();
|
||||
|
||||
apiReturnHandler.set( 'result_data', result );
|
||||
apiReturnHandler.set( 'delegate', responseObject.get( 'delegate' ) );
|
||||
apiReturnHandler.set( 'function_name', function_name );
|
||||
apiReturnHandler.set( 'args', apiArgs );
|
||||
|
||||
if ( responseObject.get( 'onResult' ) ) {
|
||||
responseObject.get( 'onResult' )( apiReturnHandler );
|
||||
}
|
||||
|
||||
return apiReturnHandler;
|
||||
};
|
||||
|
||||
isCachableFunction( function_name ) {
|
||||
let is_cachable = false;
|
||||
|
||||
switch ( function_name ) {
|
||||
case 'getOptions':
|
||||
case 'isBranchAndDepartmentAndJobAndJobItemAndPunchTagEnabled':
|
||||
case 'getUserGroup':
|
||||
case 'getJobGroup':
|
||||
case 'getJobItemGroup':
|
||||
case 'getProductGroup':
|
||||
case 'getDocumentGroup':
|
||||
case 'getQualificationGroup':
|
||||
case 'getKPIGroup':
|
||||
case 'getHierarchyControlOptions':
|
||||
is_cachable = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return is_cachable;
|
||||
}
|
||||
|
||||
call( className, function_name, responseObject, apiArgs ) {
|
||||
var $this = this;
|
||||
var message_id;
|
||||
var base_url = ServiceCaller.getAPIURL( 'Class=' + className + '&Method=' + function_name + '&v=2' );
|
||||
var url = base_url;
|
||||
if ( LocalCacheData.getAllURLArgs() ) {
|
||||
if ( LocalCacheData.getAllURLArgs().hasOwnProperty( 'user_id' ) ) {
|
||||
url = url + '&user_id=' + LocalCacheData.getAllURLArgs().user_id;
|
||||
}
|
||||
if ( LocalCacheData.getAllURLArgs().hasOwnProperty( 'company_id' ) ) {
|
||||
url = url + '&company_id=' + LocalCacheData.getAllURLArgs().company_id;
|
||||
}
|
||||
}
|
||||
if ( Global.getStationID() ) {
|
||||
url = url + '&StationID=' + Global.getStationID();
|
||||
}
|
||||
|
||||
var apiReturnHandler;
|
||||
var async;
|
||||
|
||||
if ( responseObject && responseObject.get( 'async' ) === false ) {
|
||||
async = responseObject.get( 'async' );
|
||||
} else {
|
||||
async = true;
|
||||
}
|
||||
var cache_key;
|
||||
switch ( function_name ) {
|
||||
case 'getOptions':
|
||||
case 'isBranchAndDepartmentAndJobAndJobItemAndPunchTagEnabled':
|
||||
case 'getHierarchyControlOptions':
|
||||
case 'getUserGroup':
|
||||
case 'getJobGroup':
|
||||
case 'getJobItemGroup':
|
||||
case 'getProductGroup':
|
||||
case 'getDocumentGroup':
|
||||
case 'getQualificationGroup':
|
||||
case 'getKPIGroup':
|
||||
|
||||
if ( function_name === 'getUserGroup' ) {
|
||||
cache_key = className + '.' + 'userGroup';
|
||||
} else if ( function_name === 'getJobGroup' ) {
|
||||
cache_key = className + '.' + 'jobGroup';
|
||||
} else if ( function_name === 'getJobItemGroup' ) {
|
||||
cache_key = className + '.' + 'jobItemGroup';
|
||||
} else if ( function_name === 'getProductGroup' ) {
|
||||
cache_key = className + '.' + 'productGroup';
|
||||
} else if ( function_name === 'getDocumentGroup' ) {
|
||||
cache_key = className + '.' + 'documentGroup';
|
||||
} else if ( function_name === 'getQualificationGroup' ) {
|
||||
cache_key = className + '.' + 'qualificationGroup';
|
||||
} else if ( function_name === 'getKPIGroup' ) {
|
||||
cache_key = className + '.' + 'kPIGroup';
|
||||
} else if ( function_name === 'getHierarchyControlOptions' ) {
|
||||
cache_key = 'getHierarchyControlOptions';
|
||||
} else {
|
||||
cache_key = this.getOptionsCacheKey( apiArgs, className + '.' + function_name );
|
||||
}
|
||||
if ( responseObject.get( 'noCache' ) === true ) {
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
}
|
||||
|
||||
if ( cache_key && LocalCacheData.result_cache[cache_key] ) {
|
||||
//Use a promise to help prevent identical calls from being made before the first one returns and sets the cache.
|
||||
if ( LocalCacheData.result_cache[cache_key].pending ) {
|
||||
TTPromise.add( 'ServiceCaller', cache_key );
|
||||
TTPromise.wait( 'ServiceCaller', cache_key, function() {
|
||||
this.getCache( cache_key, responseObject, function_name, apiArgs );
|
||||
}.bind( this ) );
|
||||
} else {
|
||||
return this.getCache( cache_key, responseObject, function_name, apiArgs );
|
||||
}
|
||||
|
||||
return apiReturnHandler;
|
||||
|
||||
}
|
||||
break;
|
||||
case 'setUserGroup':
|
||||
case 'deleteUserGroup':
|
||||
cache_key = className + '.' + 'userGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setJobGroup':
|
||||
case 'deleteJobGroup':
|
||||
cache_key = className + '.' + 'jobGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setJobItemGroup':
|
||||
case 'deleteJobItemGroup':
|
||||
cache_key = className + '.' + 'jobItemGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setProductGroup':
|
||||
case 'deleteProductGroup':
|
||||
cache_key = className + '.' + 'productGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setDocumentGroup':
|
||||
case 'deleteDocumentGroup':
|
||||
cache_key = className + '.' + 'documentGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setQualificationGroup':
|
||||
case 'deleteQualificationGroup':
|
||||
cache_key = className + '.' + 'qualificationGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
case 'setKPIGroup':
|
||||
case 'deleteKPIGroup':
|
||||
cache_key = className + '.' + 'kPIGroup';
|
||||
LocalCacheData.result_cache[cache_key] = false;
|
||||
break;
|
||||
}
|
||||
|
||||
message_id = this.getMessageId();
|
||||
|
||||
TTPromise.add( 'ServiceCaller', message_id );
|
||||
|
||||
if ( className !== 'APIProgressBar' && function_name !== 'Logout' ) {
|
||||
url = url + '&MessageID=' + message_id;
|
||||
}
|
||||
|
||||
if ( this.getIsIdempotent() == true ) {
|
||||
url = url + '&idempotent=1';
|
||||
}
|
||||
|
||||
if ( ServiceCaller.extra_url ) {
|
||||
url = url + ServiceCaller.extra_url;
|
||||
}
|
||||
|
||||
if ( !apiArgs ) {
|
||||
apiArgs = {};
|
||||
|
||||
}
|
||||
|
||||
apiArgs = { json: JSON.stringify( apiArgs ) };
|
||||
|
||||
//Try to get a stack trace for each function call so if an error occurs we know exactly what triggered the call.
|
||||
var stack_trace_str = null;
|
||||
if ( typeof Error !== 'undefined' ) {
|
||||
var stack_trace = ( new Error() );
|
||||
if ( typeof stack_trace === 'object' && stack_trace.stack && typeof stack_trace.stack === 'string' ) {
|
||||
stack_trace_str = stack_trace.stack.split( '\n' ); //This is eventually JSONified so convert it to an array for better formatting.
|
||||
} else {
|
||||
stack_trace_str = null;
|
||||
}
|
||||
stack_trace = null; // Previously null was 'delete' but not valid in JS strict mode.
|
||||
}
|
||||
|
||||
var api_called_date = new Date();
|
||||
var api_stack = {
|
||||
api: className + '.' + function_name,
|
||||
args: apiArgs.json,
|
||||
message_id: this.getMessageId(),
|
||||
api_called_date: api_called_date.toISOString(),
|
||||
stack_trace: stack_trace_str
|
||||
};
|
||||
stack_trace_str = null; // Previously null was 'delete' but not valid in JS strict mode.
|
||||
|
||||
if ( LocalCacheData.api_stack.length === 16 ) {
|
||||
LocalCacheData.api_stack.pop();
|
||||
}
|
||||
|
||||
if ( function_name !== 'sendErrorReport' ) {
|
||||
LocalCacheData.api_stack.unshift( api_stack );
|
||||
}
|
||||
|
||||
if ( className !== 'APIProgressBar' && function_name !== 'Login' && function_name !== 'getPreLoginData' && function_name !== 'listenForMultiFactorAuthentication' ) {
|
||||
ProgressBar.showProgressBar( message_id );
|
||||
}
|
||||
|
||||
if ( this.isCachableFunction( function_name ) === true ) {
|
||||
LocalCacheData.result_cache[cache_key] = { pending: true };
|
||||
}
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
dataType: 'JSON',
|
||||
data: apiArgs,
|
||||
headers: {
|
||||
//#1568 - Add "fragment" to POST variables in API calls so the server can get it...
|
||||
//Encoding is a must, otherwise HTTP requests will be corrupted on some web browsers (ie: Mobile Safari)
|
||||
//This caused the corrupted requests for things like: "POST_/api/json/api_php?Class"
|
||||
//Also it must use dashes instead of underscores for separators.
|
||||
'Request-Uri-Fragment': encodeURIComponent( LocalCacheData.fullUrlParameterStr ),
|
||||
|
||||
//Handle CSRF tokens and related headers here.
|
||||
'X-Client-ID': 'Browser-TimeTrex',
|
||||
'X-CSRF-Token': getCookie( 'CSRF-Token' ),
|
||||
},
|
||||
type: 'POST',
|
||||
async: async,
|
||||
url: url,
|
||||
beforeSend: function( jqXHR ) {
|
||||
$.ajax.request_start_time = Date.now();
|
||||
this.jqXHR = jqXHR;
|
||||
$.xhrPool.push( this ); //Track all pending AJAX requests so we can cancel them if needed.
|
||||
},
|
||||
complete: function( jqXHR ) {
|
||||
var index = $.xhrPool.indexOf( this );
|
||||
if ( index > -1 ) {
|
||||
$.xhrPool.splice( index, 1 ); //Remove completed AJAX request from pool.
|
||||
}
|
||||
|
||||
var request_total_time = ( Date.now() - $.ajax.request_start_time ); //milliseconds
|
||||
if ( request_total_time > 1000 ) { //Only log API calls that are slow.
|
||||
if ( typeof ( gtag ) !== 'undefined' && APIGlobal.pre_login_data.analytics_enabled === true ) {
|
||||
gtag( 'event', 'api_call', {
|
||||
class: className,
|
||||
method: function_name,
|
||||
response_time: request_total_time
|
||||
} );
|
||||
Debug.Text( 'AJAX Response: Class: ' + className + ' Method: ' + function_name + ' Time: ' + request_total_time + 'ms', 'ServiceCaller.js', 'ServiceCaller', 'complete', 11 );
|
||||
}
|
||||
}
|
||||
},
|
||||
success: function( result ) {
|
||||
//Debug.Arr(result, 'Response from API. message_id: '+ message_id, 'ServiceCaller.js', 'ServiceCaller', null, 10);
|
||||
|
||||
//Resets message_id so it changes on the next API call. Only do this on success, so idempotent requests that error out don't get a new key on the next call.
|
||||
// FIXME: async API calls on the same api object can conflict with one another though.
|
||||
// Take for instance onFormItemChange() triggering async api.Validate*(), when api.set*() is called, the idempotent=1 can be enabled for the validation with the same key.
|
||||
// Then the set*() might incorrectly return the result from the validate()
|
||||
// This is partially fixed by ignoring idempotency on all validate*() calls in the API, which it probably should anyways. However we need a proper fix for this in JS.
|
||||
$this.setMessageId( null );
|
||||
|
||||
if ( Global.enable_api_tracing == true ) {
|
||||
var api_trace_label = '%cAPI Request:%c ' + className + '->' + function_name + '(...) [Expand for Details]';
|
||||
console.groupCollapsed( api_trace_label, 'font-weight: bold', 'font-weight: normal' );
|
||||
console.log( '%c' + className + '->' + function_name + '%c(' + $this.prettyPrintAPIArguments( apiArgs ) + ')', 'font-weight: bold', 'font-weight: normal' );
|
||||
|
||||
var api_trace_raw_request_label = '%cRaw Request:%c [Expand for Details]';
|
||||
console.groupCollapsed( api_trace_raw_request_label, 'font-weight: bold', 'font-weight: normal' );
|
||||
console.log( '%cURL:%c ' + url, 'font-weight: bold', 'font-weight: normal' );
|
||||
console.log( '%cRaw POST Body (non-URLEncoded):%c json=' + apiArgs.json + '', 'font-weight: bold', 'font-weight: normal' );
|
||||
console.log( '%ccURL Command:%c curl -k --location --request POST --cookie "' + Global.getSessionIDKey() + '=<SessionID>" --form \'json=' + apiArgs.json + '\' "' + base_url + '"', 'font-weight: bold', 'font-weight: normal' );
|
||||
console.groupEnd( api_trace_raw_request_label );
|
||||
|
||||
var api_trace_response_label = '%cResponse:%c [Expand for Details]';
|
||||
console.groupCollapsed( api_trace_response_label, 'font-weight: bold', 'font-weight: normal' );
|
||||
console.log( JSON.stringify( result, null, 2 ) );
|
||||
console.groupEnd( api_trace_response_label );
|
||||
|
||||
console.groupEnd( api_trace_label );
|
||||
api_trace_raw_request_label = null; // Previously null was 'delete' but not valid in JS strict mode.
|
||||
api_trace_response_label = null; // Previously null was 'delete' but not valid in JS strict mode.
|
||||
api_trace_label = null; // Previously null was 'delete' but not valid in JS strict mode.
|
||||
}
|
||||
|
||||
if ( !Global.isSet( result ) ) {
|
||||
result = true;
|
||||
}
|
||||
if ( className !== 'APIProgressBar' && function_name !== 'Login' && function_name !== 'getPreLoginData' && function_name !== 'listenForMultiFactorAuthentication' ) {
|
||||
ProgressBar.removeProgressBar( message_id );
|
||||
}
|
||||
|
||||
apiReturnHandler = new APIReturnHandler();
|
||||
apiReturnHandler.set( 'result_data', result );
|
||||
apiReturnHandler.set( 'delegate', responseObject.get( 'delegate' ) );
|
||||
apiReturnHandler.set( 'function_name', function_name );
|
||||
apiReturnHandler.set( 'args', apiArgs );
|
||||
|
||||
if ( !apiReturnHandler.isValid() && ( apiReturnHandler.getCode() === 'EXCEPTION' || apiReturnHandler.getCode() === 'EXCEPTION_CSRF' ) ) {
|
||||
Debug.Text( 'api-exception: Code: ' + apiReturnHandler.getCode() + ' Error: ' + apiReturnHandler.getDescription() +' Message ID: '+ message_id, 'ServiceCaller.js', 'ServiceCaller', null, 10);
|
||||
if ( apiReturnHandler.getCode() === 'EXCEPTION_CSRF' ) { //Don't bother recording CSRF exceptions.
|
||||
Global.sendAnalyticsEvent( 'service-caller', 'error:api-exception', 'api-exception: Code: ' + apiReturnHandler.getCode() + ' Error: ' + apiReturnHandler.getDescription() );
|
||||
TAlertManager.showAlert( apiReturnHandler.getDescription(), 'Error', function() {
|
||||
window.location.reload();
|
||||
} );
|
||||
} else {
|
||||
Global.sendErrorReport( 'api-exception: Code: ' + apiReturnHandler.getCode() + ' Error: ' + apiReturnHandler.getDescription(), 'ServiceCaller.js' );
|
||||
TAlertManager.showAlert( $.i18n._( 'API Exception' ) + ': ' + apiReturnHandler.getDescription(), 'Error' );
|
||||
}
|
||||
|
||||
//Error: Uncaught ReferenceError: promise_key is not defined
|
||||
if ( typeof promise_key != 'undefined' ) {
|
||||
TTPromise.reject( 'ServiceCaller', message_id );
|
||||
} else {
|
||||
Debug.Text( 'ERROR: Unable to release promise because key is NULL.', 'ServiceCaller.js', 'ServiceCaller', null, 10 );
|
||||
}
|
||||
return;
|
||||
} else if ( !apiReturnHandler.isValid() && apiReturnHandler.getCode() === 'SESSION' ) {
|
||||
//Debug.Text('API returned session expired: '+ message_id, 'ServiceCaller.js', 'ServiceCaller', null, 10);
|
||||
Global.Logout(); //clearSessionCookie() in Logout() helps skip other API calls or prevent the UI from thinking we are still logged in.
|
||||
ServiceCaller.cancel_all_error = true;
|
||||
LocalCacheData.login_error_string = $.i18n._( 'Session expired, please login again.' );
|
||||
if ( window.location.href == Global.getBaseURL() + '#!m=' + 'Login' ) {
|
||||
// Prevent a partially loaded login screen when SessionID cookie is set but not valid on server.
|
||||
// However if the session is expired on the server, and the user tries to navigate to some other page,
|
||||
// there could be multiple API calls queued up, which causes this reload() to be triggered many times,
|
||||
// and network requests to be aborted, which triggers error messages. Disable the reload for now as in theory it shouldn't be needed.
|
||||
// This reload also gets rid of the "Session expired, please login again" error message, which is not ideal.
|
||||
//window.location.reload();
|
||||
} else {
|
||||
var paths = Global.getBaseURL().replace( ServiceCaller.root_url, '' ).split( '/' );
|
||||
if ( paths.indexOf( 'quick_punch' ) > 0 ) {
|
||||
Global.setURLToBrowser( Global.getBaseURL() + '#!m=' + 'QuickPunchLogin' );
|
||||
} else if ( paths.indexOf( 'portal' ) > 0 ) {
|
||||
if ( LocalCacheData.getAllURLArgs().company_id ) {
|
||||
LocalCacheData.setPortalLoginUser( null );
|
||||
Global.setURLToBrowser( Global.getBaseURL() + '#!m=PortalJobVacancy&company_id=' + LocalCacheData.getAllURLArgs().company_id );
|
||||
}
|
||||
} else {
|
||||
if ( !LocalCacheData.getAllURLArgs().company_id ) {
|
||||
Global.setURLToBrowser( Global.getBaseURL() + '#!m=' + 'Login' );
|
||||
}
|
||||
}
|
||||
}
|
||||
TTPromise.resolve( 'ServiceCaller', message_id );
|
||||
return;
|
||||
} else if ( !apiReturnHandler.isValid() && apiReturnHandler.getCode() === 'DOWN_FOR_MAINTENANCE' ) {
|
||||
Global.sendAnalyticsEvent( 'service-caller', 'error:down-for-maintenance', 'error:down-for-maintenance: Code: ' + apiReturnHandler.getCode() + ' Error: ' + apiReturnHandler.getDescription() );
|
||||
|
||||
//Before the location.replace because after that point we can't be sure of execution.
|
||||
TTPromise.resolve( 'ServiceCaller', message_id );
|
||||
//replace instead of assignment to ensure that the DOWN_FOR_MAINTENANCE page does not end up in the back button history.
|
||||
window.location.replace( ServiceCaller.root_url + LocalCacheData.loginData.base_url + 'html5/DownForMaintenance.php?exception=DOWN_FOR_MAINTENANCE' );
|
||||
return;
|
||||
} else if ( apiReturnHandler.getCode() === 'REAUTHENTICATE' ) {
|
||||
let session_data = apiReturnHandler.getResult();
|
||||
Global.showAuthenticationModal( LocalCacheData.current_open_primary_controller.viewId, session_data.session_type, session_data.mfa, true, ( result ) => {
|
||||
Debug.Text( 'User Reauthenticated: ' + result, 'ServiceCaller.js', 'ServiceCaller', 'call', 10 );
|
||||
Global.hideAuthenticationModal();
|
||||
|
||||
//After authentication is complete, reattempt the API call automatically so that the user does not need to click "Save" or repeat the action.
|
||||
$this.repeatAPICall( className, function_name, apiArgs, responseObject )
|
||||
} );
|
||||
|
||||
TTPromise.resolve( 'ServiceCaller', message_id );
|
||||
return;
|
||||
} else {
|
||||
//Debug.Text('API returned result: '+ message_id, 'ServiceCaller.js', 'ServiceCaller', null, 10);
|
||||
|
||||
//only cache data when api return is successful and can be trusted (ie not logged out or session expired.)
|
||||
if ( $this.isCachableFunction( function_name ) === true ) {
|
||||
LocalCacheData.result_cache[cache_key] = result;
|
||||
TTPromise.resolve( 'ServiceCaller', cache_key );
|
||||
}
|
||||
|
||||
//Error: Function expected in /interface/html5/services/ServiceCaller.js?v=9.0.0-20150822-090205 line 269
|
||||
if ( responseObject.get( 'onResult' ) && typeof ( responseObject.get( 'onResult' ) ) == 'function' ) {
|
||||
responseObject.get( 'onResult' )( apiReturnHandler );
|
||||
}
|
||||
}
|
||||
|
||||
TTPromise.resolve( 'ServiceCaller', message_id );
|
||||
},
|
||||
|
||||
error: function( jqXHR, textStatus, errorThrown ) {
|
||||
TTPromise.reject( 'ServiceCaller', message_id );
|
||||
if ( className !== 'APIProgressBar' && function_name !== 'Login' && function_name !== 'getPreLoginData' && function_name !== 'listenForMultiFactorAuthentication' ) {
|
||||
ProgressBar.removeProgressBar( message_id );
|
||||
}
|
||||
|
||||
if ( $this.isCachableFunction( function_name ) === true && LocalCacheData.result_cache[cache_key] && LocalCacheData.result_cache[cache_key].pending ) {
|
||||
//Issue #3185 - getOptions() calls were not rejecting promises when an error occurred.
|
||||
//Such as when the factory did not have unique_columns in _getFactoryOptions.
|
||||
delete LocalCacheData.result_cache[cache_key];
|
||||
TTPromise.reject( 'ServiceCaller', cache_key );
|
||||
}
|
||||
|
||||
if ( ServiceCaller.cancel_all_error ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Text( 'AJAX Request Error: ' + errorThrown + ' Message: ' + textStatus + ' HTTP Code: ' + jqXHR.status, 'ServiceCaller.js', 'ServiceCaller', 'call', 10 );
|
||||
if ( jqXHR.responseText && jqXHR.responseText.indexOf( 'User not authenticated' ) >= 0 ) {
|
||||
ServiceCaller.cancel_all_error = true;
|
||||
|
||||
LocalCacheData.login_error_string = $.i18n._( 'Session timed out, please login again.' );
|
||||
|
||||
Global.clearSessionCookie();
|
||||
//$.cookie( 'SessionID', null, {expires: 30, path: LocalCacheData.cookie_path} );
|
||||
Global.setURLToBrowser( Global.getBaseURL() + '#!m=' + 'Login' );
|
||||
|
||||
return;
|
||||
|
||||
} else {
|
||||
if ( jqXHR.responseText && $.type( jqXHR.responseText ) === 'string' ) {
|
||||
TAlertManager.showNetworkErrorAlert( jqXHR, textStatus, errorThrown );
|
||||
}
|
||||
}
|
||||
|
||||
if ( jqXHR.status === 200 && !jqXHR.responseText ) {
|
||||
apiReturnHandler = new APIReturnHandler();
|
||||
apiReturnHandler.set( 'result_data', true );
|
||||
apiReturnHandler.set( 'delegate', responseObject.get( 'delegate' ) );
|
||||
apiReturnHandler.set( 'function_name', function_name );
|
||||
apiReturnHandler.set( 'args', apiArgs );
|
||||
|
||||
if ( responseObject.get( 'onResult' ) ) {
|
||||
responseObject.get( 'onResult' )( apiReturnHandler );
|
||||
}
|
||||
return apiReturnHandler;
|
||||
} else {
|
||||
if ( jqXHR.status === 0 || ( jqXHR.status >= 400 && jqXHR.status <= 599 ) ) {
|
||||
//Status=0 (No response from server at all), 4xx/5xx is critical server failure.
|
||||
//Server can't respond properly due to 4xx/5xx error code, so display a message to the user. Can't redirect to down_for_maintenance page as that could be a 404 as well.
|
||||
TAlertManager.showNetworkErrorAlert( jqXHR, textStatus, errorThrown );
|
||||
ProgressBar.cancelProgressBar();
|
||||
}
|
||||
|
||||
if ( responseObject.get( 'onError' ) && typeof ( responseObject.get( 'onError' ) ) == 'function' ) {
|
||||
responseObject.get( 'onError' )( apiReturnHandler );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return apiReturnHandler;
|
||||
}
|
||||
}
|
||||
|
||||
ServiceCaller.getAPIURL = function( rest_url ) {
|
||||
return ServiceCaller.base_url + ServiceCaller.base_api_url + '?' + rest_url;
|
||||
};
|
||||
|
||||
ServiceCaller.getURLByObjectType = function( object_type ) {
|
||||
var append_csrf = false;
|
||||
var append_cache_buster = false;
|
||||
|
||||
var retval = null;
|
||||
|
||||
var base_url = ServiceCaller.base_url + 'interface/send_file.php?api=1';
|
||||
|
||||
switch ( object_type.toLowerCase() ) {
|
||||
case 'upload':
|
||||
retval = ServiceCaller.base_url + 'interface/upload_file.php'
|
||||
append_csrf = false;
|
||||
break;
|
||||
case 'import_csv_example':
|
||||
retval = ServiceCaller.base_url + 'interface/html5/views/wizard/import_csv/'
|
||||
append_csrf = false;
|
||||
break;
|
||||
case 'file_download':
|
||||
retval = base_url; //Must allow for appending '&object_type=...' on the end.
|
||||
append_csrf = true;
|
||||
break;
|
||||
case 'company_logo':
|
||||
retval = base_url + '&object_type=company_logo';
|
||||
append_csrf = true;
|
||||
append_cache_buster = true;
|
||||
break;
|
||||
case 'invoice_config':
|
||||
retval = base_url + '&object_type=invoice_config';
|
||||
append_csrf = true;
|
||||
break;
|
||||
case 'user_photo':
|
||||
retval = base_url + '&object_type=user_photo';
|
||||
append_csrf = true;
|
||||
break;
|
||||
|
||||
case 'primary_company_logo':
|
||||
retval = base_url + '&object_type=primary_company_logo';
|
||||
break;
|
||||
case 'smcopyright':
|
||||
retval = base_url + '&object_type=smcopyright';
|
||||
break;
|
||||
case 'copyright':
|
||||
retval = base_url + '&object_type=copyright';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
//Append CSRF-Token.
|
||||
if ( append_csrf == true ) {
|
||||
retval += '&X-CSRF-Token=' + getCookie( 'CSRF-Token' );
|
||||
}
|
||||
|
||||
if ( append_cache_buster == true ) {
|
||||
retval += '&t=' + new Date().getTime();
|
||||
}
|
||||
|
||||
return retval;
|
||||
};
|
||||
|
||||
//Abort in-flight AJAX calls on logout.
|
||||
ServiceCaller.abortAll = function() {
|
||||
$.each( $.xhrPool, function( index, ajax_obj ) {
|
||||
if ( typeof ajax_obj == 'object' && ajax_obj.jqXHR && typeof ajax_obj.jqXHR == 'object' && typeof ajax_obj.jqXHR.abort === 'function' ) {
|
||||
if ( ajax_obj.url && ajax_obj.url.indexOf( 'Method=Logout' ) == -1 ) { //Don't abort the Logout call.
|
||||
Debug.Text( ' Aborting API call: ' + ajax_obj.url, 'ServiceCaller.js', 'ServiceCaller', 'abortAll', 10 );
|
||||
ajax_obj.jqXHR.abort();
|
||||
} else {
|
||||
Debug.Text( 'Not aborting Logout API call...', 'ServiceCaller.js', 'ServiceCaller', 'abortAll', 10 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
ServiceCaller.base_url = null;
|
||||
ServiceCaller.base_api_url = null;
|
||||
ServiceCaller.root_url = null;
|
||||
ServiceCaller.cancel_all_error = false;
|
||||
ServiceCaller.extra_url = false;
|
||||
@@ -0,0 +1,191 @@
|
||||
import mitt from 'mitt';
|
||||
const EventBus = mitt();
|
||||
|
||||
//Issue #3049 - Moved class static fields outside of main TTEventBus class as Safari v14.1 and older do not support class field declarations.
|
||||
window.TTEventBusStatics = { AUTO_CLEAR_ON_EXIT: true, mitt: EventBus, _events_by_listener_scope: {} }; //Constants and external libraries
|
||||
//_events_by_listener_scope - Internal data element, only to be accessed/changed via functions in this class. Static as its shared across scope_id's.
|
||||
|
||||
/**
|
||||
* How to understand the ID's used in this class.
|
||||
* this.scope_id: is tied to the owner of the instance of TTEventBus.
|
||||
* so that they can be removed when the owner of that scope is unloaded/unmounted.
|
||||
* mitt_event_id: is passed to the mitt event library. It uses the event scope id rather than stored instance scope_id,
|
||||
* because listeners within a view/component might listen to different scope_id's depending on event.
|
||||
* unique_listener_id: is only needed for debugging currently, to be able to differentiate between two different listeners
|
||||
* listening to the same event on the same scope, but using different event handler functions.
|
||||
*
|
||||
*
|
||||
*/
|
||||
class TTEventBus {
|
||||
// Standard mitt calls we want to expose for backwards compatibility to our old code.
|
||||
// static on = TTEventBusStatics.mitt.on;
|
||||
// static off = TTEventBusStatics.mitt.off;
|
||||
|
||||
constructor( options = {} ) {
|
||||
// TTEventBus will happily works for both Views and Vue Components using a single id variable, but tracking them both might be more useful in the future.
|
||||
this._options = options; // Unlikely to use directly, but will store here for debugging and future options.
|
||||
this.scope_id = null; //scope_id of the listening view or component. Not neccessarily the scope of an event. When this scope unloads, we want to clear listener events related to that scope.
|
||||
this._setInstanceScopeId( this.generateScopeIdFromOptions( options ) );
|
||||
Debug.Text( 'constructor called ('+ this.scope_id +').', 'TTEventBus.js', 'TTEventBus', 'constructor', 11 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope is created depending on availability of view id and component id. In most cases, the standard is to either use the view_id or component_id, to use both would overcomplicate storage of the components id's in the views..
|
||||
* @param options
|
||||
* @returns {string|boolean}
|
||||
*/
|
||||
generateScopeIdFromOptions( options ) {
|
||||
// E.g. Schedule.vue-schedule-control-bar
|
||||
// var scope_string = '';
|
||||
// if( options?.view_id ) {
|
||||
// scope_string += options.view_id;
|
||||
// }
|
||||
// if( options?.component_id ) {
|
||||
// if( scope_string !== '' ) { scope_string += '.' }
|
||||
// scope_string += options.component_id;
|
||||
// }
|
||||
//
|
||||
// return scope_string;
|
||||
if ( options && options.view_id && options.component_id ) {
|
||||
Debug.Warn( 'Are you sure you want to set both view and component id? This complicates things.', 'TTEventBus.js', 'TTEventBus', 'generateScopeIdFromOptions', 2 );
|
||||
return options.view_id + '.' + options.component_id;
|
||||
} else if ( options && options.view_id ) {
|
||||
return options.view_id;
|
||||
} else if ( options && options.component_id ) {
|
||||
return options.component_id;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't call this directly, as we need to generate the scope_id via generateScopeFromOptions first.
|
||||
* @param scope_id
|
||||
* @private
|
||||
*/
|
||||
_setInstanceScopeId( scope_id ) {
|
||||
return this.scope_id = scope_id;
|
||||
}
|
||||
|
||||
getInstanceScopeId() {
|
||||
return this.scope_id;
|
||||
}
|
||||
|
||||
generateMittId( scope_id, event_id ) {
|
||||
// E.g. Schedule.vue-schedule-control-bar.scheduleModeOnChange
|
||||
return scope_id + '.' + event_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event that should only last for that view/vue component and be removed when scope is destroyed/unloaded.
|
||||
* @param event_scope The scope_id related to the event.
|
||||
* @param event_id id of the event, should be unique within the provided scope.
|
||||
* @param event_handler Function to call when event is triggered.
|
||||
* @param auto_clear_on_exit Specifies if this event should not be auto cleared when the vue/component is unloaded. Set using TTEventBusStatics.AUTO_CLEAR_ON_EXIT
|
||||
*/
|
||||
on( event_scope, event_id, event_handler, auto_clear_on_exit ) {
|
||||
TTEventBusStatics._events_by_listener_scope[ this.scope_id ] = TTEventBusStatics._events_by_listener_scope[ this.scope_id ] || [];
|
||||
|
||||
// If we want unique ID's then use TTUUID.generateUUID(), but we want unique to a scope, so that duplicates can be prevented.
|
||||
var mitt_event_id = this.generateMittId( event_scope, event_id);
|
||||
var unique_listener_id = this.scope_id + ':' + mitt_event_id + ':' + TTUUID.generateUUID();
|
||||
|
||||
TTEventBusStatics._events_by_listener_scope[ this.scope_id ].push( {
|
||||
unique_listener_id: unique_listener_id,
|
||||
mitt_event_id: mitt_event_id,
|
||||
event_scope: event_scope,
|
||||
event_id: event_id,
|
||||
event_handler: event_handler,
|
||||
auto_clear_on_exit: auto_clear_on_exit
|
||||
} );
|
||||
TTEventBusStatics.mitt.on( mitt_event_id, event_handler );
|
||||
Debug.Text( this.scope_id + ': Listener created for ('+ unique_listener_id +').', 'TTEventBus.js', 'TTEventBus', 'on', 11 );
|
||||
|
||||
return unique_listener_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger EventBus event, but converts the scope_id and event_id into the mitt event id that the event is registered with.
|
||||
* @param event_scope The scope_id related to the event.
|
||||
* @param event_id id of the event, should be unique within the provided scope.
|
||||
* @param event_data Object containing event data as parameters.
|
||||
*/
|
||||
emit( event_scope, event_id, event_data ) {
|
||||
var mitt_event_id = this.generateMittId( event_scope, event_id);
|
||||
Debug.Text( this.scope_id + ': Event emitted for ('+ mitt_event_id +').', 'TTEventBus.js', 'TTEventBus', 'emit', 11 );
|
||||
|
||||
return TTEventBusStatics.mitt.emit( mitt_event_id, event_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: UNFINISHED.
|
||||
* TODO: Improve this by adding ability to remove by scope and name, or scope, name and callback, or by unique ID.
|
||||
* Warning: This will remove all events that match the scope_id and event_id, even if there are multiple.
|
||||
* @param scope_id the scope of the event that needs to be switched off.
|
||||
* @param event_id the event_id of the event tyhat needs to be switched off.
|
||||
* @returns {void|number}
|
||||
*/
|
||||
off( scope_id, event_id ) {
|
||||
var scope_array = TTEventBusStatics._events_by_listener_scope[ scope_id ];
|
||||
if( scope_array === undefined ) {
|
||||
// scope_id not found.
|
||||
Debug.Error( 'Error: invalid params passed. scope_id not found.', 'TTEventBus.js', 'EventBus', 'off', 1 );
|
||||
return -1;
|
||||
}
|
||||
var removeIndex = scope_array.map( item => item.event_id ).indexOf( event_id ); // TODO: Will only match the FIRST found, problem for multiple listeners like in Schedule.scheduleModeOnChange
|
||||
if( removeIndex >= 0 ) {
|
||||
var stored_event = scope_array[ removeIndex ];
|
||||
scope_array.splice(removeIndex, 1);
|
||||
Debug.Text( this.scope_id + ': Listener removed for ('+ stored_event.mitt_event_id +').', 'TTEventBus.js', 'EventBus', 'off', 11 );
|
||||
|
||||
return TTEventBusStatics.mitt.off( stored_event.mitt_event_id, stored_event.event_handler );
|
||||
} else {
|
||||
// event_id not found in array.
|
||||
Debug.Error( 'Error: invalid params passed. event_id not found.', 'TTEventBus.js', 'EventBus', 'off', 1 );
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to trigger allOff() when unloading a view/component, using stored scope_id.
|
||||
* @returns {number|boolean}
|
||||
*/
|
||||
autoClear() {
|
||||
Debug.Text( 'Auto off triggered for ('+ this.scope_id +').', 'TTEventBus.js', 'EventBus', 'autoClear', 11 );
|
||||
return this.allOff( this.scope_id );
|
||||
}
|
||||
/**
|
||||
* This removes all events registered on the given scope. This will only apply to events that have the AUTO_CLEAR_ON_EXIT flag.
|
||||
* @param scope_id
|
||||
*/
|
||||
allOff( scope_id ) {
|
||||
var scope_array = TTEventBusStatics._events_by_listener_scope[ scope_id ];
|
||||
if( scope_array === undefined ) {
|
||||
// scope_id not found.
|
||||
Debug.Text( 'Scope not found. But could be normal if this is a global function triggered on a scope with no events.', 'TTEventBus.js', 'EventBus', 'allOff', 2 );
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
//Loop in reverse to easily remove array values.
|
||||
for ( let i = scope_array.length - 1; i >= 0; i-- ) {
|
||||
if ( scope_array[i].auto_clear_on_exit ) {
|
||||
// Remove event
|
||||
TTEventBusStatics.mitt.off( scope_array[i].mitt_event_id, scope_array[i].event_handler );
|
||||
Debug.Text( 'Auto removed ' + scope_array[i].mitt_event_id + ' event on scope close.', 'TTEventBus.js', 'EventBus', 'allOff', 2 );
|
||||
scope_array.splice( i, 1 );
|
||||
} else {
|
||||
Debug.Text( 'Event does not have AUTO_CLEAR_ON_EXIT. Skipping ' + scope_array[i].mitt_event_id, 'TTEventBus.js', 'EventBus', 'allOff', 2 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !TTEventBusStatics._events_by_listener_scope[ scope_id ] || TTEventBusStatics._events_by_listener_scope[ scope_id ].length === 0 ) {
|
||||
//Remove empty scope array.
|
||||
delete TTEventBusStatics._events_by_listener_scope[ scope_id ];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default TTEventBus;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file houses any common utils that will be used by Vue.
|
||||
* Similar to Global.js but class based, and Vue specific.
|
||||
*/
|
||||
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import main_ui_router from '@/components/main_ui_router';
|
||||
import PrimeVue from 'primevue/config';
|
||||
|
||||
class TTVueUtils {
|
||||
constructor() {
|
||||
this._dynamic_vue_components = {};
|
||||
}
|
||||
|
||||
mountComponent( mount_id, mount_component, root_props ) {
|
||||
if( mount_id === undefined ) {
|
||||
Debug.Error( 'Error: Invalid parameters passed to function.', 'TTVueUtils.js', 'TTVueUtils', 'mountComponent', 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
if( document.getElementById( mount_id ) === null ) {
|
||||
Debug.Error( 'Error: mount_id "'+ mount_id + '" does not exist in the DOM.', 'TTVueUtils.js', 'TTVueUtils', 'mountComponent', 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
if( this._dynamic_vue_components[ mount_id ] !== undefined ) {
|
||||
Debug.Error( 'Error: component ('+ mount_id +') already exists and mounted.', 'TTVueUtils.js', 'TTVueUtils', 'mountComponent', 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
root_props = root_props || {};
|
||||
root_props.component_id = root_props.component_id || mount_id;
|
||||
let mount_reference = '#' + mount_id;
|
||||
let vue_app_instance = createApp( mount_component, root_props ); // rootProps is useful to pass in data without the need for EventBus.
|
||||
|
||||
vue_app_instance.use( PrimeVue, { ripple: true, inputStyle: 'filled' }); // From: AppConfig.vue this.$primevue.config.inputStyle value is filled/outlined as we dont use AppConfig in TT.
|
||||
vue_app_instance.use( main_ui_router ); // #VueContextMenu# FIXES: Failed to resolve component: router-link when TTOverlayMenuButton is opened. Because each component is a separate Vue instance, and they did not globally 'use' the Router, only in main ui.
|
||||
let vue_component_instance = vue_app_instance.mount( mount_reference ); // e.g. '#tt-edit-view-test'
|
||||
|
||||
this._dynamic_vue_components[ mount_id ] = {
|
||||
mount_id: mount_id,
|
||||
_vue_app_instance: vue_app_instance, // Be very careful using these from outside Vue. Could make for messy code!
|
||||
_vue_component_instance: vue_component_instance // Be very careful using these from outside Vue. Could make for messy code!
|
||||
};
|
||||
|
||||
return this._dynamic_vue_components[ mount_id ];
|
||||
}
|
||||
unmountComponent ( mount_id ) {
|
||||
if( this._dynamic_vue_components[ mount_id ] && this._dynamic_vue_components[ mount_id ]._vue_component_instance ) {
|
||||
this._dynamic_vue_components[ mount_id ]._vue_app_instance.unmount();
|
||||
delete this._dynamic_vue_components[ mount_id ];
|
||||
Debug.Text( 'Component successfully unmounted ('+ mount_id +').', 'TTVueUtils.js', 'TTVueUtils', 'unmountComponent', 2 );
|
||||
return true;
|
||||
} else {
|
||||
Debug.Text( 'Unable to unmount component. Component not found ('+ mount_id +'). Maybe already removed?', 'TTVueUtils.js', 'TTVueUtils', 'unmountComponent', 2 );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new TTVueUtils() // Export this way to share one instance of the class across the app.
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ServiceCaller } from '@/services/ServiceCaller';
|
||||
|
||||
class TimeTrexClientAPI extends ServiceCaller {
|
||||
constructor( class_name, key_name ) {
|
||||
super();
|
||||
|
||||
this.className = class_name;
|
||||
|
||||
if ( !key_name ) {
|
||||
key_name = class_name.replace( 'API', '' );
|
||||
}
|
||||
this.key_name = key_name;
|
||||
|
||||
return this.enableNoSuchMethod( this );
|
||||
}
|
||||
|
||||
enableNoSuchMethod( obj ) {
|
||||
return new Proxy( obj, {
|
||||
get( target, property_key ) {
|
||||
if ( property_key in target ) {
|
||||
return target[property_key];
|
||||
} else if ( typeof target.__noSuchMethod__ == 'function' ) {
|
||||
return function( ...args ) {
|
||||
return target.__noSuchMethod__.call( target, property_key, args );
|
||||
};
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
TimeTrexClientAPI.prototype.__noSuchMethod__ = function( method_name, args ) {
|
||||
//Debug.Text('Magic Method: '+ method_name + ' Class: '+ this.service_caller.className +' Args: '+ args, 'TimeTrexClientAPI.js', 'TimeTrexClientAPI', '__noSuchMethod__', 11);
|
||||
return this.argumentsHandler( this.className, method_name, args );
|
||||
};
|
||||
|
||||
const tt_api_target = {};
|
||||
const tt_api_class_handler = {
|
||||
get( target, class_name ) {
|
||||
//Debug.Text('Proxy Handler: Class: ' + class_name, 'TimeTrexClientAPI.js', 'TimeTrexClientAPI', 'get', 11);
|
||||
return new TimeTrexClientAPI( class_name );
|
||||
},
|
||||
};
|
||||
|
||||
export const TTAPI = new Proxy( tt_api_target, tt_api_class_handler );
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
importScripts( '../dist/firebase-app.js' );
|
||||
importScripts( '../dist/firebase-messaging.js' );
|
||||
|
||||
var firebaseConfig = {
|
||||
apiKey: "AIzaSyB9tM0QYb1D3JF07RqpeG-14ADGhezGRws",
|
||||
authDomain: "timetrex-app.firebaseapp.com",
|
||||
databaseURL: "https://timetrex-app.firebaseio.com",
|
||||
projectId: "timetrex-app",
|
||||
storageBucket: "timetrex-app.appspot.com",
|
||||
messagingSenderId: "462133047262",
|
||||
appId: "1:462133047262:web:1705b6bfca364bcd99b74f"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
|
||||
firebase.initializeApp( firebaseConfig );
|
||||
|
||||
// Retrieve an instance of Firebase Messaging so that it can handle background messages.
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
messaging.onBackgroundMessage( function( payload ) {
|
||||
//Find an open client to send a background notification to.
|
||||
payload.messageType = 'background';
|
||||
self.clients.matchAll( { includeUncontrolled: true } ).then( function( clients ) {
|
||||
clients.forEach( function( client ) {
|
||||
client.postMessage( payload );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user