diff --git a/apps/chrome-devtools/README.md b/apps/chrome-devtools/README.md index 6867167501..2c3296fde2 100644 --- a/apps/chrome-devtools/README.md +++ b/apps/chrome-devtools/README.md @@ -8,6 +8,8 @@ The extension comes with the following features: - **Visual Testing toggle** - **Rule Engine current state**: rule engine state, rule engine logs, etc. - **Configuration**: display and modification of the application components configuration. +- **Theming**: display and modification of the application theming variables. +- **States**: save customization to be able to apply it later or share it. ## Details diff --git a/apps/chrome-devtools/package.json b/apps/chrome-devtools/package.json index fa6214b85a..a249d77642 100644 --- a/apps/chrome-devtools/package.json +++ b/apps/chrome-devtools/package.json @@ -15,7 +15,7 @@ "build": "yarn nx build chrome-devtools", "postbuild:patch": "yarn patch:package && yarn patch:manifest && yarn patch:extension", "patch:package": "cpy 'package.json' 'dist' && patch-package-json-main", - "copy:assets": "cpy 'src/manifest.json' 'src/devtools.html' dist --flat && cpy './src/assets/**' dist", + "copy:assets": "cpy 'src/manifest.json' 'src/devtools.html' 'src/options.html' dist --flat && cpy './src/assets/**' dist", "patch:manifest": "node scripts/sanitize-manifest.cjs", "build:extension": "tsc -b tsconfig.extension.json", "patch:extension": "node scripts/sanitize-extension.cjs && node scripts/set-manifest-version.cjs", @@ -87,6 +87,7 @@ "@angular/router": "~18.0.0", "@design-factory/design-factory": "~17.1.0", "@ng-bootstrap/ng-bootstrap": "^17.0.0", + "@ng-select/ng-select": "^12.0.7", "@ngrx/entity": "~18.0.0", "@ngrx/store": "~18.0.0", "@o3r/application": "workspace:^", diff --git a/apps/chrome-devtools/project.json b/apps/chrome-devtools/project.json index d2f46a34e6..cc4b25bb38 100644 --- a/apps/chrome-devtools/project.json +++ b/apps/chrome-devtools/project.json @@ -154,13 +154,15 @@ "outputs": [ "{projectRoot}/dist/assets/**", "{projectRoot}/dist/manifest.json", - "{projectRoot}/dist/devtools.html" + "{projectRoot}/dist/devtools.html", + "{projectRoot}/dist/options.html" ], "inputs": [ "global", "{projectRoot}/src/assets/**", "{projectRoot}/src/manifest.json", - "{projectRoot}/src/devtools.html" + "{projectRoot}/src/devtools.html", + "{projectRoot}/src/options.html" ] }, "publish-extension": { diff --git a/apps/chrome-devtools/schemas/state.schema.json b/apps/chrome-devtools/schemas/state.schema.json new file mode 100644 index 0000000000..9af49b2d7a --- /dev/null +++ b/apps/chrome-devtools/schemas/state.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "OtterDevtoolsChromeExtensionState", + "description": "Schema of Otter Devtools chrome extension state", + "type": "object", + "required": [ + "color", + "colorContrast", + "name" + ], + "properties": { + "color": { + "description": "Background color to identify the state in the selection widget.", + "type": "string" + }, + "colorContrast": { + "description": "Text color for the state in the selection widget in contrast with the background color.", + "type": "string" + }, + "name": { + "description": "User friendly name to identify the state in the Chrome Extension state panel.", + "type": "string" + }, + "configurations": { + "type": "object", + "description": "List of the configuration-override to apply on the application.", + "additionalProperties": { + "type": "object" + } + }, + "localizations": { + "type": "object", + "description": "List of the localization-override to apply on the application.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "stylingVariables": { + "description": "List of the css-variable-override to apply on the application.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/apps/chrome-devtools/src/app-devtools/app.component.html b/apps/chrome-devtools/src/app-devtools/app.component.html index 0d6fabcd32..7b1e51916c 100644 --- a/apps/chrome-devtools/src/app-devtools/app.component.html +++ b/apps/chrome-devtools/src/app-devtools/app.component.html @@ -1,42 +1,75 @@ - - +
+ +
+
+ + +
+ + {{item.name}} +
+
+ +
+ + {{item.name}} +
+
+
+
+
+
diff --git a/apps/chrome-devtools/src/app-devtools/app.component.ts b/apps/chrome-devtools/src/app-devtools/app.component.ts index b89bae5306..f60b2c97d9 100644 --- a/apps/chrome-devtools/src/app-devtools/app.component.ts +++ b/apps/chrome-devtools/src/app-devtools/app.component.ts @@ -1,21 +1,33 @@ -import { AsyncPipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; +import { AsyncPipe, JsonPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { DfSelectModule, DfTooltipModule } from '@design-factory/design-factory'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; -import { type RulesetExecutionDebug, RulesetHistoryPresModule } from '@o3r/rules-engine'; -import { Observable, Subscription } from 'rxjs'; +import { RulesetHistoryPresModule } from '@o3r/rules-engine'; import { AppConnectionComponent } from '../components/app-connection/app-connection.component'; -import { ComponentPanelPresComponent } from './component-panel/component-panel-pres.component'; +import type { State } from '../extension/interface'; +import { StateService } from '../services'; import { ChromeExtensionConnectionService, isApplicationInformationMessage } from '../services/connection.service'; import { RulesetHistoryService } from '../services/ruleset-history.service'; +import { ComponentPanelPresComponent } from './component-panel/component-panel-pres.component'; import { ConfigPanelPresComponent } from './config-panel/config-panel-pres.component'; import { DebugPanelPresComponent } from './debug-panel/debug-panel-pres.component'; import { DebugPanelService } from './debug-panel/debug-panel.service'; import { LocalizationPanelPresComponent } from './localization-panel/localization-panel-pres.component'; +import { StatePanelComponent } from './state-panel/state-panel.component'; import { ThemingPanelPresComponent } from './theming-panel/theming-panel-pres.component'; @Component({ selector: 'app-root', templateUrl: './app.component.html', + styles: ` + :host ::ng-deep ng-select.local-change .ng-select-container { + border-color: var(--bs-recommend-warning-color); + border-width: medium; + box-sizing: content-box; + } + `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ @@ -27,32 +39,50 @@ import { ThemingPanelPresComponent } from './theming-panel/theming-panel-pres.co AppConnectionComponent, LocalizationPanelPresComponent, ThemingPanelPresComponent, - AsyncPipe + AsyncPipe, + StatePanelComponent, + FormsModule, + ReactiveFormsModule, + DfSelectModule, + DfTooltipModule, + JsonPipe ] }) -export class AppComponent implements OnDestroy { - private readonly subscription = new Subscription(); +export class AppComponent { + private readonly stateService = inject(StateService); + private readonly formBuilder = inject(FormBuilder); + private readonly connectionService = inject(ChromeExtensionConnectionService); + private readonly debugPanelService = inject(DebugPanelService); + private readonly rulesetHistoryService = inject(RulesetHistoryService); - public rulesetExecutions$: Observable; + public readonly activeStateName = computed(() => this.stateService.activeState()?.name); + public readonly states = computed(() => Object.values(this.stateService.states())); + public readonly hasLocalChanges = this.stateService.hasLocalChanges; + public form = this.formBuilder.group({ + activeStateName: new FormControl(this.activeStateName()) + }); - constructor( - connectionService: ChromeExtensionConnectionService, - debugPanelService: DebugPanelService, - rulesetHistoryService: RulesetHistoryService - ) { - this.rulesetExecutions$ = rulesetHistoryService.rulesetExecutions$; + public rulesetExecutions$ = this.rulesetHistoryService.rulesetExecutions$; - this.subscription.add( - connectionService.message$.subscribe((message) => { - if (isApplicationInformationMessage(message)) { - debugPanelService.update(message); - } - }) - ); + constructor() { + effect(() => { + this.form.controls.activeStateName.setValue(this.activeStateName(), { emitEvent: false }); + }); + const message = toSignal(this.connectionService.message$); + effect(() => { + const msg = message(); + if (isApplicationInformationMessage(msg)) { + this.debugPanelService.update(msg); + } + }); + const activateStateNameFormValueChanges = toSignal(this.form.controls.activeStateName.valueChanges, { initialValue: this.activeStateName() }); + effect(() => { + const stateName = activateStateNameFormValueChanges(); + void this.stateService.setActiveState(stateName); + }, { allowSignalWrites: true }); } - /** @inheritDoc */ - public ngOnDestroy() { - this.subscription.unsubscribe(); + public stateCompareWithFn(state: State, selectedStateName: string) { + return state.name === selectedStateName; } } diff --git a/apps/chrome-devtools/src/app-devtools/config-panel/config-panel-pres.component.ts b/apps/chrome-devtools/src/app-devtools/config-panel/config-panel-pres.component.ts index 301aa0a3ce..808b22d292 100644 --- a/apps/chrome-devtools/src/app-devtools/config-panel/config-panel-pres.component.ts +++ b/apps/chrome-devtools/src/app-devtools/config-panel/config-panel-pres.component.ts @@ -4,9 +4,9 @@ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angul import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { ConfigurationModel } from '@o3r/configuration'; import { combineLatest, Observable } from 'rxjs'; -import { filter, map, startWith } from 'rxjs/operators'; +import { map, startWith } from 'rxjs/operators'; import { ConfigFormComponent } from '../../components/config-form/config-form.component'; -import { ChromeExtensionConnectionService, isConfigurationsMessage } from '../../services/connection.service'; +import { ChromeExtensionConnectionService, filterAndMapMessage, isConfigurationsMessage } from '../../services/connection.service'; @Component({ selector: 'o3r-config-panel-pres', @@ -37,9 +37,10 @@ export class ConfigPanelPresComponent { } ); const configs$ = connectionService.message$.pipe( - filter(isConfigurationsMessage), - map((message) => Object.values(message.configurations) - .filter((config): config is ConfigurationModel => !!config) + filterAndMapMessage( + isConfigurationsMessage, + (message) => Object.values(message.configurations) + .filter((config): config is ConfigurationModel => !!config) ) ); this.filteredConfigs$ = combineLatest([ diff --git a/apps/chrome-devtools/src/app-devtools/debug-panel/debug-panel-pres.template.html b/apps/chrome-devtools/src/app-devtools/debug-panel/debug-panel-pres.template.html index 13d47c1946..a79fab9476 100644 --- a/apps/chrome-devtools/src/app-devtools/debug-panel/debug-panel-pres.template.html +++ b/apps/chrome-devtools/src/app-devtools/debug-panel/debug-panel-pres.template.html @@ -3,6 +3,7 @@

Information