Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chrome-ext): support states #1787

Merged
merged 3 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/chrome-devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion apps/chrome-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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:^",
Expand Down
6 changes: 4 additions & 2 deletions apps/chrome-devtools/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
49 changes: 49 additions & 0 deletions apps/chrome-devtools/schemas/state.schema.json
Original file line number Diff line number Diff line change
@@ -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": {
cpaulve-1A marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}
}
}
111 changes: 72 additions & 39 deletions apps/chrome-devtools/src/app-devtools/app.component.html
Original file line number Diff line number Diff line change
@@ -1,42 +1,75 @@
<app-connection>
<ul ngbNav #nav="ngbNav" class="nav-tabs px-2 pt-2 navbar-light bg-light">
<li [ngbNavItem]="1">
<a ngbNavLink>General</a>
<ng-template ngbNavContent>
<o3r-debug-panel-pres></o3r-debug-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Rules Engine</a>
<ng-template ngbNavContent>
<o3r-ruleset-history-pres [rulesetExecutions]="rulesetExecutions$ | async"></o3r-ruleset-history-pres>
</ng-template>
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Configuration</a>
<ng-template ngbNavContent>
<o3r-config-panel-pres></o3r-config-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="4">
<a ngbNavLink>Component</a>
<ng-template ngbNavContent>
<o3r-component-panel-pres></o3r-component-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="5">
<a ngbNavLink>Localization</a>
<ng-template ngbNavContent>
<o3r-localization-panel-pres></o3r-localization-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="6">
<a ngbNavLink>Theming</a>
<ng-template ngbNavContent>
<o3r-theming-panel-pres></o3r-theming-panel-pres>
</ng-template>
</li>
</ul>

<div class="d-flex align-items-stretch">
<ul ngbNav #nav="ngbNav" class="nav-tabs px-2 pt-2 navbar-light bg-light flex-fill">
<li [ngbNavItem]="1">
<a ngbNavLink>General</a>
<ng-template ngbNavContent>
<o3r-debug-panel-pres></o3r-debug-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Rules Engine</a>
<ng-template ngbNavContent>
<o3r-ruleset-history-pres [rulesetExecutions]="rulesetExecutions$ | async"></o3r-ruleset-history-pres>
</ng-template>
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Configuration</a>
<ng-template ngbNavContent>
<o3r-config-panel-pres></o3r-config-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="4">
<a ngbNavLink>Component</a>
<ng-template ngbNavContent>
<o3r-component-panel-pres></o3r-component-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="5">
<a ngbNavLink>Localization</a>
<ng-template ngbNavContent>
<o3r-localization-panel-pres></o3r-localization-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="6">
<a ngbNavLink>Theming</a>
<ng-template ngbNavContent>
<o3r-theming-panel-pres></o3r-theming-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="7">
<a ngbNavLink>States</a>
<ng-template ngbNavContent>
<o3r-state-panel></o3r-state-panel>
</ng-template>
</li>
</ul>
<form [formGroup]="form" class="px-4 {{hasLocalChanges() ? 'pt-2' : 'pt-3'}} ng-pristine ng-valid ng-touched border-bottom ms-auto">
<div>
<ng-select
[ngbTooltip]="
hasLocalChanges()
? 'There are some local changes unsaved'
: null
"
[style.width]="'250px'" [class.local-change]="hasLocalChanges()"
formControlName="activeStateName" placeholder="No selected state"
[items]="states()" [searchable]="false" [markFirst]="false" [compareWith]="stateCompareWithFn" bindLabel="name" bindValue="name">
<ng-template ng-label-tmp let-item="item">
<div class="d-flex align-items-baseline gap-2">
<span class="rounded-circle" [style.height]="'10px'" [style.width]="'10px'" [style.background]="item.color"></span>
<span>{{item.name}}</span>
</div>
</ng-template>
<ng-template ng-option-tmp let-item="item">
<div class="d-flex align-items-baseline gap-2">
<span class="rounded-circle" [style.height]="'10px'" [style.width]="'10px'" [style.background]="item.color"></span>
<span>{{item.name}}</span>
</div>
</ng-template>
</ng-select>
</div>
</form>
</div>
<div [ngbNavOutlet]="nav" class="mt-2 px-2"></div>
</app-connection>
80 changes: 55 additions & 25 deletions apps/chrome-devtools/src/app-devtools/app.component.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -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<RulesetExecutionDebug[]>;
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(() => {
cpaulve-1A marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Comment on lines +85 to 87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public stateCompareWithFn(state: State, selectedStateName: string) {
return state.name === selectedStateName;
}
public stateCompareWithFn = (state: State, selectedStateName: string) => state.name === selectedStateName;

there is no problem here but if we were to use this, it could lead to some issues as this method is passed as input in the template and lose the component context (I assume)

Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<h4 class="d-inline-block">Information</h4>
<button type="button" class="btn btn-link mb-2 text-decoration-none" (click)="refreshInfo()">&#10227;</button>
<ul>
<li>App name: <b>{{info.appName}}</b></li>
<li>App version: <b>{{info.appVersion}}</b></li>
<li>Mode: <b>{{info.isProduction ? 'Production' : 'Development'}}</b></li>
@if (info.sessionId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ApplicationInformationContentMessage } from '@o3r/application';
import { ReplaySubject } from 'rxjs';

export interface ExtendedApplicationInformation {
appName: string;
appVersion: string;
sessionId?: string;
sessionGeneratedTime?: Date;
Expand All @@ -26,6 +27,7 @@ export class DebugPanelService {
*/
public update(message: ApplicationInformationContentMessage) {
this.applicationInformationSubject.next({
appName: message.appName,
appVersion: message.appVersion,
sessionId: message.session?.id,
sessionGeneratedTime: message.session?.generatedTime,
Expand Down
Loading
Loading