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.
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * @author Jelle De Loecker <jelle@elevenways.be>
5
5
  * @since 0.1.0
6
- * @version 1.0.0
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.2.6
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.2.0
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
- val = RegExp.interpretWildcard('*' + val + '*', 'i');
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
- result = [],
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
  ];
@@ -37,5 +37,6 @@ SettingsController.setAction(async function editor(conduit) {
37
37
 
38
38
  this.set('settings_config', settings_config);
39
39
 
40
+ this.setToolbarManagerVariable();
40
41
  this.render('chimera/settings/editor');
41
42
  });
@@ -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.0.0
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
+ });