Skip to content

Commit 58263ed

Browse files
Comments editing enabled, saving test feature.
1 parent 1ef8312 commit 58263ed

13 files changed

+251
-64
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
# Caché Visual Editor (alpha)
1+
# Caché Visual Editor (pre-alpha)
22

33
A Web-based user interface for InterSystems Caché which allows to create and manage classes
44
literally without touching any code.
55

66
### Preview
77

8-
![Screenshot 1](https://cloud.githubusercontent.com/assets/4989256/13725979/c9e3a45e-e8bb-11e5-874f-28cff147f2e1.png)
8+
![Screenshot 1](https://cloud.githubusercontent.com/assets/4989256/13891951/f52ed7e6-ed5c-11e5-9636-6a9aac876325.png)
99

10-
![Screenshot 2](https://cloud.githubusercontent.com/assets/4989256/13726020/3a87df26-e8bd-11e5-92f8-eecf743c5e6e.png)
10+
![Screenshot 2](https://cloud.githubusercontent.com/assets/4989256/13891950/f52d5574-ed5c-11e5-8377-e9ca1940cd52.png)
1111

1212
### Installation
1313

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cache-visual-editor",
33
"printableName": "Cache Visual Editor",
4-
"version": "0.3.2",
4+
"version": "0.4.1",
55
"description": "Visual class editor for InterSystems Caché",
66
"main": "index.js",
77
"keywords": [
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/// [ <!-- @echo package.printableName --> v<!-- @echo package.version --> ]
2+
/// This is a REST WEB application that allows user to edit class definitions.
3+
/// The POST method here processes the %request.Content to a JSON %proxyObject.
4+
Class VisualEditor.REST.Editor extends %CSP.REST {
5+
6+
XData UrlMap
7+
{
8+
<Routes>
9+
<Route Url="/save" Method="POST" Call="Save"/>
10+
</Routes>
11+
}
12+
13+
/// GET parameter "ns" get parameter - namespace to modify classes in.
14+
/// POST JSON body is like
15+
/// { "TestPack.TestClass": { "properties": { "TestInt": { "Description": "Test desc!" } } } }
16+
ClassMethod Save() As %Status
17+
{
18+
set ns = %request.Get("ns")
19+
if (##class(%SYS.Namespace).Exists(ns)) {
20+
znspace ns
21+
}
22+
do %request.Content.%CopyToArray(.classes)
23+
set response = ##class(%ZEN.proxyObject).%New()
24+
set response.error = 0
25+
set response.modified = 0
26+
set className = $order(classes(""))
27+
while (className '= "") {
28+
do classes(className).Properties.%CopyToArray(.properties)
29+
set propertyName = $order(properties(""))
30+
while (propertyName '= "") {
31+
set pDef = ##class(%Dictionary.PropertyDefinition).%OpenId(className_"||"_propertyName)
32+
if (pDef = "") {
33+
set response.error = response.error _ "Property " _ propertyName _ " not found in "
34+
_ className _ $Char(10)
35+
set propertyName = $order(properties(propertyName))
36+
continue
37+
}
38+
39+
set pDef.Description = properties(propertyName).Description
40+
set error = $System.Status.GetErrorText(pDef.%Save())
41+
if (error '= "") {
42+
set response.error = response.error _ error _ $Char(10)
43+
} else {
44+
set response.modified = response.modified + 1
45+
}
46+
47+
set propertyName = $order(properties(propertyName))
48+
}
49+
50+
set className = $order(classes(className))
51+
}
52+
do response.%ToJSON(, "o")
53+
return $$$OK
54+
}
55+
56+
/// This method is a handler that is executed before each request.
57+
ClassMethod OnPreDispatch() As %Status
58+
{
59+
set %response.CharSet = "utf-8"
60+
set %response.ContentType="application/json"
61+
62+
// Parsing POST request body
63+
#dim obj As %ZEN.proxyObject
64+
return:'$isobject(%request.Content) $$$OK
65+
set content = %request.Content.Read($$$MaxStringLength)
66+
set content = $ZCVT(content, "I", "UTF8")
67+
set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(content, , .obj, 1)
68+
return:$$$ISERR(st) st
69+
return:'$IsObject(obj) $$$ERROR("Unable to parse request body")
70+
set %request.Content = obj
71+
72+
return $$$OK
73+
}
74+
75+
}

source/cache/VisualEditor.Router.cls

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ XData UrlMap
1111
<Route Url="/js/index.js" Method="GET" Call="Js"/>
1212
<Map Prefix="/Informer" Forward="VisualEditor.REST.Informer"/>
1313
<Map Prefix="/Resources" Forward="VisualEditor.REST.Resources"/>
14+
<Map Prefix="/Editor" Forward="VisualEditor.REST.Editor"/>
1415
</Routes>
1516
}
1617

source/client/index.html

+10-7
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
<div class="pages">
1717
<div class="page" id="classBuilder">
1818
<div class="header">
19-
<div class="medium menu icon"></div>
20-
<div class="medium back icon" id="backButton"></div>
21-
<span class="title">
22-
<select id="topNamespace" class="topNamespace">
23-
<option>Loading...</option>
24-
</select><span id="topTitle"></span>
25-
</span>
19+
<div class="panel">
20+
<div class="medium menu icon"></div>
21+
<div class="medium back icon" id="backButton"></div>
22+
<span class="title">
23+
<select id="topNamespace" class="topNamespace">
24+
<option>Loading...</option>
25+
</select><span id="topTitle"></span>
26+
</span>
27+
<div class="medium save icon" id="saveIndicator"></div>
28+
</div>
2629
</div>
2730
<div class="body" id="classBuilderBody">
2831

source/client/js/classEditor/card.js

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { loadLevel } from "./index";
2-
import { block } from "../utils";
2+
import { block } from "../domUtils";
33
import { enableItem } from "./card/item";
44

5-
function getPropertyBlock (prop) {
6-
let item = block(`div`, `header`),
5+
function getPropertyBlock (classData, classBlockName, classBlockPropName) {
6+
let prop = classData[classBlockName][classBlockPropName],
7+
item = block(`div`, `header`),
78
icon = block(`div`, `icon ${ prop["Private"] ? "private" : "public" }`),
89
text = block(`span`, `label`),
910
pName = block(`span`, `name`),
@@ -20,24 +21,30 @@ function getPropertyBlock (prop) {
2021
text.appendChild(pType);
2122
}
2223
item.appendChild(text);
23-
enableItem(item, prop);
24+
enableItem(item, classData, classBlockName, classBlockPropName);
2425
return item;
2526
}
2627

27-
function getBlock (key, data) {
28+
/**
29+
* Applies block markup according to class metadata.
30+
* @param {"Parameters"|"Properties"|"Methods"|"Queries"|"XDatas"|"Indices"} classBlockName
31+
* @param {*} classData - Class metadata.
32+
* @returns {Element}
33+
*/
34+
function getBlock (classBlockName, classData) {
2835

2936
let section = block(`div`, `section`), body, header;
30-
for (let prop in data[key]) {
37+
for (let classBlockPropName in classData[classBlockName]) {
3138
header = block(`div`, `header`);
32-
header.textContent = key;
39+
header.textContent = classBlockName;
3340
body = block(`div`, `body`);
3441
section.appendChild(header);
3542
section.appendChild(body);
3643
break;
3744
}
38-
for (let prop in data[key]) {
45+
for (let classBlockPropName in classData[classBlockName]) {
3946
let div = block(`div`, `item`);
40-
div.appendChild(getPropertyBlock(data[key][prop]));
47+
div.appendChild(getPropertyBlock(classData, classBlockName, classBlockPropName));
4148
body.appendChild(div);
4249
}
4350
return section;

source/client/js/classEditor/card/item.js

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
1-
import { block, insertAfter, clearSelection } from "../../utils";
1+
import { block, insertAfter, clearSelection } from "../../domUtils";
22
import { updateGrid } from "../index";
3+
import { addChange } from "../changes";
34

4-
function createView (data) {
5+
/**
6+
* Creates and returns interactive class block property editor.
7+
* @param data
8+
* @param classData
9+
* @param classBlockName
10+
* @param classBlockPropName
11+
* @returns {Element}
12+
*/
13+
function createView (data, classData, classBlockName, classBlockPropName) {
514
let container = block(`div`, `detailed`),
615
comment = block(`div`, `comment`);
716
container.appendChild(comment);
817
comment.setAttribute("contenteditable", "true");
18+
comment.addEventListener("input", () => {
19+
addChange(
20+
[classData["Name"], classBlockName, classBlockPropName, "Description"],
21+
comment.innerHTML.replace(/<br\s*\/?>/, "<br/>\n")
22+
);
23+
});
924
if (data["Description"])
1025
comment.textContent = data["Description"];
1126
return container;
1227
}
1328

14-
export function enableItem (element, data) {
29+
/**
30+
* Enables block item interactivity.
31+
* @param {HTMLElement} element
32+
* @param {*} classData
33+
* @param classBlockName
34+
* @param classBlockPropName
35+
*/
36+
export function enableItem (element, classData, classBlockName, classBlockPropName) {
1537

16-
let opened = false,
38+
let data = classData[classBlockName][classBlockPropName],
39+
opened = false,
1740
container;
1841

1942
element.addEventListener("click", () => {
20-
if (!container) container = createView(data);
43+
if (!container) container = createView(data, classData, classBlockName, classBlockPropName);
2144
if (opened = !opened) {
2245
insertAfter(container, element);
2346
} else if (container.parentNode) {
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { save } from "../server";
2+
import { changeIsMade } from "./index";
3+
4+
let changes = {};
5+
6+
/**
7+
* Adds a change to pending changes.
8+
* @param path - ["ClassName", "properties", "PropName", "ParamName"]
9+
* @param value
10+
*/
11+
export function addChange (path = [], value) {
12+
let obj = changes,
13+
prop = path.pop();
14+
path.forEach((p) => obj = obj[p] || (() => obj[p] = {})() );
15+
obj[prop] = value;
16+
changeIsMade();
17+
}
18+
19+
export function saveChanges (namespace, callback) {
20+
save(namespace, changes, (res) => {
21+
22+
if (!res["error"])
23+
changes = {};
24+
25+
callback(res);
26+
27+
});
28+
}

source/client/js/classEditor/index.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getList } from "../server";
22
import { AutoGrid } from "../autoGrid";
33
import { getCardElement } from "./card";
4-
import { block } from "../utils";
4+
import { block } from "../domUtils";
5+
import { saveChanges } from "./changes";
56

67
var PATH = "",
78
INITIALIZED = false,
@@ -14,11 +15,15 @@ let initCallbacks = [];
1415
*/
1516
let grid = onInit(() => grid = new AutoGrid(document.querySelector("#classBuilderBody")));
1617

18+
/**
19+
* Function which updates current grid.
20+
*/
1721
export function updateGrid () {
1822
grid.updateGrid();
1923
}
2024

2125
/**
26+
* Behaviors for elements on page.
2227
* @type {HTMLElement}
2328
*/
2429
let backButton = onInit(() => {
@@ -44,12 +49,28 @@ let backButton = onInit(() => {
4449
updateHeaderNamespaceWidth(NAMESPACE);
4550
loadLevel("");
4651
});
52+
}),
53+
saveButton = onInit(() => {
54+
saveButton = document.querySelector("#saveIndicator");
55+
saveButton.style.opacity = 0;
56+
saveButton.addEventListener("click", () => {
57+
saveChanges(NAMESPACE, (res) => {
58+
if (!res["error"]) saveButton.style.opacity = 0;
59+
});
60+
});
4761
});
4862

4963
/**
50-
* This function uses a bit of trick to compute the width of the namespace element in header, but it
51-
* does its job.
52-
* @param {string} namespace
64+
* This function applies visual effects regarding to changes were made and
65+
* indicates that changes are needed to be saved.
66+
*/
67+
export function changeIsMade () {
68+
saveButton.style.opacity = 1;
69+
}
70+
71+
/**
72+
* This function uses a bit of trick to compute the width of the namespace element in header.
73+
* @param {string} namespace - Currently selected namespace.
5374
*/
5475
function updateHeaderNamespaceWidth (namespace) {
5576
let temp = block("select", "topNamespace"),
@@ -85,12 +106,9 @@ function setTitle (text) {
85106
}
86107

87108
/**
88-
* This namespace should be trusted - setting wrong namespace will result as "no data".
109+
* Displays the classes and packages located on the current level.
110+
* @param {string} level - Class part.
89111
*/
90-
function setNamespace (namespace) {
91-
topNamespace.textContent = namespace;
92-
}
93-
94112
export function loadLevel (level) {
95113

96114
PATH = level;
@@ -118,9 +136,13 @@ export function onInit (callback) {
118136
callback();
119137
else
120138
initCallbacks.push(callback);
121-
return "Duck";
139+
return "Duck"; // Property has to be redefined in onInit callback.
122140
}
123141

142+
/**
143+
* Application entry point.
144+
* @param {*} data - Server response to the /Informer/init request.
145+
*/
124146
export function init (data) {
125147

126148
INITIALIZED = true;
File renamed without changes.

source/client/js/server.js

+14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ export function getList (namespace, level, callback) {
6161
);
6262
}
6363

64+
/**
65+
* Saves
66+
* @param {string} namespace
67+
* @param {*} data - Data like { "Pack.Class": { "properties": { "Pr": { "Description": "Test" }}}}
68+
* @param {server~dataCallback} callback
69+
*/
70+
export function save (namespace, data, callback) {
71+
load(
72+
`${ BASE_URL }/Editor/save${ getParams("ns", namespace) }`,
73+
data,
74+
callback
75+
)
76+
}
77+
6478
/**
6579
* Retrieves the basic configuration.
6680
* @param {server~dataCallback} callback

0 commit comments

Comments
 (0)