tina4ruby 0.5.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +360 -559
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +242 -77
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +43 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1336 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +484 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +337 -31
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +40 -4
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +314 -23
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +134 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +57 -21
- metadata +51 -19
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
var formToken = null;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sends an http request
|
|
5
|
-
* @param url
|
|
6
|
-
* @param request
|
|
7
|
-
* @param method
|
|
8
|
-
* @param callback
|
|
9
|
-
*/
|
|
10
|
-
function sendRequest(url, request, method, callback) {
|
|
11
|
-
// Default values
|
|
12
|
-
if (url === undefined) url = "";
|
|
13
|
-
if (request === undefined) request = null;
|
|
14
|
-
if (method === undefined) method = 'GET';
|
|
15
|
-
|
|
16
|
-
const xhr = new XMLHttpRequest();
|
|
17
|
-
xhr.open(method, url, true);
|
|
18
|
-
|
|
19
|
-
// Add authorization header if token exists
|
|
20
|
-
if (formToken !== null) {
|
|
21
|
-
xhr.setRequestHeader('Authorization', 'Bearer ' + formToken);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ────────────────────────────────────────────────
|
|
25
|
-
// Content-Type logic – only set when appropriate
|
|
26
|
-
// ────────────────────────────────────────────────
|
|
27
|
-
let isFormData = request instanceof FormData;
|
|
28
|
-
|
|
29
|
-
if (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
|
|
30
|
-
if (isFormData) {
|
|
31
|
-
//DO not touch this
|
|
32
|
-
} else if (typeof request === 'object' && request !== null) {
|
|
33
|
-
//Becomes a JSON String
|
|
34
|
-
request = JSON.stringify(request);
|
|
35
|
-
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
|
36
|
-
} else if (typeof request === 'string') {
|
|
37
|
-
// Already a string – assume JSON or let server decide
|
|
38
|
-
// You can set charset=UTF-8 if you're sure it's JSON/text
|
|
39
|
-
xhr.setRequestHeader('Content-Type', 'text/plain; charset=UTF-8');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ────────────────────────────────────────────────
|
|
44
|
-
// Response handling
|
|
45
|
-
// ────────────────────────────────────────────────
|
|
46
|
-
xhr.onload = function () {
|
|
47
|
-
let content = xhr.response;
|
|
48
|
-
|
|
49
|
-
// Update token if server sent a fresh one
|
|
50
|
-
const freshToken = xhr.getResponseHeader('FreshToken');
|
|
51
|
-
if (freshToken && freshToken !== '') {
|
|
52
|
-
formToken = freshToken;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
content = JSON.parse(content);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
// Not JSON → keep as raw string/text
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (typeof callback === 'function') {
|
|
62
|
-
callback(content, xhr.status, xhr);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Optional: handle errors
|
|
67
|
-
xhr.onerror = function () {
|
|
68
|
-
if (typeof callback === 'function') {
|
|
69
|
-
callback(null, xhr.status, xhr);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Send the body (or null)
|
|
74
|
-
xhr.send(request);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Gets form data based on a form Id
|
|
79
|
-
* @param formId
|
|
80
|
-
* @returns {FormData}
|
|
81
|
-
*/
|
|
82
|
-
function getFormData(formId) {
|
|
83
|
-
let data = new FormData();
|
|
84
|
-
let elements = document.querySelectorAll("#" + formId + " select, #" + formId + " input, #" + formId + " textarea");
|
|
85
|
-
for (let ie = 0; ie < elements.length; ie++ )
|
|
86
|
-
{
|
|
87
|
-
let element = elements[ie];
|
|
88
|
-
//refresh the token
|
|
89
|
-
if (element.name === 'formToken' && formToken !== null) {
|
|
90
|
-
element.value = formToken;
|
|
91
|
-
}
|
|
92
|
-
if (element.name) {
|
|
93
|
-
if (element.type === 'file') {
|
|
94
|
-
for (let i = 0; i < element.files.length; i++) {
|
|
95
|
-
let fileData = element.files[i];
|
|
96
|
-
let elementName = element.name;
|
|
97
|
-
if (fileData !== undefined) {
|
|
98
|
-
if (element.files.length > 1 && !elementName.includes('[')) {
|
|
99
|
-
elementName = elementName + '[]';
|
|
100
|
-
}
|
|
101
|
-
data.append(elementName, fileData, fileData.name);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} else if (element.type === 'checkbox' || element.type === 'radio') {
|
|
105
|
-
if (element.checked) {
|
|
106
|
-
data.append(element.name, element.value)
|
|
107
|
-
} else {
|
|
108
|
-
if (element.type !== 'radio') {
|
|
109
|
-
data.append(element.name, "0")
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
if (element.value === '') {
|
|
114
|
-
element.value = null;
|
|
115
|
-
}
|
|
116
|
-
data.append(element.name, element.value);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return data;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Handles the data returned from a request
|
|
125
|
-
* @param data
|
|
126
|
-
* @param targetElement
|
|
127
|
-
*/
|
|
128
|
-
function handleHtmlData(data, targetElement) {
|
|
129
|
-
//Strip out the scripts
|
|
130
|
-
if (data === "") return '';
|
|
131
|
-
const parser = new DOMParser();
|
|
132
|
-
const htmlData = parser.parseFromString(data.includes !== undefined && data.includes('<html>') ? data : '<body>'+data+'</body></html>', 'text/html');
|
|
133
|
-
const body = htmlData.querySelector('body');
|
|
134
|
-
const scripts = body.querySelectorAll('script');
|
|
135
|
-
// remove the script tags
|
|
136
|
-
body.querySelectorAll('script').forEach(script => script.remove());
|
|
137
|
-
|
|
138
|
-
if (targetElement !== null) {
|
|
139
|
-
if (body.children.length > 0) {
|
|
140
|
-
document.getElementById(targetElement).replaceChildren(...body.children);
|
|
141
|
-
} else {
|
|
142
|
-
document.getElementById(targetElement).replaceChildren(body.innerHTML);
|
|
143
|
-
}
|
|
144
|
-
if (scripts) {
|
|
145
|
-
scripts.forEach(script => {
|
|
146
|
-
const newScript = document.createElement("script");
|
|
147
|
-
newScript.type = 'text/javascript';
|
|
148
|
-
newScript.async = true;
|
|
149
|
-
newScript.textContent = script.innerText;
|
|
150
|
-
document.getElementById(targetElement).append(newScript);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
if (scripts) {
|
|
155
|
-
scripts.forEach(script => {
|
|
156
|
-
const newScript = document.createElement("script");
|
|
157
|
-
newScript.type = 'text/javascript';
|
|
158
|
-
newScript.async = true;
|
|
159
|
-
newScript.textContent = script.innerText;
|
|
160
|
-
document.body.append(newScript);
|
|
161
|
-
console.log(newScript);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return body.innerHTML;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return '';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Loads a page to a target html element
|
|
173
|
-
* @param loadURL
|
|
174
|
-
* @param targetElement
|
|
175
|
-
* @param callback
|
|
176
|
-
* @callback
|
|
177
|
-
*/
|
|
178
|
-
function loadPage(loadURL, targetElement, callback = null) {
|
|
179
|
-
if (targetElement === undefined) targetElement = 'content';
|
|
180
|
-
sendRequest(loadURL, null, "GET", function(data) {
|
|
181
|
-
let processedHTML = '';
|
|
182
|
-
if (document.getElementById(targetElement) !== null) {
|
|
183
|
-
processedHTML = handleHtmlData(data, targetElement);
|
|
184
|
-
} else {
|
|
185
|
-
if (callback) {
|
|
186
|
-
callback(data);
|
|
187
|
-
} else {
|
|
188
|
-
console.log('TINA4 - define targetElement or callback for loadPage', data);
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (callback) {
|
|
194
|
-
callback(processedHTML, data);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Shows a form from a URL in a target html element
|
|
201
|
-
* @param action
|
|
202
|
-
* @param loadURL
|
|
203
|
-
* @param targetElement
|
|
204
|
-
* @param callback
|
|
205
|
-
*/
|
|
206
|
-
function showForm(action, loadURL, targetElement, callback = null) {
|
|
207
|
-
if (targetElement === undefined) targetElement = 'form';
|
|
208
|
-
|
|
209
|
-
if (action === 'create') action = 'GET';
|
|
210
|
-
if (action === 'edit') action = 'GET';
|
|
211
|
-
if (action === 'delete') action = 'DELETE';
|
|
212
|
-
|
|
213
|
-
sendRequest(loadURL, null, action, function(data) {
|
|
214
|
-
let processedHTML = '';
|
|
215
|
-
if (data.message !== undefined) {
|
|
216
|
-
processedHTML = handleHtmlData ((data.message), targetElement);
|
|
217
|
-
} else {
|
|
218
|
-
if (document.getElementById(targetElement) !== null) {
|
|
219
|
-
processedHTML = handleHtmlData (data, targetElement);
|
|
220
|
-
} else {
|
|
221
|
-
if (callback) {
|
|
222
|
-
callback(data);
|
|
223
|
-
} else {
|
|
224
|
-
console.log('TINA4 - define targetElement or callback for showForm', data);
|
|
225
|
-
}
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (callback) {
|
|
231
|
-
callback(processedHTML);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Post URL posts data to a specific url
|
|
238
|
-
* @param url
|
|
239
|
-
* @param data
|
|
240
|
-
* @param targetElement
|
|
241
|
-
* @param callback
|
|
242
|
-
*/
|
|
243
|
-
function postUrl(url, data, targetElement, callback= null) {
|
|
244
|
-
sendRequest(url, data, 'POST', function(data) {
|
|
245
|
-
let processedHTML = '';
|
|
246
|
-
if (data.message !== undefined) {
|
|
247
|
-
processedHTML = handleHtmlData ((data.message), targetElement);
|
|
248
|
-
} else {
|
|
249
|
-
if (document.getElementById(targetElement) !== null) {
|
|
250
|
-
processedHTML = handleHtmlData (data, targetElement);
|
|
251
|
-
} else {
|
|
252
|
-
if (callback) {
|
|
253
|
-
callback(data);
|
|
254
|
-
} else {
|
|
255
|
-
console.log('TINA4 - define targetElement or callback for postUrl', data);
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (callback) {
|
|
262
|
-
callback(processedHTML,data)
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Saves a form to a POST end point
|
|
269
|
-
* @param formId
|
|
270
|
-
* @param targetURL
|
|
271
|
-
* @param targetElement
|
|
272
|
-
* @param callback - optional
|
|
273
|
-
*/
|
|
274
|
-
function saveForm(formId, targetURL, targetElement, callback = null) {
|
|
275
|
-
if (targetElement === undefined) targetElement = 'message';
|
|
276
|
-
//compile a data model
|
|
277
|
-
let data = getFormData(formId);
|
|
278
|
-
|
|
279
|
-
postUrl(targetURL, data, targetElement, callback);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Alias of saveForm
|
|
284
|
-
* @param formId
|
|
285
|
-
* @param targetURL
|
|
286
|
-
* @param targetElement
|
|
287
|
-
* @param callback
|
|
288
|
-
*/
|
|
289
|
-
function postForm(formId, targetURL, targetElement, callback = null){
|
|
290
|
-
saveForm(formId, targetURL, targetElement, callback)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Alias of saveForm
|
|
295
|
-
* @param formId
|
|
296
|
-
* @param targetURL
|
|
297
|
-
* @param targetElement
|
|
298
|
-
* @param callback
|
|
299
|
-
*/
|
|
300
|
-
function submitForm(formId, targetURL, targetElement, callback = null){
|
|
301
|
-
saveForm(formId, targetURL, targetElement, callback)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Shows a message
|
|
306
|
-
* @param message
|
|
307
|
-
*/
|
|
308
|
-
function showMessage(message) {
|
|
309
|
-
document.getElementById('message').innerHTML = '<div class="alert alert-info alert-dismissible fade show"><strong>Info</strong> ' + message + '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Set cookie
|
|
314
|
-
* @param name
|
|
315
|
-
* @param value
|
|
316
|
-
* @param days
|
|
317
|
-
*/
|
|
318
|
-
function setCookie(name, value, days) {
|
|
319
|
-
let expires = "";
|
|
320
|
-
if (days) {
|
|
321
|
-
let date = new Date();
|
|
322
|
-
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
323
|
-
expires = "; expires=" + date.toUTCString();
|
|
324
|
-
}
|
|
325
|
-
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Get cookie
|
|
330
|
-
* @param name
|
|
331
|
-
* @returns {null|string}
|
|
332
|
-
*/
|
|
333
|
-
function getCookie(name) {
|
|
334
|
-
let nameEQ = name + "=";
|
|
335
|
-
let ca = document.cookie.split(';');
|
|
336
|
-
for (let i = 0; i < ca.length; i++) {
|
|
337
|
-
var c = ca[i];
|
|
338
|
-
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
|
339
|
-
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
//https://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen
|
|
345
|
-
const popupCenter = ({url, title, w, h}) => {
|
|
346
|
-
// Fixes dual-screen position Most browsers Firefox
|
|
347
|
-
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX;
|
|
348
|
-
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY;
|
|
349
|
-
|
|
350
|
-
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
|
351
|
-
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
|
352
|
-
|
|
353
|
-
const systemZoom = width / window.screen.availWidth;
|
|
354
|
-
const left = (width - w) / 2 / systemZoom + dualScreenLeft
|
|
355
|
-
const top = (height - h) / 2 / systemZoom + dualScreenTop
|
|
356
|
-
const newWindow = window.open(url, title,
|
|
357
|
-
`
|
|
358
|
-
directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,
|
|
359
|
-
width=${w / systemZoom},
|
|
360
|
-
height=${h / systemZoom},
|
|
361
|
-
top=${top},
|
|
362
|
-
left=${left}
|
|
363
|
-
`
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
if (window.focus) newWindow.focus();
|
|
367
|
-
return newWindow;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Opens a popup window
|
|
372
|
-
* @param pdfReportPath
|
|
373
|
-
*/
|
|
374
|
-
function openReport(pdfReportPath){
|
|
375
|
-
if (pdfReportPath.indexOf("No data available") < 0){
|
|
376
|
-
open(pdfReportPath, "content", "target=_blank, toolbar=no, scrollbars=yes, resizable=yes, width=800, height=600, top=0, left=0");
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
window.alert("Sorry , unable to print a report according to your selection!");
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function getRoute(loadURL, callback) {
|
|
384
|
-
sendRequest(loadURL, null, 'GET', function(data) {
|
|
385
|
-
callback(handleHtmlData (data, null));
|
|
386
|
-
});
|
|
387
|
-
}
|