-
Notifications
You must be signed in to change notification settings - Fork 104
/
api.service.js
306 lines (284 loc) · 10.2 KB
/
api.service.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
import $ from 'jquery';
/**
* Singleton class
* Handle Semantic-ui api functionality throughout the app.
*/
class ApiService {
static getInstance() {
return this.instance;
}
constructor() {
if (!this.instance) {
this.instance = this;
this.afterSuccessCallbacks = [];
}
return this.instance;
}
/**
* Execute js code.
* This function should be call using .call() by
* passing proper context for 'this'.
* ex: apiService.evalResponse.call(this, code, jQuery)
* By passig the jQuery reference, $ var use by code that need to be eval
* will work just fine, even if $ is not assign globally.
*
* @param code // javascript to be eval.
* @param $ // reference to jQuery.
*/
evalResponse(code, $) { // eslint-disable-line
eval(code); // eslint-disable-line
}
/**
* Setup semantic-ui api callback with this service.
* @param settings
*/
setService(settings) {
// settings.onResponse = this.handleResponse;
settings.successTest = this.successTest;
settings.onFailure = this.onFailure;
settings.onSuccess = this.onSuccess;
settings.onAbort = this.onAbort;
}
onAbort(message) {
console.warn(message);
}
/**
* Handle a server response success
* If successTest return true, then this function is call;
* Within this function "this" is place in proper context
* and allow us to properly eval the response.
* Furthermore, the dom element responsible of the api call is returned if needed.
*
* Change in response object property from eval to atkjs.
* Under certain circumstance, response.eval was run and execute prior to onSuccess eval,
* thus causing some code to be running twice.
* To avoid conflict, property name in response was change from eval to atkjs.
* Which mean response.atkjs now contains code to be eval.
*
* @param response
* @param element
*/
onSuccess(response, element) {
let result;
try {
if (response.success) {
if (response && response.html && response.id) {
// prevent modal duplication.
// apiService.removeModalDuplicate(response.html);
const modalIDs = [];
$(response.html).find('.ui.modal[id]').each((i, e) => {
modalIDs.push('#' + $(e).attr('id'));
});
if (modalIDs.length) {
$('.ui.dimmer.modals.page').find(modalIDs.join(', ')).remove();
}
result = $('#' + response.id).replaceWith(response.html);
if (!result.length) {
// TODO Find a better solution for long term.
// Need a way to gracefully abort server request.
// when user cancel a request by selecting another request.
console.error('Unable to replace element with id: ' + response.id);
// throw({message:'Unable to replace element with id: '+ response.id});
}
}
if (response && response.portals) {
// Create app portal from json response.
const portals = Object.keys(response.portals);
portals.forEach((portalID) => {
const m = $('.ui.dimmer.modals.page, .atk-side-panels').find('#' + portalID);
if (m.length === 0) {
$(document.body).append(response.portals[portalID].html);
atk.apiService.evalResponse(response.portals[portalID].js, jQuery);
}
});
}
if (response && response.atkjs) {
// Call evalResponse with proper context, js code and jQuery as $ var.
atk.apiService.evalResponse.call(this, response.atkjs, jQuery);
}
if (atk.apiService.afterSuccessCallbacks.length > 0) {
const self = this;
const callbacks = atk.apiService.afterSuccessCallbacks;
callbacks.forEach((callback) => {
atk.apiService.evalResponse.call(self, callback, jQuery);
});
atk.apiService.afterSuccessCallbacks.splice(0);
}
} else if (response.isServiceError) {
// service can still throw an error
throw ({ message: response.message }); // eslint-disable-line
}
} catch (e) {
atk.apiService.showErrorModal(atk.apiService.getErrorHtml(e.message));
}
}
/**
* Will wrap semantic ui api call into a Promise.
* Can be used to retrieve json data from the server.
* Using this will bypass regular successTest i.e. any
* atkjs (javascript) return from server will not be evaluated.
*
* Make sure to control the server output when using
* this function. It must at least return {success: true} in order for
* the Promise to resolve properly, will reject otherwise.
*
* ex: $app->terminateJson(['success' => true, 'data' => $data]);
*
* @param url // the url to fetch data
* @param settings // the Semantic api settings object.
* @param el // the element to apply Semantic Ui context.
*
* @returns {Promise<any>}
*/
suiFetch(url, settings = {}, el = 'body') {
const $el = $(el);
const apiSettings = Object.assign(settings);
if (!('on' in apiSettings)) {
apiSettings.on = 'now';
}
if (!('method' in apiSettings)) {
apiSettings.method = 'get';
}
apiSettings.url = url;
return new Promise((resolve, reject) => {
apiSettings.onFailure = function (r) {
atk.apiService.onFailure(r);
reject(r);
};
apiSettings.onSuccess = function (r, e) {
resolve(r);
};
$el.api(apiSettings);
});
}
/**
* Accumulate callbacks function to run after onSuccess.
* Callback is a string containing code to be eval.
*
* @param callback
*/
onAfterSuccess(callback) {
this.afterSuccessCallbacks.push(callback);
}
/**
* Check server response and clear api.data object.
* - return true will call onSuccess
* - return false will call onFailure
* @param response
* @returns {boolean}
*/
successTest(response) {
this.data = {};
if (response.success) {
return true;
}
return false;
}
/**
* Make our own ajax request test if need to.
* if a plugin must call $.ajax or $.getJson directly instead of semantic-ui api,
* we could send the json response to this.
* @param response
* @param content
*/
atkSuccessTest(response, content = null) {
if (response.success) {
this.onSuccess(response, content);
} else {
this.onFailure(response);
}
}
/**
* Handle a server response failure.
*
* @param response
*/
onFailure(response) {
// if json is returned, it should contains the error within message property
if (Object.prototype.hasOwnProperty.call(response, 'success') && !response.success) {
if (Object.prototype.hasOwnProperty.call(response, 'useWindow') && response.useWindow) {
atk.apiService.showErrorWindow(response.message);
} else {
atk.apiService.showErrorModal(response.message);
}
} else {
// check if we have html returned by server with <body> content.
const body = response.match(/<body[^>]*>[\s\S]*<\/body>/gi);
if (body) {
atk.apiService.showErrorModal(body);
} else {
atk.apiService.showErrorModal(response);
}
}
}
/**
* Display App error in a semantic-ui modal.
* @param errorMsg
*/
showErrorModal(errorMsg) {
// catch application error and display them in a new modal window.
const m = $('<div>')
.appendTo('body')
.addClass('ui scrolling modal')
.css('padding', '1em')
.html(errorMsg);
m.modal({
duration: 100,
allowMultiple: false,
onHide: function () {
m.children().remove();
return true;
},
})
.modal('show')
.modal('refresh');
}
/**
* Display App error in a separate window.
* @param errorMsg
*/
showErrorWindow(errorMsg) {
const error = $('<div class="atk-exception">')
.css({
padding: '8px',
'background-color': 'rgba(0, 0, 0, 0.5)',
margin: 'auto',
width: '100%',
height: '100%',
position: 'fixed',
top: 0,
bottom: 0,
'z-index': '100000',
'overflow-y': 'scroll',
})
.html($('<div>')
.css({
width: '70%',
'margin-top': '4%',
'margin-bottom': '4%',
'margin-left': 'auto',
'margin-right': 'auto',
background: 'white',
padding: '4px',
'overflow-x': 'scroll',
}).html(errorMsg)
.prepend($('<i class="ui big close icon"></i>').css('float', 'right').click(function () {
const $this = $(this).parents('.atk-exception');
$this.hide();
$this.remove();
})));
error.appendTo('body');
}
getErrorHtml(error) {
return `<div class="ui negative icon message">
<i class="warning sign icon"></i>
<div class="content">
<div class="header">Javascript Error</div>
<div>${error}</div>
</div>
</div>`;
}
}
const apiService = new ApiService();
Object.freeze(apiService);
export default apiService;