Commit b2ea30dc authored by Jerome Cambon's avatar Jerome Cambon
Browse files

json properties file is now fully generated

parent 600da9ee
......@@ -7,3 +7,57 @@ This sdk provides tooling to build custom widgets for UI Designer.
Generates Bonita UI Designer custom widgets from web components.
### Generating the json properties file
#### Adding custom tags for UI Designer information
You may add specific information to web component properties, so that UI Designer can handle them.
All custom tags is JSDoc-like, using the '-@tag' notation (e.g. -@bond).
---
**NOTE:**
The custom tags should always be defined before any other JSDoc tag.
---
- Binding
```
/**
* -@bond constant
*/
```
- Constraints
```
/**
* -@constraints {"min": "1", "max": "12"}
*/
```
- Choice values
```
/**
* -@choiceValues {"left"|"top"}
*/
```
- showFor
```
/**
* -@showFor properties.labelHidden.value === false
*/
```
- label
```
/**
* -@label Min value
*/
```
- caption
```
/**
* -@caption Any variable: <i>myData</i> or <i>myData.attribute</i>
*/
```
The builder is using https://github.com/runem/web-component-analyzer.
See the section "How to document your components using JSDoc" from its documentation
for more details about adding information on the component (@element) and its properties (@prop).
This diff is collapsed.
......@@ -8,6 +8,8 @@
"scripts": {},
"dependencies": {
"@types/node": "^14.14.35",
"jdenticon": "^3.1.0",
"jsdoc": "^3.6.6",
"typescript": "^4.1.3",
"web-component-analyzer": "^1.1.6"
}
......
import {PropertyConstraint} from "./PropertyConstraint.js";
export class CustomTagHandler {
public static readonly CUSTOM_TAG = "-@";
public bond: string | undefined;
public caption: string | undefined;
public choiceValues: Array<String> | undefined;
public constraints: PropertyConstraint | undefined;
public label: string | undefined;
public showFor: string | undefined;
private desc: string;
constructor(tagsDescription: string) {
// e.g.
// Position of the label
// @type {choice}
// -@choiceValues {"left"|"top"}
// -@bond constant
// -@showFor properties.labelHidden.value === false
this.desc = tagsDescription;
if (this.desc) {
this.parse();
}
}
private parse() {
let index = this.getTagIndex(0);
while (index != -1) {
let tag = this.getTag(index);
this.processTag(tag);
index = this.getTagIndex(index + 1);
}
}
private getTag(index: number): string {
let tag;
let tagIndex = this.getTagIndex(index);
tag = this.desc.substring(tagIndex);
let nextLineIndex = tag.indexOf("\n");
if (nextLineIndex != -1) {
tag = tag.substring(0, nextLineIndex);
}
return tag;
}
private getTagIndex(index: number): number {
return this.desc.indexOf(CustomTagHandler.CUSTOM_TAG, index);
}
private processTag(tag: string) {
// e.g. -@bond constant
let firstSpaceIndex = tag.indexOf(" ");
let tagName = tag.substring(2, firstSpaceIndex);
let tagValue = tag.substring(firstSpaceIndex+1).trim();
switch (tagName) {
case 'bond':
this.bond = tagValue;
break;
case 'caption':
this.caption = tagValue;
break;
case 'choiceValues':
this.setChoiceValues(tagValue);
break;
case 'constraints':
this.setConstraints(tagValue);
break;
case 'label':
this.label = tagValue;
break;
case 'showFor':
this.showFor = tagValue;
break;
default:
console.log(`Error: invalid tag: ${tagName}`);
}
}
private setChoiceValues(choice: string) {
// e.g. -@choiceValues {"left"|"top"}
this.choiceValues = [];
choice = this.removeFirstAndLastChar(choice);
let values = choice.split("|")
for (let value of values) {
value = value.trim();
value = this.removeQuotes(value);
this.choiceValues.push(value);
}
}
private setConstraints(constraint: string) {
// e.g. -@constraints {"min": "1", "max": "12"}
constraint = this.removeFirstAndLastChar(constraint);
let values = constraint.split(",")
let min, max;
for (let value of values) {
value = value.trim();
value = this.removeQuotes(value);
let items = value.split(":");
if (items[0] === "min") {
min = items[1].trim();
} else if (items[0] === "max") {
max = items[1].trim();
}
}
this.constraints = new PropertyConstraint(min, max);
}
private removeFirstAndLastChar(str: string): string {
return str.substring(1, str.length-1);
}
private removeQuotes(str: string) {
// Remove leading and trailing double quotes
return str.replace(/\"/g, "");
}
}
......@@ -2,6 +2,8 @@ import {analyzeText, AnalyzeTextResult, transformAnalyzerResult} from "web-compo
import fs from "fs";
import {PropertiesInfo} from "./PropertiesInfo.js";
import {Property} from "./Property.js";
import jdenticon from "jdenticon/standalone";
import {CustomTagHandler} from "./CustomTagHandler.js";
export class CustomWidgetBuilder {
......@@ -17,6 +19,9 @@ export class CustomWidgetBuilder {
private getPropertiesInfo(): PropertiesInfo {
let info = JSON.parse(this.analyzeFile()).tags[0];
if (!info) {
throw new Error(`Cannot get any information from file ${this.wcFile}\nExiting...`);
}
let wcName = info.name;
let id = CustomWidgetBuilder.toCamelCase(wcName);
let name = CustomWidgetBuilder.getDisplayName(wcName);
......@@ -48,7 +53,7 @@ export class CustomWidgetBuilder {
private analyzeFile(): string {
let fileStr = fs.readFileSync(this.wcFile, "utf8").toString();
let result: AnalyzeTextResult = analyzeText(fileStr);
return transformAnalyzerResult("json", result.results, result.program, {visibility: "public"});
return transformAnalyzerResult("json", result.results, result.program, {visibility: "private"});
}
private static getProperties(props: any): Array<Property> {
......@@ -58,26 +63,43 @@ export class CustomWidgetBuilder {
// We don't want to expose the 'lang' property here
continue;
}
let label = CustomWidgetBuilder.getDisplayName(prop.name);
if (!prop.type) {
// No type: potentially not a property, or we can't do anything with it...
continue;
}
let customTagHandler;
let help;
if (prop.description) {
help = CustomWidgetBuilder.getHelp(prop.description);
}
let name = prop.name;
let help = prop.description;
let type = CustomWidgetBuilder.getPropertyType(prop.type);
let defaultValue = prop.default;
properties.push(new Property(label, name, type, defaultValue, help));
let defaultValue = CustomWidgetBuilder.getDefaultValue(prop.default);
customTagHandler = new CustomTagHandler(prop.description);
let label = customTagHandler.label;
if (!label) {
label = CustomWidgetBuilder.getDisplayName(prop.name);
}
properties.push(new Property(label, name, type, defaultValue, help,
customTagHandler.bond, customTagHandler.constraints, customTagHandler.showFor,
customTagHandler.choiceValues, customTagHandler.caption));
}
return properties;
}
private static toCamelCase(wcName: string): string {
// e.g. pb-input -> pbInput
return wcName.replace(/-([a-z])/g, (g) => {return g[1].toUpperCase()});
}
private static getDisplayName(wcName: string): string {
// e.g. pb-input -> Input
// wc-example -> WcExample
// required -> Required
// labelWidth -> Label width
// allowHTML -> Allow html
let name = wcName.replace(/^(pb-)/,"");
// camel case to words
name = CustomWidgetBuilder.fromCamelCase(name);
// name = name.replace( /([A-Z])/g, " $1" ).toLowerCase();
// dash notation to camel case
name = CustomWidgetBuilder.toCamelCase(name);
// First letter uppercase
return name.charAt(0).toUpperCase() + name.slice(1);
}
......@@ -87,8 +109,19 @@ export class CustomWidgetBuilder {
}
private static generateIcon(): string {
// TODO
return "";
let randomString = Math.random().toString(36).substring(2, 15);
return jdenticon.toSvg(randomString, 30);
}
private static getDefaultValue(value: string): string | number | boolean {
// Transform string to boolean or number
// e.g. "false" -> false
// "4" -> 4
try {
return JSON.parse(value);
} catch (err) {
return value;
}
}
private static getPropertyType(wcType: string): string {
......@@ -99,13 +132,35 @@ export class CustomWidgetBuilder {
wcType = wcType.replace(" | undefined","");
if (wcType === 'number') {
return 'integer';
switch (wcType) {
case 'number':
return 'integer';
case 'string':
return 'text';
default:
return wcType;
}
if (wcType === 'string') {
return 'text';
}
private static getHelp(description: string): string | undefined {
// Help field end at first custom tag
let help = description.substring(0, description.indexOf(CustomTagHandler.CUSTOM_TAG)).trim();
if (help.length === 0) {
return undefined;
}
return wcType;
}
private static toCamelCase(str: string): string {
// e.g. pb-input -> pbInput
return str.replace(/-([a-z])/g, (g) => {return g[1].toUpperCase()});
}
private static fromCamelCase(str: string): string {
// e.g. allowHTML -> Allow html
return str
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1 $2')
.toLowerCase();
}
}
......
import {PropertyConstraint} from "./PropertyConstraint";
import {PropertyChoice} from "./PropertyChoice";
export class Property {
public label: string;
public name: string;
public help: string | undefined;
public type: string;
public defaultValue: string;
public defaultValue: string | number | boolean;
public constraints: PropertyConstraint | undefined;
public showFor: string | undefined;
public bond: string | undefined;
public choiceValues: PropertyChoice | undefined;
public choiceValues: Array<String> | undefined;
public caption: string | undefined;
constructor(label: string, name: string, type: string, defaultValue: string, help?: string, bond?: string, constraints?: PropertyConstraint,
showFor?: string, choiceValues?: PropertyChoice) {
constructor(label: string, name: string, type: string, defaultValue: string | number | boolean, help?: string, bond?: string, constraints?: PropertyConstraint,
showFor?: string, choiceValues?: Array<String>, caption?: string) {
this.label = label;
this.name = name;
this.help = help;
......@@ -24,6 +24,7 @@ export class Property {
this.showFor = showFor;
this.bond = bond;
this.choiceValues = choiceValues;
this.caption = caption;
}
......
export class PropertyChoice {
public choices: Array<String>;
constructor(choices: Array<String>) {
this.choices = choices;
}
}
export class PropertyConstraint {
public min: string;
public max: string;
public min: string | undefined;
public max: string | undefined;
constructor(min: string, max: string) {
constructor(min: string | undefined, max: string | undefined) {
this.min = min;
this.max = max;
}
......
import { html, css, LitElement, property } from 'lit-element';
@customElement('wc-example')
export class WcExample extends LitElement {
static styles = css`
:host {
display: block;
padding: 25px;
color: var(--wc-example-text-color, #000);
}
`;
@property({ type: String })
title = 'Hey there';
@property({ type: Number }) counter = 5;
__increment() {
this.counter += 1;
}
render() {
return html`
<h2>${this.title} Nr. ${this.counter}!</h2>
<button @click=${this.__increment}>increment</button>
`;
}
}
......@@ -41,46 +41,95 @@ export class PbInput extends LitElement {
@property({ attribute: 'required', type: Boolean, reflect: true })
required: boolean = false;
/**
* -@label Value min length
* -@constraints {"min": "0"}
*/
@property({ attribute: 'min-length', type: Number, reflect: true })
minLength: number | undefined;
/**
* -@label Value max length
* -@constraints {"min": "1"}
*/
@property({ attribute: 'max-length', type: Number, reflect: true })
maxLength: number | undefined;
@property({ attribute: 'readonly', type: Boolean, reflect: true })
readOnly: boolean = false;
/**
* -@bond constant
*/
@property({ attribute: 'label-hidden', type: Boolean, reflect: true })
labelHidden: boolean = false;
/**
* -@showFor properties.labelHidden.value === false
* -@bond interpolation
*/
@property({ attribute: 'label', type: String, reflect: true })
label: string = "";
/**
* Position of the label
* @type {"left"|"top"}
* -@choiceValues {"left"|"top"}
* -@bond constant
* -@showFor properties.labelHidden.value === false
* @type {choice}
*/
@property({ attribute: 'label-position', type: String, reflect: true })
labelPosition: string = "top";
/**
* -@showFor properties.labelHidden.value === false
* -@bond constant
* -@constraints {"min": "1", "max": "12"}
*/
@property({ attribute: 'label-width', type: Number, reflect: true })
labelWidth: number = 4;
/**
* Short hint that describes the expected value
* -@bond interpolation
*/
@property({ attribute: 'placeholder', type: String, reflect: true })
placeholder: string = "";
/**
* Value of the input
* -@caption Any variable: <i>myData</i> or <i>myData.attribute</i>
* -@bond variable
*/
@property({ attribute: 'value', type: String, reflect: true })
value: string = "";
/**
* @type {choice}
* -@choiceValues {"text"|"number"|"email"|"password"}
* -@bond constant
*/
@property({ attribute: 'type', type: String, reflect: true })
type: string = "text";
/**
* -@label Min value
* -@showFor properties.type.value === 'number'
*/
@property({ attribute: 'min', type: Number, reflect: true })
min: number|undefined;
/**
* -@label Max value
* -@showFor properties.type.value === 'number'
*/
@property({ attribute: 'max', type: Number, reflect: true })
max: number | undefined;
/**
* Specifies the legal number intervals between values
* -@showFor properties.type.value === 'number'
*/
@property({ attribute: 'step', type: Number, reflect: true })
step: number = 1;
......
import {css, customElement, html, LitElement, property} from 'lit-element';
import {unsafeHTML} from "lit-html/directives/unsafe-html";
// @ts-ignore
import bootstrapStyle from './style.scss';
import {get, listenForLangChanged, registerTranslateConfig, use} from "lit-translate";
import * as i18n_en from "./i18n/en.json";
import * as i18n_es from "./i18n/es-ES.json";
import * as i18n_fr from "./i18n/fr.json";
import * as i18n_ja from "./i18n/ja.json";
import * as i18n_pt from "./i18n/pt-BR.json";
// Registers i18n loader
registerTranslateConfig({
loader: (lang) => Promise.resolve(PbText.getCatalog(lang))
});
@customElement('pb-text')
export class PbText extends LitElement {
@property({ attribute: 'lang', type: String, reflect: true })
lang: string = "en";
// Common properties below are handled by the div above pb-text:
// @property({ attribute: 'width', type: String, reflect: true })
// private width: string = "12";
//
// @property({ attribute: 'css-classes', type: String, reflect: true })
// private cssClasses: string = "";
//
// @property({ attribute: 'hidden', type: Boolean, reflect: true })
// private hidden: boolean = false;
@property({ attribute: 'id', type: String, reflect: true })
private idRoot: string = "";
@property({ attribute: 'label-hidden', type: Boolean, reflect: true })
labelHidden: boolean = false;
@property({ attribute: 'label', type: String, reflect: true })
label: string = "";
@property({ attribute: 'label-position', type: String, reflect: true })
labelPosition: string = "top";
@property({ attribute: 'label-width', type: String, reflect: true })
labelWidth: number = 4;
@property({ attribute: 'text', type: String, reflect: true })
text: string = "";
// User should take care to sanitize the 'text' content before using this.
// See e.g. https://github.com/google/closure-library/blob/master/closure/goog/html/sanitizer/htmlsanitizer.js
@property({ attribute: 'allow-html', type: Boolean, reflect: true })
allowHTML: boolean = false;
@property({ attribute: 'alignment', type: String, reflect: true })
alignment: string = "left";
constructor() {
super();
listenForLangChanged(() => {
if (this.label === "") {
this.label = get("defaultLabel");
}
});
}
async attributeChangedCallback(name: string, old: string|null, value: string|null) {
super.attributeChangedCallback(name, old, value);
if (name === 'lang') {
use(this.lang).then();
}
}
static getCatalog(lang: string) {
switch(lang) {
case "es":
case "es-ES":
return i18n_es;
case "fr":
return i18n_fr;