Commit 600da9ee authored by Jerome Cambon's avatar Jerome Cambon
Browse files

Custom Widget Builder : first step to generate properties json file

parent 0aa7ecab
# bonita-ui-designer-sdk
\ No newline at end of file
# bonita-ui-designer-sdk
This sdk provides tooling to build custom widgets for UI Designer.
## Custom Widget Builder
Generates Bonita UI Designer custom widgets from web components.
## editors
/.idea
/.vscode
## system files
.DS_Store
## npm
/node_modules/
/npm-debug.log
## testing
/coverage/
## temp folders
/.tmp/
# build
/_site/
/build/
/dist/
/out-tsc/
This diff is collapsed.
{
"name": "poc-widget-builder",
"version": "0.0.1",
"description": "",
"author": "poc-widget-builder",
"license": "GPLv2",
"type": "module",
"scripts": {},
"dependencies": {
"@types/node": "^14.14.35",
"typescript": "^4.1.3",
"web-component-analyzer": "^1.1.6"
}
}
import {analyzeText, AnalyzeTextResult, transformAnalyzerResult} from "web-component-analyzer";
import fs from "fs";
import {PropertiesInfo} from "./PropertiesInfo.js";
import {Property} from "./Property.js";
export class CustomWidgetBuilder {
private wcFile: string = "";
private outputDir: string = "";
public generatePropertiesFile(wcFile: string, outputDir: string) {
this.wcFile = wcFile;
this.outputDir = outputDir;
this.createPropertiesFile(this.getPropertiesInfo());
}
private getPropertiesInfo(): PropertiesInfo {
let info = JSON.parse(this.analyzeFile()).tags[0];
let wcName = info.name;
let id = CustomWidgetBuilder.toCamelCase(wcName);
let name = CustomWidgetBuilder.getDisplayName(wcName);
let type = "widget";
let template = CustomWidgetBuilder.getTemplate(id);
let description = info.description;
let order = "1";
let icon = CustomWidgetBuilder.generateIcon();
let properties = CustomWidgetBuilder.getProperties(info.properties);
return new PropertiesInfo(id, name, type, template, description, order, icon, properties);
}
private createPropertiesFile(propertiesInfo: PropertiesInfo) {
//TODO replace console.log() by logging
let output = JSON.stringify(propertiesInfo, null, 2)
console.log(output);
let filePath = `${this.outputDir}/${propertiesInfo.id}.json`;
fs.writeFile(filePath, output, (err) => {
if (err) {
console.log(`ERROR: Cannot write properties file ${filePath}`);
console.log(`[${err.message}]`);
} else {
console.log(`${filePath} has been generated!`);
}
});
}
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"});
}
private static getProperties(props: any): Array<Property> {
let properties: Array<Property> = [];
for (let prop of props) {
if (prop.name === 'lang') {
// We don't want to expose the 'lang' property here
continue;
}
let label = CustomWidgetBuilder.getDisplayName(prop.name);
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));
}
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
// required -> Required
let name = wcName.replace(/^(pb-)/,"");
return name.charAt(0).toUpperCase() + name.slice(1);
}
private static getTemplate(id: string): string {
// e.g. pbInput -> @pbInput.tpl.html
return `@${id}.tpl.html`;
}
private static generateIcon(): string {
// TODO
return "";
}
private static getPropertyType(wcType: string): string {
// Mapping from web component type to UID type
// number -> integer
// string -> text
// e,g, number | undefined -> number
wcType = wcType.replace(" | undefined","");
if (wcType === 'number') {
return 'integer';
}
if (wcType === 'string') {
return 'text';
}
return wcType;
}
}
import {Property} from "./Property";
export class PropertiesInfo {
public id: string;
public name: string;
public type: string;
public template: string;
public description: string;
public order: string;
public icon: string;
public properties: Array<Property>;
constructor(
id: string, name: string, type: string, template: string, description: string, order: string, icon: string,
properties: Array<Property>
) {
this.id = id;
this.name = name;
this.type = type;
this.template = template;
this.description = description;
this.order = order;
this.icon = icon;
this.properties = properties;
}
}
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 constraints: PropertyConstraint | undefined;
public showFor: string | undefined;
public bond: string | undefined;
public choiceValues: PropertyChoice | undefined;
constructor(label: string, name: string, type: string, defaultValue: string, help?: string, bond?: string, constraints?: PropertyConstraint,
showFor?: string, choiceValues?: PropertyChoice) {
this.label = label;
this.name = name;
this.help = help;
this.type = type;
this.defaultValue = defaultValue;
this.constraints = constraints;
this.showFor = showFor;
this.bond = bond;
this.choiceValues = choiceValues;
}
}
export class PropertyChoice {
public choices: Array<String>;
constructor(choices: Array<String>) {
this.choices = choices;
}
}
export class PropertyConstraint {
public min: string;
public max: string;
constructor(min: string, max: string) {
this.min = min;
this.max = max;
}
}
import {CustomWidgetBuilder} from "./CustomWidgetBuilder.js";
let wcFile;
let outputDir;
for (let param of process.argv) {
if (param.startsWith("wcFile")) {
wcFile = getParameter(param);
}
if (param.startsWith("outputDir")) {
outputDir = getParameter(param);
}
}
console.log(`Generating widget for ${wcFile}...\n`);
new CustomWidgetBuilder().generatePropertiesFile(wcFile, outputDir);
function getParameter(param: string): any {
return param.substr(param.indexOf('=') + 1);
}
import {css, customElement, html, LitElement, property} from 'lit-element';
// @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(PbInput.getCatalog(lang))
});
/**
* Input field, optionally with a label, where the user can enter information
*/
@customElement('pb-input')
export class PbInput extends LitElement {
private name = "pbInput";
@property({ attribute: 'lang', type: String, reflect: true })
lang: string = "en";
// Common properties below are handled by the div above pb-input:
// @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 })
id: string = "";
@property({ attribute: 'required', type: Boolean, reflect: true })
required: boolean = false;
@property({ attribute: 'min-length', type: Number, reflect: true })
minLength: number | undefined;
@property({ attribute: 'max-length', type: Number, reflect: true })
maxLength: number | undefined;
@property({ attribute: 'readonly', type: Boolean, reflect: true })
readOnly: boolean = false;
@property({ attribute: 'label-hidden', type: Boolean, reflect: true })
labelHidden: boolean = false;
@property({ attribute: 'label', type: String, reflect: true })
label: string = "";
/**
* Position of the label
* @type {"left"|"top"}
*/
@property({ attribute: 'label-position', type: String, reflect: true })
labelPosition: string = "top";
@property({ attribute: 'label-width', type: Number, reflect: true })
labelWidth: number = 4;
@property({ attribute: 'placeholder', type: String, reflect: true })
placeholder: string = "";
@property({ attribute: 'value', type: String, reflect: true })
value: string = "";
@property({ attribute: 'type', type: String, reflect: true })
type: string = "text";
@property({ attribute: 'min', type: Number, reflect: true })
min: number|undefined;
@property({ attribute: 'max', type: Number, reflect: true })
max: number | undefined;
@property({ attribute: 'step', type: Number, reflect: true })
step: number = 1;
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;
case "ja":
return i18n_ja;
case "pt":
case "pt-BR":
return i18n_pt;
default:
return i18n_en;
}
}
static get styles() {
return css`
:host {
display: block;
font-family: sans-serif;
text-align: left;
}
.input-elem {
font-size: 14px;
height: 20px;
}
.label-elem {
font-size: 14px;
font-weight: 700;
padding-left: 0
}
/* Add a red star after required inputs */
.label-required:after {
content: " *";
color: #C00;
}
.text-right {
text-align: right;
}
`;
}
render() {
return html`
<style>${bootstrapStyle}</style>
<div id="${this.id}" class="container">
<div class="row">
${this.getLabel()}
<input
class="${this.getInputCssClass()}"
id="input"
name="${this.name}"
type="${this.type}"
min="${this.min}"
max="${this.max}"
step="${this.step}"
value="${this.value}"
@input=${(e: any) => this.valueChanged(e.target.value)}
placeholder="${this.placeholder}"
minlength="${this.minLength}"
maxlength="${this.maxLength}"
?readonly="${this.readOnly}"
/>
</div>
</div>
`;
}
private getLabel() {
if (this.labelHidden) {
return html``;
}
return html`
<label
class="${this.getLabelCssClass()}"
for="input"
>${this.label}</label>
`
}
private getLabelCssClass() : string {
return (this.required ? "label-required " : "") + "label-elem form-horizontal col-form-label " +
(!this.labelHidden && this.labelPosition === 'left' ? "col-" + this.labelWidth + " text-right" : "col-12");
}
private getInputCssClass() : string {
return "form-control input-elem col";
}
private valueChanged(value: string) {
this.dispatchEvent(new CustomEvent('valueChange', { detail: value }));
}
}
{
"compilerOptions": {
/* Basic Options */
"target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": ["es2020"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["./src/**/*", "./test/**/*"]
}
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