json-object-editor 0.10.664 → 0.10.668
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 +9 -1
- package/css/joe-styles.css +4390 -4324
- package/css/joe.css +4391 -4325
- package/css/joe.min.css +1 -1
- package/es5-build/js/JsonObjectEditor_es5.jquery.craydent.js +11059 -11049
- package/js/JsonObjectEditor.jquery.craydent.js +11599 -11543
- package/js/joe-test.js +9669 -9659
- package/js/joe.js +11600 -11544
- package/js/joe.min.js +1 -1
- package/js/joe_es5.js +11071 -11061
- package/package.json +1 -1
- package/server/plugins/chatgpt.js +114 -13
- package/server/schemas/ai_prompt.js +13 -2
- package/server/schemas/ai_response.js +1 -0
- package/web-components/joe-workflow-widget.js +314 -0
package/package.json
CHANGED
|
@@ -887,6 +887,7 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
887
887
|
// in markdown fences (```json ... ```), and may prepend/append prose. This
|
|
888
888
|
// helper strips fences and tries to isolate the first well-formed JSON
|
|
889
889
|
// object/array substring so JSON.parse has the best chance of succeeding.
|
|
890
|
+
// Handles cases where tool call logs are concatenated before the actual JSON.
|
|
890
891
|
function extractJsonText(raw) {
|
|
891
892
|
if (!raw) { return ''; }
|
|
892
893
|
let t = String(raw).trim();
|
|
@@ -906,19 +907,100 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
906
907
|
}
|
|
907
908
|
t = t.trim();
|
|
908
909
|
}
|
|
909
|
-
|
|
910
|
+
|
|
911
|
+
// Handle cases where tool call logs (small JSON objects like {"tool":"..."})
|
|
912
|
+
// are concatenated before the actual response JSON (larger JSON object).
|
|
913
|
+
// Find all JSON objects and pick the largest one that's not a tool log.
|
|
914
|
+
const jsonCandidates = [];
|
|
915
|
+
const firstBrace = t.indexOf('{');
|
|
916
|
+
const firstBracket = t.indexOf('[');
|
|
917
|
+
const lastBrace = Math.max(t.lastIndexOf('}'), t.lastIndexOf(']'));
|
|
918
|
+
|
|
919
|
+
if (firstBrace === -1 && firstBracket === -1) {
|
|
920
|
+
return '';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const startPos = (firstBrace === -1) ? firstBracket :
|
|
924
|
+
((firstBracket === -1) ? firstBrace : Math.min(firstBrace, firstBracket));
|
|
925
|
+
|
|
926
|
+
if (startPos === -1 || lastBrace === -1 || lastBrace <= startPos) {
|
|
927
|
+
return t.trim();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Find all potential JSON objects
|
|
931
|
+
for (let i = startPos; i <= lastBrace; i++) {
|
|
932
|
+
if (t[i] !== '{' && t[i] !== '[') continue;
|
|
933
|
+
|
|
934
|
+
// Find matching closing brace/bracket
|
|
935
|
+
let depth = 0;
|
|
936
|
+
let inString = false;
|
|
937
|
+
let escape = false;
|
|
938
|
+
let endPos = -1;
|
|
939
|
+
|
|
940
|
+
for (let j = i; j <= lastBrace; j++) {
|
|
941
|
+
const char = t[j];
|
|
942
|
+
if (escape) {
|
|
943
|
+
escape = false;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (char === '\\') {
|
|
947
|
+
escape = true;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (char === '"') {
|
|
951
|
+
inString = !inString;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (!inString) {
|
|
955
|
+
if (char === '{' || char === '[') {
|
|
956
|
+
depth++;
|
|
957
|
+
} else if (char === '}' || char === ']') {
|
|
958
|
+
depth--;
|
|
959
|
+
if (depth === 0) {
|
|
960
|
+
endPos = j;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (endPos !== -1) {
|
|
968
|
+
const candidate = t.substring(i, endPos + 1);
|
|
969
|
+
// Skip tool call logs - they match pattern {"tool":"..."}
|
|
970
|
+
const isToolLog = /^\s*{\s*"tool"\s*:/.test(candidate);
|
|
971
|
+
try {
|
|
972
|
+
JSON.parse(candidate);
|
|
973
|
+
jsonCandidates.push({
|
|
974
|
+
text: candidate,
|
|
975
|
+
length: candidate.length,
|
|
976
|
+
isToolLog: isToolLog
|
|
977
|
+
});
|
|
978
|
+
} catch (e) {
|
|
979
|
+
// Not valid JSON, skip
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Find the largest non-tool-log JSON object, or largest overall if all are tool logs
|
|
985
|
+
if (jsonCandidates.length > 0) {
|
|
986
|
+
// Filter out tool logs first
|
|
987
|
+
const nonToolLogs = jsonCandidates.filter(c => !c.isToolLog);
|
|
988
|
+
const candidatesToUse = nonToolLogs.length > 0 ? nonToolLogs : jsonCandidates;
|
|
989
|
+
|
|
990
|
+
// Sort by length (descending) and return the largest
|
|
991
|
+
candidatesToUse.sort((a, b) => b.length - a.length);
|
|
992
|
+
return candidatesToUse[0].text.trim();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Fallback: try simple first-to-last extraction
|
|
910
996
|
if (t[0] !== '{' && t[0] !== '[') {
|
|
911
|
-
const
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
else if (firstBracket === -1) { first = firstBrace; }
|
|
916
|
-
else { first = Math.min(firstBrace, firstBracket); }
|
|
917
|
-
const lastBrace = Math.max(t.lastIndexOf('}'), t.lastIndexOf(']'));
|
|
918
|
-
if (first !== -1 && lastBrace !== -1 && lastBrace > first) {
|
|
919
|
-
t = t.slice(first, lastBrace + 1);
|
|
997
|
+
const first = startPos;
|
|
998
|
+
const last = lastBrace;
|
|
999
|
+
if (first !== -1 && last !== -1 && last > first) {
|
|
1000
|
+
t = t.slice(first, last + 1);
|
|
920
1001
|
}
|
|
921
1002
|
}
|
|
1003
|
+
|
|
922
1004
|
return t.trim();
|
|
923
1005
|
}
|
|
924
1006
|
|
|
@@ -1357,9 +1439,27 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1357
1439
|
|
|
1358
1440
|
// const response = await openai.chat.completions.create(payload);
|
|
1359
1441
|
|
|
1442
|
+
// Extract JSON from response to strip tool logs, reasoning text, and other non-JSON content.
|
|
1443
|
+
// This is critical for prompts that explicitly require JSON-only output.
|
|
1444
|
+
const rawResponseText = response.output_text || "";
|
|
1445
|
+
let cleanedResponseText = rawResponseText;
|
|
1446
|
+
try {
|
|
1447
|
+
const extractedJson = extractJsonText(rawResponseText);
|
|
1448
|
+
if (extractedJson && extractedJson.trim().length > 0) {
|
|
1449
|
+
// Validate it's actually valid JSON
|
|
1450
|
+
JSON.parse(extractedJson);
|
|
1451
|
+
cleanedResponseText = extractedJson;
|
|
1452
|
+
}
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
// If extraction fails or JSON is invalid, fall back to raw text
|
|
1455
|
+
// (some prompts may not be JSON-formatted)
|
|
1456
|
+
console.warn('[chatgpt.executeJOEAiPrompt] Failed to extract JSON from response, using raw text:', e.message);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1360
1459
|
const saved = await saveAiResponseRefactor({
|
|
1361
1460
|
prompt,
|
|
1362
|
-
ai_response_content:
|
|
1461
|
+
ai_response_content: cleanedResponseText,
|
|
1462
|
+
ai_response_raw: rawResponseText,
|
|
1363
1463
|
user_prompt: finalInput || '',
|
|
1364
1464
|
params,
|
|
1365
1465
|
referenced_object_ids: referencedObjectIds,
|
|
@@ -1382,7 +1482,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1382
1482
|
}
|
|
1383
1483
|
}catch(_e){}
|
|
1384
1484
|
|
|
1385
|
-
return { success: true, ai_response_id: saved._id,response:
|
|
1485
|
+
return { success: true, ai_response_id: saved._id,response:cleanedResponseText,usage:response.usage };
|
|
1386
1486
|
} catch (e) {
|
|
1387
1487
|
console.error('❌ executeJOEAiPrompt error:', e);
|
|
1388
1488
|
return { error: "Failed to execute AI prompt.",message: e.message };
|
|
@@ -1402,7 +1502,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1402
1502
|
max_tokens: prompt.max_tokens ?? 1200
|
|
1403
1503
|
};
|
|
1404
1504
|
}
|
|
1405
|
-
async function saveAiResponseRefactor({ prompt, ai_response_content, user_prompt, params, referenced_object_ids,response_id,usage,user,ai_assistant_id, mcp_enabled, mcp_toolset, mcp_selected_tools, mcp_instructions_mode, mcp_tools_used }) {
|
|
1505
|
+
async function saveAiResponseRefactor({ prompt, ai_response_content, ai_response_raw, user_prompt, params, referenced_object_ids,response_id,usage,user,ai_assistant_id, mcp_enabled, mcp_toolset, mcp_selected_tools, mcp_instructions_mode, mcp_tools_used }) {
|
|
1406
1506
|
var response_keys = [];
|
|
1407
1507
|
try {
|
|
1408
1508
|
response_keys = Object.keys(JSON.parse(ai_response_content));
|
|
@@ -1437,6 +1537,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1437
1537
|
prompt_name: prompt.name,
|
|
1438
1538
|
prompt_method:prompt.prompt_method,
|
|
1439
1539
|
response: ai_response_content,
|
|
1540
|
+
response_raw: ai_response_raw || null,
|
|
1440
1541
|
response_json: parsedResponse,
|
|
1441
1542
|
response_keys: response_keys,
|
|
1442
1543
|
response_id:response_id||'',
|
|
@@ -98,8 +98,19 @@ var schema = {
|
|
|
98
98
|
'name',
|
|
99
99
|
'info',
|
|
100
100
|
{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']
|
|
101
|
+
{name:'content_items',type:'objectList',display:'Content Items',
|
|
102
|
+
properties:['itemtype','reference'],
|
|
103
|
+
comment:`
|
|
104
|
+
<div>
|
|
105
|
+
<p>Define which objects are automatically passed when running this prompt via <code>select_prompt</code>.</p>
|
|
106
|
+
<ul>
|
|
107
|
+
<li><b><code>itemtype</code></b> - Schema name (e.g., <code>"task"</code>)</li>
|
|
108
|
+
<li><b><code>reference</code></b> - Parameter name for the object's <code>_id</code> (e.g., <code>"task"</code>)</li>
|
|
109
|
+
</ul>
|
|
110
|
+
<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>
|
|
111
|
+
<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>
|
|
112
|
+
</div>
|
|
113
|
+
`
|
|
103
114
|
},
|
|
104
115
|
{section_start:'Input'},
|
|
105
116
|
{name: 'functions', type: 'code', display: 'Helper Functions', language:'javascript',comment: `
|
|
@@ -30,6 +30,7 @@ var schema = {
|
|
|
30
30
|
{ name:'model_used', type:'string' },
|
|
31
31
|
{ name:'response_type', type:'string' },
|
|
32
32
|
{ name:'response', type:'string' },
|
|
33
|
+
{ name:'response_raw', type:'string' },
|
|
33
34
|
{ name:'response_json', type:'object' },
|
|
34
35
|
{ name:'response_keys', type:'string', isArray:true },
|
|
35
36
|
{ name:'response_id', type:'string' },
|
|
@@ -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);
|