json-object-editor 0.10.665 → 0.10.670
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 +2 -0
- package/TESTING_PHASE1_PROGRESS.md +216 -0
- package/_www/ai-jobs.html +218 -0
- package/_www/mcp-nav.js +21 -12
- package/_www/mcp-test.html +287 -287
- package/capp/capp.css +3 -2
- package/css/joe-ai.css +71 -0
- package/css/joe-styles.css +4390 -4324
- package/css/joe.css +4391 -4325
- package/css/joe.min.css +1 -1
- package/docs/protocol_report_template.html +479 -0
- package/docs/protocol_report_template.js +563 -0
- package/es5-build/js/JsonObjectEditor_es5.jquery.craydent.js +11059 -11049
- package/js/JsonObjectEditor.jquery.craydent.js +11618 -11543
- package/js/joe-ai.js +3220 -2301
- package/js/joe-test.js +9669 -9659
- package/js/joe.js +11619 -11544
- package/js/joe.min.js +1 -1
- package/js/joe_es5.js +11071 -11061
- package/package.json +1 -1
- package/pages/template.html +2 -0
- package/server/fields/core.js +16 -2
- package/server/init.js +9 -1
- package/server/modules/AiJobs.js +412 -0
- package/server/modules/MCP.js +1481 -1368
- package/server/modules/Server.js +3 -0
- package/server/plugins/chatgpt.js +2332 -2113
- package/server/schemas/ai_prompt.js +22 -4
- package/server/schemas/report.js +1 -1
- package/server/schemas/task.js +1 -0
- package/web-components/field-jobs-container.js +224 -0
- package/web-components/joe-workflow-widget.js +314 -0
|
@@ -69,8 +69,15 @@ var schema = {
|
|
|
69
69
|
return _joe.Filter.Options.status();
|
|
70
70
|
},
|
|
71
71
|
filters:function(i){
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
var schemas = [];
|
|
73
|
+
var subs = [];
|
|
74
|
+
_joe.current.list.map(function(status){
|
|
75
|
+
schemas = schemas.concat(status.datasets||[]);
|
|
76
|
+
});
|
|
77
|
+
(new Set(schemas)).map(function(schema){
|
|
78
|
+
subs.push({name:schema,filter:{datasets:{$in:[schema]}}})
|
|
79
|
+
});
|
|
80
|
+
return subs.concat(_joe.Filter.Options.tags({group:'tags',collapsed:true}));
|
|
74
81
|
},
|
|
75
82
|
stripeColor: function(item) {
|
|
76
83
|
//use the stripe color from ai_assistant status object if it has one
|
|
@@ -98,8 +105,19 @@ var schema = {
|
|
|
98
105
|
'name',
|
|
99
106
|
'info',
|
|
100
107
|
{name:"prompt_method",placeholder:"name of method to call in plugin", comment:'use executeJOEAiPrompt to use the JOE ui here for all smarts', default:"executeJOEAiPrompt",display:"Prompt Plugin Method",type:'text'},
|
|
101
|
-
{name:'content_items',type:'objectList',
|
|
102
|
-
properties:['itemtype','reference']
|
|
108
|
+
{name:'content_items',type:'objectList',display:'Content Items',
|
|
109
|
+
properties:['itemtype','reference'],
|
|
110
|
+
comment:`
|
|
111
|
+
<div>
|
|
112
|
+
<p>Define which objects are automatically passed when running this prompt via <code>select_prompt</code>.</p>
|
|
113
|
+
<ul>
|
|
114
|
+
<li><b><code>itemtype</code></b> - Schema name (e.g., <code>"task"</code>)</li>
|
|
115
|
+
<li><b><code>reference</code></b> - Parameter name for the object's <code>_id</code> (e.g., <code>"task"</code>)</li>
|
|
116
|
+
</ul>
|
|
117
|
+
<p>When running from a matching object: <code>params[reference]</code> = object <code>_id</code>, <code>content_objects[itemtype]</code> = full object (available in helper functions).</p>
|
|
118
|
+
<p><b>Note:</b> Only objects matching the current object's <code>itemtype</code> are auto-passed. For multiple objects, call programmatically or load in helper functions.</p>
|
|
119
|
+
</div>
|
|
120
|
+
`
|
|
103
121
|
},
|
|
104
122
|
{section_start:'Input'},
|
|
105
123
|
{name: 'functions', type: 'code', display: 'Helper Functions', language:'javascript',comment: `
|
package/server/schemas/report.js
CHANGED
|
@@ -113,7 +113,7 @@ var schema = {
|
|
|
113
113
|
}},
|
|
114
114
|
{section_end:'Template'},
|
|
115
115
|
{section_start:'preview'},
|
|
116
|
-
{name:'examples', type:'content',run:function(item){
|
|
116
|
+
{name:'examples', reloadable:true,type:'content',run:function(item){
|
|
117
117
|
return $J.schema('report').methods.renderPreviews(item);
|
|
118
118
|
}
|
|
119
119
|
/*run:function(item){
|
package/server/schemas/task.js
CHANGED
|
@@ -193,6 +193,7 @@ var task = function(){return{
|
|
|
193
193
|
'name',
|
|
194
194
|
'info',
|
|
195
195
|
{extend:'description',specs:{
|
|
196
|
+
queryJobs:true,
|
|
196
197
|
ai:{prompt:'Summarize the task in a few sentences. Take into account the project the task is associated with if there is one.'}}
|
|
197
198
|
},
|
|
198
199
|
{section_end:'overview'},
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
class FieldJobsContainer extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.objectId = null;
|
|
5
|
+
this.fieldName = null;
|
|
6
|
+
this.jobs = [];
|
|
7
|
+
this.updateInterval = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static get observedAttributes() {
|
|
11
|
+
return ['data-object-id', 'data-field-name'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
this.objectId = this.getAttribute('data-object-id');
|
|
16
|
+
this.fieldName = this.getAttribute('data-field-name');
|
|
17
|
+
this.render();
|
|
18
|
+
this.startElapsedTimeUpdates();
|
|
19
|
+
|
|
20
|
+
// Immediately poll for jobs on connect
|
|
21
|
+
if (this.objectId && this.fieldName) {
|
|
22
|
+
var self = this;
|
|
23
|
+
$.get('/API/aijobs/' + encodeURIComponent(this.objectId) + '/' + encodeURIComponent(this.fieldName))
|
|
24
|
+
.then(function(data) {
|
|
25
|
+
if (data && data.jobs && Array.isArray(data.jobs)) {
|
|
26
|
+
self.updateJobs(data.jobs);
|
|
27
|
+
} else {
|
|
28
|
+
self.updateJobs([]);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.fail(function(err) {
|
|
32
|
+
// Silently fail - will retry on next poll
|
|
33
|
+
if (window.DEBUG_MODE || (window.$c && $c.DEBUG_MODE)) {
|
|
34
|
+
console.warn('[field-jobs-container] initial poll failed:', err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnectedCallback() {
|
|
41
|
+
this.stopElapsedTimeUpdates();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
45
|
+
if (name === 'data-object-id') {
|
|
46
|
+
this.objectId = newValue;
|
|
47
|
+
} else if (name === 'data-field-name') {
|
|
48
|
+
this.fieldName = newValue;
|
|
49
|
+
}
|
|
50
|
+
if (this.objectId && this.fieldName) {
|
|
51
|
+
this.render();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update jobs from server response
|
|
57
|
+
*/
|
|
58
|
+
updateJobs(jobs) {
|
|
59
|
+
if (!Array.isArray(jobs)) {
|
|
60
|
+
jobs = [];
|
|
61
|
+
}
|
|
62
|
+
this.jobs = jobs;
|
|
63
|
+
this.render();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calculate elapsed seconds from startTime
|
|
68
|
+
*/
|
|
69
|
+
calculateElapsed(startTime) {
|
|
70
|
+
if (!startTime) return 0;
|
|
71
|
+
try {
|
|
72
|
+
var start = new Date(startTime);
|
|
73
|
+
var now = new Date();
|
|
74
|
+
var elapsedMs = now - start;
|
|
75
|
+
return Math.floor(elapsedMs / 1000);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start elapsed time updates (every second)
|
|
83
|
+
*/
|
|
84
|
+
startElapsedTimeUpdates() {
|
|
85
|
+
if (this.updateInterval) return; // Already running
|
|
86
|
+
|
|
87
|
+
this.updateInterval = setInterval(() => {
|
|
88
|
+
// Check if we have any active jobs
|
|
89
|
+
var hasActive = this.jobs.some(function(job) {
|
|
90
|
+
return (job.status === 'running' || job.status === 'starting') &&
|
|
91
|
+
(job.total == null || job.progress == null || job.progress < job.total);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (hasActive) {
|
|
95
|
+
// Force re-render to update elapsed times
|
|
96
|
+
this.render();
|
|
97
|
+
} else {
|
|
98
|
+
// Stop if no active jobs
|
|
99
|
+
this.stopElapsedTimeUpdates();
|
|
100
|
+
}
|
|
101
|
+
}, 1000);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop elapsed time updates
|
|
106
|
+
*/
|
|
107
|
+
stopElapsedTimeUpdates() {
|
|
108
|
+
if (this.updateInterval) {
|
|
109
|
+
clearInterval(this.updateInterval);
|
|
110
|
+
this.updateInterval = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render the component
|
|
116
|
+
*/
|
|
117
|
+
render() {
|
|
118
|
+
// Build endpoint URL for token link (if we have both objectId and fieldName)
|
|
119
|
+
var endpointUrl = null;
|
|
120
|
+
var tokenLinkHtml = '';
|
|
121
|
+
var fullToken = null;
|
|
122
|
+
|
|
123
|
+
// Try to get a sample token from jobs, or construct lookup key
|
|
124
|
+
if (this.jobs.length > 0 && this.jobs[0].token) {
|
|
125
|
+
fullToken = this.jobs[0].token;
|
|
126
|
+
} else if (this.objectId && this.fieldName) {
|
|
127
|
+
// If no jobs, show lookup key format
|
|
128
|
+
fullToken = this.objectId + '_' + this.fieldName;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.objectId && this.fieldName) {
|
|
132
|
+
endpointUrl = '/API/aijobs/' + encodeURIComponent(this.objectId) + '/' + encodeURIComponent(this.fieldName);
|
|
133
|
+
var titleAttr = fullToken ? 'title="' + fullToken.replace(/"/g, '"') + '"' : 'title="View endpoint"';
|
|
134
|
+
tokenLinkHtml = '<a href="' + endpointUrl + '" target="_blank" class="field-jobs-token-link" ' + titleAttr + '>[' + this.objectId.substring(0, 8) + '...|' + this.fieldName + ']</a>';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!this.objectId || !this.fieldName) {
|
|
138
|
+
var html = '<div class="field-jobs-title">';
|
|
139
|
+
html += '<span class="field-jobs-title-text">0 active jobs</span>';
|
|
140
|
+
html += tokenLinkHtml;
|
|
141
|
+
html += '</div>';
|
|
142
|
+
this.innerHTML = html;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Filter active jobs (for count)
|
|
147
|
+
var activeJobs = this.jobs.filter(function(job) {
|
|
148
|
+
return job.status !== 'complete' && job.status !== 'error';
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Update token link with full token if we have jobs (recalculate in case jobs were added)
|
|
152
|
+
if (this.jobs.length > 0 && this.jobs[0].token) {
|
|
153
|
+
fullToken = this.jobs[0].token;
|
|
154
|
+
if (endpointUrl) {
|
|
155
|
+
var titleAttr = 'title="' + fullToken.replace(/"/g, '"') + '"';
|
|
156
|
+
tokenLinkHtml = '<a href="' + endpointUrl + '" target="_blank" class="field-jobs-token-link" ' + titleAttr + '>[' + this.objectId.substring(0, 8) + '...|' + this.fieldName + ']</a>';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build HTML
|
|
161
|
+
var html = '';
|
|
162
|
+
|
|
163
|
+
// Title with active job count and token link on the right
|
|
164
|
+
html += '<div class="field-jobs-title">';
|
|
165
|
+
html += '<span class="field-jobs-title-text">' + activeJobs.length + ' active job' + (activeJobs.length !== 1 ? 's' : '') + '</span>';
|
|
166
|
+
html += tokenLinkHtml;
|
|
167
|
+
html += '</div>';
|
|
168
|
+
|
|
169
|
+
// Job rows (show all jobs, including completed)
|
|
170
|
+
if (this.jobs.length > 0) {
|
|
171
|
+
var self = this;
|
|
172
|
+
this.jobs.forEach(function(job) {
|
|
173
|
+
var jobName = job.promptName || job.promptId || 'Job';
|
|
174
|
+
var elapsedSeconds = job.elapsedSeconds != null ? job.elapsedSeconds : self.calculateElapsed(job.startTime);
|
|
175
|
+
var elapsedText = elapsedSeconds > 0 ? ' (' + elapsedSeconds + 's)' : '';
|
|
176
|
+
|
|
177
|
+
// Status text (capitalized)
|
|
178
|
+
var statusText = '';
|
|
179
|
+
if (job.status) {
|
|
180
|
+
statusText = ' - ' + job.status.charAt(0).toUpperCase() + job.status.slice(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var percent = null;
|
|
184
|
+
if (job.total != null && job.progress != null) {
|
|
185
|
+
percent = Math.round((job.progress / job.total) * 100);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var statusClass = '';
|
|
189
|
+
if (job.status === 'complete') {
|
|
190
|
+
statusClass = 'field-jobs-complete';
|
|
191
|
+
percent = 100;
|
|
192
|
+
} else if (job.status === 'error') {
|
|
193
|
+
statusClass = 'field-jobs-error';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
html += '<div class="field-jobs-row ' + statusClass + '">';
|
|
197
|
+
html += '<div class="field-jobs-row-content">';
|
|
198
|
+
html += '<div class="field-jobs-row-title">' + jobName + statusText + elapsedText + '</div>';
|
|
199
|
+
html += '<div class="field-jobs-row-message">' + (job.message || '') + '</div>';
|
|
200
|
+
html += '</div>';
|
|
201
|
+
if (percent != null) {
|
|
202
|
+
html += '<div class="field-jobs-row-percent">' + percent + '%</div>';
|
|
203
|
+
} else {
|
|
204
|
+
html += '<div class="field-jobs-row-percent">—</div>';
|
|
205
|
+
}
|
|
206
|
+
html += '</div>';
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.innerHTML = html;
|
|
211
|
+
|
|
212
|
+
// Restart elapsed time updates if we have active jobs
|
|
213
|
+
var hasActive = this.jobs.some(function(job) {
|
|
214
|
+
return (job.status === 'running' || job.status === 'starting') &&
|
|
215
|
+
(job.total == null || job.progress == null || job.progress < job.total);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (hasActive) {
|
|
219
|
+
this.startElapsedTimeUpdates();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
window.customElements.define('field-jobs-container', FieldJobsContainer);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
class JoeWorkflowWidget extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.updateTimer = null;
|
|
5
|
+
this._rendering = false;
|
|
6
|
+
this._renderScheduled = false;
|
|
7
|
+
this.config = this.getDefaultConfig();
|
|
8
|
+
this.joeIndex = null;
|
|
9
|
+
this.widgetFieldName = null;
|
|
10
|
+
this._liveObj = null; // constructed snapshot for live completion when autoUpdate enabled
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ['schema', '_id'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
this.schemaName = this.getAttribute('schema');
|
|
19
|
+
this.objectId = this.getAttribute('_id');
|
|
20
|
+
this.classList.add('joe-workflow-widget');
|
|
21
|
+
|
|
22
|
+
// Get workflow config from field definition
|
|
23
|
+
this.workflowField = this.closest('.joe-object-field');
|
|
24
|
+
if (this.workflowField) {
|
|
25
|
+
var fieldName = this.workflowField.getAttribute('data-name');
|
|
26
|
+
this.widgetFieldName = fieldName;
|
|
27
|
+
if (fieldName && window._joe) {
|
|
28
|
+
var fieldDef = window._joe.getField(fieldName);
|
|
29
|
+
this.config = (fieldDef && fieldDef.workflow_config) || this.getDefaultConfig();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.config = this.config || this.getDefaultConfig();
|
|
34
|
+
|
|
35
|
+
// Determine joeIndex for navigation and updates
|
|
36
|
+
try{
|
|
37
|
+
var overlay = this.closest('.joe-overlay');
|
|
38
|
+
if(overlay && overlay.getAttribute('data-joeindex') !== null){
|
|
39
|
+
this.joeIndex = parseInt(overlay.getAttribute('data-joeindex'), 10);
|
|
40
|
+
if(isNaN(this.joeIndex)){ this.joeIndex = null; }
|
|
41
|
+
}
|
|
42
|
+
}catch(e){}
|
|
43
|
+
|
|
44
|
+
// Get schema name from JOE if not provided
|
|
45
|
+
if (!this.schemaName && window._joe && window._joe.current && window._joe.current.schema) {
|
|
46
|
+
this.schemaName = window._joe.current.schema.__schemaname || window._joe.current.schema.name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.scheduleRender();
|
|
50
|
+
this.setupUpdateListener();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getDefaultConfig() {
|
|
54
|
+
return {
|
|
55
|
+
sections: 'all',
|
|
56
|
+
fields: 'all',
|
|
57
|
+
excludeSections: ['system'],
|
|
58
|
+
excludeFields: ['_id', 'created', 'joeUpdated', 'itemtype', 'tags', 'status'],
|
|
59
|
+
mustBeTrue: [],
|
|
60
|
+
autoUpdate: false // Disabled by default to prevent lockups
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setupUpdateListener() {
|
|
65
|
+
// Only set up listeners if autoUpdate is enabled
|
|
66
|
+
if (!this.config || !this.config.autoUpdate) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Simplest live update (no tech debt):
|
|
71
|
+
// On change, compute a constructed snapshot once (debounced) and re-render from that.
|
|
72
|
+
// Critically, we never call _jco(true) from render() to avoid re-entrancy loops.
|
|
73
|
+
if(this.joeIndex === null || !window.getJoe){ return; }
|
|
74
|
+
var self = this;
|
|
75
|
+
var overlay = this.closest('.joe-overlay');
|
|
76
|
+
if(!overlay){ return; }
|
|
77
|
+
this._onFormChange = function(ev){
|
|
78
|
+
if(ev && ev.target && self.contains(ev.target)){ return; }
|
|
79
|
+
clearTimeout(self.updateTimer);
|
|
80
|
+
self.updateTimer = setTimeout(function(){
|
|
81
|
+
try{
|
|
82
|
+
var j = getJoe(self.joeIndex);
|
|
83
|
+
self._liveObj = (j && j.constructObjectFromFields) ? j.constructObjectFromFields(self.joeIndex) : null;
|
|
84
|
+
}catch(e){
|
|
85
|
+
self._liveObj = null;
|
|
86
|
+
}
|
|
87
|
+
self.scheduleRender();
|
|
88
|
+
}, 350);
|
|
89
|
+
};
|
|
90
|
+
overlay.addEventListener('change', this._onFormChange, true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
disconnectedCallback() {
|
|
94
|
+
try{
|
|
95
|
+
var overlay = this.closest('.joe-overlay');
|
|
96
|
+
if(overlay && this._onFormChange){
|
|
97
|
+
overlay.removeEventListener('change', this._onFormChange, true);
|
|
98
|
+
}
|
|
99
|
+
}catch(e){}
|
|
100
|
+
if (this.updateTimer) {
|
|
101
|
+
clearTimeout(this.updateTimer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
attributeChangedCallback(attr, oldValue, newValue) {
|
|
106
|
+
if (oldValue !== newValue) {
|
|
107
|
+
if (attr === 'schema') {
|
|
108
|
+
this.schemaName = newValue;
|
|
109
|
+
} else if (attr === '_id') {
|
|
110
|
+
this.objectId = newValue;
|
|
111
|
+
}
|
|
112
|
+
this.scheduleRender();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
scheduleRender() {
|
|
117
|
+
if (this._renderScheduled) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this._renderScheduled = true;
|
|
121
|
+
var self = this;
|
|
122
|
+
// Defer to avoid re-entrancy with JOE render/construct cycles
|
|
123
|
+
setTimeout(function() {
|
|
124
|
+
self._renderScheduled = false;
|
|
125
|
+
self.render();
|
|
126
|
+
}, 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render() {
|
|
130
|
+
var self = this;
|
|
131
|
+
if (this._rendering) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this._rendering = true;
|
|
135
|
+
var joe = window._joe;
|
|
136
|
+
if (!joe || !joe.current) {
|
|
137
|
+
this.innerHTML = '<div class="joe-workflow-error">Workflow widget requires active JOE instance</div>';
|
|
138
|
+
this._rendering = false;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var fields = joe.current.fields || [];
|
|
143
|
+
var sections = joe.current.sections || {};
|
|
144
|
+
// IMPORTANT: Do NOT call _jco(true) here. It triggers constructObjectFromFields which can
|
|
145
|
+
// cause re-entrant rerenders / attributeChangedCallback loops and lock up the UI.
|
|
146
|
+
// Use the live object snapshot. (When autoUpdate is re-enabled later, we can switch to a safer source.)
|
|
147
|
+
var currentObj = this._liveObj || joe.current.object || {};
|
|
148
|
+
|
|
149
|
+
// Filter sections to show
|
|
150
|
+
var sectionsToShow = [];
|
|
151
|
+
if (this.config.sections === 'all') {
|
|
152
|
+
for (var secId in sections) {
|
|
153
|
+
if (this.config.excludeSections && this.config.excludeSections.indexOf(secId) !== -1) continue;
|
|
154
|
+
if (secId === 'system') continue;
|
|
155
|
+
if (!sections[secId] || !sections[secId].fields || !sections[secId].fields.length) continue;
|
|
156
|
+
sectionsToShow.push(secId);
|
|
157
|
+
}
|
|
158
|
+
} else if (Array.isArray(this.config.sections)) {
|
|
159
|
+
sectionsToShow = this.config.sections.filter(function(secId) {
|
|
160
|
+
return sections[secId] &&
|
|
161
|
+
(!self.config.excludeSections || self.config.excludeSections.indexOf(secId) === -1);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Calculate progress for each section
|
|
166
|
+
var sectionProgress = {};
|
|
167
|
+
sectionsToShow.forEach(function(sectionId) {
|
|
168
|
+
var section = sections[sectionId];
|
|
169
|
+
if (!section) return;
|
|
170
|
+
|
|
171
|
+
var fieldsInSection = section.fields || [];
|
|
172
|
+
if (fieldsInSection.length === 0) return;
|
|
173
|
+
|
|
174
|
+
var completed = 0;
|
|
175
|
+
var totalCount = 0;
|
|
176
|
+
var missing = [];
|
|
177
|
+
fieldsInSection.forEach(function(fname) {
|
|
178
|
+
var field = joe.getField(fname) || { name: fname, type: 'text' };
|
|
179
|
+
var ftype = (joe.propAsFuncOrValue && joe.propAsFuncOrValue(field.type, currentObj)) || field.type || 'text';
|
|
180
|
+
ftype = (ftype || '').toString().toLowerCase();
|
|
181
|
+
// Never include content fields in completion counts (widgets, labels, etc).
|
|
182
|
+
if(ftype === 'content'){ return; }
|
|
183
|
+
if(self.config.excludeFields && self.config.excludeFields.indexOf(field.name) !== -1){ return; }
|
|
184
|
+
if(self.config.fields === 'requiredOnly' && !joe.propAsFuncOrValue(field.required, currentObj)){ return; }
|
|
185
|
+
if(field.hidden && joe.propAsFuncOrValue(field.hidden, currentObj)){ return; }
|
|
186
|
+
if(field.condition && !joe.propAsFuncOrValue(field.condition, currentObj)){ return; }
|
|
187
|
+
totalCount++;
|
|
188
|
+
if (self.isFieldComplete(field, currentObj)) {
|
|
189
|
+
completed++;
|
|
190
|
+
}else{
|
|
191
|
+
missing.push(field.display || field.label || field.name);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if(!totalCount){ return; }
|
|
196
|
+
var percentage = Math.round((completed / totalCount) * 100);
|
|
197
|
+
sectionProgress[sectionId] = {
|
|
198
|
+
name: section.name || sectionId,
|
|
199
|
+
completed: completed,
|
|
200
|
+
total: totalCount,
|
|
201
|
+
percentage: percentage,
|
|
202
|
+
isComplete: percentage === 100,
|
|
203
|
+
anchor: section.anchor || sectionId,
|
|
204
|
+
missing: missing
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Render widget HTML
|
|
209
|
+
var widgetTitle = this.config.title;
|
|
210
|
+
if (!widgetTitle && this.schemaName) {
|
|
211
|
+
// Capitalize first letter of schema name
|
|
212
|
+
widgetTitle = (this.schemaName.charAt(0).toUpperCase() + this.schemaName.slice(1)) + ' Workflow';
|
|
213
|
+
}
|
|
214
|
+
if (!widgetTitle) {
|
|
215
|
+
widgetTitle = 'Workflow'; // fallback
|
|
216
|
+
}
|
|
217
|
+
var html = '<div class="joe-workflow-widget-header">' + widgetTitle + '</div>';
|
|
218
|
+
|
|
219
|
+
var hasSections = false;
|
|
220
|
+
sectionsToShow.forEach(function(sectionId) {
|
|
221
|
+
var progress = sectionProgress[sectionId];
|
|
222
|
+
if (!progress) return;
|
|
223
|
+
hasSections = true;
|
|
224
|
+
|
|
225
|
+
var statusHtml = progress.isComplete
|
|
226
|
+
? '<span class="joe-workflow-checkmark" title="Complete">✓</span>'
|
|
227
|
+
: '<span class="joe-workflow-percentage" title="' + progress.completed + ' of ' + progress.total + ' fields complete">' + progress.percentage + '%</span>';
|
|
228
|
+
|
|
229
|
+
var ji = (self.joeIndex !== null ? self.joeIndex : 0);
|
|
230
|
+
var safeSectionIdHtml = (sectionId || '').toString()
|
|
231
|
+
.replace(/&/g, '&')
|
|
232
|
+
.replace(/"/g, '"');
|
|
233
|
+
// Use double-quotes inside gotoSection(...) to avoid JS escaping issues.
|
|
234
|
+
var clickAction = 'onclick="try{ getJoe(' + ji + ').gotoSection("' + safeSectionIdHtml + '"); }catch(e){}" style="cursor:pointer;"';
|
|
235
|
+
|
|
236
|
+
var tip = '';
|
|
237
|
+
if(progress.missing && progress.missing.length){
|
|
238
|
+
tip = progress.missing.join(' | ')
|
|
239
|
+
.replace(/&/g, '&')
|
|
240
|
+
.replace(/"/g, '"')
|
|
241
|
+
.replace(/</g, '<')
|
|
242
|
+
.replace(/>/g, '>');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
html += '<div class="joe-workflow-section" ' + clickAction + (tip ? (' title="' + tip + '"') : '') + '>';
|
|
246
|
+
html += '<div class="joe-workflow-section-label">' + progress.name + '</div>';
|
|
247
|
+
html += '<div class="joe-workflow-section-status">' + statusHtml + '</div>';
|
|
248
|
+
html += '</div>';
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!hasSections) {
|
|
252
|
+
html += '<div class="joe-workflow-empty">No sections to track</div>';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.innerHTML = html;
|
|
256
|
+
this._rendering = false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
isFieldComplete(field, obj) {
|
|
260
|
+
var value = obj[field.name];
|
|
261
|
+
var fieldType = (field.type || '').toLowerCase();
|
|
262
|
+
var mustBeTrue = this.config.mustBeTrue && Array.isArray(this.config.mustBeTrue)
|
|
263
|
+
&& this.config.mustBeTrue.indexOf(field.name) !== -1;
|
|
264
|
+
|
|
265
|
+
if (value === undefined || value === null) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
switch (fieldType) {
|
|
270
|
+
case 'text':
|
|
271
|
+
case 'rendering':
|
|
272
|
+
case 'code':
|
|
273
|
+
case 'wysiwyg':
|
|
274
|
+
case 'url':
|
|
275
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
276
|
+
|
|
277
|
+
case 'objectreference':
|
|
278
|
+
return Array.isArray(value) ? value.length > 0 : !!value;
|
|
279
|
+
|
|
280
|
+
case 'objectlist':
|
|
281
|
+
return Array.isArray(value) && value.length > 0;
|
|
282
|
+
|
|
283
|
+
case 'number':
|
|
284
|
+
return typeof value === 'number' && !isNaN(value);
|
|
285
|
+
|
|
286
|
+
case 'boolean':
|
|
287
|
+
if (mustBeTrue) {
|
|
288
|
+
return value === true;
|
|
289
|
+
}
|
|
290
|
+
return typeof value === 'boolean';
|
|
291
|
+
|
|
292
|
+
case 'date':
|
|
293
|
+
case 'date-time':
|
|
294
|
+
return !!value;
|
|
295
|
+
|
|
296
|
+
case 'select':
|
|
297
|
+
return value !== '' && value !== null && value !== undefined;
|
|
298
|
+
|
|
299
|
+
case 'uploader':
|
|
300
|
+
return Array.isArray(value) && value.length > 0;
|
|
301
|
+
|
|
302
|
+
default:
|
|
303
|
+
if (Array.isArray(value)) {
|
|
304
|
+
return value.length > 0;
|
|
305
|
+
}
|
|
306
|
+
if (typeof value === 'object') {
|
|
307
|
+
return Object.keys(value).length > 0;
|
|
308
|
+
}
|
|
309
|
+
return !!value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
window.customElements.define('joe-workflow-widget', JoeWorkflowWidget);
|