json-object-editor 0.10.633 → 0.10.638

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 CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.10.638
2
+ - Matrix: Added schema relationship visualization page at `/_www/matrix.html` with interactive D3.js force-directed graph showing schema-to-schema relationships. Features include app-based filtering, clickable nodes with schema properties panel, SVG icons, and link labels that appear on node selection.
3
+
4
+ ## 0.10.637
5
+ - Favicon: Added environment-aware favicon switching - development (localhost) uses `favicon-dev.png` with red "D" indicator, production uses default `favicon.ico`. Implemented via `js/favicon-env.js` script included in all templates and MCP debug pages.
6
+
7
+ ## 0.10.635
8
+ - AI Autofill: "Allow web search" is now supported in autofill—if a field's `ai.allowWeb` is set (schema, UI, or API), the `/API/plugin/chatgpt/autofill` endpoint will enable OpenAI's built-in `web_search` for that call. This lets the assistant fetch up-to-date info or run online research when generating field values.
9
+
10
+
1
11
  ## 0.10.632
2
12
  - AI / Widget: Responses-based tool runner for `<joe-ai-widget>` wired to MCP tools via `chatgpt.runWithTools`, plus safer user handling (explicit user id/name/color on `ai_widget_conversation`).
3
13
  - Auth: Updated Google OAuth token exchange to use the current `oauth2.googleapis.com/token` endpoint and surface detailed error payloads when login fails.
@@ -0,0 +1,126 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>JOE — Schema Matrix</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <link rel="stylesheet" href="/JsonObjectEditor/web-components/joe-matrix.css"/>
8
+ <style>
9
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:0;padding:0;overflow:hidden}
10
+ #matrix-container{width:100vw;height:100vh;position:relative}
11
+
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <div id="mcp-nav"></div>
16
+ <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
17
+
18
+ <div id="matrix-container">
19
+ <div class="matrix-controls">
20
+ <label>View Mode:</label>
21
+ <select id="view-mode">
22
+ <option value="schema-to-schema">Schema to Schema (3a)</option>
23
+ <option value="object-relationships" disabled>Object Relationships (3b) - Coming Soon</option>
24
+ <option value="aggregate-relationships" disabled>Aggregate Relationships (3c) - Coming Soon</option>
25
+ </select>
26
+ <label>Filter by App:</label>
27
+ <select id="app-filter">
28
+ <option value="">All</option>
29
+ </select>
30
+ <button id="reset-view">Reset View</button>
31
+ <button id="fit-to-screen">Fit to Screen</button>
32
+ </div>
33
+ <joe-matrix id="joe-matrix-main"></joe-matrix>
34
+ </div>
35
+
36
+ <script src="/JsonObjectEditor/js/plugins/c3/d3.v3.min.js"></script>
37
+ <script src="/JsonObjectEditor/web-components/joe-matrix.js"></script>
38
+ <script>
39
+ (function(){
40
+ // URL parameter helpers
41
+ function getUrlParam(name) {
42
+ var params = new URLSearchParams(window.location.search);
43
+ return params.get(name) || '';
44
+ }
45
+
46
+ function setUrlParam(name, value) {
47
+ var url = new URL(window.location);
48
+ if (value) {
49
+ url.searchParams.set(name, value);
50
+ } else {
51
+ url.searchParams.delete(name);
52
+ }
53
+ window.history.replaceState({}, '', url);
54
+ }
55
+
56
+ // View mode change handler
57
+ var viewMode = document.getElementById('view-mode');
58
+ var appFilter = document.getElementById('app-filter');
59
+ var resetBtn = document.getElementById('reset-view');
60
+ var fitBtn = document.getElementById('fit-to-screen');
61
+ var matrixEl = document.getElementById('joe-matrix-main');
62
+
63
+ // Read app parameter from URL on load
64
+ var initialApp = getUrlParam('app');
65
+ if (initialApp && appFilter) {
66
+ // Wait for options to be populated, then set the value
67
+ var checkInterval = setInterval(function() {
68
+ if (appFilter.options.length > 1) {
69
+ clearInterval(checkInterval);
70
+ // Check if the app exists in the options
71
+ var found = false;
72
+ for (var i = 0; i < appFilter.options.length; i++) {
73
+ if (appFilter.options[i].value === initialApp) {
74
+ appFilter.value = initialApp;
75
+ found = true;
76
+ break;
77
+ }
78
+ }
79
+ if (found && matrixEl && matrixEl.setAppFilter) {
80
+ matrixEl.setAppFilter(initialApp);
81
+ }
82
+ }
83
+ }, 100);
84
+ // Clear interval after 5 seconds to avoid infinite checking
85
+ setTimeout(function() { clearInterval(checkInterval); }, 5000);
86
+ }
87
+
88
+ if(viewMode){
89
+ viewMode.addEventListener('change', function(e){
90
+ if(matrixEl){
91
+ matrixEl.setAttribute('mode', e.target.value);
92
+ }
93
+ });
94
+ }
95
+
96
+ if(appFilter){
97
+ appFilter.addEventListener('change', function(e){
98
+ var selectedApp = e.target.value || '';
99
+ // Update URL parameter
100
+ setUrlParam('app', selectedApp);
101
+ // Update matrix filter
102
+ if(matrixEl && matrixEl.setAppFilter){
103
+ matrixEl.setAppFilter(selectedApp);
104
+ }
105
+ });
106
+ }
107
+
108
+ if(resetBtn){
109
+ resetBtn.addEventListener('click', function(){
110
+ if(matrixEl && matrixEl.resetView){
111
+ matrixEl.resetView();
112
+ }
113
+ });
114
+ }
115
+
116
+ if(fitBtn){
117
+ fitBtn.addEventListener('click', function(){
118
+ if(matrixEl && matrixEl.fitToScreen){
119
+ matrixEl.fitToScreen();
120
+ }
121
+ });
122
+ }
123
+ })();
124
+ </script>
125
+ </body>
126
+ </html>
package/_www/mcp-nav.js CHANGED
@@ -8,6 +8,7 @@
8
8
  '<a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a>',
9
9
  '<a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a>',
10
10
  '<a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a>',
11
+ '<a href="/matrix.html" target="matrix_win" rel="noopener">Matrix</a>',
11
12
  '<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
12
13
  '<a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>',
13
14
  '<span style="margin-left:auto"></span>',
@@ -42,6 +43,11 @@
42
43
  document.addEventListener('DOMContentLoaded', insert);
43
44
  document.addEventListener('DOMContentLoaded', updateInstance);
44
45
  }else{ insert(); updateInstance(); }
46
+
47
+ // Load favicon environment script for production detection
48
+ var faviconScript = document.createElement('script');
49
+ faviconScript.src = '/JsonObjectEditor/js/favicon-env.js';
50
+ document.head.appendChild(faviconScript);
45
51
  })();
46
52
 
47
53
 
@@ -1,116 +1,116 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title>JOE MCP Prompt</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <style>
8
- body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;}
9
- textarea{width:100%;height:360px;font-family:ui-monospace,Menlo,Consolas,monospace}
10
- .small{font-size:12px;color:#666}
11
- </style>
12
- </head>
13
- <body>
14
- <div id="mcp-nav"></div>
15
- <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
16
- <h1>Starter Agent Instructions</h1>
17
- <div class="small">Copy into your Custom GPT or Assistant system prompt. This text is loaded from docs/joe_agent_custom_gpt_instructions_v_2.md (source of truth).</div>
18
-
19
- <h3>Downloads</h3>
20
- <div class="small">Quickly export JSON helpful for agent setup and offline review.</div>
21
- <div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;">
22
- <label for="base" style="margin:0">Base URL</label>
23
- <input id="base" value="" placeholder="http://localhost:2025" style="min-width:280px"/>
24
- <button id="downloadApps">Download apps.json</button>
25
- <button id="downloadHydrate">Download hydrate-<date>.json</button>
26
- <span id="dlStatus" class="small"></span>
27
- </div>
28
- <textarea readonly id="prompt" placeholder="Loading instructions from docs…"></textarea>
29
-
30
- <script>
31
- (function(){
32
- var $ = function(id){ return document.getElementById(id); };
33
- var base = $('base');
34
- var dlBtn = $('downloadApps');
35
- var dlHydrateBtn = $('downloadHydrate');
36
- var dlStatus = $('dlStatus');
37
- var promptBox = $('prompt');
38
- base.value = base.value || location.origin;
39
-
40
- function setStatus(msg, ok){
41
- dlStatus.textContent = msg||'';
42
- dlStatus.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
43
- }
44
-
45
- async function fetchJSON(url, opts){
46
- const res = await fetch(url, opts);
47
- const ct = res.headers.get('content-type')||'';
48
- const isJSON = ct.indexOf('application/json') >= 0;
49
- if(!res.ok){
50
- let detail = isJSON ? await res.json().catch(function(){return {};}) : await res.text();
51
- throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
52
- }
53
- return isJSON ? res.json() : res.text();
54
- }
55
-
56
- async function callMCP(method, params){
57
- const url = base.value.replace(/\/$/,'') + '/mcp';
58
- const body = { jsonrpc: '2.0', id: String(Date.now()), method: method, params: params||{} };
59
- return fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
60
- }
61
-
62
- dlBtn.onclick = async function(){
63
- setStatus('Fetching apps...', null);
64
- try{
65
- const resp = await callMCP('listApps', {});
66
- const data = (resp && (resp.result||resp)) || {};
67
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
68
- const url = URL.createObjectURL(blob);
69
- const a = document.createElement('a');
70
- a.href = url;
71
- a.download = 'apps.json';
72
- document.body.appendChild(a);
73
- a.click();
74
- setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
75
- setStatus('Downloaded', true);
76
- } catch(e){
77
- setStatus(e.message||String(e), false);
78
- }
79
- };
80
-
81
- // Load system instructions from docs (source of truth)
82
- (function loadDocs(){
83
- var path = '/JsonObjectEditor/docs/joe_agent_custom_gpt_instructions_v_3.md';
84
- fetch(path).then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.text(); })
85
- .then(function(text){ promptBox.value = text; })
86
- .catch(function(e){ promptBox.value = 'Failed to load instructions: '+(e.message||String(e)); });
87
- })();
88
-
89
- dlHydrateBtn.onclick = async function(){
90
- setStatus('Fetching hydrate...', null);
91
- try{
92
- const resp = await callMCP('hydrate', {});
93
- const data = (resp && (resp.result||resp)) || {};
94
- const now = new Date();
95
- const yyyy = now.getFullYear();
96
- const mm = String(now.getMonth()+1).padStart(2,'0');
97
- const dd = String(now.getDate()).padStart(2,'0');
98
- const fname = `hydrate_${yyyy}-${mm}-${dd}.json`;
99
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
100
- const url = URL.createObjectURL(blob);
101
- const a = document.createElement('a');
102
- a.href = url;
103
- a.download = fname;
104
- document.body.appendChild(a);
105
- a.click();
106
- setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
107
- setStatus('Downloaded', true);
108
- } catch(e){
109
- setStatus(e.message||String(e), false);
110
- }
111
- };
112
- })();
113
- </script>
114
- </body>
115
- </html>
116
-
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>JOE MCP Prompt</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;}
9
+ textarea{width:100%;height:360px;font-family:ui-monospace,Menlo,Consolas,monospace}
10
+ .small{font-size:12px;color:#666}
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div id="mcp-nav"></div>
15
+ <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
16
+ <h1>Starter Agent Instructions</h1>
17
+ <div class="small">Copy into your Custom GPT or Assistant system prompt. This text is loaded from docs/joe_agent_custom_gpt_instructions_v_2.md (source of truth).</div>
18
+
19
+ <h3>Downloads</h3>
20
+ <div class="small">Quickly export JSON helpful for agent setup and offline review.</div>
21
+ <div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;">
22
+ <label for="base" style="margin:0">Base URL</label>
23
+ <input id="base" value="" placeholder="http://localhost:2025" style="min-width:280px"/>
24
+ <button id="downloadApps">Download apps.json</button>
25
+ <button id="downloadHydrate">Download hydrate-<date>.json</button>
26
+ <span id="dlStatus" class="small"></span>
27
+ </div>
28
+ <textarea readonly id="prompt" placeholder="Loading instructions from docs…"></textarea>
29
+
30
+ <script>
31
+ (function(){
32
+ var $ = function(id){ return document.getElementById(id); };
33
+ var base = $('base');
34
+ var dlBtn = $('downloadApps');
35
+ var dlHydrateBtn = $('downloadHydrate');
36
+ var dlStatus = $('dlStatus');
37
+ var promptBox = $('prompt');
38
+ base.value = base.value || location.origin;
39
+
40
+ function setStatus(msg, ok){
41
+ dlStatus.textContent = msg||'';
42
+ dlStatus.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
43
+ }
44
+
45
+ async function fetchJSON(url, opts){
46
+ const res = await fetch(url, opts);
47
+ const ct = res.headers.get('content-type')||'';
48
+ const isJSON = ct.indexOf('application/json') >= 0;
49
+ if(!res.ok){
50
+ let detail = isJSON ? await res.json().catch(function(){return {};}) : await res.text();
51
+ throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
52
+ }
53
+ return isJSON ? res.json() : res.text();
54
+ }
55
+
56
+ async function callMCP(method, params){
57
+ const url = base.value.replace(/\/$/,'') + '/mcp';
58
+ const body = { jsonrpc: '2.0', id: String(Date.now()), method: method, params: params||{} };
59
+ return fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
60
+ }
61
+
62
+ dlBtn.onclick = async function(){
63
+ setStatus('Fetching apps...', null);
64
+ try{
65
+ const resp = await callMCP('listApps', {});
66
+ const data = (resp && (resp.result||resp)) || {};
67
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
68
+ const url = URL.createObjectURL(blob);
69
+ const a = document.createElement('a');
70
+ a.href = url;
71
+ a.download = 'apps.json';
72
+ document.body.appendChild(a);
73
+ a.click();
74
+ setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
75
+ setStatus('Downloaded', true);
76
+ } catch(e){
77
+ setStatus(e.message||String(e), false);
78
+ }
79
+ };
80
+
81
+ // Load system instructions from docs (source of truth)
82
+ (function loadDocs(){
83
+ var path = '/JsonObjectEditor/docs/joe_agent_custom_gpt_instructions_v_3.md';
84
+ fetch(path).then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.text(); })
85
+ .then(function(text){ promptBox.value = text; })
86
+ .catch(function(e){ promptBox.value = 'Failed to load instructions: '+(e.message||String(e)); });
87
+ })();
88
+
89
+ dlHydrateBtn.onclick = async function(){
90
+ setStatus('Fetching hydrate...', null);
91
+ try{
92
+ const resp = await callMCP('hydrate', {});
93
+ const data = (resp && (resp.result||resp)) || {};
94
+ const now = new Date();
95
+ const yyyy = now.getFullYear();
96
+ const mm = String(now.getMonth()+1).padStart(2,'0');
97
+ const dd = String(now.getDate()).padStart(2,'0');
98
+ const fname = `hydrate_${yyyy}-${mm}-${dd}.json`;
99
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
100
+ const url = URL.createObjectURL(blob);
101
+ const a = document.createElement('a');
102
+ a.href = url;
103
+ a.download = fname;
104
+ document.body.appendChild(a);
105
+ a.click();
106
+ setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
107
+ setStatus('Downloaded', true);
108
+ } catch(e){
109
+ setStatus(e.message||String(e), false);
110
+ }
111
+ };
112
+ })();
113
+ </script>
114
+ </body>
115
+ </html>
116
+
@@ -180,6 +180,10 @@
180
180
  return Math.round(score);
181
181
  }
182
182
 
183
+ // Read app parameter from URL
184
+ var urlParams = new URLSearchParams(window.location.search);
185
+ var urlApp = urlParams.get('app');
186
+
183
187
  // Populate app filter options
184
188
  appFilterSel = document.getElementById('appFilter');
185
189
  if (appFilterSel){
@@ -187,6 +191,17 @@
187
191
  appNames.forEach(function(a){
188
192
  var opt=document.createElement('option'); opt.value=a; opt.textContent=a; appFilterSel.appendChild(opt);
189
193
  });
194
+
195
+ // Set filter from URL parameter if present (before initial render)
196
+ if (urlApp) {
197
+ // Check if the app exists in the options
198
+ for (var i = 0; i < appFilterSel.options.length; i++) {
199
+ if (appFilterSel.options[i].value === urlApp) {
200
+ appFilterSel.value = urlApp;
201
+ break;
202
+ }
203
+ }
204
+ }
190
205
  }
191
206
 
192
207
  function getFilteredData(){
@@ -267,7 +282,7 @@
267
282
  });
268
283
  }
269
284
 
270
- // Default sort by name asc
285
+ // Default sort by name asc (filter will be applied automatically if appFilterSel.value is set)
271
286
  sortData('name');
272
287
  updateHeaderIndicators();
273
288
 
@@ -277,7 +292,22 @@
277
292
  });
278
293
 
279
294
  // Filter handler
280
- if (appFilterSel){ appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); }; }
295
+ if (appFilterSel){
296
+ appFilterSel.onchange = function(){
297
+ // Update URL parameter
298
+ var selectedApp = appFilterSel.value || '';
299
+ var url = new URL(window.location);
300
+ if (selectedApp) {
301
+ url.searchParams.set('app', selectedApp);
302
+ } else {
303
+ url.searchParams.delete('app');
304
+ }
305
+ window.history.replaceState({}, '', url);
306
+
307
+ // Apply filter
308
+ sortData(sortState.key || 'name');
309
+ };
310
+ }
281
311
  setStatus('Loaded '+list.length+' schemas', true);
282
312
  }catch(e){
283
313
  setStatus(e.message||String(e), false);
@@ -3128,13 +3128,17 @@ html.no-touch .joe-text-autocomplete-option:hover {
3128
3128
  padding: 5px;
3129
3129
  cursor: move;
3130
3130
  position:relative;
3131
- margin: 1px 0;
3131
+ margin: 4px 4px 4px 2px;
3132
3132
  transition: background .1s;
3133
3133
  background:#f9f9f9;
3134
+ line-height: 1em;
3135
+ border: 1px solid #eee;
3136
+ border-radius: 2px;
3134
3137
  }
3135
3138
  html.no-touch .joe-multisorter-bin li:hover,
3136
3139
  html.no-touch .joe-buckets-bin li:hover{
3137
- background:rgba(0,0,0,.1);
3140
+ background:rgba(0,0,0,.05);
3141
+ border: 1px solid #fff;
3138
3142
  }
3139
3143
 
3140
3144
 
package/css/joe.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* --------------------------------------------------------
2
2
  *
3
- * json-object-editor - v0.10.632
3
+ * json-object-editor - v0.10.636
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
@@ -3616,13 +3616,16 @@ html.no-touch .joe-text-autocomplete-option:hover {
3616
3616
  padding: 5px;
3617
3617
  cursor: move;
3618
3618
  position:relative;
3619
- margin: 1px 0;
3619
+ margin: 4px 4px 4px 2px;
3620
3620
  transition: background .1s;
3621
3621
  background:#f9f9f9;
3622
+ line-height: 1em;
3623
+ border: 1px solid #eee;
3622
3624
  }
3623
3625
  html.no-touch .joe-multisorter-bin li:hover,
3624
3626
  html.no-touch .joe-buckets-bin li:hover{
3625
- background:rgba(0,0,0,.1);
3627
+ background:rgba(0,0,0,.05);
3628
+ border: 1px solid #fff;
3626
3629
  }
3627
3630
 
3628
3631