alchemy-chimera 1.3.0-alpha.3 → 1.3.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.
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +297 -0
- package/assets/scripts/chimera/chimera.js +12 -0
- package/assets/stylesheets/chimera/chimera.scss +308 -0
- package/config/routes.js +38 -0
- package/controller/00-chimera_controller.js +36 -6
- package/controller/chimera_editor_controller.js +53 -9
- package/controller/chimera_settings_controller.js +1 -0
- package/controller/chimera_static_controller.js +163 -2
- package/controller/system_task_controller.js +224 -0
- package/element/chimera_dashboard_button.js +100 -0
- package/element/chimera_run_task_button.js +93 -0
- package/element/chimera_task_monitor.js +672 -0
- package/lib/chimera_config.js +94 -0
- package/lib/toolbar_buttons.js +78 -0
- package/model/00_chimera_model.js +11 -0
- package/model/chimera_dashboard_config_model.js +44 -0
- package/package.json +4 -4
- package/view/chimera/dashboard.hwk +4 -3
- package/view/chimera/editor/task_monitor.hwk +32 -0
- package/view/chimera/sidebar.hwk +0 -0
- package/view/chimera/toolbar/customize_dashboard_button.hwk +26 -0
- package/view/chimera/toolbar/monitor_button.hwk +8 -0
- package/view/chimera/toolbar/reset_dashboard_button.hwk +26 -0
- package/view/chimera/toolbar/run_task_button.hwk +25 -0
- package/view/elements/chimera_task_monitor.hwk +80 -0
- package/view/layouts/chimera_body.hwk +46 -4
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
5
5
|
* @since 0.1.0
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.3.0
|
|
7
7
|
*/
|
|
8
8
|
let ChimeraController = Function.inherits('Alchemy.Controller', 'Alchemy.Controller.Chimera', function Chimera(conduit, options) {
|
|
9
9
|
|
|
10
10
|
Chimera.super.call(this, conduit, options);
|
|
11
11
|
|
|
12
|
-
// Set the theme
|
|
13
|
-
if (alchemy.plugins.chimera.theme) {
|
|
12
|
+
// Set the theme (only for HTTP conduits, not WebSocket connections)
|
|
13
|
+
if (alchemy.plugins.chimera.theme && !conduit.websocket) {
|
|
14
14
|
this.view_render.setTheme(alchemy.plugins.chimera.theme);
|
|
15
15
|
}
|
|
16
16
|
});
|
|
@@ -20,15 +20,18 @@ let ChimeraController = Function.inherits('Alchemy.Controller', 'Alchemy.Control
|
|
|
20
20
|
*
|
|
21
21
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
22
22
|
* @since 1.2.4
|
|
23
|
-
* @version 1.
|
|
23
|
+
* @version 1.3.0
|
|
24
24
|
*/
|
|
25
25
|
ChimeraController.setMethod(function beforeAction() {
|
|
26
26
|
|
|
27
|
+
// Skip for WebSocket linkup connections - they don't have routes or params
|
|
28
|
+
if (this.conduit.websocket) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
const model = this.conduit.params.model,
|
|
28
33
|
is_system_route = this.conduit.route.is_system_route;
|
|
29
34
|
|
|
30
|
-
this.set('toolbar_manager', this.toolbar_manager);
|
|
31
|
-
|
|
32
35
|
// If this is not a system route and no model is defined in the parameters,
|
|
33
36
|
// do not queue a model fallback
|
|
34
37
|
if (is_system_route && !model) {
|
|
@@ -60,4 +63,31 @@ ChimeraController.enforceProperty(function toolbar_manager(new_value) {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
return new_value;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set the toolbar manager as a template variable.
|
|
70
|
+
* This must be called at the END of each action, after the document context
|
|
71
|
+
* has been set up. Calling it too early (like in beforeAction) causes the
|
|
72
|
+
* manager to be serialized before the document_watcher is set.
|
|
73
|
+
*
|
|
74
|
+
* For actions that don't call setContextDocument(), this will clear the
|
|
75
|
+
* document_watcher to ensure the avatar doesn't persist from previous pages.
|
|
76
|
+
*
|
|
77
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
78
|
+
* @since 1.3.0
|
|
79
|
+
* @version 1.3.0
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} options
|
|
82
|
+
* @param {Boolean} options.has_document Whether this action has a document context
|
|
83
|
+
*/
|
|
84
|
+
ChimeraController.setMethod(function setToolbarManagerVariable(options) {
|
|
85
|
+
|
|
86
|
+
// If no document context was set for this action, clear the document watcher
|
|
87
|
+
// This ensures the avatar doesn't persist when navigating from edit to list pages
|
|
88
|
+
if (!options?.has_document) {
|
|
89
|
+
this.toolbar_manager.setDocument(null);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.set('toolbar_manager', this.toolbar_manager);
|
|
63
93
|
});
|
|
@@ -51,10 +51,10 @@ Editor.setMethod(function setTitle(title) {
|
|
|
51
51
|
*
|
|
52
52
|
* @param {Document} doc
|
|
53
53
|
*/
|
|
54
|
-
Editor.setMethod(function setContextDocument(doc) {
|
|
54
|
+
Editor.setMethod(async function setContextDocument(doc) {
|
|
55
55
|
let document_watcher = this.toolbar_manager.setDocument(doc);
|
|
56
56
|
this.toolbar_manager.setModel(doc.$model_name);
|
|
57
|
-
document_watcher.addWatcher(this.conduit);
|
|
57
|
+
await document_watcher.addWatcher(this.conduit);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -79,6 +79,7 @@ Editor.setAction(function index(conduit, model_name) {
|
|
|
79
79
|
|
|
80
80
|
this.set('widget_config', widget_config);
|
|
81
81
|
|
|
82
|
+
this.setToolbarManagerVariable();
|
|
82
83
|
this.render('chimera/editor/index');
|
|
83
84
|
});
|
|
84
85
|
|
|
@@ -133,6 +134,7 @@ Editor.setAction(async function add(conduit, model_name) {
|
|
|
133
134
|
this.set('widget_config', widget_config);
|
|
134
135
|
this.setTitle(model.constructor.title + ' Add');
|
|
135
136
|
|
|
137
|
+
this.setToolbarManagerVariable();
|
|
136
138
|
this.render('chimera/editor/add');
|
|
137
139
|
});
|
|
138
140
|
|
|
@@ -201,13 +203,17 @@ Editor.setAction(async function edit(conduit, model_name, pk_val) {
|
|
|
201
203
|
this.set('add_preview_button', true);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
this.setContextDocument(record);
|
|
206
|
+
await this.setContextDocument(record);
|
|
205
207
|
|
|
206
208
|
this.set('record_pk', record.$pk);
|
|
207
209
|
this.set('model_name', model.model_name);
|
|
208
210
|
this.set('widget_config', widget_config);
|
|
209
211
|
this.setTitle(model.constructor.title + ' Edit');
|
|
210
212
|
|
|
213
|
+
// Set toolbar_manager AFTER document context is set up
|
|
214
|
+
// Pass has_document: true since we have a document context
|
|
215
|
+
this.setToolbarManagerVariable({has_document: true});
|
|
216
|
+
|
|
211
217
|
this.render('chimera/editor/edit');
|
|
212
218
|
});
|
|
213
219
|
|
|
@@ -283,15 +289,52 @@ Editor.setAction(async function trash(conduit, model_name, pk_val) {
|
|
|
283
289
|
|
|
284
290
|
this.set('back_url', referer);
|
|
285
291
|
this.set('record', record);
|
|
292
|
+
this.setToolbarManagerVariable();
|
|
286
293
|
this.render('chimera/editor/trash');
|
|
287
294
|
});
|
|
288
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The task monitor action - shows live progress of a running task
|
|
298
|
+
*
|
|
299
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
300
|
+
* @since 1.3.0
|
|
301
|
+
* @version 1.3.0
|
|
302
|
+
*
|
|
303
|
+
* @param {Conduit} conduit
|
|
304
|
+
* @param {String} task_history_id
|
|
305
|
+
*/
|
|
306
|
+
Editor.setAction(async function taskMonitor(conduit, task_history_id) {
|
|
307
|
+
|
|
308
|
+
let TaskHistory = this.getModel('System.TaskHistory');
|
|
309
|
+
let history_doc = await TaskHistory.findByPk(task_history_id);
|
|
310
|
+
|
|
311
|
+
if (!history_doc) {
|
|
312
|
+
return conduit.notFound('Task history record not found');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Get the associated System.Task document if available
|
|
316
|
+
let system_task = null;
|
|
317
|
+
if (history_doc.system_task_id) {
|
|
318
|
+
let SystemTask = this.getModel('System.Task');
|
|
319
|
+
system_task = await SystemTask.findByPk(history_doc.system_task_id);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.setTitle('Task Monitor: ' + (system_task?.title || history_doc.type));
|
|
323
|
+
|
|
324
|
+
this.set('task_history_id', task_history_id);
|
|
325
|
+
this.set('history_doc', history_doc);
|
|
326
|
+
this.set('system_task', system_task);
|
|
327
|
+
|
|
328
|
+
this.setToolbarManagerVariable();
|
|
329
|
+
this.render('chimera/editor/task_monitor');
|
|
330
|
+
});
|
|
331
|
+
|
|
289
332
|
/**
|
|
290
333
|
* The records API action
|
|
291
334
|
*
|
|
292
335
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
293
336
|
* @since 1.0.0
|
|
294
|
-
* @version 1.
|
|
337
|
+
* @version 1.3.0
|
|
295
338
|
*
|
|
296
339
|
* @param {Conduit} conduit
|
|
297
340
|
* @param {String} model_name
|
|
@@ -343,15 +386,12 @@ Editor.setAction(async function records(conduit, model_name) {
|
|
|
343
386
|
continue;
|
|
344
387
|
}
|
|
345
388
|
|
|
346
|
-
|
|
347
|
-
crit.where(key).equals(val);
|
|
389
|
+
crit.where(key).matchesFilter(val);
|
|
348
390
|
}
|
|
349
391
|
}
|
|
350
392
|
|
|
351
393
|
let records = await model.find('all', crit),
|
|
352
|
-
|
|
353
|
-
record,
|
|
354
|
-
main;
|
|
394
|
+
record;
|
|
355
395
|
|
|
356
396
|
records.keepPrivateFields();
|
|
357
397
|
|
|
@@ -377,7 +417,11 @@ Editor.setAction(async function records(conduit, model_name) {
|
|
|
377
417
|
})
|
|
378
418
|
});
|
|
379
419
|
|
|
420
|
+
// Get any custom row actions defined on the model
|
|
421
|
+
let custom_actions = model.chimera.getRowActionsFor(record);
|
|
422
|
+
|
|
380
423
|
record.$hold.actions = [
|
|
424
|
+
...custom_actions,
|
|
381
425
|
edit_action,
|
|
382
426
|
trash_action,
|
|
383
427
|
];
|
|
@@ -12,15 +12,176 @@ const ChimeraStatic = Function.inherits('Alchemy.Controller.Chimera', 'Static');
|
|
|
12
12
|
*
|
|
13
13
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
14
14
|
* @since 0.1.0
|
|
15
|
-
* @version 1.
|
|
15
|
+
* @version 1.4.0
|
|
16
16
|
*
|
|
17
17
|
* @param {Conduit} conduit
|
|
18
18
|
*/
|
|
19
|
-
ChimeraStatic.setAction(function dashboard(conduit) {
|
|
19
|
+
ChimeraStatic.setAction(async function dashboard(conduit) {
|
|
20
|
+
|
|
21
|
+
const DashboardConfig = this.getModel('Chimera.DashboardConfig');
|
|
22
|
+
|
|
23
|
+
let user_id = conduit.session('UserData')?.$pk;
|
|
24
|
+
let config;
|
|
25
|
+
let is_user_dashboard = false;
|
|
26
|
+
|
|
27
|
+
// First, try to find a user-specific dashboard
|
|
28
|
+
if (user_id) {
|
|
29
|
+
config = await DashboardConfig.find('first', {
|
|
30
|
+
conditions: {
|
|
31
|
+
user_id: user_id,
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (config) {
|
|
36
|
+
is_user_dashboard = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fall back to the default dashboard
|
|
41
|
+
if (!config) {
|
|
42
|
+
config = await DashboardConfig.find('first', {
|
|
43
|
+
conditions: {
|
|
44
|
+
is_default: true,
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If no default exists, create one
|
|
50
|
+
if (!config) {
|
|
51
|
+
config = DashboardConfig.createDocument({
|
|
52
|
+
name : 'Default Dashboard',
|
|
53
|
+
is_default : true,
|
|
54
|
+
widgets : null,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await config.save();
|
|
58
|
+
|
|
59
|
+
// Re-fetch to ensure we have the full document with _id
|
|
60
|
+
config = await DashboardConfig.findByPk(config.$pk);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get the toolbar manager (created by ChimeraController base class)
|
|
64
|
+
let manager = this.toolbar_manager;
|
|
65
|
+
|
|
66
|
+
// Set the document on the manager for editing
|
|
67
|
+
manager.setDocument(config);
|
|
68
|
+
|
|
69
|
+
// Add dashboard-specific info and buttons to the toolbar manager
|
|
70
|
+
manager.is_user_dashboard = is_user_dashboard;
|
|
71
|
+
manager.has_user_id = !!user_id;
|
|
72
|
+
|
|
73
|
+
// Add dashboard management buttons
|
|
74
|
+
if (user_id) {
|
|
75
|
+
if (is_user_dashboard) {
|
|
76
|
+
// User has a custom dashboard - show reset button
|
|
77
|
+
manager.addTemplateToRender('buttons', 'chimera/toolbar/reset_dashboard_button');
|
|
78
|
+
} else {
|
|
79
|
+
// User is viewing default - show customize button
|
|
80
|
+
manager.addTemplateToRender('buttons', 'chimera/toolbar/customize_dashboard_button');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
20
84
|
this.set('pagetitle', 'Dashboard');
|
|
85
|
+
this.set('dashboard_config', config);
|
|
86
|
+
this.set('is_user_dashboard', is_user_dashboard);
|
|
87
|
+
this.setToolbarManagerVariable();
|
|
21
88
|
this.render('chimera/dashboard');
|
|
22
89
|
});
|
|
23
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Create a personalized dashboard for the current user
|
|
93
|
+
*
|
|
94
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
95
|
+
* @since 1.4.0
|
|
96
|
+
* @version 1.4.0
|
|
97
|
+
*
|
|
98
|
+
* @param {Conduit} conduit
|
|
99
|
+
*/
|
|
100
|
+
ChimeraStatic.setAction(async function customizeDashboard(conduit) {
|
|
101
|
+
|
|
102
|
+
const DashboardConfig = this.getModel('Chimera.DashboardConfig');
|
|
103
|
+
|
|
104
|
+
let user_id = conduit.session('UserData')?.$pk;
|
|
105
|
+
|
|
106
|
+
if (!user_id) {
|
|
107
|
+
return conduit.error(new Error('You must be logged in to customize the dashboard'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if user already has a custom dashboard
|
|
111
|
+
let existing = await DashboardConfig.find('first', {
|
|
112
|
+
conditions: {
|
|
113
|
+
user_id: user_id,
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (existing) {
|
|
118
|
+
return conduit.end({
|
|
119
|
+
success: true,
|
|
120
|
+
message: 'You already have a customized dashboard',
|
|
121
|
+
pk: existing.$pk,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Find the default dashboard to copy from
|
|
126
|
+
let default_config = await DashboardConfig.find('first', {
|
|
127
|
+
conditions: {
|
|
128
|
+
is_default: true,
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Create a new dashboard for this user
|
|
133
|
+
let user_dashboard = DashboardConfig.createDocument({
|
|
134
|
+
name : 'My Dashboard',
|
|
135
|
+
is_default : false,
|
|
136
|
+
user_id : user_id,
|
|
137
|
+
widgets : default_config?.widgets || null,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await user_dashboard.save();
|
|
141
|
+
|
|
142
|
+
conduit.end({
|
|
143
|
+
success: true,
|
|
144
|
+
message: 'Dashboard customized',
|
|
145
|
+
pk: user_dashboard.$pk,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reset the user's dashboard back to default
|
|
151
|
+
*
|
|
152
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
153
|
+
* @since 1.4.0
|
|
154
|
+
* @version 1.4.0
|
|
155
|
+
*
|
|
156
|
+
* @param {Conduit} conduit
|
|
157
|
+
*/
|
|
158
|
+
ChimeraStatic.setAction(async function resetDashboard(conduit) {
|
|
159
|
+
|
|
160
|
+
const DashboardConfig = this.getModel('Chimera.DashboardConfig');
|
|
161
|
+
|
|
162
|
+
let user_id = conduit.session('UserData')?.$pk;
|
|
163
|
+
|
|
164
|
+
if (!user_id) {
|
|
165
|
+
return conduit.error(new Error('You must be logged in to reset the dashboard'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Find and remove the user's custom dashboard
|
|
169
|
+
let user_dashboard = await DashboardConfig.find('first', {
|
|
170
|
+
conditions: {
|
|
171
|
+
user_id: user_id,
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (user_dashboard) {
|
|
176
|
+
await user_dashboard.remove();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
conduit.end({
|
|
180
|
+
success: true,
|
|
181
|
+
message: 'Dashboard reset to default',
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
24
185
|
/**
|
|
25
186
|
* The sidebar action
|
|
26
187
|
*
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Task controller for Chimera
|
|
3
|
+
*
|
|
4
|
+
* @constructor
|
|
5
|
+
* @extends Alchemy.Controller.Chimera
|
|
6
|
+
*
|
|
7
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
8
|
+
* @since 1.3.0
|
|
9
|
+
* @version 1.3.0
|
|
10
|
+
*/
|
|
11
|
+
const SystemTask = Function.inherits('Alchemy.Controller.Chimera', 'SystemTask');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run a task manually
|
|
15
|
+
*
|
|
16
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
17
|
+
* @since 1.3.0
|
|
18
|
+
* @version 1.3.0
|
|
19
|
+
*
|
|
20
|
+
* @param {Conduit} conduit
|
|
21
|
+
* @param {Document.SystemTask} system_task The task to run (auto-populated from route)
|
|
22
|
+
*/
|
|
23
|
+
SystemTask.setAction(async function run(conduit, system_task) {
|
|
24
|
+
|
|
25
|
+
if (!system_task) {
|
|
26
|
+
return conduit.error(new Error('Task not found'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!system_task.type) {
|
|
30
|
+
return conduit.error(new Error('Task has no type configured'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!system_task.enabled) {
|
|
34
|
+
return conduit.error(new Error('Task is disabled'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
let task_instance = await alchemy.task_service.startTaskManually(system_task);
|
|
39
|
+
|
|
40
|
+
// Get the task history document ID for live monitoring
|
|
41
|
+
let task_history_id = task_instance.id;
|
|
42
|
+
|
|
43
|
+
conduit.end({
|
|
44
|
+
success : true,
|
|
45
|
+
message : 'Task started successfully',
|
|
46
|
+
task_id : system_task.$pk,
|
|
47
|
+
task_history_id : task_history_id,
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
conduit.error(err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Monitor a running task via linkup
|
|
56
|
+
*
|
|
57
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
58
|
+
* @since 1.3.0
|
|
59
|
+
* @version 1.3.0
|
|
60
|
+
*
|
|
61
|
+
* @param {Conduit} conduit
|
|
62
|
+
* @param {Linkup} linkup
|
|
63
|
+
* @param {Object} config
|
|
64
|
+
*/
|
|
65
|
+
SystemTask.setAction(async function monitor(conduit, linkup, config) {
|
|
66
|
+
|
|
67
|
+
const task_history_id = config?.task_history_id;
|
|
68
|
+
|
|
69
|
+
if (!task_history_id) {
|
|
70
|
+
linkup.submit('error', {message: 'No task history ID provided'});
|
|
71
|
+
linkup.destroy();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get the TaskHistory model and find the record
|
|
76
|
+
const TaskHistory = Model.get('System.TaskHistory');
|
|
77
|
+
const history_doc = await TaskHistory.findByPk(task_history_id);
|
|
78
|
+
|
|
79
|
+
if (!history_doc) {
|
|
80
|
+
linkup.submit('error', {message: 'Task history record not found'});
|
|
81
|
+
linkup.destroy();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Find the running task instance
|
|
86
|
+
const running = alchemy.shared('Task.running', 'Array');
|
|
87
|
+
let task_instance = running.find(t => t.id === task_history_id);
|
|
88
|
+
|
|
89
|
+
// Helper to get current task state
|
|
90
|
+
// Prefers in-memory task instance values over database values when available,
|
|
91
|
+
// because the database save may be pending (async)
|
|
92
|
+
const getTaskState = async () => {
|
|
93
|
+
|
|
94
|
+
let is_running,
|
|
95
|
+
started_at,
|
|
96
|
+
ended_at,
|
|
97
|
+
had_error;
|
|
98
|
+
|
|
99
|
+
if (task_instance) {
|
|
100
|
+
// Use in-memory task instance state (more up-to-date than database)
|
|
101
|
+
is_running = task_instance.has_started && !task_instance.has_stopped;
|
|
102
|
+
started_at = task_instance.started ? new Date(task_instance.started) : history_doc.started_at;
|
|
103
|
+
ended_at = task_instance.stopped ? new Date(task_instance.stopped) : history_doc.ended_at;
|
|
104
|
+
had_error = task_instance.error ? true : history_doc.had_error;
|
|
105
|
+
} else {
|
|
106
|
+
// Task instance not in memory - re-fetch from database for latest state
|
|
107
|
+
let fresh_doc = await TaskHistory.findByPk(task_history_id);
|
|
108
|
+
is_running = fresh_doc?.is_running ?? false;
|
|
109
|
+
started_at = fresh_doc?.started_at ?? history_doc.started_at;
|
|
110
|
+
ended_at = fresh_doc?.ended_at ?? history_doc.ended_at;
|
|
111
|
+
had_error = fresh_doc?.had_error ?? history_doc.had_error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id : task_history_id,
|
|
116
|
+
type : history_doc.type,
|
|
117
|
+
started_at : started_at,
|
|
118
|
+
ended_at : ended_at,
|
|
119
|
+
is_running : is_running,
|
|
120
|
+
had_error : had_error,
|
|
121
|
+
percentage : task_instance?.percentage ?? null,
|
|
122
|
+
reports : task_instance?.reports ?? [],
|
|
123
|
+
is_paused : task_instance?.is_paused ?? false,
|
|
124
|
+
can_stop : task_instance?.can_be_stopped ?? false,
|
|
125
|
+
can_pause : task_instance?.can_be_paused ?? false,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Send initial state
|
|
130
|
+
linkup.submit('state', await getTaskState());
|
|
131
|
+
|
|
132
|
+
// Check if task is still actively running
|
|
133
|
+
const is_task_running = task_instance && task_instance.has_started && !task_instance.has_stopped;
|
|
134
|
+
|
|
135
|
+
if (is_task_running) {
|
|
136
|
+
// Task is actively running - subscribe to its events
|
|
137
|
+
|
|
138
|
+
// When task completes, send final state and re-fetch the document
|
|
139
|
+
const check_completion = async () => {
|
|
140
|
+
// Re-fetch the history document to get final state
|
|
141
|
+
const updated_doc = await TaskHistory.findByPk(task_history_id);
|
|
142
|
+
|
|
143
|
+
if (updated_doc && !updated_doc.is_running) {
|
|
144
|
+
linkup.submit('complete', {
|
|
145
|
+
ended_at : updated_doc.ended_at,
|
|
146
|
+
had_error : updated_doc.had_error,
|
|
147
|
+
error_message: updated_doc.error_message,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Listen for report events
|
|
153
|
+
const on_report = async (report) => {
|
|
154
|
+
linkup.submit('report', report);
|
|
155
|
+
linkup.submit('progress', {
|
|
156
|
+
percentage: task_instance.percentage,
|
|
157
|
+
is_paused: task_instance.is_paused,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// When the task is done, send the completion state and clean up listener
|
|
161
|
+
if (report.done) {
|
|
162
|
+
task_instance.off('report', on_report);
|
|
163
|
+
await check_completion();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
task_instance.on('report', on_report);
|
|
168
|
+
|
|
169
|
+
// Handle stop command from client
|
|
170
|
+
linkup.on('stop', async () => {
|
|
171
|
+
if (task_instance && task_instance.can_be_stopped) {
|
|
172
|
+
await task_instance.stop();
|
|
173
|
+
linkup.submit('stopped');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Handle pause command from client
|
|
178
|
+
linkup.on('pause', () => {
|
|
179
|
+
if (task_instance && task_instance.can_be_paused && !task_instance.is_paused) {
|
|
180
|
+
task_instance.pause();
|
|
181
|
+
linkup.submit('paused');
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Handle resume command from client
|
|
186
|
+
linkup.on('resume', () => {
|
|
187
|
+
if (task_instance && task_instance.is_paused) {
|
|
188
|
+
task_instance.resume();
|
|
189
|
+
linkup.submit('resumed');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Clean up when linkup is destroyed
|
|
194
|
+
linkup.on('destroyed', () => {
|
|
195
|
+
if (task_instance) {
|
|
196
|
+
task_instance.off('report', on_report);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
} else {
|
|
201
|
+
// Task is not running (either completed or never started)
|
|
202
|
+
// Send completed state using the most up-to-date values
|
|
203
|
+
let ended_at, had_error, error_message;
|
|
204
|
+
|
|
205
|
+
if (task_instance && task_instance.has_stopped) {
|
|
206
|
+
// Task instance exists but has stopped - use in-memory values
|
|
207
|
+
ended_at = task_instance.stopped ? new Date(task_instance.stopped) : null;
|
|
208
|
+
had_error = task_instance.error ? true : false;
|
|
209
|
+
error_message = task_instance.error?.message || null;
|
|
210
|
+
} else {
|
|
211
|
+
// No task instance - re-fetch from database for latest state
|
|
212
|
+
let fresh_doc = await TaskHistory.findByPk(task_history_id);
|
|
213
|
+
ended_at = fresh_doc?.ended_at;
|
|
214
|
+
had_error = fresh_doc?.had_error;
|
|
215
|
+
error_message = fresh_doc?.error_message;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
linkup.submit('complete', {
|
|
219
|
+
ended_at : ended_at,
|
|
220
|
+
had_error : had_error,
|
|
221
|
+
error_message: error_message,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chimera-dashboard-button element
|
|
3
|
+
* A button that handles "customize" and "reset" dashboard actions
|
|
4
|
+
*
|
|
5
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
6
|
+
* @since 1.4.0
|
|
7
|
+
* @version 1.4.0
|
|
8
|
+
*/
|
|
9
|
+
const DashboardButton = Function.inherits('Alchemy.Element.Form.Button', 'Alchemy.Element.Chimera', 'DashboardButton');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Set the custom element prefix to 'chimera'
|
|
13
|
+
*
|
|
14
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
15
|
+
* @since 1.4.0
|
|
16
|
+
* @version 1.4.0
|
|
17
|
+
*/
|
|
18
|
+
DashboardButton.setStatic('custom_element_prefix', 'chimera');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The action to perform: 'customize' or 'reset'
|
|
22
|
+
*
|
|
23
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
24
|
+
* @since 1.4.0
|
|
25
|
+
* @version 1.4.0
|
|
26
|
+
*/
|
|
27
|
+
DashboardButton.setAttribute('action');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The element has been added to the dom for the first time
|
|
31
|
+
*
|
|
32
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
33
|
+
* @since 1.4.0
|
|
34
|
+
* @version 1.4.0
|
|
35
|
+
*/
|
|
36
|
+
DashboardButton.setMethod(function introduced() {
|
|
37
|
+
|
|
38
|
+
// Call parent introduced first (sets up click/keyup listeners)
|
|
39
|
+
introduced.super.call(this);
|
|
40
|
+
|
|
41
|
+
// Listen to the activate event (emitted by parent Button class on click)
|
|
42
|
+
this.addEventListener('activate', async e => {
|
|
43
|
+
await this.handleAction();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle the dashboard action
|
|
49
|
+
*
|
|
50
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
51
|
+
* @since 1.4.0
|
|
52
|
+
* @version 1.4.0
|
|
53
|
+
*/
|
|
54
|
+
DashboardButton.setMethod(async function handleAction() {
|
|
55
|
+
|
|
56
|
+
let action = this.action;
|
|
57
|
+
|
|
58
|
+
if (!action || (action !== 'customize' && action !== 'reset')) {
|
|
59
|
+
console.error('Invalid dashboard button action:', action);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.setState('busy');
|
|
64
|
+
|
|
65
|
+
let route_name = action === 'customize'
|
|
66
|
+
? 'Chimera.Static#customizeDashboard'
|
|
67
|
+
: 'Chimera.Static#resetDashboard';
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await this.hawkejs_helpers.Alchemy.getResource({
|
|
71
|
+
name : route_name,
|
|
72
|
+
method : 'post',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.setState('done');
|
|
76
|
+
|
|
77
|
+
// After a short delay, replace this button with the opposite action button
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
let template = action === 'customize'
|
|
80
|
+
? 'chimera/toolbar/reset_dashboard_button'
|
|
81
|
+
: 'chimera/toolbar/customize_dashboard_button';
|
|
82
|
+
|
|
83
|
+
hawkejs.renderToElements(template, {}, (err, elements) => {
|
|
84
|
+
|
|
85
|
+
if (err) {
|
|
86
|
+
console.error('Failed to render replacement button:', err);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (elements && elements.length > 0) {
|
|
91
|
+
this.replaceWith(elements[0]);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}, 500);
|
|
95
|
+
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('Failed to execute dashboard action:', err);
|
|
98
|
+
this.setState('error', 3000, 'default');
|
|
99
|
+
}
|
|
100
|
+
});
|