Commit e6c710eb authored by Rafael Monnerat's avatar Rafael Monnerat

erp5_json_editor: Introduce JSON Editor

  This gadget aims to edit json based on json schema.
parent 4170ce5a
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Folder" module="OFS.Folder"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_json_editor</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
url_list = [
"ref-parser.min.js",
"json-editor.gadget.css",
"json-editor.gadget.html",
"json-editor.gadget.js",
"jsoneditor.js"
]
return url_list
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSection_getJsonEditorPrecacheManifestList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
div.json-editor-container .je-object__container .level-1 {
display: none !important;
}
div.json-editor-container .container {
width: 100%;
padding-right: var(--bs-gutter-x, 0.75rem);
padding-left: var(--bs-gutter-x, 0.75rem);
margin-right: 0px;
margin-left: 0px;
}
div.json-editor-container p {
margin-bottom: 1rem;
}
div.json-editor-container label {
display: inline-block;
}
div.json-editor-container .alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
div.json-editor-container .alert {
position: relative;
padding: 1rem 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
div.json-editor-container .form-label {
margin-bottom: 0.5rem;
}
div.json-editor-container .bg-light {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
}
div.json-editor-container .card {
display: flex;
flex-direction: column;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
padding: 1rem 1rem;
background-color: #fff;
}
div.json-editor-container .je-switcher {
background-color: transparent;
height: auto;
width: auto;
margin-bottom: 0px;
margin-left: 5px;
padding: 0px 0px 0px 3px;
}
div.json-editor-container .btn {
vertical-align: middle;
border: 1px solid transparent;
padding: 0.25rem 0.5rem;
flex: 1 1 auto;
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
font-size: 0.875rem;
border-radius: 0.2rem;
}
div.json-editor-container .btn:hover {
color: #fff;
background-color: #5c636a;
border-color: #565e64;
}
div.json-editor-container .fas {
font-family: 'FontAwesome';
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
div.json-editor-container .row {
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 0;
display: flex;
flex-wrap: wrap;
margin-top: calc(-1 * var(--bs-gutter-y));
margin-right: calc(-0.5 * var(--bs-gutter-x));
margin-left: calc(-0.5 * var(--bs-gutter-x));
}
div.json-editor-container .row > * {
flex-shrink: 0;
width: 100%;
max-width: 100%;
padding-right: calc(var(--bs-gutter-x) * 0.5);
padding-left: calc(var(--bs-gutter-x) * 0.5);
margin-top: var(--bs-gutter-y);
}
div.json-editor-container .my-3 {
margin-bottom: 1rem!important;
}
div.json-editor-container .form-control:disabled,
div.json-editor-container .form-control[readonly] {
background-color: #e9ecef !important;
opacity: 1;
}
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>json-editor.gadget.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JSON Editor</title>
<!-- <link rel="stylesheet" id="iconlib-link" href="font-awesome.5.1/font-awesome.5.1.css"> -->
<link rel="stylesheet" href="json-editor.gadget.css">
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="domsugar.js"></script>
<script src="ref-parser.min.js"></script>
<script src="jsoneditor.js"></script>
<script src="json-editor.gadget.js"></script>
</head>
<body>
<div class="container">
<div class='json-editor-container'></div>
<input type='hidden' class='json-editor-value'></input>
</div>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>json-editor.gadget.html</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*jslint nomen: true, indent: 2 */
/*global window, rJS, RSVP, JSONEditor, domsugar, JSON, $RefParser, URL */
(function (window, rJS, RSVP, JSONEditor, domsugar, JSON, $RefParser, URL) {
'use strict';
JSONEditor.AbstractEditor.prototype.getDefault = function () {
/* Append an empty value and never load the default value on the field */
if (this.schema.enum !== undefined) {
this.schema.enum.unshift("");
return "";
}
return undefined;
};
function isEmpty(obj) {
return obj === undefined || obj === '' ||
(
obj === Object(obj) &&
Object.keys(obj).length === 0 &&
(obj.constructor === Object || obj.constructor === Array)
);
}
JSONEditor.defaults.editors.object.prototype.getValue = function () {
if (!this.dependenciesFulfilled) {
return undefined;
}
/* original code uses super.getValue() but we cannot use super here */
var result = this.value;
if (result && (this.jsoneditor.options.remove_empty_properties || this.options.remove_empty_properties)) {
Object.keys(result).forEach(function (key) {
if (isEmpty(result[key])) {
delete result[key];
}
});
}
return result;
};
JSONEditor.AbstractEditor.prototype.preBuild = function () {
if (this.jsoneditor.options.readonly) {
this.schema.readOnly = this.jsoneditor.options.readonly;
}
};
if (JSONEditor.defaults.editors.select.prototype.original_preBuild === undefined) {
JSONEditor.defaults.editors.select.prototype.original_preBuild = JSONEditor.defaults.editors.select.prototype.preBuild;
}
JSONEditor.defaults.editors.select.prototype.preBuild = function () {
if (this.jsoneditor.options.readonly) {
this.schema.readOnly = this.jsoneditor.options.readonly;
}
if (this.schema.enum !== undefined) {
this.schema.enum.unshift("");
}
this.original_preBuild();
if (this.schema.type === 'boolean') {
/* the original code on preBuild include an empty first value if the value
is not required, but we always want the empty value */
if (this.isRequired()) {
this.enum_display.unshift(' ');
this.enum_options.unshift('undefined');
this.enum_values.unshift(undefined);
}
}
};
JSONEditor.defaults.editors.select.prototype.getValue = function () {
if (this.value === "") {
return undefined;
}
if (this.value === undefined) {
return undefined;
}
if (!this.dependenciesFulfilled) {
return undefined;
}
return this.typecast(this.value);
};
JSONEditor.defaults.editors.select.prototype.typecast = function (value) {
if (this.schema.type === 'boolean') {
return value === 'undefined' || value === undefined ? undefined : !!value;
}
if (this.schema.type === 'number' && value === "") {
return undefined;
}
if (this.schema.type === 'integer' && value === "") {
return undefined;
}
if (this.schema.type === 'number') {
return parseFloat(value) || 0;
}
if (this.schema.type === 'integer') {
return Math.floor(parseFloat(value) || 0);
}
if (this.schema.enum && value === undefined) {
return undefined;
}
if (value === undefined) {
return undefined;
}
return value.toString();
};
/* The original code would remove the field if value is undefined */
JSONEditor.defaults.editors.object.prototype.setValue = function (value, initial) {
var object_editor = this;
value = value || {};
if (typeof value !== 'object' || Array.isArray(value)) {
value = {};
}
/* First, set the values for all of the defined properties */
// @ts-ignore
Object.entries(this.cached_editors).forEach(function (entry) {
var i = entry[0],
editor = entry[1];
/* Value explicitly set */
if (value[i] !== undefined) {
object_editor.addObjectProperty(i);
editor.setValue(value[i], initial);
editor.activate();
/* Otherwise if it is read only remove the field */
} else if (editor.schema.readOnly) {
object_editor.removeObjectProperty(i);
/* Otherwise, set the value to the default */
} else {
editor.setValue(editor.getDefault(), initial);
}
});
// @ts-ignore
Object.entries(value).forEach(function (entry) {
var i = entry[0],
val = entry[1];
if (!object_editor.cached_editors[i]) {
object_editor.addObjectProperty(i);
if (object_editor.editors[i]) {
object_editor.editors[i].setValue(val, initial, !!object_editor.editors[i].template);
}
}
});
object_editor.refreshValue();
object_editor.layoutEditors();
object_editor.onChange();
};
JSONEditor.defaults.editors.string.prototype.setValueToInputField = function (value) {
this.input.value = value === undefined ? '' : value;
/* ERP5: Once you set the value to the input, you also
updates the field value, otherwise the getValue will miss the value */
this.value = this.input.value;
};
/* Backward compatibility with the usage of textarea property
if converts into json-editor proper property */
JSONEditor.defaults.editors.string.prototype.preBuild = function () {
if ((this.schema.textarea === true) || (this.schema.textarea === 1)) {
this.schema.format = 'textarea';
}
if (this.jsoneditor.options.readonly) {
this.schema.readOnly = this.jsoneditor.options.readonly;
}
};
rJS(window)
.declareAcquiredMethod("notifySubmit", "notifySubmit")
.declareJob("deferNotifySubmit", function () {
// Ensure error will be correctly handled
return this.notifySubmit();
})
.declareAcquiredMethod("notifyChange", "notifyChange")
.declareJob("deferNotifyChange", function () {
// Ensure error will be correctly handled
return this.notifyChange();
})
.declareMethod('render', function (options) {
var gadget = this;
function deferNotifyChange() {
if (!gadget.state.ignoredChangeDuringInitialization && gadget.state.editable) {
return gadget.deferNotifyChange();
}
// Ignore the first attempt since editor trigger change on the after the
// end of the rendering, so ignore the first attempt is reaquired.
// Later calls that trigger change
gadget.state.ignoredChangeDuringInitialization = false;
}
gadget.deferNotifyChangeBinded = deferNotifyChange.bind(gadget);
return gadget.changeState({
schema_url: options.schema_url,
value: options.value || '{}',
editable: options.editable,
key: options.key,
ignoredChangeDuringInitialization: true,
// Force refresh in any case
render_timestamp: new Date().getTime()
});
})
.onStateChange(function () {
var gadget = this,
json_editor_container = gadget.element.querySelector('.json-editor-container');
if (!gadget.state.schema_url) {
return domsugar(json_editor_container);
}
return new RSVP.Queue()
.push(function () {
var schema_url = new URL(gadget.state.schema_url, window.location.href);
return $RefParser.dereference(schema_url.href);
})
.push(function (schema) {
return new JSONEditor(domsugar(json_editor_container), {
schema: schema,
ajax: true,
theme: 'bootstrap5',
show_errors: 'always',
//iconlib: 'fontawesome5',
object_layout: 'normal',
disable_collapse: false,
disable_edit_json: true,
disable_properties: false,
keep_only_existing_values: false,
use_default_values: false,
disable_array_reorder: true,
disable_array_delete_all_rows: true,
disable_array_delete_last_row: true,
no_additional_properties: false,
remove_empty_properties: true,
keep_oneof_values: false,
startval: JSON.parse(gadget.state.value),
readonly: gadget.state.editable ? false : true
});
})
.push(function (editor) {
gadget.editor = editor;
gadget.editor.on('change', gadget.deferNotifyChangeBinded.bind(gadget));
// return complex object is not possible throught iframe
// it seems to crash rjs somewhere. To check...
// https://lab.nexedi.com/nexedi/renderjs/blob/master/renderjs.js#L2070
//return editor;
});
})
.declareMethod('getContent', function () {
var form_data = {};
if (this.editor === undefined) {
return form_data;
}
if (this.state.editable) {
form_data[this.state.key] = JSON.stringify(this.editor.getValue());
// Change the value state in place
// This will prevent the gadget to be changed if
// its parent call render with the same value
// (as ERP5 does in case of formulator error)
this.state.value = form_data[this.state.key];
}
return form_data;
})
.declareMethod('checkValidity', function () {
if (this.state.errors !== undefined) {
return this.state.errors.length === 0;
}
return true;
});
}(window, rJS, RSVP, JSONEditor, domsugar, JSON, $RefParser, URL));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>json-editor.gadget.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
div.json-editor-container {
& .je-object__container .level-1 {
display: none !important;
}
& .container {
width: 100%;
padding-right: var(--bs-gutter-x,.75rem);
padding-left: var(--bs-gutter-x,.75rem);
margin-right: 0px;
margin-left: 0px;
}
& p {
margin-bottom: 1rem;
}
& label {
display: inline-block;
}
& .alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
& .alert {
position: relative;
padding: 1rem 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
& .form-label {
margin-bottom: 0.5rem;
}
& .bg-light {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important;
}
& .card {
display: flex;
flex-direction: column;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
padding: 1rem 1rem;
background-color: #fff
}
& .je-switcher {
background-color: transparent;
height: auto;
width: auto;
margin-bottom: 0px;
margin-left: 5px;
padding: 0px 0px 0px 3px;
}
& .btn {
vertical-align: middle;
border: 1px solid transparent;
padding: 0.25rem 0.5rem;
flex: 1 1 auto;
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
font-size: .875rem;
border-radius: 0.2rem;
}
& .btn:hover {
color: #fff;
background-color: #5c636a;
border-color: #565e64;
}
& .fas {
font-family: 'FontAwesome';
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
& .row {
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 0;
display: flex;
flex-wrap: wrap;
margin-top: calc(-1 * var(--bs-gutter-y));
margin-right: calc(-.5 * var(--bs-gutter-x));
margin-left: calc(-.5 * var(--bs-gutter-x));
}
& .row>* {
flex-shrink: 0;
width: 100%;
max-width: 100%;
padding-right: calc(var(--bs-gutter-x) * .5);
padding-left: calc(var(--bs-gutter-x) * .5);
margin-top: var(--bs-gutter-y)
}
& .my-3 {
margin-bottom: 1rem!important;
}
& .form-control:disabled, .form-control[readonly] {
background-color: #e9ecef !important;
opacity: 1;
}
}
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>json-editor.gadget.less</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/plain</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>jsoneditor.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>jsoneditor.js</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>ref-parser.min.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_font
\ No newline at end of file
https://github.com/json-editor/json-editor as Json Editor using json schemas
\ No newline at end of file
erp5_json_editor
\ No newline at end of file
erp5_json_editor
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment