From d4a44256c48dffe9151f60c07bcd3892d7a4b1f2 Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:46:35 +0200 Subject: [PATCH] Handle of additional parameters in command editor (buttons and UI pages) --- package.json | 2 +- server/app.js | 37 +++++ server/package-lock.json | 134 ++++++++++++++++++ server/package.json | 4 +- server/remote.js | 23 +++ .../activity-editor.component.html | 1 - .../command-editor.component.html | 133 +++++++++++------ .../command-editor.component.ts | 79 ++++++++++- .../ui-command-editor.component.html | 4 +- src/app/helper.ts | 8 +- src/app/interfaces.ts | 44 +++--- .../remote-browser.component.ts | 2 +- src/app/server.service.ts | 9 +- 13 files changed, 399 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index d9959df..d544b38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucr-tool", - "version": "1.3.2", + "version": "1.4.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/server/app.js b/server/app.js index 6b16a9b..fe20fa2 100644 --- a/server/app.js +++ b/server/app.js @@ -15,10 +15,23 @@ import {Remote} from "./remote.js"; import {getConfigFile, writeConfigFile} from "./config.js"; import {program} from 'commander'; import cors from 'cors'; +import process from 'process'; +import { fileURLToPath } from 'url'; +import open from 'open'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +try { + process.chdir(__dirname); +} +catch (err) { +} let LISTEN_PORT = "8000"; const UPLOAD_DIR = './uploads'; const RESOURCES_DIR = './resources'; + + + var app = express(); var storage = multer.diskStorage({ destination: function (req, file, callback) { @@ -468,6 +481,29 @@ app.delete('/api/remote/:address/activities/:activity_id/ui/pages/:page_id', asy } }) + +app.get('/api/remote/:address/entities/:entity_id', async (req, res, next) => { + const address = req.params.address; + const entity_id = req.params.entity_id; + let user = REMOTE_USER + if (req.body?.user) + user = req.body?.user; + const configFile = getConfigFile(); + const remoteEntry = configFile?.remotes?.find(remote => remote.address === address); + if (!remoteEntry) + { + res.status(404).json(address); + return; + } + const remote = new Remote(remoteEntry.address, remoteEntry.port, remoteEntry.user, remoteEntry.token, remoteEntry.api_key); + try { + res.status(200).json(await remote.getEntity(entity_id)); + } catch (error) + { + errorHandler(error, req, res, next); + } +}) + app.delete('/api/remote/:address/entities/:entity_id', async (req, res, next) => { const address = req.params.address; const entity_id = req.params.entity_id; @@ -1010,6 +1046,7 @@ if (fs.existsSync(WORKING_FOLDER)) // console.dir(rc2Model.entities_catalog, {depth: null, colors: true}); app.listen(LISTEN_PORT, function () { console.log(`Listening on port ${LISTEN_PORT}!`); + open(`http://localhost:${LISTEN_PORT}`); }); diff --git a/server/package-lock.json b/server/package-lock.json index e0e6cf9..173e8a3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -20,6 +20,8 @@ "morgan": "~1.9.1", "multer": "^1.4.5-lts.1", "nodemon": "^3.1.4", + "open": "^10.1.0", + "process": "^0.11.10", "rimraf": "^6.0.1" }, "devDependencies": { @@ -614,6 +616,20 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -828,6 +844,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -836,6 +878,17 @@ "node": ">=10" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1252,6 +1305,20 @@ "node": ">=8" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1279,6 +1346,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1298,6 +1382,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1617,6 +1715,23 @@ "node": ">= 0.8" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-cancelable": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", @@ -1682,6 +1797,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1826,6 +1949,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/server/package.json b/server/package.json index 671134f..eb44f71 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "name": "server", "type": "module", - "version": "1.3.2", + "version": "1.4.0", "main": "app.js", "scripts": { "start": "node app.js" @@ -19,6 +19,8 @@ "morgan": "~1.9.1", "multer": "^1.4.5-lts.1", "nodemon": "^3.1.4", + "open": "^10.1.0", + "process": "^0.11.10", "rimraf": "^6.0.1" }, "devDependencies": { diff --git a/server/remote.js b/server/remote.js index 84b756f..ad031a2 100644 --- a/server/remote.js +++ b/server/remote.js @@ -234,6 +234,29 @@ export class Remote } } + async getEntity(entityId) + { + let headers = this.getHeaders(); + const limit = 100; + const options = { + ...this.getOptions(), + searchParams: { + limit, + page: 1 + } + } + const url = this.getURL() + `/api/entities/${entityId}`; + const res = await got.get(url, options); + let resBody; + try { + if (res?.body) resBody = JSON.parse(res.body); + return resBody; + } catch (err) { + console.error('Error', err, res?.body); + throw(err); + } + } + async getEntities() { let headers = this.getHeaders(); diff --git a/src/app/activity-editor/activity-editor.component.html b/src/app/activity-editor/activity-editor.component.html index fb7c87a..3a31f46 100644 --- a/src/app/activity-editor/activity-editor.component.html +++ b/src/app/activity-editor/activity-editor.component.html @@ -88,7 +88,6 @@

Create or clone activity : {{Helper.getEntityName(updatedActivity)}}

- Features (note that additional command parameters - delay, repeat...- are not covered yet) :
  • Clone, copy, paste an activity or import from a file
  • Replace an entity by another within the activity.
  • diff --git a/src/app/activity-editor/command-editor/command-editor.component.html b/src/app/activity-editor/command-editor/command-editor.component.html index a67f309..637780e 100644 --- a/src/app/activity-editor/command-editor/command-editor.component.html +++ b/src/app/activity-editor/command-editor/command-editor.component.html @@ -1,49 +1,92 @@ -
    - -
    - +
    +
    + +
    + +
    +
    + + + {{Helper.getEntityName(selectedEntity)}} + + + {{Helper.getEntityName(item)}} ({{item.entity_id}}) + + +
    +
    + {{uiCommand!.media_player_id}} +
    +
    + +
    + +
    +
    + + + {{Helper.getEntityName(selectedEntity)}} + + + {{Helper.getEntityName(item)}} ({{item.entity_id}}) + + +
    +
    + +
    +
    + + + {{Helper.getEntityName(selectedCommand)}} + + + {{Helper.getEntityName(item)}} + + +
    +
    -
    - - - {{Helper.getEntityName(selectedEntity)}} - - - {{Helper.getEntityName(item)}} ({{item.entity_id}}) - - +
    + +
    + +
    + + +
    + + + {{command?.params?.[param.param]}} + + + {{item.value}} + + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + Unhandled parameter type : {{param.type}} +
    +
    +
    +
    -
    - {{uiCommand!.media_player_id}} -
    - - -
    - -
    -
    - - - {{Helper.getEntityName(selectedEntity)}} - - - {{Helper.getEntityName(item)}} ({{item.entity_id}}) - - -
    -
    - -
    -
    - - - {{Helper.getEntityName(selectedCommand)}} - - - {{Helper.getEntityName(item)}} - - -
    -
    diff --git a/src/app/activity-editor/command-editor/command-editor.component.ts b/src/app/activity-editor/command-editor/command-editor.component.ts index b739655..1b9c743 100644 --- a/src/app/activity-editor/command-editor/command-editor.component.ts +++ b/src/app/activity-editor/command-editor/command-editor.component.ts @@ -16,13 +16,15 @@ import { ActivityPageCommand, Command, Entity, - EntityCommand, + EntityCommand, EntityCommandParameter, EntityFeature, Remote, RemoteData, RemoteMap } from "../../interfaces"; -import {NgIf} from "@angular/common"; +import {NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault} from "@angular/common"; import {FormsModule} from "@angular/forms"; import {ToastModule} from "primeng/toast"; +import {InputNumberModule} from "primeng/inputnumber"; +import {CheckboxModule} from "primeng/checkbox"; @Component({ selector: 'app-command-editor', @@ -32,7 +34,13 @@ import {ToastModule} from "primeng/toast"; SharedModule, NgIf, FormsModule, - ToastModule + ToastModule, + NgSwitch, + NgSwitchCase, + NgForOf, + InputNumberModule, + CheckboxModule, + NgSwitchDefault ], templateUrl: './command-editor.component.html', styleUrl: './command-editor.component.css', @@ -51,6 +59,10 @@ export class CommandEditorComponent implements OnInit { uiCommand: ActivityPageCommand | undefined; @Input('uiCommand') set _uiCommand(value: ActivityPageCommand | undefined) { this.uiCommand = value; + this.command = undefined; + if (this.uiCommand?.command && typeof this.uiCommand?.command !== 'string') { + this.command = (this.uiCommand.command as Command); + } if (value?.command) this.backupCommand = JSON.parse(JSON.stringify(value.command)); this.initSelection(); @@ -136,12 +148,33 @@ export class CommandEditorComponent implements OnInit { if (this.uiCommand?.media_player_id) this.selectedEntity = this.activityEntities?.find(entity => entity.entity_id == this.uiCommand?.media_player_id); - else if (command) - this.selectedEntity = this.activityEntities?.find(entity => entity.entity_id === command?.entity_id); - + else if (command?.entity_id && this.remote) + { + this.server.getRemotetEntity(this.remote, command.entity_id).subscribe(entity => { + this.selectedEntity = entity; + this.activityEntities = this.activityEntities.map(activityEntity => + activityEntity.entity_id === entity.entity_id ? entity : activityEntity); + console.log("Extracted entity", entity); + this.updateSelection(); + }) + } this.updateSelection(); } + getSelectionItems(parameter: EntityCommandParameter) + { + const source = parameter.items?.source; + const field = parameter.items?.field; + if (!source || !field) + { + if (parameter.values) return Helper.getItems(parameter.values); + return []; + } + if ((this.selectedEntity as any)?.[source]?.[field]) + return Helper.getItems((this.selectedEntity as any)?.[source]?.[field]) + return []; + } + updateSelection() { this.selectedCommand = undefined; @@ -182,7 +215,27 @@ export class CommandEditorComponent implements OnInit { (entityCommand.id.startsWith(this.selectedEntity?.entity_type!) && command.cmd_id.endsWith(entityCommand.cmd_id))); } + + if (!this.selectedCommand || !this.selectedCommand.params) delete this.command?.params; + + if (this.command && this.selectedCommand) + { + if (this.selectedCommand.params && !this.command.params) + this.command.params = {}; + + // Remove invalid params + if (this.command.params) + for (const [key, value] of Object.entries(this.command.params)) { + if (!this.selectedCommand.params?.find(param => param.param === key)) + delete this.command.params[key]; + } + + /*this.selectedCommand.params?.forEach(params => { + + })*/ + } } + console.log("Entity & command selected", this.selectedEntity, this.selectedCommand); this.cdr.detectChanges(); } @@ -209,6 +262,20 @@ export class CommandEditorComponent implements OnInit { if (!command || !this.selectedEntity) return; command.entity_id = this.selectedEntity.entity_id!; command.cmd_id = this.selectedCommand.id; + + if (!this.selectedCommand || !this.selectedCommand.params) delete this.command?.params; + if (this.command && this.selectedCommand) { + if (this.selectedCommand.params && !this.command.params) + this.command.params = {}; + + // Remove invalid params + if (this.command.params) + for (const [key, value] of Object.entries(this.command.params)) { + if (!this.selectedCommand.params?.find(param => param.param === key)) + delete this.command.params[key]; + } + } + this.messageService.add({key: 'commandEditor', severity: "info", summary: `Entity ${Helper.getEntityName(this.selectedEntity)}`, detail: `Entity id : ${this.selectedEntity?.entity_id}, command ${this.selectedCommand.cmd_id} assigned`}); this.cdr.detectChanges(); diff --git a/src/app/activity-editor/ui-command-editor/ui-command-editor.component.html b/src/app/activity-editor/ui-command-editor/ui-command-editor.component.html index 10df933..1c2adad 100644 --- a/src/app/activity-editor/ui-command-editor/ui-command-editor.component.html +++ b/src/app/activity-editor/ui-command-editor/ui-command-editor.component.html @@ -22,7 +22,7 @@ optionLabel="label" optionValue="value" (ngModelChange)="itemTypeSelected($event)">
    - +
    @@ -43,7 +43,7 @@

    - +

    diff --git a/src/app/helper.ts b/src/app/helper.ts index 4f1898b..54107da 100644 --- a/src/app/helper.ts +++ b/src/app/helper.ts @@ -455,9 +455,13 @@ export class Helper }, baseObject|| self); } - static getValues(table: any[], field_name: string) { + static getValues(table: any[], field_name?: string) { const values = new Set(); table.forEach(item => { + if (!field_name) { + values.add(item); + return; + } const value = Helper.resolve(field_name, item); if (value) { values.add(value) @@ -466,7 +470,7 @@ export class Helper return Array.from(values).sort(); } - static getItems(table: any[], field_name: string) { + static getItems(table: any[], field_name?: string) { return Helper.getValues(table, field_name).map(value => { return {name: value.toString(), value} }); diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts index fbb65c3..c5ca94d 100644 --- a/src/app/interfaces.ts +++ b/src/app/interfaces.ts @@ -9,6 +9,27 @@ export interface EntityFeature ] } +export interface EntityCommandParameter { + items?:{ + field: string; + source: string; + } + name: { + en: string; + fr?: string; + de?: string + } + param?: string; + type: "enum"|"number"|"regex"|"bool"|"selection"; + values?: string[]; + default?: number; + min?: number; + max?: number; + step?: number; + unit?: string; + source?: string; +} + export interface EntityCommand { id: string; @@ -18,27 +39,7 @@ export interface EntityCommand fr?: string; de?: string } - params?: [ - { - items?:{ - field: string; - source: string; - } - name: { - en: string; - fr?: string; - de?: string - } - param?: string; - type: "enum"|"number"|"regex"|"bool"|"selection"; - values?: string[]; - default?: number; - min?: number; - max?: number; - step?: number; - unit?: string; - } - ] + params?: EntityCommandParameter[]; } @@ -212,6 +213,7 @@ export interface Entity entity_type: string; integration?: string | EntityIntegration; features?:string[]; + attributes?: any; options?: { button_mapping?:ButtonMapping[]; user_interface?: { diff --git a/src/app/remote-browser/remote-browser.component.ts b/src/app/remote-browser/remote-browser.component.ts index f8b1b47..18ee2bd 100644 --- a/src/app/remote-browser/remote-browser.component.ts +++ b/src/app/remote-browser/remote-browser.component.ts @@ -317,7 +317,7 @@ export class RemoteBrowserComponent implements OnInit, AfterViewInit { this.cdr.detectChanges(); return; } - this.server.getEntity($event.query).subscribe(results => { + this.server.findEntities($event.query).subscribe(results => { this.suggestions = Helper.queryEntity($event.query, results); this.cdr.detectChanges(); }) diff --git a/src/app/server.service.ts b/src/app/server.service.ts index 9724e08..9034529 100644 --- a/src/app/server.service.ts +++ b/src/app/server.service.ts @@ -200,6 +200,13 @@ export class ServerService { })) } + getRemotetEntity(remote: Remote, entityId: string): Observable + { + return this.http.get(`/api/remote/${remote.address}/entities/${entityId}`).pipe(map(results => { + return results; + })) + } + getRemoteActivities(remote: Remote): Observable { return this.http.get(`/api/remote/${remote.address}/activities`).pipe(map(activities => { @@ -448,7 +455,7 @@ export class ServerService { })) } - getEntity(query: string): Observable + findEntities(query: string): Observable { return this.http.get('/api/entity/'+query).pipe(map(results => { return results;