From fd8ff8faa25da39cc12d12c987d7d0f2d045045b Mon Sep 17 00:00:00 2001
From: Jan Ulrych <ulrych@marbes.cz>
Date: Tue, 2 Apr 2024 00:45:20 +0200
Subject: [PATCH 1/9] =?UTF-8?q?Re=20#11123=20-=20Neza=C4=8Di=C5=A1t=C4=9Bn?=
 =?UTF-8?q?=C3=A1=20logika=20pro=20ozna=C4=8Den=C3=AD=20c=C3=ADlov=C3=A9ho?=
 =?UTF-8?q?=20stavu=20sc=C3=A9n=C3=A1=C5=99e?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/app/app.component.html                    |  58 ++++++++
 src/app/app.component.ts                      |  79 ++++++++++-
 src/app/model/final-state-target.interface.ts |  10 ++
 src/app/model/html-element.interface.ts       |   1 +
 src/app/model/message.interface.ts            |   7 +
 src/app/utils/attribute-grabber.util.ts       |  24 +++-
 src/app/utils/constants.util.ts               |   4 +-
 src/app/utils/storage.util.ts                 |  47 ++++++-
 .../pages/overview/overview.component.html    |  10 ++
 .../view/pages/overview/overview.component.ts |   1 +
 src/core/background.ts                        |  46 ++++---
 src/core/listeners/change.listener.ts         |   2 +-
 src/core/listeners/click.listener.ts          | 128 ++++++++++++++++--
 src/core/listeners/copy.listener.ts           |   4 +-
 src/core/listeners/cut.listener.ts            |   4 +-
 src/core/listeners/keydown.listener.ts        |   2 +-
 src/core/listeners/paste.listener.ts          |   4 +-
 src/core/listeners/resize.listener.ts         |   4 +-
 src/core/listeners/select.listener.ts         |   2 +-
 19 files changed, 392 insertions(+), 45 deletions(-)
 create mode 100644 src/app/model/final-state-target.interface.ts
 create mode 100644 src/app/model/message.interface.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 1f5b28f..1162083 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,8 +1,66 @@
 <div id="scenario-tracker-content">
+  <ng-container *ngIf="finalStateTarget">
+    <h5>Text:</h5>
+    <p>{{finalStateTarget.text}}</p>
+    <h5>Attributes:</h5>
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
+      <input type="checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+      {{attribute}} = {{finalStateTarget.attributes[attribute]}}
+    </div>
+    <h5>Explicitly set styles:</h5>
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
+      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+      style.{{attribute}} = {{finalStateTarget.styles[attribute]}}
+    </div>
+<!--    <h5>Computed styles:</h5>--> <!-- TODO: Commented out just because it is too long, but we should show it to the user -->
+<!--    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">-->
+<!--      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">-->
+<!--      style.{{attribute}} = {{finalStateTarget.computedStyles[attribute]}}-->
+<!--    </div>-->
+    <custom-button
+            buttonText="Cancel"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-trash"
+            additionalClasses="warning-button"
+            (click)="deleteFinalStateTarget()"
+    >
+    </custom-button>
+    <custom-button
+            buttonText="Add"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-plus"
+            additionalClasses="success-button"
+            (click)="addFinalState()"
+      >
+    </custom-button>
+  </ng-container>
+
+
+  <h5>Final states:</h5>
+  <div *ngFor="let finalState of finalStates; let i = index">
+    <b>{{finalState.element.tagName}}</b>
+    <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
+      <!-- show only first 3 -->
+      <p *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
+    </div>
+    <custom-button
+            buttonText="Remove"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-trash"
+            additionalClasses="delete-button"
+            (click)="deleteFinalState(i)"
+    >
+    </custom-button>
+  </div>
+
   <overview
     [deleteActionHandler]="deleteAction"
     [toggleRecordingHandler]="toggleRecording"
     [deleteAllActionsHandler]="deleteAllActions"
+    [markFinalStateHandler]="markFinalState"
     [actions]="actions"
     [recording]="recording"
   >
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index bf3c213..0422152 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,6 +1,18 @@
 import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
 import { Action } from './model/action.interface';
-import { getActions, getRecording, saveActions, saveRecording } from './utils/storage.util';
+import {
+  getActions,
+  getFinalStates,
+  getRecording,
+  saveActions, saveFinalStates,
+  saveMarkFinalState,
+  saveRecording
+} from './utils/storage.util';
+import {Message} from "./model/message.interface";
+import {Result} from "./model/result.interface";
+import {FinalStateTarget} from "./model/final-state-target.interface";
+import {ObjectKeys} from "./model/object-keys.interface";
+import {HtmlElement} from "./model/html-element.interface";
 
 @Component({
   selector: 'app-root',
@@ -9,17 +21,75 @@ import { getActions, getRecording, saveActions, saveRecording } from './utils/st
 })
 export class AppComponent implements OnInit {
   actions: Action[] = [];
+  finalStates: Result[] = [];
   recording: boolean = false;
+  finalStateTarget?: FinalStateTarget = undefined;
+  savedAttributes: ObjectKeys = {};
+  protected readonly Object = Object; // just for the template
 
   constructor(private ref: ChangeDetectorRef){}
 
   ngOnInit(): void {
+    chrome.runtime.onMessage.addListener((message: Message) => {
+      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
+        console.log("Final state target:", message)
+        this.finalStateTarget = message.finalStateClickTarget;
+        this.savedAttributes = {};
+        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
+      }
+    });
     chrome.storage.onChanged.addListener(() => {
       this.getData();
     });
     this.getData();
   }
 
+  public deleteFinalStateTarget(){
+    console.log("Asdf")
+
+    const xPath = this.finalStateTarget!.xPath;
+    this.finalStateTarget = undefined;
+    this.savedAttributes = {};
+
+    this.ref.detectChanges();
+    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
+      chrome.tabs.sendMessage(tabs[0].id!, xPath);
+    })
+  }
+
+  public saveAttribute(event: Event, attribute: string, value: string){
+    if((event.target as HTMLInputElement).checked){
+      console.log(`Saving attribute ${attribute}=${value}`)
+      this.savedAttributes[attribute] = value;
+    } else {
+      console.log(`Deleting attribute ${attribute}=${value}`)
+      delete this.savedAttributes[attribute];
+    }
+    console.log(this.savedAttributes)
+  }
+
+  public addFinalState(){
+    const element: HtmlElement = {
+      tagName: this.finalStateTarget!.tagName,
+      globalAttributes: {},
+      elementAttributes: {},
+      xPath: this.finalStateTarget!.xPath
+    };
+
+    const finalState: Result = {
+      element: element,
+      selectedAttributes: this.savedAttributes
+    };
+    this.finalStates.push(finalState);
+    saveFinalStates(this.finalStates);
+    this.deleteFinalStateTarget();
+  }
+
+  public deleteFinalState = (index: number) => {
+    this.finalStates.splice(index, 1);
+    saveFinalStates(this.finalStates);
+  }
+
   /**
    * Loads all data needed by the component.
    */
@@ -30,6 +100,9 @@ export class AppComponent implements OnInit {
     await getRecording().then(recording => {
       this.recording = recording;
     });
+    await getFinalStates().then(finalStates => {
+      this.finalStates = finalStates;
+    });
     this.ref.detectChanges();
     // we need to automatically detect changes, because chrome.storage.onChanged.addListener
     // is outside of the Angular scope
@@ -52,4 +125,8 @@ export class AppComponent implements OnInit {
     this.actions.splice(index, 1);
     saveActions(this.actions);
   }
+
+  public markFinalState = () => {
+    saveMarkFinalState(true);
+  }
 }
diff --git a/src/app/model/final-state-target.interface.ts b/src/app/model/final-state-target.interface.ts
new file mode 100644
index 0000000..694252c
--- /dev/null
+++ b/src/app/model/final-state-target.interface.ts
@@ -0,0 +1,10 @@
+import {ObjectKeys} from "./object-keys.interface";
+
+export interface FinalStateTarget{
+  tagName: string,
+  text: string,
+  xPath: string,
+  attributes: ObjectKeys,
+  styles: ObjectKeys,
+  computedStyles: ObjectKeys
+}
\ No newline at end of file
diff --git a/src/app/model/html-element.interface.ts b/src/app/model/html-element.interface.ts
index b09c341..8c7dd1f 100644
--- a/src/app/model/html-element.interface.ts
+++ b/src/app/model/html-element.interface.ts
@@ -2,6 +2,7 @@ import { ObjectKeys } from "./object-keys.interface";
 
 export interface HtmlElement{
   tagName: string,
+  xPath: string,
   globalAttributes: ObjectKeys,
   elementAttributes: ObjectKeys
 }
\ No newline at end of file
diff --git a/src/app/model/message.interface.ts b/src/app/model/message.interface.ts
new file mode 100644
index 0000000..4202d48
--- /dev/null
+++ b/src/app/model/message.interface.ts
@@ -0,0 +1,7 @@
+import {Action} from "./action.interface";
+import {FinalStateTarget} from "./final-state-target.interface";
+
+export interface Message{
+  action?: Action,
+  finalStateClickTarget?: FinalStateTarget
+}
\ No newline at end of file
diff --git a/src/app/utils/attribute-grabber.util.ts b/src/app/utils/attribute-grabber.util.ts
index 2b4bca8..0759684 100644
--- a/src/app/utils/attribute-grabber.util.ts
+++ b/src/app/utils/attribute-grabber.util.ts
@@ -1,12 +1,12 @@
-import { HtmlElement } from "../model/html-element.interface";
+import {HtmlElement} from "../model/html-element.interface";
 
 export const extractAllAttributes = (element: HTMLElement): HtmlElement => {
-  const extracted: HtmlElement = {
+  return {
     tagName: element.tagName,
+    xPath: getElementsXpath(element),
     globalAttributes: extractGlobalAttributes(element),
     elementAttributes: extractElementAttributes(element)
   };
-  return extracted;
 }
 
 /**
@@ -369,4 +369,22 @@ const extractFromVideo = (element: HTMLVideoElement): {} => {
     controls: element.controls,
     controlsList: element.getAttribute('controlslist')
   }
+}
+
+export const getElementsXpath = (element: HTMLElement): string => {
+  if (element.tagName == 'HTML')
+    return '/HTML[1]';
+  if (element === document.body)
+    return '/HTML[1]/BODY[1]';
+
+  var ix = 0;
+  var siblings = element.parentNode!.childNodes;
+  for (var i = 0; i < siblings.length; i++) {
+    var sibling = siblings[i];
+    if (sibling === element)
+      return getElementsXpath(element.parentNode! as HTMLElement) + '/' + element.tagName + '[' + (ix + 1) + ']';
+    if (sibling.nodeType === 1 && (sibling as HTMLElement).tagName === element.tagName)
+      ix++;
+  }
+  return ""
 }
\ No newline at end of file
diff --git a/src/app/utils/constants.util.ts b/src/app/utils/constants.util.ts
index 366335c..efe449b 100644
--- a/src/app/utils/constants.util.ts
+++ b/src/app/utils/constants.util.ts
@@ -1,2 +1,4 @@
 export const KEY_ACTIONS = "actions";
-export const KEY_RECORDING = "recording";
\ No newline at end of file
+export const KEY_RECORDING = "recording";
+export const KEY_MARK_FINAL_STATE = "markFinalState";
+export const KEY_FINAL_STATES = "finalStates";
\ No newline at end of file
diff --git a/src/app/utils/storage.util.ts b/src/app/utils/storage.util.ts
index 25e4409..905c9d6 100644
--- a/src/app/utils/storage.util.ts
+++ b/src/app/utils/storage.util.ts
@@ -1,5 +1,6 @@
 import { Action } from "../model/action.interface";
-import { KEY_ACTIONS, KEY_RECORDING } from "./constants.util";
+import {KEY_ACTIONS, KEY_FINAL_STATES, KEY_MARK_FINAL_STATE, KEY_RECORDING} from "./constants.util";
+import {Result} from "../model/result.interface";
 
 /**
  * Loads all actions from the storage.
@@ -25,6 +26,30 @@ export async function getRecording(): Promise<boolean>{
   return recording;
 }
 
+/**
+ * Loads the recording status from the storage.
+ * @returns Whether we are recording or not.
+ */
+export async function getMarkFinalState(): Promise<boolean>{
+  let markFinalState: boolean = false;
+  await chrome.storage.local.get(KEY_MARK_FINAL_STATE).then(data => {
+    markFinalState = data[KEY_MARK_FINAL_STATE] as boolean;
+  });
+  return markFinalState;
+}
+
+/**
+ * Loads the list of final states from the storage.
+ * @returns List of final states.
+ */
+export async function getFinalStates(): Promise<Result[]>{
+  let finalStates: Result[] = [];
+  await chrome.storage.local.get(KEY_FINAL_STATES).then(data => {
+    finalStates = data[KEY_FINAL_STATES] as Result[];
+  });
+  return finalStates;
+}
+
 /**
  * Saves actions to the storage.
  * @param actions Actions to be saved.
@@ -43,4 +68,24 @@ export async function saveRecording(recording: boolean) {
   await chrome.storage.local.set({
     recording: recording
   });
+}
+
+/**
+ * Saves the flag that the next click is gonna mark the final state.
+ * @param markFinalState Whether the next click is gonna mark the final state.
+ */
+export async function saveMarkFinalState(markFinalState: boolean) {
+  await chrome.storage.local.set({
+    markFinalState: markFinalState
+  });
+}
+
+/**
+ * Saves the list of final states to the storage.
+ * @param finalStates Final states to be saved.
+ */
+export async function saveFinalStates(finalStates: Result[]) {
+  await chrome.storage.local.set({
+    finalStates: finalStates
+  });
 }
\ No newline at end of file
diff --git a/src/app/view/pages/overview/overview.component.html b/src/app/view/pages/overview/overview.component.html
index 3ee8721..5ee6efc 100644
--- a/src/app/view/pages/overview/overview.component.html
+++ b/src/app/view/pages/overview/overview.component.html
@@ -2,6 +2,16 @@
 <h1>Scenario Tracker</h1>
 
 <!-- Controls -->
+<custom-button
+        buttonText="Mark final state"
+        size="M"
+        iconType="fontawesome"
+        icon="fa fa-solid fa-flag-checkered"
+        additionalClasses="info-button"
+        (click)="markFinalStateHandler()">
+</custom-button>
+
+<div style="display: inline-block; width: 10px;"></div>
 <custom-button 
   buttonText="Delete all actions"
   size="M"
diff --git a/src/app/view/pages/overview/overview.component.ts b/src/app/view/pages/overview/overview.component.ts
index 636b6d2..f174fb8 100644
--- a/src/app/view/pages/overview/overview.component.ts
+++ b/src/app/view/pages/overview/overview.component.ts
@@ -15,6 +15,7 @@ export class OverviewComponent implements OnInit {
   @Input() deleteActionHandler!: Function;
   @Input() deleteAllActionsHandler!: Function;
   @Input() toggleRecordingHandler!: Function;
+  @Input() markFinalStateHandler!: Function;
 
   readonly ActionEnum = ActionEnum
 
diff --git a/src/core/background.ts b/src/core/background.ts
index 796bd3f..d654c9a 100644
--- a/src/core/background.ts
+++ b/src/core/background.ts
@@ -1,6 +1,8 @@
 /// <reference types="chrome"/>
 
-import { getActions, getRecording, saveActions } from "src/app/utils/storage.util";
+import {getActions, getRecording, saveActions} from "src/app/utils/storage.util";
+import {Message} from "../app/model/message.interface";
+import {Action} from "../app/model/action.interface";
 
 console.log("Background loaded")
 
@@ -9,27 +11,35 @@ chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true}).catch((error)
 
 // clear the local storage on refresh / reload
 // prepare the storage for actions
-if(true){ // set to false if you dont want to clear the storage on each extension refresh
+if (true) { // set to false if you dont want to clear the storage on each extension refresh
   chrome.runtime.onInstalled.addListener(() => {
     chrome.storage.local.clear();
-    chrome.storage.local.set({ actions: [], recording: false });
-  })
+    chrome.storage.local.set({
+      actions: [],
+      recording: false,
+      markFinalState: false,
+      finalStates: []
+    });
+  });
 }
 
 // listens for incoming messages from content script
-chrome.runtime.onMessage.addListener((meesage: any) => {
-  console.log("Message received: ", meesage);
+chrome.runtime.onMessage.addListener((message: Message) => {
+  console.log("Message received: ", message);
 
-  // determine whether we are recording
-  getRecording().then(recording => {
-    if(recording === true){ // if we are recording, save the action
-      // load actions from storage
-      getActions().then(actions => {
-        // add new action to the loaded actions
-        actions.push(meesage);
-        // save the updated action list
-        saveActions(actions);
-      });
-    }
-  })
+  if (message.action) { // in background script, we listen only for actions performed by actions
+    const action: Action = message.action;
+    // determine whether we are recording
+    getRecording().then(recording => {
+      if (recording) { // if we are recording, save the action
+        // load actions from storage
+        getActions().then(actions => {
+          // add new action to the loaded actions
+          actions.push(action);
+          // save the updated action list
+          saveActions(actions);
+        });
+      }
+    })
+  }
 })
\ No newline at end of file
diff --git a/src/core/listeners/change.listener.ts b/src/core/listeners/change.listener.ts
index 332c4c9..f43ff6a 100644
--- a/src/core/listeners/change.listener.ts
+++ b/src/core/listeners/change.listener.ts
@@ -17,5 +17,5 @@ document.addEventListener("change", (event) => {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 })
\ No newline at end of file
diff --git a/src/core/listeners/click.listener.ts b/src/core/listeners/click.listener.ts
index 9df457f..3e6f838 100644
--- a/src/core/listeners/click.listener.ts
+++ b/src/core/listeners/click.listener.ts
@@ -1,13 +1,119 @@
-import { ActionEnum } from "src/app/model/action.enum";
-import { DocumentAction } from "../../app/model/document-action.interface";
-import { HtmlElement } from "src/app/model/html-element.interface";
-import { extractAllAttributes, extractElementAttributes, extractGlobalAttributes } from "src/app/utils/attribute-grabber.util";
-import { Action } from "src/app/model/action.interface";
+import {ActionEnum} from "src/app/model/action.enum";
+import {DocumentAction} from "../../app/model/document-action.interface";
+import {HtmlElement} from "src/app/model/html-element.interface";
+import {extractAllAttributes, getElementsXpath} from "src/app/utils/attribute-grabber.util";
+import {Action} from "src/app/model/action.interface";
+import {getMarkFinalState, saveMarkFinalState} from "../../app/utils/storage.util";
+import {Message} from "../../app/model/message.interface";
+import {FinalStateTarget} from "../../app/model/final-state-target.interface";
+import {ObjectKeys} from "../../app/model/object-keys.interface";
 
 document.addEventListener("click", function (event) {
-  console.log("Click detected:", event.target);
+  getMarkFinalState().then(markFinalState => {
+    const target: HTMLElement = event.target as HTMLElement;
+    if (markFinalState) {
+      if(target instanceof HTMLLabelElement){
+        const label = target as HTMLLabelElement;
+        const labelFor = label.getAttribute('for');
+        console.log(labelFor)
+        if(labelFor !== null && labelFor !== ''){
+          // when we click on label, two click events are fired:
+          // first event for the label
+          // second event for the input that the label is for (attribute 'for')
+          // therefore if the label has the 'for' attribute specified, we do not use it
+          return;
+        }
+      }
+      markFinalStateHandler(target);
+    } else {
+      clickListenerHandler(target);
+    }
+  });
+});
+
+const onBeforeUnload = (e: BeforeUnloadEvent) => {
+  // Cancel the event
+  e.preventDefault();
+  // unfortunately, we cannot set custom message to the dialog, as is it considered dangerous
+}
+
+chrome.storage.onChanged.addListener(() => {
+  getMarkFinalState().then(markFinalState => {
+    if (markFinalState) {
+      // try to keep the user on the page, so the selected element is visible
+      window.addEventListener('beforeunload', onBeforeUnload);
+    } else {
+      window.removeEventListener('beforeunload', onBeforeUnload);
+    }
+  });
+})
+
+chrome.runtime.onMessage.addListener((xPath: string) => {
+  const target = (document.evaluate(xPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as HTMLElement);
+  target.style.outline = "";
+})
+
+const markFinalStateHandler = (target: HTMLElement) => {
+  console.log("Final state detected:", target);
+  const xPath: string = getElementsXpath(target);
+  console.log("XPATH:", xPath);
+
+  // enable normal click listener
+  saveMarkFinalState(false);
+
+  const attributes: ObjectKeys = {};
+  for (let attribute of Array.from(target.attributes)) {
+    if (attribute.name === 'style') {
+      continue;
+    }
+    attributes[attribute.name] = attribute.value;
+  }
+
+  const styles: ObjectKeys = {};
+  for (let attribute of Object.keys(target.style)) {
+    const value = target.style.getPropertyValue(attribute);
+    if (value !== '') {
+      styles[attribute] = target.style.getPropertyValue(attribute);
+    }
+  }
+
+  const computedStyles: ObjectKeys = {};
+  for (let entry of Object.entries(getComputedStyle(target))) {
+    if (entry[1] !== '' && !isNumeric(entry[0])) { // only styles that has some value and those, whose key is not numeric
+      computedStyles[entry[0]] = entry[1];
+    }
+  }
+
+  let text: string;
+  if (target instanceof HTMLInputElement) {
+    text = (target as HTMLInputElement).value;
+  } else {
+    text = target.innerText;
+  }
+
+  const finalStateTarget: FinalStateTarget = {
+    tagName: target.tagName,
+    text: text,
+    xPath: xPath,
+    attributes: attributes,
+    styles: styles,
+    computedStyles: computedStyles
+  }
+
+  const message: Message = {
+    finalStateClickTarget: finalStateTarget
+  };
+
+  // send the chosen element to the component
+  chrome.runtime.sendMessage(message);
+
+  // highlight the element on the page
+  target.style.outline = "5px solid rgba(218,172,0,0.8)";
+}
+
+const clickListenerHandler = (target: HTMLElement) => {
+  console.log("Click detected:", target);
 
-  const target = event.target as HTMLElement;
   const element: HtmlElement = extractAllAttributes(target);
   const documentAction: DocumentAction = {
     target: element
@@ -18,5 +124,9 @@ document.addEventListener("click", function (event) {
     recordedOn: new Date(),
   };
   // send message about the event to the background script
-  chrome.runtime.sendMessage(action);
-});
\ No newline at end of file
+  chrome.runtime.sendMessage({action: action});
+}
+
+const isNumeric = (value: string) => {
+  return /^-?\d+$/.test(value);
+}
\ No newline at end of file
diff --git a/src/core/listeners/copy.listener.ts b/src/core/listeners/copy.listener.ts
index bc9aa17..f482b51 100644
--- a/src/core/listeners/copy.listener.ts
+++ b/src/core/listeners/copy.listener.ts
@@ -2,7 +2,7 @@ import { ActionEnum } from "src/app/model/action.enum";
 import { Action } from "src/app/model/action.interface";
 import { DocumentAction } from "src/app/model/document-action.interface";
 import { HtmlElement } from "src/app/model/html-element.interface";
-import { extractAllAttributes, extractElementAttributes, extractGlobalAttributes } from "src/app/utils/attribute-grabber.util";
+import { extractAllAttributes } from "src/app/utils/attribute-grabber.util";
 
 document.addEventListener("copy", function (event) {
   const selection = document.getSelection();
@@ -21,5 +21,5 @@ document.addEventListener("copy", function (event) {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 });
\ No newline at end of file
diff --git a/src/core/listeners/cut.listener.ts b/src/core/listeners/cut.listener.ts
index 6955bda..2969644 100644
--- a/src/core/listeners/cut.listener.ts
+++ b/src/core/listeners/cut.listener.ts
@@ -2,7 +2,7 @@ import { ActionEnum } from "src/app/model/action.enum";
 import { Action } from "src/app/model/action.interface";
 import { DocumentAction } from "src/app/model/document-action.interface";
 import { HtmlElement } from "src/app/model/html-element.interface";
-import { extractAllAttributes, extractElementAttributes, extractGlobalAttributes } from "src/app/utils/attribute-grabber.util";
+import { extractAllAttributes } from "src/app/utils/attribute-grabber.util";
 
 document.addEventListener("cut", function (event) {
   const selection = document.getSelection();
@@ -21,5 +21,5 @@ document.addEventListener("cut", function (event) {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 });
\ No newline at end of file
diff --git a/src/core/listeners/keydown.listener.ts b/src/core/listeners/keydown.listener.ts
index e48f7a0..c4a1b02 100644
--- a/src/core/listeners/keydown.listener.ts
+++ b/src/core/listeners/keydown.listener.ts
@@ -17,5 +17,5 @@ document.addEventListener("keydown", (event) => {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 })
\ No newline at end of file
diff --git a/src/core/listeners/paste.listener.ts b/src/core/listeners/paste.listener.ts
index dca689a..ab3c318 100644
--- a/src/core/listeners/paste.listener.ts
+++ b/src/core/listeners/paste.listener.ts
@@ -2,7 +2,7 @@ import { ActionEnum } from "src/app/model/action.enum";
 import { Action } from "src/app/model/action.interface";
 import { DocumentAction } from "src/app/model/document-action.interface";
 import { HtmlElement } from "src/app/model/html-element.interface";
-import { extractAllAttributes, extractElementAttributes, extractGlobalAttributes } from "src/app/utils/attribute-grabber.util";
+import { extractAllAttributes } from "src/app/utils/attribute-grabber.util";
 
 document.addEventListener("paste", function (event) {
   const pasted = event.clipboardData
@@ -23,5 +23,5 @@ document.addEventListener("paste", function (event) {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 });
\ No newline at end of file
diff --git a/src/core/listeners/resize.listener.ts b/src/core/listeners/resize.listener.ts
index 10b491b..cdd16e4 100644
--- a/src/core/listeners/resize.listener.ts
+++ b/src/core/listeners/resize.listener.ts
@@ -1,5 +1,3 @@
-
-
 import { ActionEnum } from "src/app/model/action.enum";
 import { Action } from "src/app/model/action.interface";
 import { WindowAction } from "src/app/model/window-action.interface";
@@ -21,5 +19,5 @@ const resized = () => {
     recordedOn: new Date(),
     windowAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 }
\ No newline at end of file
diff --git a/src/core/listeners/select.listener.ts b/src/core/listeners/select.listener.ts
index 24a60ac..f4e5b79 100644
--- a/src/core/listeners/select.listener.ts
+++ b/src/core/listeners/select.listener.ts
@@ -21,5 +21,5 @@ document.addEventListener("select", (event) => {
     recordedOn: new Date(),
     documentAction: documentAction
   };
-  chrome.runtime.sendMessage(action);
+  chrome.runtime.sendMessage({action: action});
 })
\ No newline at end of file
-- 
GitLab


From fcce975c50d0da525b4378094555b7d8eecfefc4 Mon Sep 17 00:00:00 2001
From: Jan Ulrych <ulrych@marbes.cz>
Date: Tue, 2 Apr 2024 12:12:18 +0200
Subject: [PATCH 2/9] =?UTF-8?q?Re=20#11123=20-=20Vy=C4=8Di=C5=A1t=C4=9Bn?=
 =?UTF-8?q?=C3=AD,=20odd=C4=9Blen=C3=AD=20do=20v=C3=ADce=20soubor=C5=AF,?=
 =?UTF-8?q?=20koment=C3=A1=C5=99e?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/app/app.component.html                    |  6 +-
 src/app/app.component.ts                      | 22 ++++-
 src/app/model/message.interface.ts            |  3 +-
 src/app/utils/attribute-grabber.util.ts       | 24 ++----
 src/app/utils/element.util.ts                 | 44 ++++++++++
 src/app/utils/storage.util.ts                 |  8 +-
 .../pages/overview/overview.component.html    |  2 +
 .../view/pages/overview/overview.component.ts |  2 +
 src/core/content-script.ts                    | 13 ++-
 src/core/listeners/click.listener.ts          | 86 ++++++++++---------
 10 files changed, 139 insertions(+), 71 deletions(-)
 create mode 100644 src/app/utils/element.util.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 1162083..2631a37 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,4 +1,5 @@
 <div id="scenario-tracker-content">
+  <!-- START: MARK FINAL STATE -->
   <ng-container *ngIf="finalStateTarget">
     <h5>Text:</h5>
     <p>{{finalStateTarget.text}}</p>
@@ -12,7 +13,8 @@
       <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
       style.{{attribute}} = {{finalStateTarget.styles[attribute]}}
     </div>
-<!--    <h5>Computed styles:</h5>--> <!-- TODO: Commented out just because it is too long, but we should show it to the user -->
+    <!-- TODO: It is now commented out just because the output is too long, but we should show it to the user -->
+<!--    <h5>Computed styles:</h5>-->
 <!--    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">-->
 <!--      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">-->
 <!--      style.{{attribute}} = {{finalStateTarget.computedStyles[attribute]}}-->
@@ -37,7 +39,6 @@
     </custom-button>
   </ng-container>
 
-
   <h5>Final states:</h5>
   <div *ngFor="let finalState of finalStates; let i = index">
     <b>{{finalState.element.tagName}}</b>
@@ -55,6 +56,7 @@
     >
     </custom-button>
   </div>
+  <!-- END: MARK FINAL STATE -->
 
   <overview
     [deleteActionHandler]="deleteAction"
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 0422152..7bf70a9 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -21,15 +21,18 @@ import {HtmlElement} from "./model/html-element.interface";
 })
 export class AppComponent implements OnInit {
   actions: Action[] = [];
-  finalStates: Result[] = [];
   recording: boolean = false;
+  // START: MARK FINAL STATE
+  finalStates: Result[] = [];
   finalStateTarget?: FinalStateTarget = undefined;
   savedAttributes: ObjectKeys = {};
   protected readonly Object = Object; // just for the template
+  // END: MARK FINAL STATE
 
   constructor(private ref: ChangeDetectorRef){}
 
   ngOnInit(): void {
+    // START: MARK FINAL STATE
     chrome.runtime.onMessage.addListener((message: Message) => {
       if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
         console.log("Final state target:", message)
@@ -38,22 +41,28 @@ export class AppComponent implements OnInit {
         this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
       }
     });
+    // END: MARK FINAL STATE
     chrome.storage.onChanged.addListener(() => {
       this.getData();
     });
     this.getData();
   }
 
+  // START: MARK FINAL STATE
+  /**
+   * Deletes the clicked final state target and sets all variables to default values.
+   */
   public deleteFinalStateTarget(){
-    console.log("Asdf")
-
     const xPath = this.finalStateTarget!.xPath;
     this.finalStateTarget = undefined;
     this.savedAttributes = {};
 
     this.ref.detectChanges();
+    const message: Message = {
+      xPath: xPath
+    }
     chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
-      chrome.tabs.sendMessage(tabs[0].id!, xPath);
+      chrome.tabs.sendMessage(tabs[0].id!, message);
     })
   }
 
@@ -89,6 +98,7 @@ export class AppComponent implements OnInit {
     this.finalStates.splice(index, 1);
     saveFinalStates(this.finalStates);
   }
+  // END: MARK FINAL STATE
 
   /**
    * Loads all data needed by the component.
@@ -100,9 +110,11 @@ export class AppComponent implements OnInit {
     await getRecording().then(recording => {
       this.recording = recording;
     });
+    // START: MARK FINAL STATE
     await getFinalStates().then(finalStates => {
       this.finalStates = finalStates;
     });
+    // END: MARK FINAL STATE
     this.ref.detectChanges();
     // we need to automatically detect changes, because chrome.storage.onChanged.addListener
     // is outside of the Angular scope
@@ -126,7 +138,9 @@ export class AppComponent implements OnInit {
     saveActions(this.actions);
   }
 
+  // START: MARK FINAL STATE
   public markFinalState = () => {
     saveMarkFinalState(true);
   }
+  // END: MARK FINAL STATE
 }
diff --git a/src/app/model/message.interface.ts b/src/app/model/message.interface.ts
index 4202d48..f2fb5b2 100644
--- a/src/app/model/message.interface.ts
+++ b/src/app/model/message.interface.ts
@@ -3,5 +3,6 @@ import {FinalStateTarget} from "./final-state-target.interface";
 
 export interface Message{
   action?: Action,
-  finalStateClickTarget?: FinalStateTarget
+  finalStateClickTarget?: FinalStateTarget,
+  xPath?: string
 }
\ No newline at end of file
diff --git a/src/app/utils/attribute-grabber.util.ts b/src/app/utils/attribute-grabber.util.ts
index 0759684..655651a 100644
--- a/src/app/utils/attribute-grabber.util.ts
+++ b/src/app/utils/attribute-grabber.util.ts
@@ -1,5 +1,11 @@
 import {HtmlElement} from "../model/html-element.interface";
+import {getElementsXpath} from "./element.util";
 
+/**
+ * Extracts all attributes from the given element.
+ * @param element HTML element.
+ * @returns       Interface that can be saved.
+ */
 export const extractAllAttributes = (element: HTMLElement): HtmlElement => {
   return {
     tagName: element.tagName,
@@ -369,22 +375,4 @@ const extractFromVideo = (element: HTMLVideoElement): {} => {
     controls: element.controls,
     controlsList: element.getAttribute('controlslist')
   }
-}
-
-export const getElementsXpath = (element: HTMLElement): string => {
-  if (element.tagName == 'HTML')
-    return '/HTML[1]';
-  if (element === document.body)
-    return '/HTML[1]/BODY[1]';
-
-  var ix = 0;
-  var siblings = element.parentNode!.childNodes;
-  for (var i = 0; i < siblings.length; i++) {
-    var sibling = siblings[i];
-    if (sibling === element)
-      return getElementsXpath(element.parentNode! as HTMLElement) + '/' + element.tagName + '[' + (ix + 1) + ']';
-    if (sibling.nodeType === 1 && (sibling as HTMLElement).tagName === element.tagName)
-      ix++;
-  }
-  return ""
 }
\ No newline at end of file
diff --git a/src/app/utils/element.util.ts b/src/app/utils/element.util.ts
new file mode 100644
index 0000000..50ab0ad
--- /dev/null
+++ b/src/app/utils/element.util.ts
@@ -0,0 +1,44 @@
+export const getElementsXpath = (element: HTMLElement): string => {
+  if (element.tagName == 'HTML')
+    return '/HTML[1]';
+  if (element === document.body)
+    return '/HTML[1]/BODY[1]';
+
+  var ix = 0;
+  var siblings = element.parentNode!.childNodes;
+  for (var i = 0; i < siblings.length; i++) {
+    var sibling = siblings[i];
+    if (sibling === element)
+      return getElementsXpath(element.parentNode! as HTMLElement) + '/' + element.tagName + '[' + (ix + 1) + ']';
+    if (sibling.nodeType === 1 && (sibling as HTMLElement).tagName === element.tagName)
+      ix++;
+  }
+  return ""
+}
+
+export const getElementByXpath = (xPath: string): HTMLElement => {
+  return (document.evaluate(xPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as HTMLElement);
+}
+
+export const cancelElementHighlightingByXpath = (xPath: string) => {
+  const element: HTMLElement = getElementByXpath(xPath);
+  element.style.outline = "";
+}
+
+export const highlightElementByXpath = (xPath: string) => {
+  const element: HTMLElement = getElementByXpath(xPath);
+  element.style.outline = "5px solid rgba(218,172,0,0.8)";
+}
+
+/**
+ * @param target Whether the element is a 'label' element with 'for' attribute.
+ */
+export const isLabelFor = (target: HTMLElement): boolean => {
+  if(!(target instanceof HTMLLabelElement)){
+    return false;
+  }
+
+  const label = target as HTMLLabelElement;
+  const labelFor = label.getAttribute('for');
+  return labelFor !== null && labelFor !== '';
+}
\ No newline at end of file
diff --git a/src/app/utils/storage.util.ts b/src/app/utils/storage.util.ts
index 905c9d6..5d49c4a 100644
--- a/src/app/utils/storage.util.ts
+++ b/src/app/utils/storage.util.ts
@@ -56,7 +56,7 @@ export async function getFinalStates(): Promise<Result[]>{
  */
 export async function saveActions(actions: Action[]) {
   await chrome.storage.local.set({
-    actions: actions
+    [KEY_ACTIONS]: actions
   });
 }
 
@@ -66,7 +66,7 @@ export async function saveActions(actions: Action[]) {
  */
 export async function saveRecording(recording: boolean) {
   await chrome.storage.local.set({
-    recording: recording
+    [KEY_RECORDING]: recording
   });
 }
 
@@ -76,7 +76,7 @@ export async function saveRecording(recording: boolean) {
  */
 export async function saveMarkFinalState(markFinalState: boolean) {
   await chrome.storage.local.set({
-    markFinalState: markFinalState
+    [KEY_MARK_FINAL_STATE]: markFinalState
   });
 }
 
@@ -86,6 +86,6 @@ export async function saveMarkFinalState(markFinalState: boolean) {
  */
 export async function saveFinalStates(finalStates: Result[]) {
   await chrome.storage.local.set({
-    finalStates: finalStates
+    [KEY_FINAL_STATES]: finalStates
   });
 }
\ No newline at end of file
diff --git a/src/app/view/pages/overview/overview.component.html b/src/app/view/pages/overview/overview.component.html
index 5ee6efc..34e0eaf 100644
--- a/src/app/view/pages/overview/overview.component.html
+++ b/src/app/view/pages/overview/overview.component.html
@@ -2,6 +2,7 @@
 <h1>Scenario Tracker</h1>
 
 <!-- Controls -->
+<!-- START: MARK FINAL STATE -->
 <custom-button
         buttonText="Mark final state"
         size="M"
@@ -10,6 +11,7 @@
         additionalClasses="info-button"
         (click)="markFinalStateHandler()">
 </custom-button>
+<!-- END: MARK FINAL STATE -->
 
 <div style="display: inline-block; width: 10px;"></div>
 <custom-button 
diff --git a/src/app/view/pages/overview/overview.component.ts b/src/app/view/pages/overview/overview.component.ts
index f174fb8..b0d0be5 100644
--- a/src/app/view/pages/overview/overview.component.ts
+++ b/src/app/view/pages/overview/overview.component.ts
@@ -15,7 +15,9 @@ export class OverviewComponent implements OnInit {
   @Input() deleteActionHandler!: Function;
   @Input() deleteAllActionsHandler!: Function;
   @Input() toggleRecordingHandler!: Function;
+  // START: MARK FINAL STATE
   @Input() markFinalStateHandler!: Function;
+  // END: MARK FINAL STATE
 
   readonly ActionEnum = ActionEnum
 
diff --git a/src/core/content-script.ts b/src/core/content-script.ts
index bf19e18..cd49310 100644
--- a/src/core/content-script.ts
+++ b/src/core/content-script.ts
@@ -1,5 +1,8 @@
 /// <reference types="chrome"/>
 
+import {cancelElementHighlightingByXpath} from "../app/utils/element.util";
+import {Message} from "../app/model/message.interface";
+
 console.log("Content script loaded")
 
 // import event listeners
@@ -10,4 +13,12 @@ import "./listeners/cut.listener"
 import "./listeners/keydown.listener"
 import "./listeners/select.listener"
 import "./listeners/resize.listener"
-import "./listeners/change.listener"
\ No newline at end of file
+import "./listeners/change.listener"
+
+// listener for canceling the highlighting of certain element
+// needs to be in content-script, because it is the only one, that has access to the DOM of the webpage
+chrome.runtime.onMessage.addListener((message: Message) => {
+  if(message.xPath){
+    cancelElementHighlightingByXpath(message.xPath);
+  }
+})
\ No newline at end of file
diff --git a/src/core/listeners/click.listener.ts b/src/core/listeners/click.listener.ts
index 3e6f838..85c89de 100644
--- a/src/core/listeners/click.listener.ts
+++ b/src/core/listeners/click.listener.ts
@@ -1,89 +1,70 @@
 import {ActionEnum} from "src/app/model/action.enum";
 import {DocumentAction} from "../../app/model/document-action.interface";
 import {HtmlElement} from "src/app/model/html-element.interface";
-import {extractAllAttributes, getElementsXpath} from "src/app/utils/attribute-grabber.util";
+import {extractAllAttributes} from "src/app/utils/attribute-grabber.util";
 import {Action} from "src/app/model/action.interface";
 import {getMarkFinalState, saveMarkFinalState} from "../../app/utils/storage.util";
 import {Message} from "../../app/model/message.interface";
 import {FinalStateTarget} from "../../app/model/final-state-target.interface";
 import {ObjectKeys} from "../../app/model/object-keys.interface";
+import {highlightElementByXpath, isLabelFor, getElementsXpath} from "../../app/utils/element.util";
 
+// add click listener to the DOM
 document.addEventListener("click", function (event) {
   getMarkFinalState().then(markFinalState => {
     const target: HTMLElement = event.target as HTMLElement;
     if (markFinalState) {
-      if(target instanceof HTMLLabelElement){
-        const label = target as HTMLLabelElement;
-        const labelFor = label.getAttribute('for');
-        console.log(labelFor)
-        if(labelFor !== null && labelFor !== ''){
-          // when we click on label, two click events are fired:
-          // first event for the label
-          // second event for the input that the label is for (attribute 'for')
-          // therefore if the label has the 'for' attribute specified, we do not use it
-          return;
-        }
-      }
+      // the click indicates final state
       markFinalStateHandler(target);
     } else {
+      // the click is just a normal user interaction with the website
       clickListenerHandler(target);
     }
   });
 });
 
-const onBeforeUnload = (e: BeforeUnloadEvent) => {
-  // Cancel the event
-  e.preventDefault();
-  // unfortunately, we cannot set custom message to the dialog, as is it considered dangerous
-}
-
-chrome.storage.onChanged.addListener(() => {
-  getMarkFinalState().then(markFinalState => {
-    if (markFinalState) {
-      // try to keep the user on the page, so the selected element is visible
-      window.addEventListener('beforeunload', onBeforeUnload);
-    } else {
-      window.removeEventListener('beforeunload', onBeforeUnload);
-    }
-  });
-})
-
-chrome.runtime.onMessage.addListener((xPath: string) => {
-  const target = (document.evaluate(xPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue as HTMLElement);
-  target.style.outline = "";
-})
-
 const markFinalStateHandler = (target: HTMLElement) => {
   console.log("Final state detected:", target);
-  const xPath: string = getElementsXpath(target);
-  console.log("XPATH:", xPath);
+
+  if(isLabelFor(target)){
+    // When we click on label, two click events are fired:
+    // - first event for the label,
+    // - second event for the input that the label is for (label's attribute 'for').
+    // Therefore if the label has the 'for' attribute specified, we do not use it.
+    // (if we assume that labels do not change and are generally not used for recognizing final state)
+    return;
+  }
 
   // enable normal click listener
   saveMarkFinalState(false);
 
+  // extract all attributes
   const attributes: ObjectKeys = {};
   for (let attribute of Array.from(target.attributes)) {
-    if (attribute.name === 'style') {
+    if (attribute.name === 'style') { // the style attribute is extracted below
       continue;
     }
     attributes[attribute.name] = attribute.value;
   }
 
+  // extract all explicitly set styles
   const styles: ObjectKeys = {};
   for (let attribute of Object.keys(target.style)) {
     const value = target.style.getPropertyValue(attribute);
-    if (value !== '') {
+    if (value !== '') { // only styles, whose value is not empty
       styles[attribute] = target.style.getPropertyValue(attribute);
     }
   }
 
+  // extract all computed styles
   const computedStyles: ObjectKeys = {};
   for (let entry of Object.entries(getComputedStyle(target))) {
-    if (entry[1] !== '' && !isNumeric(entry[0])) { // only styles that has some value and those, whose key is not numeric
+    if (entry[1] !== '' && !isNumeric(entry[0])) { // only styles that have some value and those, whose key is not numeric
       computedStyles[entry[0]] = entry[1];
     }
   }
 
+  // extract the text of the element
   let text: string;
   if (target instanceof HTMLInputElement) {
     text = (target as HTMLInputElement).value;
@@ -91,6 +72,8 @@ const markFinalStateHandler = (target: HTMLElement) => {
     text = target.innerText;
   }
 
+  const xPath: string = getElementsXpath(target);
+
   const finalStateTarget: FinalStateTarget = {
     tagName: target.tagName,
     text: text,
@@ -108,7 +91,7 @@ const markFinalStateHandler = (target: HTMLElement) => {
   chrome.runtime.sendMessage(message);
 
   // highlight the element on the page
-  target.style.outline = "5px solid rgba(218,172,0,0.8)";
+  highlightElementByXpath(xPath);
 }
 
 const clickListenerHandler = (target: HTMLElement) => {
@@ -127,6 +110,27 @@ const clickListenerHandler = (target: HTMLElement) => {
   chrome.runtime.sendMessage({action: action});
 }
 
+// when we listen for click that marks final state, we want to prevent
+// user from redirecting to another page if he marks for example <a> as final state
+chrome.storage.onChanged.addListener(() => {
+  getMarkFinalState().then(markFinalState => {
+    if (markFinalState) {
+      // try to keep the user on the page, so the selected element is visible
+      window.addEventListener('beforeunload', onBeforeUnload);
+    } else {
+      // remove the dialog
+      window.removeEventListener('beforeunload', onBeforeUnload);
+    }
+  });
+});
+
+const onBeforeUnload = (e: BeforeUnloadEvent) => {
+  // Cancel the event
+  e.preventDefault();
+  // unfortunately, we cannot set custom message to the dialog, as is it considered dangerous
+  // -> we need to emphasize this in user docs
+}
+
 const isNumeric = (value: string) => {
   return /^-?\d+$/.test(value);
 }
\ No newline at end of file
-- 
GitLab


From cb6dc0b29d5201ff4efe1d3550d59b0e8ad4bbd5 Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Wed, 3 Apr 2024 18:54:36 +0200
Subject: [PATCH 3/9] =?UTF-8?q?Re=20#11122=20-=20inicializace=20v=C4=9Btve?=
 =?UTF-8?q?,=20p=C5=99esunut=C3=AD=20v=C4=9Bc=C3=AD=20z=20final=20state=20?=
 =?UTF-8?q?z=20app.module=20do=20nov=C3=A9=20final-state=20componenty?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/app/app.component.html                    |  59 --------
 src/app/app.component.ts                      |  75 ----------
 src/app/app.module.ts                         |   4 +-
 .../final-stage/final-stage.component.css     |   0
 .../final-stage/final-stage.component.html    |  60 ++++++++
 .../final-stage/final-stage.component.spec.ts |  23 +++
 .../final-stage/final-stage.component.ts      | 135 ++++++++++++++++++
 7 files changed, 221 insertions(+), 135 deletions(-)
 create mode 100644 src/app/view/pages/final-stage/final-stage.component.css
 create mode 100644 src/app/view/pages/final-stage/final-stage.component.html
 create mode 100644 src/app/view/pages/final-stage/final-stage.component.spec.ts
 create mode 100644 src/app/view/pages/final-stage/final-stage.component.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 2631a37..73c12c5 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,63 +1,4 @@
 <div id="scenario-tracker-content">
-  <!-- START: MARK FINAL STATE -->
-  <ng-container *ngIf="finalStateTarget">
-    <h5>Text:</h5>
-    <p>{{finalStateTarget.text}}</p>
-    <h5>Attributes:</h5>
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <input type="checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
-      {{attribute}} = {{finalStateTarget.attributes[attribute]}}
-    </div>
-    <h5>Explicitly set styles:</h5>
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
-      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
-      style.{{attribute}} = {{finalStateTarget.styles[attribute]}}
-    </div>
-    <!-- TODO: It is now commented out just because the output is too long, but we should show it to the user -->
-<!--    <h5>Computed styles:</h5>-->
-<!--    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">-->
-<!--      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">-->
-<!--      style.{{attribute}} = {{finalStateTarget.computedStyles[attribute]}}-->
-<!--    </div>-->
-    <custom-button
-            buttonText="Cancel"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-trash"
-            additionalClasses="warning-button"
-            (click)="deleteFinalStateTarget()"
-    >
-    </custom-button>
-    <custom-button
-            buttonText="Add"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-plus"
-            additionalClasses="success-button"
-            (click)="addFinalState()"
-      >
-    </custom-button>
-  </ng-container>
-
-  <h5>Final states:</h5>
-  <div *ngFor="let finalState of finalStates; let i = index">
-    <b>{{finalState.element.tagName}}</b>
-    <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
-      <!-- show only first 3 -->
-      <p *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
-    </div>
-    <custom-button
-            buttonText="Remove"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-trash"
-            additionalClasses="delete-button"
-            (click)="deleteFinalState(i)"
-    >
-    </custom-button>
-  </div>
-  <!-- END: MARK FINAL STATE -->
-
   <overview
     [deleteActionHandler]="deleteAction"
     [toggleRecordingHandler]="toggleRecording"
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 7bf70a9..6c128f8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -22,84 +22,16 @@ import {HtmlElement} from "./model/html-element.interface";
 export class AppComponent implements OnInit {
   actions: Action[] = [];
   recording: boolean = false;
-  // START: MARK FINAL STATE
-  finalStates: Result[] = [];
-  finalStateTarget?: FinalStateTarget = undefined;
-  savedAttributes: ObjectKeys = {};
-  protected readonly Object = Object; // just for the template
-  // END: MARK FINAL STATE
 
   constructor(private ref: ChangeDetectorRef){}
 
   ngOnInit(): void {
-    // START: MARK FINAL STATE
-    chrome.runtime.onMessage.addListener((message: Message) => {
-      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
-        console.log("Final state target:", message)
-        this.finalStateTarget = message.finalStateClickTarget;
-        this.savedAttributes = {};
-        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
-      }
-    });
-    // END: MARK FINAL STATE
     chrome.storage.onChanged.addListener(() => {
       this.getData();
     });
     this.getData();
   }
 
-  // START: MARK FINAL STATE
-  /**
-   * Deletes the clicked final state target and sets all variables to default values.
-   */
-  public deleteFinalStateTarget(){
-    const xPath = this.finalStateTarget!.xPath;
-    this.finalStateTarget = undefined;
-    this.savedAttributes = {};
-
-    this.ref.detectChanges();
-    const message: Message = {
-      xPath: xPath
-    }
-    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
-      chrome.tabs.sendMessage(tabs[0].id!, message);
-    })
-  }
-
-  public saveAttribute(event: Event, attribute: string, value: string){
-    if((event.target as HTMLInputElement).checked){
-      console.log(`Saving attribute ${attribute}=${value}`)
-      this.savedAttributes[attribute] = value;
-    } else {
-      console.log(`Deleting attribute ${attribute}=${value}`)
-      delete this.savedAttributes[attribute];
-    }
-    console.log(this.savedAttributes)
-  }
-
-  public addFinalState(){
-    const element: HtmlElement = {
-      tagName: this.finalStateTarget!.tagName,
-      globalAttributes: {},
-      elementAttributes: {},
-      xPath: this.finalStateTarget!.xPath
-    };
-
-    const finalState: Result = {
-      element: element,
-      selectedAttributes: this.savedAttributes
-    };
-    this.finalStates.push(finalState);
-    saveFinalStates(this.finalStates);
-    this.deleteFinalStateTarget();
-  }
-
-  public deleteFinalState = (index: number) => {
-    this.finalStates.splice(index, 1);
-    saveFinalStates(this.finalStates);
-  }
-  // END: MARK FINAL STATE
-
   /**
    * Loads all data needed by the component.
    */
@@ -110,11 +42,6 @@ export class AppComponent implements OnInit {
     await getRecording().then(recording => {
       this.recording = recording;
     });
-    // START: MARK FINAL STATE
-    await getFinalStates().then(finalStates => {
-      this.finalStates = finalStates;
-    });
-    // END: MARK FINAL STATE
     this.ref.detectChanges();
     // we need to automatically detect changes, because chrome.storage.onChanged.addListener
     // is outside of the Angular scope
@@ -138,9 +65,7 @@ export class AppComponent implements OnInit {
     saveActions(this.actions);
   }
 
-  // START: MARK FINAL STATE
   public markFinalState = () => {
     saveMarkFinalState(true);
   }
-  // END: MARK FINAL STATE
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 83d6fd9..d74c2da 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -8,6 +8,7 @@ import { TimeDeltaComponent } from './view/components/time-delta/time-delta.comp
 import { ActionTypeComponent } from './view/components/action-type/action-type.component';
 import { HumanDescComponent } from './view/components/human-desc/human-desc.component';
 import { TechnicalDescComponent } from './view/components/technical-desc/technical-desc.component';
+import { FinalStageComponent } from './view/pages/final-stage/final-stage.component';
 
 @NgModule({
   declarations: [
@@ -17,7 +18,8 @@ import { TechnicalDescComponent } from './view/components/technical-desc/technic
     TimeDeltaComponent,
     ActionTypeComponent,
     HumanDescComponent,
-    TechnicalDescComponent
+    TechnicalDescComponent,
+    FinalStageComponent,
   ],
   imports: [
     BrowserModule
diff --git a/src/app/view/pages/final-stage/final-stage.component.css b/src/app/view/pages/final-stage/final-stage.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
new file mode 100644
index 0000000..f9ae98b
--- /dev/null
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -0,0 +1,60 @@
+<div id="scenario-tracker-content">
+  <!-- START: MARK FINAL STATE -->
+  <ng-container *ngIf="finalStateTarget">
+    <h5>Text:</h5>
+    <p>{{finalStateTarget.text}}</p>
+    <h5>Attributes:</h5>
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
+      <input type="checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+      {{attribute}} = {{finalStateTarget.attributes[attribute]}}
+    </div>
+    <h5>Explicitly set styles:</h5>
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
+      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+      style.{{attribute}} = {{finalStateTarget.styles[attribute]}}
+    </div>
+    <!-- TODO: It is now commented out just because the output is too long, but we should show it to the user -->
+<!--    <h5>Computed styles:</h5>-->
+<!--    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">-->
+<!--      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">-->
+<!--      style.{{attribute}} = {{finalStateTarget.computedStyles[attribute]}}-->
+<!--    </div>-->
+    <custom-button
+            buttonText="Cancel"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-trash"
+            additionalClasses="warning-button"
+            (click)="deleteFinalStateTarget()"
+    >
+    </custom-button>
+    <custom-button
+            buttonText="Add"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-plus"
+            additionalClasses="success-button"
+            (click)="addFinalState()"
+      >
+    </custom-button>
+  </ng-container>
+
+  <h5>Final states:</h5>
+  <div *ngFor="let finalState of finalStates; let i = index">
+    <b>{{finalState.element.tagName}}</b>
+    <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
+      <!-- show only first 3 -->
+      <p *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
+    </div>
+    <custom-button
+            buttonText="Remove"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-trash"
+            additionalClasses="delete-button"
+            (click)="deleteFinalState(i)"
+    >
+    </custom-button>
+  </div>
+  <!-- END: MARK FINAL STATE -->
+</div>
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.spec.ts b/src/app/view/pages/final-stage/final-stage.component.spec.ts
new file mode 100644
index 0000000..0c969af
--- /dev/null
+++ b/src/app/view/pages/final-stage/final-stage.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FinalStageComponent } from './final-stage.component';
+
+describe('FinalStageComponent', () => {
+  let component: FinalStageComponent;
+  let fixture: ComponentFixture<FinalStageComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [FinalStageComponent]
+    })
+    .compileComponents();
+    
+    fixture = TestBed.createComponent(FinalStageComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
new file mode 100644
index 0000000..4e606c3
--- /dev/null
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -0,0 +1,135 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Action } from 'src/app/model/action.interface';
+import { ActionEnum } from 'src/app/model/action.enum';
+
+import { Message } from 'src/app/model/message.interface';
+
+import { Result } from 'src/app/model/result.interface';
+import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
+import { ObjectKeys } from 'src/app/model/object-keys.interface';
+import { HtmlElement } from 'src/app/model/html-element.interface';
+
+
+import { saveFinalStates } from 'src/app/utils/storage.util';
+import { getFinalStates } from 'src/app/utils/storage.util';
+
+import { saveMarkFinalState } from 'src/app/utils/storage.util';
+import { ChangeDetectorRef } from '@angular/core';
+
+
+@Component({
+  selector: 'final-stage',
+  templateUrl: './final-stage.component.html',
+  styleUrls: ['./final-stage.component.css']
+})
+
+export class FinalStageComponent implements OnInit {
+
+    // START: MARK FINAL STATE
+    finalStates: Result[] = [];
+    finalStateTarget?: FinalStateTarget = undefined;
+    savedAttributes: ObjectKeys = {};
+    protected readonly Object = Object; // just for the template
+    // END: MARK FINAL STATE
+
+  constructor(private ref: ChangeDetectorRef){}  
+
+  ngOnInit(): void {
+     // START: MARK FINAL STATE
+    chrome.runtime.onMessage.addListener((message: Message) => {
+      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
+         console.log("Final state target:", message)
+        this.finalStateTarget = message.finalStateClickTarget;
+        this.savedAttributes = {};
+        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
+      }
+    });
+    // END: MARK FINAL STATE
+    /*
+    chrome.storage.onChanged.addListener(() => {
+      this.getData();
+    });
+    this.getData();
+    */
+  }
+
+  // START: MARK FINAL STATE
+  /**
+   * Deletes the clicked final state target and sets all variables to default values.
+   */
+  public deleteFinalStateTarget(){
+    const xPath = this.finalStateTarget!.xPath;
+    this.finalStateTarget = undefined;
+    this.savedAttributes = {};
+
+    this.ref.detectChanges();
+    const message: Message = {
+      xPath: xPath
+    }
+    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
+      chrome.tabs.sendMessage(tabs[0].id!, message);
+    })
+  }
+
+  public saveAttribute(event: Event, attribute: string, value: string){
+    if((event.target as HTMLInputElement).checked){
+      console.log(`Saving attribute ${attribute}=${value}`)
+      this.savedAttributes[attribute] = value;
+    } else {
+      console.log(`Deleting attribute ${attribute}=${value}`)
+      delete this.savedAttributes[attribute];
+    }
+    console.log(this.savedAttributes)
+  }
+
+  public addFinalState(){
+    const element: HtmlElement = {
+      tagName: this.finalStateTarget!.tagName,
+      globalAttributes: {},
+      elementAttributes: {},
+      xPath: this.finalStateTarget!.xPath
+    };
+
+    const finalState: Result = {
+      element: element,
+      selectedAttributes: this.savedAttributes
+    };
+    this.finalStates.push(finalState);
+    saveFinalStates(this.finalStates);
+    this.deleteFinalStateTarget();
+  }
+
+  public deleteFinalState = (index: number) => {
+    this.finalStates.splice(index, 1);
+    saveFinalStates(this.finalStates);
+  }
+  // END: MARK FINAL STATE
+
+    /**
+   * Loads all data needed by the component.
+   */
+  private async getData() {
+    /*
+    await getActions().then(actions => {
+      this.actions = actions;
+    });
+    await getRecording().then(recording => {
+      this.recording = recording;
+    });
+    */
+    // START: MARK FINAL STATE
+    await getFinalStates().then(finalStates => {
+      this.finalStates = finalStates;
+    });
+    // END: MARK FINAL STATE
+    this.ref.detectChanges();
+    // we need to automatically detect changes, because chrome.storage.onChanged.addListener
+    // is outside of the Angular scope
+  }
+
+    // START: MARK FINAL STATE
+    public markFinalState = () => {
+      saveMarkFinalState(true);
+    }
+    // END: MARK FINAL STATE
+}
-- 
GitLab


From 7ae28533dbb29d1b6a5684f3963981245726fbec Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Wed, 3 Apr 2024 23:43:50 +0200
Subject: [PATCH 4/9] =?UTF-8?q?Re=2011122=20-=20p=C5=99euspo=C5=99=C3=A1d?=
 =?UTF-8?q?=C3=A1n=C3=AD=20projektu,=20za=C5=99=C3=ADzen=20p=C5=99echod=20?=
 =?UTF-8?q?stav=C5=AF=20mezi=20overview=20a=20final=20state?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/app/app.component.html                    |  17 ++-
 src/app/app.component.ts                      |  87 ++++++++++++
 .../final-stage/final-stage.component.html    |  19 ++-
 .../final-stage/final-stage.component.ts      | 127 ++----------------
 .../view/pages/overview/overview.component.ts |   2 +-
 5 files changed, 133 insertions(+), 119 deletions(-)

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 73c12c5..6c6dc53 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,5 +1,8 @@
+
+
 <div id="scenario-tracker-content">
-  <overview
+ 
+  <overview *ngIf="showOverview"
     [deleteActionHandler]="deleteAction"
     [toggleRecordingHandler]="toggleRecording"
     [deleteAllActionsHandler]="deleteAllActions"
@@ -8,4 +11,16 @@
     [recording]="recording"
   >
   </overview>
+
+  <final-stage *ngIf="!showOverview"
+               [actions]="actions"
+               
+               [finalStateTarget] = "finalStateTarget"
+               [savedAttributes]="savedAttributes"
+               [saveAttribute]="saveAttribute"
+               [deleteFinalStateTarget] = "deleteFinalStateTarget"
+               [addFinalState] = "addFinalState"
+               [deleteFinalState] = "deleteFinalState"
+               [unMarkFinalState]="unMarkFinalState"
+               ></final-stage>
 </div>
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 6c128f8..3a4595f 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -22,16 +22,86 @@ import {HtmlElement} from "./model/html-element.interface";
 export class AppComponent implements OnInit {
   actions: Action[] = [];
   recording: boolean = false;
+  showOverview: boolean = true;
+
+  // START: MARK FINAL STATE
+  finalStates: Result[] = [];
+  finalStateTarget?: FinalStateTarget = undefined;
+  savedAttributes: ObjectKeys = {};
+  //protected readonly Object = Object; // just for the template
+  // END: MARK FINAL STATE
 
   constructor(private ref: ChangeDetectorRef){}
 
   ngOnInit(): void {
+    // START: MARK FINAL STATE
+    chrome.runtime.onMessage.addListener((message: Message) => {
+      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
+        console.log("Final state target:", message)
+        this.finalStateTarget = message.finalStateClickTarget;
+        this.savedAttributes = {};
+        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
+      }
+    });
+    // END: MARK FINAL STATE
     chrome.storage.onChanged.addListener(() => {
       this.getData();
     });
     this.getData();
   }
 
+  // START: MARK FINAL STATE
+  /**
+   * Deletes the clicked final state target and sets all variables to default values.
+   */
+  public deleteFinalStateTarget(){
+    const xPath = this.finalStateTarget!.xPath;
+    this.finalStateTarget = undefined;
+    this.savedAttributes = {};
+
+    this.ref.detectChanges();
+    const message: Message = {
+      xPath: xPath
+    }
+    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
+      chrome.tabs.sendMessage(tabs[0].id!, message);
+    })
+  }
+
+  public saveAttribute(event: Event, attribute: string, value: string){
+    if((event.target as HTMLInputElement).checked){
+      console.log(`Saving attribute ${attribute}=${value}`)
+      this.savedAttributes[attribute] = value;
+    } else {
+      console.log(`Deleting attribute ${attribute}=${value}`)
+      delete this.savedAttributes[attribute];
+    }
+    console.log(this.savedAttributes)
+  }
+
+  public addFinalState(){
+    const element: HtmlElement = {
+      tagName: this.finalStateTarget!.tagName,
+      globalAttributes: {},
+      elementAttributes: {},
+      xPath: this.finalStateTarget!.xPath
+    };
+
+    const finalState: Result = {
+      element: element,
+      selectedAttributes: this.savedAttributes
+    };
+    this.finalStates.push(finalState);
+    saveFinalStates(this.finalStates);
+    this.deleteFinalStateTarget();
+  }
+
+  public deleteFinalState = (index: number) => {
+    this.finalStates.splice(index, 1);
+    saveFinalStates(this.finalStates);
+  }
+  // END: MARK FINAL STATE
+
   /**
    * Loads all data needed by the component.
    */
@@ -42,6 +112,11 @@ export class AppComponent implements OnInit {
     await getRecording().then(recording => {
       this.recording = recording;
     });
+    // START: MARK FINAL STATE
+    await getFinalStates().then(finalStates => {
+      this.finalStates = finalStates;
+    });
+    // END: MARK FINAL STATE
     this.ref.detectChanges();
     // we need to automatically detect changes, because chrome.storage.onChanged.addListener
     // is outside of the Angular scope
@@ -65,7 +140,19 @@ export class AppComponent implements OnInit {
     saveActions(this.actions);
   }
 
+  // START: MARK FINAL STATE
   public markFinalState = () => {
     saveMarkFinalState(true);
+    this.toggleView();
+  }
+
+  public unMarkFinalState = () => {
+    saveMarkFinalState(false);
+    this.toggleView();
+  }
+  // END: MARK FINAL STATE
+
+  toggleView() {
+    this.showOverview = !this.showOverview;
   }
 }
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index f9ae98b..5151bfb 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -1,4 +1,4 @@
-<div id="scenario-tracker-content">
+<!-- <div id="scenario-tracker-content"> -->
   <!-- START: MARK FINAL STATE -->
   <ng-container *ngIf="finalStateTarget">
     <h5>Text:</h5>
@@ -56,5 +56,20 @@
     >
     </custom-button>
   </div>
+
+  <h5>Unmark </h5>
+  <custom-button
+            buttonText="Return "
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-arrow-left"
+            additionalClasses="delete-button"
+            (click)="unMarkFinalState()"
+    >
+    </custom-button>
+
   <!-- END: MARK FINAL STATE -->
-</div>
\ No newline at end of file
+  <!--
+</div>
+
+-->
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index 4e606c3..c4bfcd5 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -1,20 +1,8 @@
 import { Component, OnInit, Input } from '@angular/core';
 import { Action } from 'src/app/model/action.interface';
-import { ActionEnum } from 'src/app/model/action.enum';
-
-import { Message } from 'src/app/model/message.interface';
-
 import { Result } from 'src/app/model/result.interface';
 import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
 import { ObjectKeys } from 'src/app/model/object-keys.interface';
-import { HtmlElement } from 'src/app/model/html-element.interface';
-
-
-import { saveFinalStates } from 'src/app/utils/storage.util';
-import { getFinalStates } from 'src/app/utils/storage.util';
-
-import { saveMarkFinalState } from 'src/app/utils/storage.util';
-import { ChangeDetectorRef } from '@angular/core';
 
 
 @Component({
@@ -24,112 +12,21 @@ import { ChangeDetectorRef } from '@angular/core';
 })
 
 export class FinalStageComponent implements OnInit {
+    @Input() actions: Action[] = [];
+    @Input() finalStates: Result[] = [];
+    @Input() finalStateTarget?: FinalStateTarget = undefined;
+    @Input() savedAttributes: ObjectKeys = {};
 
-    // START: MARK FINAL STATE
-    finalStates: Result[] = [];
-    finalStateTarget?: FinalStateTarget = undefined;
-    savedAttributes: ObjectKeys = {};
-    protected readonly Object = Object; // just for the template
-    // END: MARK FINAL STATE
-
-  constructor(private ref: ChangeDetectorRef){}  
-
-  ngOnInit(): void {
-     // START: MARK FINAL STATE
-    chrome.runtime.onMessage.addListener((message: Message) => {
-      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
-         console.log("Final state target:", message)
-        this.finalStateTarget = message.finalStateClickTarget;
-        this.savedAttributes = {};
-        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
-      }
-    });
-    // END: MARK FINAL STATE
-    /*
-    chrome.storage.onChanged.addListener(() => {
-      this.getData();
-    });
-    this.getData();
-    */
-  }
 
-  // START: MARK FINAL STATE
-  /**
-   * Deletes the clicked final state target and sets all variables to default values.
-   */
-  public deleteFinalStateTarget(){
-    const xPath = this.finalStateTarget!.xPath;
-    this.finalStateTarget = undefined;
-    this.savedAttributes = {};
+    @Input() saveAttribute!: Function;
+    @Input() deleteFinalStateTarget!: Function;
+    @Input() addFinalState!: Function;
+    @Input() deleteFinalState!: Function;
 
-    this.ref.detectChanges();
-    const message: Message = {
-      xPath: xPath
-    }
-    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
-      chrome.tabs.sendMessage(tabs[0].id!, message);
-    })
-  }
-
-  public saveAttribute(event: Event, attribute: string, value: string){
-    if((event.target as HTMLInputElement).checked){
-      console.log(`Saving attribute ${attribute}=${value}`)
-      this.savedAttributes[attribute] = value;
-    } else {
-      console.log(`Deleting attribute ${attribute}=${value}`)
-      delete this.savedAttributes[attribute];
-    }
-    console.log(this.savedAttributes)
-  }
-
-  public addFinalState(){
-    const element: HtmlElement = {
-      tagName: this.finalStateTarget!.tagName,
-      globalAttributes: {},
-      elementAttributes: {},
-      xPath: this.finalStateTarget!.xPath
-    };
+    @Input() unMarkFinalState!: Function;
 
-    const finalState: Result = {
-      element: element,
-      selectedAttributes: this.savedAttributes
-    };
-    this.finalStates.push(finalState);
-    saveFinalStates(this.finalStates);
-    this.deleteFinalStateTarget();
-  }
-
-  public deleteFinalState = (index: number) => {
-    this.finalStates.splice(index, 1);
-    saveFinalStates(this.finalStates);
-  }
-  // END: MARK FINAL STATE
-
-    /**
-   * Loads all data needed by the component.
-   */
-  private async getData() {
-    /*
-    await getActions().then(actions => {
-      this.actions = actions;
-    });
-    await getRecording().then(recording => {
-      this.recording = recording;
-    });
-    */
-    // START: MARK FINAL STATE
-    await getFinalStates().then(finalStates => {
-      this.finalStates = finalStates;
-    });
-    // END: MARK FINAL STATE
-    this.ref.detectChanges();
-    // we need to automatically detect changes, because chrome.storage.onChanged.addListener
-    // is outside of the Angular scope
-  }
-
-    // START: MARK FINAL STATE
-    public markFinalState = () => {
-      saveMarkFinalState(true);
+    protected readonly Object = Object; // just for the template
+    ngOnInit(): void {
+    
     }
-    // END: MARK FINAL STATE
 }
diff --git a/src/app/view/pages/overview/overview.component.ts b/src/app/view/pages/overview/overview.component.ts
index b0d0be5..11c7158 100644
--- a/src/app/view/pages/overview/overview.component.ts
+++ b/src/app/view/pages/overview/overview.component.ts
@@ -20,7 +20,7 @@ export class OverviewComponent implements OnInit {
   // END: MARK FINAL STATE
 
   readonly ActionEnum = ActionEnum
-
+  
   constructor() { }
 
   ngOnInit(): void {
-- 
GitLab


From 36aba3c4d7c666f01141b1fa891c6fa8f06ab747 Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Thu, 4 Apr 2024 22:02:55 +0200
Subject: [PATCH 5/9] =?UTF-8?q?Re=2011122=20-=20p=C5=99id=C3=A1n=C3=AD=20o?=
 =?UTF-8?q?bsahu=20v=C4=9Btve=2011115=5Fkomponenta=5Fcela=5Fakce=20(=C3=BA?=
 =?UTF-8?q?kol=201115)=20=09=09-=20pro=20p=C5=99=C3=ADpadn=C3=A9=20sjednoc?=
 =?UTF-8?q?en=C3=AD=20styl=C5=AF,=20zabr=C3=A1n=C4=9Bn=C3=AD=20pot=C3=AD?=
 =?UTF-8?q?=C5=BE=C3=AD=20p=C5=99i=20mergnut=C3=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

	- vytvoření samostatné componenty pro final-stage
	- zajištění přepínání mezi overview a final-stage
	- nastylování final-stage podle návrhu ve figmě
	- k dispozici zakomentovaný ukázkový FinalStageTarget
	- vytvořeno pole se seznamem povinných atributů - dočasné řešení
---
 src/app/app.component.css                     |   3 +
 src/app/app.component.html                    |  25 ++--
 src/app/app.component.ts                      |   3 +
 src/app/app.module.ts                         |   2 +
 .../action-type/action-type.component.css     |   6 +-
 .../action-type/action-type.component.html    |   3 +-
 .../captured-action.component.css             |  92 +++++++++++++++
 .../captured-action.component.html            |  19 ++++
 .../captured-action.component.spec.ts         |  23 ++++
 .../captured-action.component.ts              |  31 +++++
 .../custom-button/custom-button.component.css |   7 ++
 .../final-stage/final-stage.component.css     |  90 +++++++++++++++
 .../final-stage/final-stage.component.html    | 107 ++++++++++++------
 .../final-stage/final-stage.component.ts      |  32 +++++-
 .../pages/overview/overview.component.html    |  34 +++---
 .../view/pages/overview/overview.component.ts |   8 ++
 16 files changed, 420 insertions(+), 65 deletions(-)
 create mode 100644 src/app/view/components/captured-action/captured-action.component.css
 create mode 100644 src/app/view/components/captured-action/captured-action.component.html
 create mode 100644 src/app/view/components/captured-action/captured-action.component.spec.ts
 create mode 100644 src/app/view/components/captured-action/captured-action.component.ts

diff --git a/src/app/app.component.css b/src/app/app.component.css
index e69de29..0583ba8 100644
--- a/src/app/app.component.css
+++ b/src/app/app.component.css
@@ -0,0 +1,3 @@
+.component {
+    width: 100%;
+}
\ No newline at end of file
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 6c6dc53..8171fee 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,6 +1,4 @@
-
-
-<div id="scenario-tracker-content">
+<div id="scenario-tracker-content" class="component">
  
   <overview *ngIf="showOverview"
     [deleteActionHandler]="deleteAction"
@@ -13,14 +11,15 @@
   </overview>
 
   <final-stage *ngIf="!showOverview"
-               [actions]="actions"
-               
-               [finalStateTarget] = "finalStateTarget"
-               [savedAttributes]="savedAttributes"
-               [saveAttribute]="saveAttribute"
-               [deleteFinalStateTarget] = "deleteFinalStateTarget"
-               [addFinalState] = "addFinalState"
-               [deleteFinalState] = "deleteFinalState"
-               [unMarkFinalState]="unMarkFinalState"
-               ></final-stage>
+    [actions]="actions"
+     
+    [finalStateTarget]="finalStateTarget"
+    [savedAttributes]="savedAttributes"
+    [saveAttribute]="saveAttribute"
+    [deleteFinalStateTarget] = "deleteFinalStateTarget"
+    [addFinalState] = "addFinalState"
+    [deleteFinalState] = "deleteFinalState"
+    [unMarkFinalState]="unMarkFinalState"
+  >
+  </final-stage>
 </div>
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 3a4595f..00770a8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -35,6 +35,8 @@ export class AppComponent implements OnInit {
 
   ngOnInit(): void {
     // START: MARK FINAL STATE
+    
+    console.log("App init")
     chrome.runtime.onMessage.addListener((message: Message) => {
       if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
         console.log("Final state target:", message)
@@ -43,6 +45,7 @@ export class AppComponent implements OnInit {
         this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
       }
     });
+    
     // END: MARK FINAL STATE
     chrome.storage.onChanged.addListener(() => {
       this.getData();
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d74c2da..6b7b19b 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -8,6 +8,7 @@ import { TimeDeltaComponent } from './view/components/time-delta/time-delta.comp
 import { ActionTypeComponent } from './view/components/action-type/action-type.component';
 import { HumanDescComponent } from './view/components/human-desc/human-desc.component';
 import { TechnicalDescComponent } from './view/components/technical-desc/technical-desc.component';
+import { CapturedActionComponent } from './view/components/captured-action/captured-action.component';
 import { FinalStageComponent } from './view/pages/final-stage/final-stage.component';
 
 @NgModule({
@@ -19,6 +20,7 @@ import { FinalStageComponent } from './view/pages/final-stage/final-stage.compon
     ActionTypeComponent,
     HumanDescComponent,
     TechnicalDescComponent,
+    CapturedActionComponent,
     FinalStageComponent,
   ],
   imports: [
diff --git a/src/app/view/components/action-type/action-type.component.css b/src/app/view/components/action-type/action-type.component.css
index 2c93e11..9eb106a 100644
--- a/src/app/view/components/action-type/action-type.component.css
+++ b/src/app/view/components/action-type/action-type.component.css
@@ -4,4 +4,8 @@
     padding: 4px 8px;  
     text-align: center;
     border-radius: 5px;
-  }
\ No newline at end of file
+  }
+
+.action-type::first-letter{
+  text-transform: uppercase;
+}  
\ No newline at end of file
diff --git a/src/app/view/components/action-type/action-type.component.html b/src/app/view/components/action-type/action-type.component.html
index 48e0ddd..b8c20d3 100644
--- a/src/app/view/components/action-type/action-type.component.html
+++ b/src/app/view/components/action-type/action-type.component.html
@@ -1,4 +1,3 @@
-<div class="action-type"
-    [style.backgroundColor] = "color">
+<div class="action-type" [style.backgroundColor] = "color">
     {{text}}
 </div>
\ No newline at end of file
diff --git a/src/app/view/components/captured-action/captured-action.component.css b/src/app/view/components/captured-action/captured-action.component.css
new file mode 100644
index 0000000..4fb67f1
--- /dev/null
+++ b/src/app/view/components/captured-action/captured-action.component.css
@@ -0,0 +1,92 @@
+.container {
+    display: flex;
+    flex-wrap: nowrap;
+    flex-direction: row;
+}
+
+.component {
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+.inner-component {
+    width: 100%;
+}
+
+#first-component {
+    padding: 5px;
+    display: flex;
+    align-items: center;
+    width: 25%;
+}
+
+#second-component {
+    padding: 5px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    width: 40%
+}
+
+#third-component {
+    padding: 5px;
+    display: flex;
+    align-items: center;
+    width: 10%;
+}
+
+#fourth-component {
+    padding: 5px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    width: 25%
+}
+
+.pl5 {
+    padding-left: 3px;
+}
+
+/* TEST */
+.flex-container {
+
+    display: flex;
+      
+    /* flex-flow: nowrap; */  
+    flex-wrap: nowrap; 
+    align-content: stretch;
+
+    background-color: #bbdefb;
+    height: 100%;
+    padding: 15px;
+    gap: 5px;
+
+  }
+
+  .flex-container > div{
+    background: #ffecb3;
+    border: 3px solid #ffcc80;
+    border-radius: 5px;
+    padding: 8px;
+  }
+
+
+  .item1 { 
+    /* flex:1 1 25%; */
+    flex-grow:1;
+    flex-shrink:1;
+    flex-basis:25%;
+  }
+			
+  .item2 { 
+    /* flex:0 1 50%; */
+    flex-shrink:1;
+    flex-basis:50%;
+  }
+			
+  .item3 { 
+    /* flex:0 1 25%; */
+    flex-basis:25%;
+    align-self:auto;
+  }
\ No newline at end of file
diff --git a/src/app/view/components/captured-action/captured-action.component.html b/src/app/view/components/captured-action/captured-action.component.html
new file mode 100644
index 0000000..f7d57e4
--- /dev/null
+++ b/src/app/view/components/captured-action/captured-action.component.html
@@ -0,0 +1,19 @@
+<div class="container">
+    <div id="first-component" class="component">
+        <action-type [text]="ActionEnum[action.type].toLowerCase()" [color]="getColor()" class="inner-component"></action-type>
+    </div>
+    <div id="second-component" class="component">
+        <technical-desc [action]="action" class="inner-component"></technical-desc>
+        <human-desc [action]="action" class="inner-component"></human-desc>
+    </div>
+    <div id="third-component" class="component">
+        <ng-container *ngIf="startTime">
+            <time-delta [actionTime]="action.recordedOn" [startTime]="startTime"></time-delta>
+        </ng-container>
+    </div>
+    <div id="fourth-component" class="component">
+        <custom-button (click)="deleteActionHandler(actionIndex)" backgroundColor="#A92D2D" icon="fa fa-trash" iconType="fontawesome" buttonText="" size="S" color="white"></custom-button>
+        <custom-button backgroundColor="#8F8F8F" icon="fa fa-eye" iconType="fontawesome" buttonText="" class="pl5" size="S" color="white"></custom-button>
+    </div>
+</div>
+<hr>
diff --git a/src/app/view/components/captured-action/captured-action.component.spec.ts b/src/app/view/components/captured-action/captured-action.component.spec.ts
new file mode 100644
index 0000000..5705bb3
--- /dev/null
+++ b/src/app/view/components/captured-action/captured-action.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CapturedActionComponent } from './captured-action.component';
+
+describe('CapturedActionComponent', () => {
+  let component: CapturedActionComponent;
+  let fixture: ComponentFixture<CapturedActionComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [CapturedActionComponent]
+    })
+    .compileComponents();
+    
+    fixture = TestBed.createComponent(CapturedActionComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/view/components/captured-action/captured-action.component.ts b/src/app/view/components/captured-action/captured-action.component.ts
new file mode 100644
index 0000000..6881e52
--- /dev/null
+++ b/src/app/view/components/captured-action/captured-action.component.ts
@@ -0,0 +1,31 @@
+import { Component, Input } from '@angular/core';
+import { Action } from 'src/app/model/action.interface';
+import { ActionEnum } from 'src/app/model/action.enum';
+
+@Component({
+  selector: 'captured-action',
+  templateUrl: './captured-action.component.html',
+  styleUrl: './captured-action.component.css'
+})
+export class CapturedActionComponent {
+  @Input() action!: Action;
+  @Input() deleteActionHandler!: Function;
+  @Input() startTime?: Date;
+  @Input() actionIndex!: number;
+
+  readonly ActionEnum = ActionEnum
+
+  getColor(): string {
+    switch(this.action.type) {
+      case ActionEnum.CLICK: return "#3C5C79"; //Blue
+      case ActionEnum.COPY: return "#823838"; //Red
+      case ActionEnum.PASTE: return "#795D3C"; //Brown
+      case ActionEnum.CUT: return "#743C79"; //Purple
+      case ActionEnum.KEYDOWN: return "#38825A"; //Green
+      case ActionEnum.RESIZE: return "#3C796F"; //Sea Blue
+      case ActionEnum.CHANGE: return "#BF9000"; //Golden
+      case ActionEnum.SELECT: return "#3F822B"; //Dark Green
+      default: return "";
+    }
+  }
+}
diff --git a/src/app/view/components/custom-button/custom-button.component.css b/src/app/view/components/custom-button/custom-button.component.css
index 556cff3..53fbb6c 100644
--- a/src/app/view/components/custom-button/custom-button.component.css
+++ b/src/app/view/components/custom-button/custom-button.component.css
@@ -104,6 +104,13 @@
     background-color: #f8f9fa;
 }
 
+.return-button {
+    /* Add your custom styles for the "Return" button here */
+    background-color: #007bff; /* Example background color */
+    color: white; /* Example text color */
+    /* Add any other styles you want */
+  }
+
 /* Possible alternatives */
 
 .alternative-button-1 {
diff --git a/src/app/view/pages/final-stage/final-stage.component.css b/src/app/view/pages/final-stage/final-stage.component.css
index e69de29..93c5be8 100644
--- a/src/app/view/pages/final-stage/final-stage.component.css
+++ b/src/app/view/pages/final-stage/final-stage.component.css
@@ -0,0 +1,90 @@
+.fs_container {
+    background-color: rgb(232, 232, 232);
+    margin: 10px auto; /* Adjust the margin to center horizontally */
+    padding: 10px; /* Optionally add padding */
+    /*display: flex; /* Use flexbox for layout */
+    /*justify-content: center; /* Center horizontally */
+    /*align-items: center; /* Center vertically */
+   
+}
+
+
+.custom-checkbox {
+    appearance: none;
+    width: 1.3em;
+    height: 1.3em;
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    vertical-align: middle;
+    cursor: pointer;
+    background-color: rgb(128, 128, 128);
+    padding: 10px; /* Remove padding */
+    display: flex; /* Use flexbox for centering */
+    justify-content: center; /* Center horizontally */
+    align-items: center; /* Center vertically */
+}
+
+.custom-checkbox:checked::after  {
+    content: '\f00c'; /* Font Awesome checkmark icon */
+    font-family: 'Font Awesome 5 Free'; /* Specify Font Awesome font family */
+    font-weight: 900; /* Adjust weight if necessary */
+    font-size: 1em;
+    color: white;
+}
+    
+.checkbox-container {
+    display: flex; /* Use flexbox for layout */
+    align-items: center; /* Align items vertically */
+    margin-bottom: 5px; /* Add margin between checkboxes */
+    flex-wrap: nowrap; /* Prevent wrapping of items */
+}
+
+.checkbox-container label {
+    margin-left: 5px; /* Add space between checkbox and text */
+}
+
+
+.button-container {
+    width: 100%; /* Ensures the container spans the full width */
+  }
+
+.return-fs-button {
+    width: 100%;
+    background-color: #ffffff; /* Button background color */
+    color: #808080; /* Grey text color */
+    border: 1px solid #808080; /* Grey border */
+    padding: 10px; /* Adjust padding as needed */
+    cursor: pointer;
+    box-sizing: border-box; /* Include padding and border in width */
+    text-decoration: none; /* Remove default link underline */
+    display: block; /* Ensures the button occupies the entire line */
+    text-align: right;
+}
+
+.return-fs-button:hover {
+    background-color: #f0f0f0; /* Lighter background color on hover */
+}
+
+.return-fs-button:active {
+    background-color: #d9d9d9; /* Darker background color when button is active */
+}  
+
+.attribute-name {
+    text-transform: capitalize;
+}
+
+.attribute-value {
+    color: #333333; /* Darker grey color */
+    text-decoration: underline; /* Underline */
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
+    margin-left: 5px;
+}
+
+.mandatory-attribute-name {
+    text-transform: lowercase;
+    color:  #333;
+}
+
+.mandatory-attribute-value {
+    color:  #333;
+}
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index 5151bfb..f0ad702 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -1,30 +1,81 @@
+<div class="button-container">
+
+<custom-button
+            buttonText="Not a final state, go back"
+            size="S"
+            iconType="fontawesome"
+            icon="fa fa-arrow-left"
+            additionalClasses="return-fs-button"
+            (click)="unMarkFinalState()"
+    >
+    </custom-button>
+</div>
+<h3>How to recognize final state?</h3>
 <!-- <div id="scenario-tracker-content"> -->
   <!-- START: MARK FINAL STATE -->
-  <ng-container *ngIf="finalStateTarget">
-    <h5>Text:</h5>
-    <p>{{finalStateTarget.text}}</p>
-    <h5>Attributes:</h5>
+  <ng-container *ngIf="finalStateTarget">  
+    <div class="fs_container">
+    <h3> Selected element: {{finalStateTarget.tagName}} </h3>
+
+    <!-- not chooseable attributes - default -->
     <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <input type="checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
-      {{attribute}} = {{finalStateTarget.attributes[attribute]}}
+      <ng-container *ngIf="mandatoryAttributes.includes(attribute)">
+        <div class="checkbox-container">
+          <input type="checkbox" class="custom-checkbox" checked style="display: none;" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+          <label>
+            <span class="mandatory-attribute-name"> {{attribute}}: </span>
+            <span class="mandatory-attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+          </label>
+        </div>
+      </ng-container>
+    </div>  
+
+    <ng-container *ngIf="finalStateTarget.text">
+      <h5>Text:</h5>
+      <p>{{finalStateTarget.text}}</p>
+    </ng-container>
+    <h4>Select characteristic properties</h4>
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
+      <ng-container *ngIf="!mandatoryAttributes.includes(attribute)">
+        <div class="checkbox-container">
+          <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+          <label>
+            <span class="attribute-name"> {{attribute}}: </span>
+            <span class="attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+          </label>
+        </div>
+      </ng-container>
     </div>
-    <h5>Explicitly set styles:</h5>
+    <h5>Styles</h5>
+    <!-- <h5>Explicitly set styles:</h5> -->
     <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
-      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
-      style.{{attribute}} = {{finalStateTarget.styles[attribute]}}
+      <div class="checkbox-container">
+        <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+        <label>
+          <span class="attribute-name"> {{attribute}}: </span>
+          <span class="attribute-value">{{finalStateTarget.styles[attribute]}} </span> 
+       </label>
+      </div>
+    </div>
+    <!-- <h5>Computed styles:</h5> -->
+    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">
+      <div class="checkbox-container">
+      <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+        <label>
+          <span class="attribute-name"> {{attribute}}: </span>
+          <span class="attribute-value">{{finalStateTarget.computedStyles[attribute]}} </span> 
+       </label>
+       </div>
     </div>
-    <!-- TODO: It is now commented out just because the output is too long, but we should show it to the user -->
-<!--    <h5>Computed styles:</h5>-->
-<!--    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">-->
-<!--      <input type="checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">-->
-<!--      style.{{attribute}} = {{finalStateTarget.computedStyles[attribute]}}-->
-<!--    </div>-->
+
+    <span style="margin-top: 1em;">
     <custom-button
             buttonText="Cancel"
             size="S"
             iconType="fontawesome"
             icon="fa fa-trash"
-            additionalClasses="warning-button"
+            additionalClasses="delete-button"
+            additionalStyles="width: 10em;"
             (click)="deleteFinalStateTarget()"
     >
     </custom-button>
@@ -33,21 +84,24 @@
             size="S"
             iconType="fontawesome"
             icon="fa fa-plus"
-            additionalClasses="success-button"
+            additionalClasses="success-button pl_fs"
+            additionalStyles="margin-left: 2em; width: 10em;"
             (click)="addFinalState()"
       >
     </custom-button>
+    </span>
+  </div>
   </ng-container>
 
-  <h5>Final states:</h5>
-  <div *ngFor="let finalState of finalStates; let i = index">
+  <h3>Final states:</h3>
+  <div *ngFor="let finalState of finalStates; let i = index" class="fs_container">
     <b>{{finalState.element.tagName}}</b>
     <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
       <!-- show only first 3 -->
       <p *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
     </div>
     <custom-button
-            buttonText="Remove"
+            buttonText=""
             size="S"
             iconType="fontawesome"
             icon="fa fa-trash"
@@ -55,18 +109,7 @@
             (click)="deleteFinalState(i)"
     >
     </custom-button>
-  </div>
-
-  <h5>Unmark </h5>
-  <custom-button
-            buttonText="Return "
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-arrow-left"
-            additionalClasses="delete-button"
-            (click)="unMarkFinalState()"
-    >
-    </custom-button>
+  </div>  
 
   <!-- END: MARK FINAL STATE -->
   <!--
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index c4bfcd5..3747622 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -4,6 +4,8 @@ import { Result } from 'src/app/model/result.interface';
 import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
 import { ObjectKeys } from 'src/app/model/object-keys.interface';
 
+import { Message } from 'src/app/model/message.interface';
+import { ChangeDetectorRef } from '@angular/core';
 
 @Component({
   selector: 'final-stage',
@@ -15,6 +17,8 @@ export class FinalStageComponent implements OnInit {
     @Input() actions: Action[] = [];
     @Input() finalStates: Result[] = [];
     @Input() finalStateTarget?: FinalStateTarget = undefined;
+    //finalStateTarget?: FinalStateTarget = undefined;  //used for testing view
+ 
     @Input() savedAttributes: ObjectKeys = {};
 
 
@@ -25,8 +29,34 @@ export class FinalStageComponent implements OnInit {
 
     @Input() unMarkFinalState!: Function;
 
+    // temporary solution - for future make better
+    mandatoryAttributes: string[] = ['id', 'class', 'name']
+
+    constructor(private ref: ChangeDetectorRef){}
+
     protected readonly Object = Object; // just for the template
     ngOnInit(): void {
-    
+       // Initialize finalStateTarget to the default value for testing purposes
+      /* 
+      this.finalStateTarget = {
+        tagName: 'div', // Example tag name
+        text: 'Assault rifle-wielding standard trooper', // Example text
+        xPath: '/HTML[1]/BODY[1]/DIV[1]', // Example XPath
+        attributes: {
+          dir: 'ltr', // Example attribute
+          army: 'Republic Clone Army', // Example additional attribute
+          id: 'random-id', // Random id
+         class: 'random-class' // Random class
+        },
+        styles: {
+          outline: 'rgba(218, 172, 0, 0.8) solid 5px', // Example style
+          color: 'blue' // Another example style
+        },
+        computedStyles: {
+          fontFamily: 'Arial', // Example computed style
+          fontSize: '16px' // Another example computed style
+        }
+      };
+      */
     }
 }
diff --git a/src/app/view/pages/overview/overview.component.html b/src/app/view/pages/overview/overview.component.html
index 34e0eaf..730799c 100644
--- a/src/app/view/pages/overview/overview.component.html
+++ b/src/app/view/pages/overview/overview.component.html
@@ -2,17 +2,6 @@
 <h1>Scenario Tracker</h1>
 
 <!-- Controls -->
-<!-- START: MARK FINAL STATE -->
-<custom-button
-        buttonText="Mark final state"
-        size="M"
-        iconType="fontawesome"
-        icon="fa fa-solid fa-flag-checkered"
-        additionalClasses="info-button"
-        (click)="markFinalStateHandler()">
-</custom-button>
-<!-- END: MARK FINAL STATE -->
-
 <div style="display: inline-block; width: 10px;"></div>
 <custom-button 
   buttonText="Delete all actions"
@@ -41,10 +30,9 @@
   additionalClasses="start-button"
   (click)="toggleRecordingHandler()" *ngIf="!recording"></custom-button>
 
-
 <!-- List of actions -->
-<div *ngFor="let action of actions; index as i">
-  <div *ngIf="action.documentAction" [ngSwitch]="action.type">
+<div *ngFor="let action of actions; index as i" class="action-block">
+  <!-- <div *ngIf="action.documentAction" [ngSwitch]="action.type">
     <h2 *ngSwitchCase="ActionEnum.CLICK">Clicked on {{action.documentAction.target.tagName}}</h2>
     <h2 *ngSwitchCase="ActionEnum.COPY">Copied '{{action.documentAction.text}}' from {{action.documentAction.target.tagName}}</h2>
     <h2 *ngSwitchCase="ActionEnum.CUT">Cut '{{action.documentAction.text}}' from {{action.documentAction.target.tagName}}</h2>
@@ -53,10 +41,24 @@
     <h2 *ngSwitchCase="ActionEnum.CHANGE">Input changed to {{action.documentAction.value}}</h2>
     <h2 *ngSwitchCase="ActionEnum.SELECT">Selected '{{action.documentAction.text}}' in {{action.documentAction.target.tagName}}</h2>
     <p>id = {{action.documentAction.target.globalAttributes['id']}}, class = {{action.documentAction.target.globalAttributes['class']}}</p>
+    <captured-action [action]="action"></captured-action>
   </div>
   <div *ngIf="action.windowAction" [ngSwitch]="action.type">
     <h2 *ngSwitchCase="ActionEnum.RESIZE">Window resized to {{action.windowAction.width}} x {{action.windowAction.height}}</h2>
   </div>
   <p>recorded at {{action.recordedOn | date:"dd.MM.yyyy hh:mm:ss"}}</p>
-  <button (click)="deleteActionHandler(i)">Delete action</button>
-</div>
\ No newline at end of file
+  <button (click)="deleteActionHandler(i)">Delete action</button> -->
+  <captured-action [action]="action" [deleteActionHandler]="deleteActionHandler" [startTime]="startTime" [actionIndex]="i"></captured-action>
+</div>
+
+<!-- START: MARK FINAL STATE -->
+<custom-button
+buttonText="Mark final state"
+size="M"
+iconType="fontawesome"
+icon="fa fa-solid fa-flag-checkered"
+additionalClasses="info-button"
+additionalStyles="margin-top: 2em; margin-left:1em;"
+(click)="markFinalStateHandler()">
+</custom-button>
+<!-- END: MARK FINAL STATE -->
\ No newline at end of file
diff --git a/src/app/view/pages/overview/overview.component.ts b/src/app/view/pages/overview/overview.component.ts
index 11c7158..e60396b 100644
--- a/src/app/view/pages/overview/overview.component.ts
+++ b/src/app/view/pages/overview/overview.component.ts
@@ -21,8 +21,16 @@ export class OverviewComponent implements OnInit {
 
   readonly ActionEnum = ActionEnum
   
+  startTime?: Date;
+
   constructor() { }
 
   ngOnInit(): void {
   }
+
+  setStartTime() {
+    if(this.startTime == null) {
+      this.startTime = new Date();
+    }
+  }
 }
-- 
GitLab


From 5e5e84ab5139a9838dd2b8cba4670ec4c8d96d28 Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Fri, 5 Apr 2024 20:44:22 +0200
Subject: [PATCH 6/9] Re #11121 + #111122
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- dle návrhu ve firmě vytvořena komponenta zobrazení cílového stavu
+ dodatečně částečně upravena komponenta označení cílového stavu

- k stylům custom-button přidáno edit button + XS velikost
- v app.component.html předávané funkci pojmenovány Handler (pro konzistenci)
- přidán seznam atributů, které se vždy zaznamenají
---
 src/app/app.component.html                    | 15 ++--
 src/app/app.component.ts                      | 33 ++++---
 src/app/app.module.ts                         |  2 +
 .../custom-button/custom-button.component.css | 22 +++--
 .../custom-button/custom-button.component.ts  |  2 +-
 .../final-state/final-state.component.css     | 50 +++++++++++
 .../final-state/final-state.component.html    | 39 +++++++++
 .../final-state/final-state.component.spec.ts | 23 +++++
 .../final-state/final-state.component.ts      | 22 +++++
 .../final-stage/final-stage.component.css     | 10 ++-
 .../final-stage/final-stage.component.html    | 85 +++++++++++++------
 .../final-stage/final-stage.component.ts      | 21 ++---
 12 files changed, 256 insertions(+), 68 deletions(-)
 create mode 100644 src/app/view/components/final-state/final-state.component.css
 create mode 100644 src/app/view/components/final-state/final-state.component.html
 create mode 100644 src/app/view/components/final-state/final-state.component.spec.ts
 create mode 100644 src/app/view/components/final-state/final-state.component.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 8171fee..7348bdf 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -4,7 +4,7 @@
     [deleteActionHandler]="deleteAction"
     [toggleRecordingHandler]="toggleRecording"
     [deleteAllActionsHandler]="deleteAllActions"
-    [markFinalStateHandler]="markFinalState"
+    [markFinalStateHandler]="markFinalStateHandler"
     [actions]="actions"
     [recording]="recording"
   >
@@ -12,14 +12,15 @@
 
   <final-stage *ngIf="!showOverview"
     [actions]="actions"
-     
+    [mainAttributes]="mainAttributes" 
     [finalStateTarget]="finalStateTarget"
     [savedAttributes]="savedAttributes"
-    [saveAttribute]="saveAttribute"
-    [deleteFinalStateTarget] = "deleteFinalStateTarget"
-    [addFinalState] = "addFinalState"
-    [deleteFinalState] = "deleteFinalState"
-    [unMarkFinalState]="unMarkFinalState"
+    [saveAttributeHandler]="saveAttributeHandler"
+    [isAttributeSavedHandler]="isAttributeSavedHandler"
+    [deleteFinalStateTargetHandler] = "deleteFinalStateTargetHandler"
+    [addFinalStateHandler] = "addFinalStateHandler"
+    [deleteFinalStateHandler] = "deleteFinalStateHandler"
+    [unMarkFinalStateHandler]="unMarkFinalStateHandler"
   >
   </final-stage>
 </div>
\ No newline at end of file
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 00770a8..464870b 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -28,6 +28,9 @@ export class AppComponent implements OnInit {
   finalStates: Result[] = [];
   finalStateTarget?: FinalStateTarget = undefined;
   savedAttributes: ObjectKeys = {};
+
+  // array of attributes of final state which are always saved when the showed up
+  mainAttributes: string[] = ['id', 'class', 'name'];
   //protected readonly Object = Object; // just for the template
   // END: MARK FINAL STATE
 
@@ -35,8 +38,6 @@ export class AppComponent implements OnInit {
 
   ngOnInit(): void {
     // START: MARK FINAL STATE
-    
-    console.log("App init")
     chrome.runtime.onMessage.addListener((message: Message) => {
       if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
         console.log("Final state target:", message)
@@ -57,7 +58,7 @@ export class AppComponent implements OnInit {
   /**
    * Deletes the clicked final state target and sets all variables to default values.
    */
-  public deleteFinalStateTarget(){
+  public deleteFinalStateTargetHandler(){
     const xPath = this.finalStateTarget!.xPath;
     this.finalStateTarget = undefined;
     this.savedAttributes = {};
@@ -71,7 +72,7 @@ export class AppComponent implements OnInit {
     })
   }
 
-  public saveAttribute(event: Event, attribute: string, value: string){
+  public saveAttributeHandler(event: Event, attribute: string, value: string){
     if((event.target as HTMLInputElement).checked){
       console.log(`Saving attribute ${attribute}=${value}`)
       this.savedAttributes[attribute] = value;
@@ -82,7 +83,19 @@ export class AppComponent implements OnInit {
     console.log(this.savedAttributes)
   }
 
-  public addFinalState(){
+  isAttributeSavedHandler(attribute: string): boolean {
+    return this.savedAttributes.hasOwnProperty(attribute);
+  }
+  
+  public addFinalStateHandler(){
+     // Call saveAttribute for each mandatory attribute
+    Object.keys(this.finalStateTarget!.attributes).forEach(attribute => {
+      if (this.mainAttributes.includes(attribute)) {
+        const mockEvent = { target: { checked: true } } as any;
+        this.saveAttributeHandler(mockEvent, attribute, this.finalStateTarget!.attributes[attribute]);
+      }
+    });
+
     const element: HtmlElement = {
       tagName: this.finalStateTarget!.tagName,
       globalAttributes: {},
@@ -96,10 +109,10 @@ export class AppComponent implements OnInit {
     };
     this.finalStates.push(finalState);
     saveFinalStates(this.finalStates);
-    this.deleteFinalStateTarget();
+    this.deleteFinalStateTargetHandler();
   }
 
-  public deleteFinalState = (index: number) => {
+  public deleteFinalStateHandler = (index: number) => {
     this.finalStates.splice(index, 1);
     saveFinalStates(this.finalStates);
   }
@@ -144,13 +157,13 @@ export class AppComponent implements OnInit {
   }
 
   // START: MARK FINAL STATE
-  public markFinalState = () => {
+  public markFinalStateHandler = () => {
     saveMarkFinalState(true);
     this.toggleView();
   }
 
-  public unMarkFinalState = () => {
-    saveMarkFinalState(false);
+  public unMarkFinalStateHandler = () => {
+    //saveMarkFinalState(false);
     this.toggleView();
   }
   // END: MARK FINAL STATE
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 6b7b19b..5a4ba8f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -10,6 +10,7 @@ import { HumanDescComponent } from './view/components/human-desc/human-desc.comp
 import { TechnicalDescComponent } from './view/components/technical-desc/technical-desc.component';
 import { CapturedActionComponent } from './view/components/captured-action/captured-action.component';
 import { FinalStageComponent } from './view/pages/final-stage/final-stage.component';
+import { FinalStateComponent } from './view/components/final-state/final-state.component';
 
 @NgModule({
   declarations: [
@@ -22,6 +23,7 @@ import { FinalStageComponent } from './view/pages/final-stage/final-stage.compon
     TechnicalDescComponent,
     CapturedActionComponent,
     FinalStageComponent,
+    FinalStateComponent,
   ],
   imports: [
     BrowserModule
diff --git a/src/app/view/components/custom-button/custom-button.component.css b/src/app/view/components/custom-button/custom-button.component.css
index 53fbb6c..63732bf 100644
--- a/src/app/view/components/custom-button/custom-button.component.css
+++ b/src/app/view/components/custom-button/custom-button.component.css
@@ -11,6 +11,11 @@
 
 /* Sizes styles */
 /* small button S */
+.XS {
+    font-size: 10px;
+    padding: 4px 8px;
+}
+
 .S {
     font-size: 12px;
     padding: 6px 10px;
@@ -104,12 +109,17 @@
     background-color: #f8f9fa;
 }
 
-.return-button {
-    /* Add your custom styles for the "Return" button here */
-    background-color: #007bff; /* Example background color */
-    color: white; /* Example text color */
-    /* Add any other styles you want */
-  }
+
+.edit-button {
+    background-color: #999; /* Example color */
+    color: #fff; /* Example text color */
+
+}
+
+.edit-button:hover {
+    background-color: #aaa; /* Example hover color */
+}
+
 
 /* Possible alternatives */
 
diff --git a/src/app/view/components/custom-button/custom-button.component.ts b/src/app/view/components/custom-button/custom-button.component.ts
index 7abaf6b..7ac7617 100644
--- a/src/app/view/components/custom-button/custom-button.component.ts
+++ b/src/app/view/components/custom-button/custom-button.component.ts
@@ -12,7 +12,7 @@ export class CustomButtonComponent {
   @Input() buttonText: string = 'Button';
   @Input() color: string = ''; // not given default value, because it overwrite possible value in styles
   @Input() backgroundColor: string = '#666;';
-  @Input() size: 'S' | 'M' | 'L' = 'M';
+  @Input() size: 'XS' | 'S' | 'M' | 'L' = 'M';
   @Input() additionalClasses: string = '';
   @Input() additionalStyles: string = '';
   @Input() iconType: 'fontawesome' | 'custom' | 'image' = 'custom';
diff --git a/src/app/view/components/final-state/final-state.component.css b/src/app/view/components/final-state/final-state.component.css
new file mode 100644
index 0000000..83ffecf
--- /dev/null
+++ b/src/app/view/components/final-state/final-state.component.css
@@ -0,0 +1,50 @@
+.container {
+    display: flex;
+    flex-wrap: nowrap;
+    flex-direction: row;
+
+    border-bottom: 2px ridge #E8E8E8;
+}
+
+.component {
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+.first-component {
+    padding: 5px;
+    display: flex;
+    align-items: flex-start;
+    text-align: left;
+    width: 25%;
+    flex-direction: column;
+}
+
+.first-component p {
+    margin: 0; /* or padding: 0; */
+}
+
+.second-component {
+    padding: 5px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    width: 50%
+}
+
+.second-component p {
+    margin: 0; /* or padding: 0; */
+}
+
+.third-component {
+    padding: 5px;
+    display: flex;
+    align-items: center;
+    width: 25%;
+}
+
+
+.main-attributes {
+    color:  #909090;
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
+}
\ No newline at end of file
diff --git a/src/app/view/components/final-state/final-state.component.html b/src/app/view/components/final-state/final-state.component.html
new file mode 100644
index 0000000..6f6df1c
--- /dev/null
+++ b/src/app/view/components/final-state/final-state.component.html
@@ -0,0 +1,39 @@
+
+
+<div class="container">
+    <div class="component first-component">
+        <p><b>{{finalState.element.tagName}}</b></p>
+        
+        <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
+            <ng-container *ngIf="mainAttributes.includes(attribute)">
+                <!-- show only first 3 -->
+                <p class="main-attributes S" *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
+            </ng-container>
+          </div>
+    </div>
+    <div class="component second-component">
+        <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
+            <!-- show only first 3 -->
+            <ng-container *ngIf="!mainAttributes.includes(attribute)">
+                <p *ngIf="j < 3"><span style="text-transform: capitalize;">{{attribute}}: </span> <b>{{finalState.selectedAttributes[attribute]}}</b></p>
+            </ng-container>
+          </div>
+    </div>    
+    <div class="component third-component">
+        <custom-button
+        buttonText=""
+        size="S"
+        iconType="fontawesome"
+        icon="fa fa-trash"
+        additionalClasses="delete-button"
+        (click)="deleteFinalState(index)"
+    >
+    </custom-button>
+
+    <custom-button backgroundColor="#8F8F8F" icon="fa fa-eye" iconType="fontawesome"
+     buttonText="" additionalStyles="margin-left:5px;" size="S" color="white"></custom-button>
+
+    
+    </div>
+    
+</div>
\ No newline at end of file
diff --git a/src/app/view/components/final-state/final-state.component.spec.ts b/src/app/view/components/final-state/final-state.component.spec.ts
new file mode 100644
index 0000000..f97ce0d
--- /dev/null
+++ b/src/app/view/components/final-state/final-state.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FinalStateComponent } from './final-state.component';
+
+describe('FinalStateComponent', () => {
+  let component: FinalStateComponent;
+  let fixture: ComponentFixture<FinalStateComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [FinalStateComponent]
+    })
+    .compileComponents();
+    
+    fixture = TestBed.createComponent(FinalStateComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/view/components/final-state/final-state.component.ts b/src/app/view/components/final-state/final-state.component.ts
new file mode 100644
index 0000000..c676af0
--- /dev/null
+++ b/src/app/view/components/final-state/final-state.component.ts
@@ -0,0 +1,22 @@
+import { Component, Input } from '@angular/core';
+
+import { Action } from 'src/app/model/action.interface';
+import { Result } from 'src/app/model/result.interface';
+import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
+import { ObjectKeys } from 'src/app/model/object-keys.interface';
+
+@Component({
+  selector: 'final-state',
+  templateUrl: './final-state.component.html',
+  styleUrls: ['./final-state.component.css']
+})
+export class FinalStateComponent {
+
+  @Input() finalState!: Result;
+
+  @Input() deleteFinalState!: Function;
+  @Input() index!: number;
+  @Input() mainAttributes: string[] = []
+
+  protected readonly Object = Object; // just for the template
+}
diff --git a/src/app/view/pages/final-stage/final-stage.component.css b/src/app/view/pages/final-stage/final-stage.component.css
index 93c5be8..d386906 100644
--- a/src/app/view/pages/final-stage/final-stage.component.css
+++ b/src/app/view/pages/final-stage/final-stage.component.css
@@ -80,11 +80,13 @@
     margin-left: 5px;
 }
 
-.mandatory-attribute-name {
+
+.main-attributes-name {
     text-transform: lowercase;
-    color:  #333;
 }
 
-.mandatory-attribute-value {
-    color:  #333;
+
+.main-attributes {
+    color:  #909090;
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
 }
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index f0ad702..d291b6e 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -6,7 +6,7 @@
             iconType="fontawesome"
             icon="fa fa-arrow-left"
             additionalClasses="return-fs-button"
-            (click)="unMarkFinalState()"
+            (click)="unMarkFinalStateHandler()"
     >
     </custom-button>
 </div>
@@ -19,12 +19,13 @@
 
     <!-- not chooseable attributes - default -->
     <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <ng-container *ngIf="mandatoryAttributes.includes(attribute)">
+      <ng-container *ngIf="mainAttributes.includes(attribute)">
         <div class="checkbox-container">
-          <input type="checkbox" class="custom-checkbox" checked style="display: none;" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+          <input type="checkbox" class="custom-checkbox" checked style="display: none;" 
+              (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
           <label>
-            <span class="mandatory-attribute-name"> {{attribute}}: </span>
-            <span class="mandatory-attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+            <span class="main-attributes main-attributes-name"> {{attribute}}: </span>
+            <span class="main-attributes">{{finalStateTarget.attributes[attribute]}} </span>
           </label>
         </div>
       </ng-container>
@@ -36,12 +37,23 @@
     </ng-container>
     <h4>Select characteristic properties</h4>
     <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <ng-container *ngIf="!mandatoryAttributes.includes(attribute)">
+      <ng-container *ngIf="!mainAttributes.includes(attribute)">
         <div class="checkbox-container">
-          <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, attribute, finalStateTarget.attributes[attribute])">
+          <input type="checkbox" class="custom-checkbox" 
+            [checked]="isAttributeSavedHandler(attribute)" 
+            (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
           <label>
-            <span class="attribute-name"> {{attribute}}: </span>
+            <span class="attribute-name"> {{attribute}} : </span>
             <span class="attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+            <custom-button
+            buttonText=""
+            size="XS"
+            iconType="fontawesome"
+            icon="fa fa-edit"
+            additionalClasses="edit-button"
+            additionalStyles="margin-left: 3px;"
+            >
+            </custom-button>
           </label>
         </div>
       </ng-container>
@@ -50,20 +62,44 @@
     <!-- <h5>Explicitly set styles:</h5> -->
     <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
       <div class="checkbox-container">
-        <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+        <input type="checkbox" class="custom-checkbox"
+          [checked]="isAttributeSavedHandler('style.'+attribute)" 
+          (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.styles[attribute])">
         <label>
           <span class="attribute-name"> {{attribute}}: </span>
-          <span class="attribute-value">{{finalStateTarget.styles[attribute]}} </span> 
+          <span class="attribute-value">{{finalStateTarget.styles[attribute]}} </span>
+          <custom-button
+            buttonText=""
+            size="XS"
+            iconType="fontawesome"
+            icon="fa fa-edit"
+            additionalClasses="edit-button"
+            additionalStyles="margin-left: 3px;"
+            >
+            </custom-button>
+
        </label>
       </div>
     </div>
     <!-- <h5>Computed styles:</h5> -->
     <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">
       <div class="checkbox-container">
-      <input type="checkbox" class="custom-checkbox" (change)="saveAttribute($event, 'style.'+attribute, finalStateTarget.attributes[attribute])">
+      <input type="checkbox" class="custom-checkbox"
+        [checked]="isAttributeSavedHandler('style.'+attribute)"
+        (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.computedStyles[attribute])">
         <label>
           <span class="attribute-name"> {{attribute}}: </span>
           <span class="attribute-value">{{finalStateTarget.computedStyles[attribute]}} </span> 
+          <custom-button
+            buttonText=""
+            size="XS"
+            iconType="fontawesome"
+            icon="fa fa-edit"
+            additionalClasses="edit-button"
+            additionalStyles="margin-left: 3px;"
+            
+            >
+            </custom-button>
        </label>
        </div>
     </div>
@@ -76,7 +112,7 @@
             icon="fa fa-trash"
             additionalClasses="delete-button"
             additionalStyles="width: 10em;"
-            (click)="deleteFinalStateTarget()"
+            (click)="deleteFinalStateTargetHandler()"
     >
     </custom-button>
     <custom-button
@@ -86,7 +122,7 @@
             icon="fa fa-plus"
             additionalClasses="success-button pl_fs"
             additionalStyles="margin-left: 2em; width: 10em;"
-            (click)="addFinalState()"
+            (click)="addFinalStateHandler()"
       >
     </custom-button>
     </span>
@@ -94,21 +130,14 @@
   </ng-container>
 
   <h3>Final states:</h3>
-  <div *ngFor="let finalState of finalStates; let i = index" class="fs_container">
-    <b>{{finalState.element.tagName}}</b>
-    <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
-      <!-- show only first 3 -->
-      <p *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
-    </div>
-    <custom-button
-            buttonText=""
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-trash"
-            additionalClasses="delete-button"
-            (click)="deleteFinalState(i)"
-    >
-    </custom-button>
+  <div *ngFor="let finalState of finalStates; let i = index">
+    <final-state 
+        [index]="i"
+        [finalState]="finalState"
+        [deleteFinalState]="deleteFinalStateHandler()"
+        [mainAttributes]="mainAttributes"
+        >
+      </final-state>
   </div>  
 
   <!-- END: MARK FINAL STATE -->
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index 3747622..62525b3 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -4,8 +4,6 @@ import { Result } from 'src/app/model/result.interface';
 import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
 import { ObjectKeys } from 'src/app/model/object-keys.interface';
 
-import { Message } from 'src/app/model/message.interface';
-import { ChangeDetectorRef } from '@angular/core';
 
 @Component({
   selector: 'final-stage',
@@ -22,17 +20,16 @@ export class FinalStageComponent implements OnInit {
     @Input() savedAttributes: ObjectKeys = {};
 
 
-    @Input() saveAttribute!: Function;
-    @Input() deleteFinalStateTarget!: Function;
-    @Input() addFinalState!: Function;
-    @Input() deleteFinalState!: Function;
+    @Input() saveAttributeHandler!: Function;
+    @Input() isAttributeSavedHandler!: Function;
+    @Input() deleteFinalStateTargetHandler!: Function;
+    @Input() addFinalStateHandler!: Function;
+    @Input() deleteFinalStateHandler!: Function;
+    @Input() unMarkFinalStateHandler!: Function;
+    
+    @Input() mainAttributes: string[] = [];
 
-    @Input() unMarkFinalState!: Function;
-
-    // temporary solution - for future make better
-    mandatoryAttributes: string[] = ['id', 'class', 'name']
-
-    constructor(private ref: ChangeDetectorRef){}
+    constructor(){}
 
     protected readonly Object = Object; // just for the template
     ngOnInit(): void {
-- 
GitLab


From ee22c82dcc8ac797a09c27b5928b8e85ede538e4 Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Sat, 6 Apr 2024 13:59:59 +0200
Subject: [PATCH 7/9] =?UTF-8?q?Re=20#11122=20-=20oprava=20u=C5=BE=C3=ADv?=
 =?UTF-8?q?=C3=A1n=C3=AD=20logiky?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- vyčištění komentářů
- přesunutí logiky final stage s app.component do final-stage.component
- přidání stránky s textem "Click on the element you want to select Waiting..."
- oprava předávání dat finalStates
- oprava volání funkce smazání položek finalStates

- oprava chybi vyvolané mergem (duplicitní startTime?: Date v overview.component)


+ změny v package-lock.json a package.json - následkem npm audit-fix --force (při řešení problémů po npm install)
---
 package-lock.json                             | 1681 +++++++++--------
 package.json                                  |    6 +-
 src/app/app.component.html                    |    8 +-
 src/app/app.component.ts                      |  101 +-
 .../custom-button/custom-button.component.css |    6 +-
 .../final-state/final-state.component.css     |    6 +-
 .../final-state/final-state.component.html    |    2 -
 .../final-state/final-state.component.ts      |    5 +-
 .../final-stage/final-stage.component.css     |   83 +-
 .../final-stage/final-stage.component.html    |  280 +--
 .../final-stage/final-stage.component.ts      |  135 +-
 .../pages/overview/overview.component.html    |    5 +-
 .../view/pages/overview/overview.component.ts |    5 +-
 13 files changed, 1214 insertions(+), 1109 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index cfa7e3c..3ae94ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
         "zone.js": "~0.14.4"
       },
       "devDependencies": {
-        "@angular-devkit/build-angular": "^17.3.2",
+        "@angular-devkit/build-angular": "^17.2.3",
         "@angular/cli": "^17.3.2",
         "@angular/compiler-cli": "^17.3.1",
         "@types/jasmine": "~3.10.0",
@@ -106,70 +106,70 @@
       }
     },
     "node_modules/@angular-devkit/build-angular": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.2.tgz",
-      "integrity": "sha512-muPCUyL0uHvRkLH4NLWiccER6P2vCm/Q5DDvqyN4XOzzY3tAHHLrKrpvY87sgd2oNJ6Ci8x7GPNcfzR5KELCnw==",
-      "dependencies": {
-        "@ampproject/remapping": "2.3.0",
-        "@angular-devkit/architect": "0.1703.2",
-        "@angular-devkit/build-webpack": "0.1703.2",
-        "@angular-devkit/core": "17.3.2",
-        "@babel/core": "7.24.0",
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.3.tgz",
+      "integrity": "sha512-AZsEHZj+k2Lxb7uQUwfEpSE6TvQhCoIgP6XLKgKxZHUOiTUVXDj84WhNcbup5SsSG1cafmoVN7APxxuSwHcoeg==",
+      "dependencies": {
+        "@ampproject/remapping": "2.2.1",
+        "@angular-devkit/architect": "0.1702.3",
+        "@angular-devkit/build-webpack": "0.1702.3",
+        "@angular-devkit/core": "17.2.3",
+        "@babel/core": "7.23.9",
         "@babel/generator": "7.23.6",
         "@babel/helper-annotate-as-pure": "7.22.5",
         "@babel/helper-split-export-declaration": "7.22.6",
         "@babel/plugin-transform-async-generator-functions": "7.23.9",
         "@babel/plugin-transform-async-to-generator": "7.23.3",
-        "@babel/plugin-transform-runtime": "7.24.0",
-        "@babel/preset-env": "7.24.0",
-        "@babel/runtime": "7.24.0",
+        "@babel/plugin-transform-runtime": "7.23.9",
+        "@babel/preset-env": "7.23.9",
+        "@babel/runtime": "7.23.9",
         "@discoveryjs/json-ext": "0.5.7",
-        "@ngtools/webpack": "17.3.2",
+        "@ngtools/webpack": "17.2.3",
         "@vitejs/plugin-basic-ssl": "1.1.0",
         "ansi-colors": "4.1.3",
-        "autoprefixer": "10.4.18",
+        "autoprefixer": "10.4.17",
         "babel-loader": "9.1.3",
         "babel-plugin-istanbul": "6.1.1",
         "browserslist": "^4.21.5",
         "copy-webpack-plugin": "11.0.0",
-        "critters": "0.0.22",
+        "critters": "0.0.20",
         "css-loader": "6.10.0",
-        "esbuild-wasm": "0.20.1",
+        "esbuild-wasm": "0.20.0",
         "fast-glob": "3.3.2",
         "http-proxy-middleware": "2.0.6",
-        "https-proxy-agent": "7.0.4",
-        "inquirer": "9.2.15",
+        "https-proxy-agent": "7.0.2",
+        "inquirer": "9.2.14",
         "jsonc-parser": "3.2.1",
         "karma-source-map-support": "1.4.0",
         "less": "4.2.0",
         "less-loader": "11.1.0",
         "license-webpack-plugin": "4.0.2",
         "loader-utils": "3.2.1",
-        "magic-string": "0.30.8",
-        "mini-css-extract-plugin": "2.8.1",
+        "magic-string": "0.30.7",
+        "mini-css-extract-plugin": "2.8.0",
         "mrmime": "2.0.0",
         "open": "8.4.2",
         "ora": "5.4.1",
         "parse5-html-rewriting-stream": "7.0.0",
         "picomatch": "4.0.1",
-        "piscina": "4.4.0",
+        "piscina": "4.3.1",
         "postcss": "8.4.35",
-        "postcss-loader": "8.1.1",
+        "postcss-loader": "8.1.0",
         "resolve-url-loader": "5.0.0",
         "rxjs": "7.8.1",
-        "sass": "1.71.1",
-        "sass-loader": "14.1.1",
+        "sass": "1.70.0",
+        "sass-loader": "14.1.0",
         "semver": "7.6.0",
         "source-map-loader": "5.0.0",
         "source-map-support": "0.5.21",
-        "terser": "5.29.1",
+        "terser": "5.27.0",
         "tree-kill": "1.2.2",
         "tslib": "2.6.2",
-        "undici": "6.7.1",
-        "vite": "5.1.5",
+        "undici": "6.6.2",
+        "vite": "5.0.12",
         "watchpack": "2.4.0",
-        "webpack": "5.90.3",
-        "webpack-dev-middleware": "6.1.2",
+        "webpack": "5.90.1",
+        "webpack-dev-middleware": "6.1.1",
         "webpack-dev-server": "4.15.1",
         "webpack-merge": "5.10.0",
         "webpack-subresource-integrity": "5.1.0"
@@ -180,7 +180,7 @@
         "yarn": ">= 1.13.0"
       },
       "optionalDependencies": {
-        "esbuild": "0.20.1"
+        "esbuild": "0.20.0"
       },
       "peerDependencies": {
         "@angular/compiler-cli": "^17.0.0",
@@ -195,7 +195,7 @@
         "ng-packagr": "^17.0.0",
         "protractor": "^7.0.0",
         "tailwindcss": "^2.0.0 || ^3.0.0",
-        "typescript": ">=5.2 <5.5"
+        "typescript": ">=5.2 <5.4"
       },
       "peerDependenciesMeta": {
         "@angular/localize": {
@@ -233,53 +233,52 @@
         }
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/@ampproject/remapping": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
-      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+    "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": {
+      "version": "0.1702.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.3.tgz",
+      "integrity": "sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ==",
       "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.5",
-        "@jridgewell/trace-mapping": "^0.3.24"
+        "@angular-devkit/core": "17.2.3",
+        "rxjs": "7.8.1"
       },
       "engines": {
-        "node": ">=6.0.0"
+        "node": "^18.13.0 || >=20.9.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
-      "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+    "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz",
+      "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==",
       "dependencies": {
-        "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
-        "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.24.0",
-        "@babel/parser": "^7.24.0",
-        "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
-        "@babel/types": "^7.24.0",
-        "convert-source-map": "^2.0.0",
-        "debug": "^4.1.0",
-        "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.3",
-        "semver": "^6.3.1"
+        "ajv": "8.12.0",
+        "ajv-formats": "2.1.1",
+        "jsonc-parser": "3.2.1",
+        "picomatch": "4.0.1",
+        "rxjs": "7.8.1",
+        "source-map": "0.7.4"
       },
       "engines": {
-        "node": ">=6.9.0"
+        "node": "^18.13.0 || >=20.9.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
       },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/babel"
+      "peerDependencies": {
+        "chokidar": "^3.5.2"
+      },
+      "peerDependenciesMeta": {
+        "chokidar": {
+          "optional": true
+        }
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-      "bin": {
-        "semver": "bin/semver.js"
+    "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core/node_modules/source-map": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+      "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+      "engines": {
+        "node": ">= 8"
       }
     },
     "node_modules/@angular-devkit/build-angular/node_modules/@babel/generator": {
@@ -641,6 +640,21 @@
         "node": ">=12"
       }
     },
+    "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.3.tgz",
+      "integrity": "sha512-+d5Q7/ctDHePYZXcg0GFwL/AbyEkPMHoCiT7pmLI0B0n87D/mYKK/qmVN1VANBrFLTuIe8RtcL0aJ9pw8HAxWA==",
+      "engines": {
+        "node": "^18.13.0 || >=20.9.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler-cli": "^17.0.0",
+        "typescript": ">=5.2 <5.4",
+        "webpack": "^5.54.0"
+      }
+    },
     "node_modules/@angular-devkit/build-angular/node_modules/@types/node": {
       "version": "20.11.30",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
@@ -662,15 +676,83 @@
         "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+    "node_modules/@angular-devkit/build-angular/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    "node_modules/@angular-devkit/build-angular/node_modules/chalk": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+      "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+      "dependencies": {
+        "agent-base": "^7.0.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/inquirer": {
+      "version": "9.2.14",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz",
+      "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==",
+      "dependencies": {
+        "@ljharb/through": "^2.3.12",
+        "ansi-escapes": "^4.3.2",
+        "chalk": "^5.3.0",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^4.1.0",
+        "external-editor": "^3.1.0",
+        "figures": "^3.2.0",
+        "lodash": "^4.17.21",
+        "mute-stream": "1.0.0",
+        "ora": "^5.4.1",
+        "run-async": "^3.0.0",
+        "rxjs": "^7.8.1",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^6.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
     },
     "node_modules/@angular-devkit/build-angular/node_modules/less": {
       "version": "4.2.0",
@@ -697,6 +779,17 @@
         "source-map": "~0.6.0"
       }
     },
+    "node_modules/@angular-devkit/build-angular/node_modules/magic-string": {
+      "version": "0.30.7",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
+      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/@angular-devkit/build-angular/node_modules/mime": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -786,53 +879,46 @@
         "tslib": "^2.1.0"
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/schema-utils": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+    "node_modules/@angular-devkit/build-angular/node_modules/sass": {
+      "version": "1.70.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+      "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
       "dependencies": {
-        "@types/json-schema": "^7.0.8",
-        "ajv": "^6.12.5",
-        "ajv-keywords": "^3.5.2"
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
       },
-      "engines": {
-        "node": ">= 10.13.0"
+      "bin": {
+        "sass": "sass.js"
       },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/webpack"
+      "engines": {
+        "node": ">=14.0.0"
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+    "node_modules/@angular-devkit/build-angular/node_modules/terser": {
+      "version": "5.27.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
+      "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
       "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
+        "@jridgewell/source-map": "^0.3.3",
+        "acorn": "^8.8.2",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
       },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv-keywords": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
-      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "peerDependencies": {
-        "ajv": "^6.9.1"
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/@angular-devkit/build-angular/node_modules/vite": {
-      "version": "5.1.5",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz",
-      "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==",
+      "version": "5.0.12",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
+      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
       "dependencies": {
         "esbuild": "^0.19.3",
-        "postcss": "^8.4.35",
+        "postcss": "^8.4.32",
         "rollup": "^4.2.0"
       },
       "bin": {
@@ -929,58 +1015,25 @@
         "node": ">=10.13.0"
       }
     },
-    "node_modules/@angular-devkit/build-angular/node_modules/webpack": {
-      "version": "5.90.3",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
-      "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
+    "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
       "dependencies": {
-        "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.11.5",
-        "@webassemblyjs/wasm-edit": "^1.11.5",
-        "@webassemblyjs/wasm-parser": "^1.11.5",
-        "acorn": "^8.7.1",
-        "acorn-import-assertions": "^1.9.0",
-        "browserslist": "^4.21.10",
-        "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.15.0",
-        "es-module-lexer": "^1.2.1",
-        "eslint-scope": "5.1.1",
-        "events": "^3.2.0",
-        "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.2.9",
-        "json-parse-even-better-errors": "^2.3.1",
-        "loader-runner": "^4.2.0",
-        "mime-types": "^2.1.27",
-        "neo-async": "^2.6.2",
-        "schema-utils": "^3.2.0",
-        "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.3.10",
-        "watchpack": "^2.4.0",
-        "webpack-sources": "^3.2.3"
-      },
-      "bin": {
-        "webpack": "bin/webpack.js"
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
       },
       "engines": {
-        "node": ">=10.13.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/webpack"
-      },
-      "peerDependenciesMeta": {
-        "webpack-cli": {
-          "optional": true
-        }
+        "node": ">=8"
       }
     },
     "node_modules/@angular-devkit/build-webpack": {
-      "version": "0.1703.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.2.tgz",
-      "integrity": "sha512-w7rVFQcZK4iTCd/MLfQWIkDkwBOfAs++txNQyS9qYID8KvLs1V+oWYd2qDBRelRv1u3YtaCIS1pQx3GFKBC3OA==",
+      "version": "0.1702.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.3.tgz",
+      "integrity": "sha512-G9F2Ori8WxJtMvOQGxTdg7d+5aAO1IPeEtMiZwFPrw65Ey6Gvfm0h2+3FnQdzeKrZmGaTk5E6gffHXJJQfCnmQ==",
       "dependencies": {
-        "@angular-devkit/architect": "0.1703.2",
+        "@angular-devkit/architect": "0.1702.3",
         "rxjs": "7.8.1"
       },
       "engines": {
@@ -993,6 +1046,57 @@
         "webpack-dev-server": "^4.0.0"
       }
     },
+    "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": {
+      "version": "0.1702.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.3.tgz",
+      "integrity": "sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ==",
+      "dependencies": {
+        "@angular-devkit/core": "17.2.3",
+        "rxjs": "7.8.1"
+      },
+      "engines": {
+        "node": "^18.13.0 || >=20.9.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz",
+      "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==",
+      "dependencies": {
+        "ajv": "8.12.0",
+        "ajv-formats": "2.1.1",
+        "jsonc-parser": "3.2.1",
+        "picomatch": "4.0.1",
+        "rxjs": "7.8.1",
+        "source-map": "0.7.4"
+      },
+      "engines": {
+        "node": "^18.13.0 || >=20.9.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "chokidar": "^3.5.2"
+      },
+      "peerDependenciesMeta": {
+        "chokidar": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular-devkit/build-webpack/node_modules/picomatch": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz",
+      "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
@@ -1001,6 +1105,14 @@
         "tslib": "^2.1.0"
       }
     },
+    "node_modules/@angular-devkit/build-webpack/node_modules/source-map": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+      "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/@angular-devkit/core": {
       "version": "17.3.2",
       "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz",
@@ -1190,48 +1302,6 @@
         "typescript": ">=5.2 <5.5"
       }
     },
-    "node_modules/@angular/compiler-cli/node_modules/@babel/core": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
-      "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
-      "dependencies": {
-        "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
-        "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.23.9",
-        "@babel/parser": "^7.23.9",
-        "@babel/template": "^7.23.9",
-        "@babel/traverse": "^7.23.9",
-        "@babel/types": "^7.23.9",
-        "convert-source-map": "^2.0.0",
-        "debug": "^4.1.0",
-        "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.3",
-        "semver": "^6.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/babel"
-      }
-    },
-    "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
-    },
-    "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@angular/core": {
       "version": "17.3.1",
       "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.1.tgz",
@@ -1340,24 +1410,24 @@
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.22.9",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz",
-      "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
+      "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.22.5",
-        "@babel/generator": "^7.22.9",
-        "@babel/helper-compilation-targets": "^7.22.9",
-        "@babel/helper-module-transforms": "^7.22.9",
-        "@babel/helpers": "^7.22.6",
-        "@babel/parser": "^7.22.7",
-        "@babel/template": "^7.22.5",
-        "@babel/traverse": "^7.22.8",
-        "@babel/types": "^7.22.5",
-        "convert-source-map": "^1.7.0",
+        "@babel/code-frame": "^7.23.5",
+        "@babel/generator": "^7.23.6",
+        "@babel/helper-compilation-targets": "^7.23.6",
+        "@babel/helper-module-transforms": "^7.23.3",
+        "@babel/helpers": "^7.23.9",
+        "@babel/parser": "^7.23.9",
+        "@babel/template": "^7.23.9",
+        "@babel/traverse": "^7.23.9",
+        "@babel/types": "^7.23.9",
+        "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.2",
+        "json5": "^2.2.3",
         "semver": "^6.3.1"
       },
       "engines": {
@@ -1368,6 +1438,11 @@
         "url": "https://opencollective.com/babel"
       }
     },
+    "node_modules/@babel/core/node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+    },
     "node_modules/@babel/core/node_modules/semver": {
       "version": "6.3.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -1436,9 +1511,9 @@
       }
     },
     "node_modules/@babel/helper-create-class-features-plugin": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz",
-      "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz",
+      "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==",
       "dependencies": {
         "@babel/helper-annotate-as-pure": "^7.22.5",
         "@babel/helper-environment-visitor": "^7.22.20",
@@ -2069,9 +2144,9 @@
       }
     },
     "node_modules/@babel/plugin-transform-block-scoping": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz",
-      "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz",
+      "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==",
       "dependencies": {
         "@babel/helper-plugin-utils": "^7.24.0"
       },
@@ -2098,11 +2173,11 @@
       }
     },
     "node_modules/@babel/plugin-transform-class-static-block": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz",
-      "integrity": "sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz",
+      "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==",
       "dependencies": {
-        "@babel/helper-create-class-features-plugin": "^7.24.1",
+        "@babel/helper-create-class-features-plugin": "^7.24.4",
         "@babel/helper-plugin-utils": "^7.24.0",
         "@babel/plugin-syntax-class-static-block": "^7.14.5"
       },
@@ -2601,12 +2676,12 @@
       }
     },
     "node_modules/@babel/plugin-transform-runtime": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz",
-      "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz",
+      "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==",
       "dependencies": {
         "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-plugin-utils": "^7.24.0",
+        "@babel/helper-plugin-utils": "^7.22.5",
         "babel-plugin-polyfill-corejs2": "^0.4.8",
         "babel-plugin-polyfill-corejs3": "^0.9.0",
         "babel-plugin-polyfill-regenerator": "^0.5.5",
@@ -2758,13 +2833,13 @@
       }
     },
     "node_modules/@babel/preset-env": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz",
-      "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz",
+      "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==",
       "dependencies": {
         "@babel/compat-data": "^7.23.5",
         "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-plugin-utils": "^7.24.0",
+        "@babel/helper-plugin-utils": "^7.22.5",
         "@babel/helper-validator-option": "^7.23.5",
         "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3",
         "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3",
@@ -2817,7 +2892,7 @@
         "@babel/plugin-transform-new-target": "^7.23.3",
         "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4",
         "@babel/plugin-transform-numeric-separator": "^7.23.4",
-        "@babel/plugin-transform-object-rest-spread": "^7.24.0",
+        "@babel/plugin-transform-object-rest-spread": "^7.23.4",
         "@babel/plugin-transform-object-super": "^7.23.3",
         "@babel/plugin-transform-optional-catch-binding": "^7.23.4",
         "@babel/plugin-transform-optional-chaining": "^7.23.4",
@@ -2877,9 +2952,9 @@
       "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
     },
     "node_modules/@babel/runtime": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
-      "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
+      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -2971,9 +3046,9 @@
       }
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz",
-      "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz",
+      "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==",
       "cpu": [
         "ppc64"
       ],
@@ -2986,9 +3061,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz",
-      "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz",
+      "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==",
       "cpu": [
         "arm"
       ],
@@ -3001,9 +3076,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz",
-      "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz",
+      "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==",
       "cpu": [
         "arm64"
       ],
@@ -3016,9 +3091,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz",
-      "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz",
+      "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==",
       "cpu": [
         "x64"
       ],
@@ -3031,9 +3106,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz",
-      "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz",
+      "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==",
       "cpu": [
         "arm64"
       ],
@@ -3046,9 +3121,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz",
-      "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz",
+      "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==",
       "cpu": [
         "x64"
       ],
@@ -3061,9 +3136,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz",
-      "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz",
+      "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==",
       "cpu": [
         "arm64"
       ],
@@ -3076,9 +3151,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz",
-      "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz",
+      "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==",
       "cpu": [
         "x64"
       ],
@@ -3091,9 +3166,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz",
-      "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz",
+      "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==",
       "cpu": [
         "arm"
       ],
@@ -3106,9 +3181,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz",
-      "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz",
+      "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==",
       "cpu": [
         "arm64"
       ],
@@ -3121,9 +3196,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz",
-      "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz",
+      "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==",
       "cpu": [
         "ia32"
       ],
@@ -3136,9 +3211,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz",
-      "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz",
+      "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==",
       "cpu": [
         "loong64"
       ],
@@ -3151,9 +3226,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz",
-      "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz",
+      "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==",
       "cpu": [
         "mips64el"
       ],
@@ -3166,9 +3241,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz",
-      "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz",
+      "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==",
       "cpu": [
         "ppc64"
       ],
@@ -3181,9 +3256,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz",
-      "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz",
+      "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==",
       "cpu": [
         "riscv64"
       ],
@@ -3196,9 +3271,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz",
-      "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz",
+      "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==",
       "cpu": [
         "s390x"
       ],
@@ -3211,9 +3286,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz",
-      "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz",
+      "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==",
       "cpu": [
         "x64"
       ],
@@ -3226,9 +3301,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz",
-      "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz",
+      "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==",
       "cpu": [
         "x64"
       ],
@@ -3241,9 +3316,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz",
-      "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz",
+      "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==",
       "cpu": [
         "x64"
       ],
@@ -3256,9 +3331,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz",
-      "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz",
+      "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==",
       "cpu": [
         "x64"
       ],
@@ -3271,9 +3346,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz",
-      "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz",
+      "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==",
       "cpu": [
         "arm64"
       ],
@@ -3286,9 +3361,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz",
-      "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz",
+      "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==",
       "cpu": [
         "ia32"
       ],
@@ -3301,9 +3376,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz",
-      "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz",
+      "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==",
       "cpu": [
         "x64"
       ],
@@ -3315,6 +3390,14 @@
         "node": ">=12"
       }
     },
+    "node_modules/@fastify/busboy": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@fortawesome/fontawesome-free": {
       "version": "5.15.4",
       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
@@ -3496,34 +3579,19 @@
       }
     },
     "node_modules/@leichtgewicht/ip-codec": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
-      "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+      "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="
     },
     "node_modules/@ljharb/through": {
       "version": "2.3.13",
       "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz",
       "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==",
       "dependencies": {
-        "call-bind": "^1.0.7"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/@ngtools/webpack": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.2.tgz",
-      "integrity": "sha512-E8zejFF4aJ8l2XcF+GgnE/1IqsZepnPT1xzulLB4LXtjVuXLFLoF9xkHQwxs7cJWWZsxd/SlNsCIcn/ezrYBcQ==",
-      "engines": {
-        "node": "^18.13.0 || >=20.9.0",
-        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
-        "yarn": ">= 1.13.0"
+        "call-bind": "^1.0.7"
       },
-      "peerDependencies": {
-        "@angular/compiler-cli": "^17.0.0",
-        "typescript": ">=5.2 <5.5",
-        "webpack": "^5.54.0"
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/@nodelib/fs.scandir": {
@@ -4240,9 +4308,9 @@
       }
     },
     "node_modules/@types/express-serve-static-core": {
-      "version": "4.17.43",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
-      "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
+      "version": "4.19.0",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz",
+      "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==",
       "dependencies": {
         "@types/node": "*",
         "@types/qs": "*",
@@ -4343,13 +4411,13 @@
       }
     },
     "node_modules/@types/serve-static": {
-      "version": "1.15.5",
-      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
-      "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+      "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
       "dependencies": {
         "@types/http-errors": "*",
-        "@types/mime": "*",
-        "@types/node": "*"
+        "@types/node": "*",
+        "@types/send": "*"
       }
     },
     "node_modules/@types/sockjs": {
@@ -4737,9 +4805,9 @@
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
     },
     "node_modules/autoprefixer": {
-      "version": "10.4.18",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
-      "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
+      "version": "10.4.17",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
+      "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
       "funding": [
         {
           "type": "opencollective",
@@ -4755,8 +4823,8 @@
         }
       ],
       "dependencies": {
-        "browserslist": "^4.23.0",
-        "caniuse-lite": "^1.0.30001591",
+        "browserslist": "^4.22.2",
+        "caniuse-lite": "^1.0.30001578",
         "fraction.js": "^4.3.7",
         "normalize-range": "^0.1.2",
         "picocolors": "^1.0.0",
@@ -5632,9 +5700,9 @@
       "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
     },
     "node_modules/critters": {
-      "version": "0.0.22",
-      "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz",
-      "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==",
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.20.tgz",
+      "integrity": "sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==",
       "dependencies": {
         "chalk": "^4.1.0",
         "css-select": "^5.1.0",
@@ -5642,7 +5710,7 @@
         "domhandler": "^5.0.2",
         "htmlparser2": "^8.0.2",
         "postcss": "^8.4.23",
-        "postcss-media-query-parser": "^0.2.3"
+        "pretty-bytes": "^5.3.0"
       }
     },
     "node_modules/critters/node_modules/ansi-styles": {
@@ -6183,9 +6251,9 @@
       "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="
     },
     "node_modules/esbuild": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz",
-      "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz",
+      "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==",
       "hasInstallScript": true,
       "optional": true,
       "bin": {
@@ -6195,35 +6263,35 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.20.1",
-        "@esbuild/android-arm": "0.20.1",
-        "@esbuild/android-arm64": "0.20.1",
-        "@esbuild/android-x64": "0.20.1",
-        "@esbuild/darwin-arm64": "0.20.1",
-        "@esbuild/darwin-x64": "0.20.1",
-        "@esbuild/freebsd-arm64": "0.20.1",
-        "@esbuild/freebsd-x64": "0.20.1",
-        "@esbuild/linux-arm": "0.20.1",
-        "@esbuild/linux-arm64": "0.20.1",
-        "@esbuild/linux-ia32": "0.20.1",
-        "@esbuild/linux-loong64": "0.20.1",
-        "@esbuild/linux-mips64el": "0.20.1",
-        "@esbuild/linux-ppc64": "0.20.1",
-        "@esbuild/linux-riscv64": "0.20.1",
-        "@esbuild/linux-s390x": "0.20.1",
-        "@esbuild/linux-x64": "0.20.1",
-        "@esbuild/netbsd-x64": "0.20.1",
-        "@esbuild/openbsd-x64": "0.20.1",
-        "@esbuild/sunos-x64": "0.20.1",
-        "@esbuild/win32-arm64": "0.20.1",
-        "@esbuild/win32-ia32": "0.20.1",
-        "@esbuild/win32-x64": "0.20.1"
+        "@esbuild/aix-ppc64": "0.20.0",
+        "@esbuild/android-arm": "0.20.0",
+        "@esbuild/android-arm64": "0.20.0",
+        "@esbuild/android-x64": "0.20.0",
+        "@esbuild/darwin-arm64": "0.20.0",
+        "@esbuild/darwin-x64": "0.20.0",
+        "@esbuild/freebsd-arm64": "0.20.0",
+        "@esbuild/freebsd-x64": "0.20.0",
+        "@esbuild/linux-arm": "0.20.0",
+        "@esbuild/linux-arm64": "0.20.0",
+        "@esbuild/linux-ia32": "0.20.0",
+        "@esbuild/linux-loong64": "0.20.0",
+        "@esbuild/linux-mips64el": "0.20.0",
+        "@esbuild/linux-ppc64": "0.20.0",
+        "@esbuild/linux-riscv64": "0.20.0",
+        "@esbuild/linux-s390x": "0.20.0",
+        "@esbuild/linux-x64": "0.20.0",
+        "@esbuild/netbsd-x64": "0.20.0",
+        "@esbuild/openbsd-x64": "0.20.0",
+        "@esbuild/sunos-x64": "0.20.0",
+        "@esbuild/win32-arm64": "0.20.0",
+        "@esbuild/win32-ia32": "0.20.0",
+        "@esbuild/win32-x64": "0.20.0"
       }
     },
     "node_modules/esbuild-wasm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz",
-      "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.0.tgz",
+      "integrity": "sha512-Lc9KeQCg1Zf8kCtfDXgy29rx0x8dOuhDWbkP76Wc64q7ctOOc1Zv1C39AxiE+y4N6ONyXtJk4HKpM7jlU7/jSA==",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -7130,6 +7198,7 @@
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
       "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
+      "dev": true,
       "dependencies": {
         "agent-base": "^7.0.2",
         "debug": "4"
@@ -7316,6 +7385,7 @@
       "version": "9.2.15",
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz",
       "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==",
+      "dev": true,
       "dependencies": {
         "@ljharb/through": "^2.3.12",
         "ansi-escapes": "^4.3.2",
@@ -7341,6 +7411,7 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
       "dependencies": {
         "color-convert": "^2.0.1"
       },
@@ -7355,6 +7426,7 @@
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
       "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+      "dev": true,
       "engines": {
         "node": "^12.17.0 || ^14.13 || >=16.0.0"
       },
@@ -7366,6 +7438,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -7376,12 +7449,14 @@
     "node_modules/inquirer/node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
     },
     "node_modules/inquirer/node_modules/rxjs": {
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+      "dev": true,
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -7390,6 +7465,7 @@
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
       "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -8277,6 +8353,7 @@
       "version": "0.30.8",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
       "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       },
@@ -8430,9 +8507,9 @@
       }
     },
     "node_modules/mini-css-extract-plugin": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz",
-      "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz",
+      "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==",
       "dependencies": {
         "schema-utils": "^4.0.0",
         "tapable": "^2.2.1"
@@ -9517,9 +9594,9 @@
       }
     },
     "node_modules/piscina": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz",
-      "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.3.1.tgz",
+      "integrity": "sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==",
       "optionalDependencies": {
         "nice-napi": "^1.0.2"
       }
@@ -9631,9 +9708,9 @@
       }
     },
     "node_modules/postcss-loader": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
-      "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz",
+      "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==",
       "dependencies": {
         "cosmiconfig": "^9.0.0",
         "jiti": "^1.20.0",
@@ -9660,11 +9737,6 @@
         }
       }
     },
-    "node_modules/postcss-media-query-parser": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
-      "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="
-    },
     "node_modules/postcss-modules-extract-imports": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
@@ -9737,6 +9809,17 @@
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
     },
+    "node_modules/pretty-bytes": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/proc-log": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
@@ -10260,6 +10343,8 @@
       "version": "1.71.1",
       "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
       "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
         "immutable": "^4.0.0",
@@ -10273,9 +10358,9 @@
       }
     },
     "node_modules/sass-loader": {
-      "version": "14.1.1",
-      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
-      "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz",
+      "integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==",
       "dependencies": {
         "neo-async": "^2.6.2"
       },
@@ -11049,23 +11134,6 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
-    "node_modules/terser": {
-      "version": "5.29.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
-      "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
-      "dependencies": {
-        "@jridgewell/source-map": "^0.3.3",
-        "acorn": "^8.8.2",
-        "commander": "^2.20.0",
-        "source-map-support": "~0.5.20"
-      },
-      "bin": {
-        "terser": "bin/terser"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/terser-webpack-plugin": {
       "version": "5.3.10",
       "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
@@ -11363,9 +11431,12 @@
       }
     },
     "node_modules/undici": {
-      "version": "6.7.1",
-      "resolved": "https://registry.npmjs.org/undici/-/undici-6.7.1.tgz",
-      "integrity": "sha512-+Wtb9bAQw6HYWzCnxrPTMVEV3Q1QjYanI0E4q02ehReMuquQdLTEFEYbfs7hcImVYKcQkWSwT6buEmSVIiDDtQ==",
+      "version": "6.6.2",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz",
+      "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==",
+      "dependencies": {
+        "@fastify/busboy": "^2.0.0"
+      },
       "engines": {
         "node": ">=18.0"
       }
@@ -11560,7 +11631,6 @@
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
       "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
-      "peer": true,
       "dependencies": {
         "glob-to-regexp": "^0.4.1",
         "graceful-fs": "^4.1.2"
@@ -11586,19 +11656,18 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.88.2",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
-      "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
-      "peer": true,
+      "version": "5.90.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
+      "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.0",
+        "@types/estree": "^1.0.5",
         "@webassemblyjs/ast": "^1.11.5",
         "@webassemblyjs/wasm-edit": "^1.11.5",
         "@webassemblyjs/wasm-parser": "^1.11.5",
         "acorn": "^8.7.1",
         "acorn-import-assertions": "^1.9.0",
-        "browserslist": "^4.14.5",
+        "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
         "enhanced-resolve": "^5.15.0",
         "es-module-lexer": "^1.2.1",
@@ -11612,7 +11681,7 @@
         "neo-async": "^2.6.2",
         "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.3.7",
+        "terser-webpack-plugin": "^5.3.10",
         "watchpack": "^2.4.0",
         "webpack-sources": "^3.2.3"
       },
@@ -11633,9 +11702,9 @@
       }
     },
     "node_modules/webpack-dev-middleware": {
-      "version": "6.1.2",
-      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz",
-      "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==",
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz",
+      "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==",
       "dependencies": {
         "colorette": "^2.0.10",
         "memfs": "^3.4.12",
@@ -11804,7 +11873,6 @@
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -11820,7 +11888,6 @@
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "peer": true,
       "peerDependencies": {
         "ajv": "^6.9.1"
       }
@@ -11828,14 +11895,12 @@
     "node_modules/webpack/node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "peer": true
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
     },
     "node_modules/webpack/node_modules/schema-utils": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
       "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-      "peer": true,
       "dependencies": {
         "@types/json-schema": "^7.0.8",
         "ajv": "^6.12.5",
@@ -12142,111 +12207,102 @@
       }
     },
     "@angular-devkit/build-angular": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.2.tgz",
-      "integrity": "sha512-muPCUyL0uHvRkLH4NLWiccER6P2vCm/Q5DDvqyN4XOzzY3tAHHLrKrpvY87sgd2oNJ6Ci8x7GPNcfzR5KELCnw==",
-      "requires": {
-        "@ampproject/remapping": "2.3.0",
-        "@angular-devkit/architect": "0.1703.2",
-        "@angular-devkit/build-webpack": "0.1703.2",
-        "@angular-devkit/core": "17.3.2",
-        "@babel/core": "7.24.0",
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.3.tgz",
+      "integrity": "sha512-AZsEHZj+k2Lxb7uQUwfEpSE6TvQhCoIgP6XLKgKxZHUOiTUVXDj84WhNcbup5SsSG1cafmoVN7APxxuSwHcoeg==",
+      "requires": {
+        "@ampproject/remapping": "2.2.1",
+        "@angular-devkit/architect": "0.1702.3",
+        "@angular-devkit/build-webpack": "0.1702.3",
+        "@angular-devkit/core": "17.2.3",
+        "@babel/core": "7.23.9",
         "@babel/generator": "7.23.6",
         "@babel/helper-annotate-as-pure": "7.22.5",
         "@babel/helper-split-export-declaration": "7.22.6",
         "@babel/plugin-transform-async-generator-functions": "7.23.9",
         "@babel/plugin-transform-async-to-generator": "7.23.3",
-        "@babel/plugin-transform-runtime": "7.24.0",
-        "@babel/preset-env": "7.24.0",
-        "@babel/runtime": "7.24.0",
+        "@babel/plugin-transform-runtime": "7.23.9",
+        "@babel/preset-env": "7.23.9",
+        "@babel/runtime": "7.23.9",
         "@discoveryjs/json-ext": "0.5.7",
-        "@ngtools/webpack": "17.3.2",
+        "@ngtools/webpack": "17.2.3",
         "@vitejs/plugin-basic-ssl": "1.1.0",
         "ansi-colors": "4.1.3",
-        "autoprefixer": "10.4.18",
+        "autoprefixer": "10.4.17",
         "babel-loader": "9.1.3",
         "babel-plugin-istanbul": "6.1.1",
         "browserslist": "^4.21.5",
         "copy-webpack-plugin": "11.0.0",
-        "critters": "0.0.22",
+        "critters": "0.0.20",
         "css-loader": "6.10.0",
-        "esbuild": "0.20.1",
-        "esbuild-wasm": "0.20.1",
+        "esbuild": "0.20.0",
+        "esbuild-wasm": "0.20.0",
         "fast-glob": "3.3.2",
         "http-proxy-middleware": "2.0.6",
-        "https-proxy-agent": "7.0.4",
-        "inquirer": "9.2.15",
+        "https-proxy-agent": "7.0.2",
+        "inquirer": "9.2.14",
         "jsonc-parser": "3.2.1",
         "karma-source-map-support": "1.4.0",
         "less": "4.2.0",
         "less-loader": "11.1.0",
         "license-webpack-plugin": "4.0.2",
         "loader-utils": "3.2.1",
-        "magic-string": "0.30.8",
-        "mini-css-extract-plugin": "2.8.1",
+        "magic-string": "0.30.7",
+        "mini-css-extract-plugin": "2.8.0",
         "mrmime": "2.0.0",
         "open": "8.4.2",
         "ora": "5.4.1",
         "parse5-html-rewriting-stream": "7.0.0",
         "picomatch": "4.0.1",
-        "piscina": "4.4.0",
+        "piscina": "4.3.1",
         "postcss": "8.4.35",
-        "postcss-loader": "8.1.1",
+        "postcss-loader": "8.1.0",
         "resolve-url-loader": "5.0.0",
         "rxjs": "7.8.1",
-        "sass": "1.71.1",
-        "sass-loader": "14.1.1",
+        "sass": "1.70.0",
+        "sass-loader": "14.1.0",
         "semver": "7.6.0",
         "source-map-loader": "5.0.0",
         "source-map-support": "0.5.21",
-        "terser": "5.29.1",
+        "terser": "5.27.0",
         "tree-kill": "1.2.2",
         "tslib": "2.6.2",
-        "undici": "6.7.1",
-        "vite": "5.1.5",
+        "undici": "6.6.2",
+        "vite": "5.0.12",
         "watchpack": "2.4.0",
-        "webpack": "5.90.3",
-        "webpack-dev-middleware": "6.1.2",
+        "webpack": "5.90.1",
+        "webpack-dev-middleware": "6.1.1",
         "webpack-dev-server": "4.15.1",
         "webpack-merge": "5.10.0",
         "webpack-subresource-integrity": "5.1.0"
       },
       "dependencies": {
-        "@ampproject/remapping": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
-          "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+        "@angular-devkit/architect": {
+          "version": "0.1702.3",
+          "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.3.tgz",
+          "integrity": "sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ==",
           "requires": {
-            "@jridgewell/gen-mapping": "^0.3.5",
-            "@jridgewell/trace-mapping": "^0.3.24"
+            "@angular-devkit/core": "17.2.3",
+            "rxjs": "7.8.1"
           }
         },
-        "@babel/core": {
-          "version": "7.24.0",
-          "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
-          "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+        "@angular-devkit/core": {
+          "version": "17.2.3",
+          "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz",
+          "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==",
           "requires": {
-            "@ampproject/remapping": "^2.2.0",
-            "@babel/code-frame": "^7.23.5",
-            "@babel/generator": "^7.23.6",
-            "@babel/helper-compilation-targets": "^7.23.6",
-            "@babel/helper-module-transforms": "^7.23.3",
-            "@babel/helpers": "^7.24.0",
-            "@babel/parser": "^7.24.0",
-            "@babel/template": "^7.24.0",
-            "@babel/traverse": "^7.24.0",
-            "@babel/types": "^7.24.0",
-            "convert-source-map": "^2.0.0",
-            "debug": "^4.1.0",
-            "gensync": "^1.0.0-beta.2",
-            "json5": "^2.2.3",
-            "semver": "^6.3.1"
+            "ajv": "8.12.0",
+            "ajv-formats": "2.1.1",
+            "jsonc-parser": "3.2.1",
+            "picomatch": "4.0.1",
+            "rxjs": "7.8.1",
+            "source-map": "0.7.4"
           },
           "dependencies": {
-            "semver": {
-              "version": "6.3.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-              "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
+            "source-map": {
+              "version": "0.7.4",
+              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+              "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
             }
           }
         },
@@ -12399,6 +12455,12 @@
           "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
           "optional": true
         },
+        "@ngtools/webpack": {
+          "version": "17.2.3",
+          "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.3.tgz",
+          "integrity": "sha512-+d5Q7/ctDHePYZXcg0GFwL/AbyEkPMHoCiT7pmLI0B0n87D/mYKK/qmVN1VANBrFLTuIe8RtcL0aJ9pw8HAxWA==",
+          "requires": {}
+        },
         "@types/node": {
           "version": "20.11.30",
           "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
@@ -12415,15 +12477,62 @@
           "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==",
           "requires": {}
         },
-        "convert-source-map": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-          "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
         },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+        "chalk": {
+          "version": "5.3.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+          "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "https-proxy-agent": {
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+          "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+          "requires": {
+            "agent-base": "^7.0.2",
+            "debug": "4"
+          }
+        },
+        "inquirer": {
+          "version": "9.2.14",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz",
+          "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==",
+          "requires": {
+            "@ljharb/through": "^2.3.12",
+            "ansi-escapes": "^4.3.2",
+            "chalk": "^5.3.0",
+            "cli-cursor": "^3.1.0",
+            "cli-width": "^4.1.0",
+            "external-editor": "^3.1.0",
+            "figures": "^3.2.0",
+            "lodash": "^4.17.21",
+            "mute-stream": "1.0.0",
+            "ora": "^5.4.1",
+            "run-async": "^3.0.0",
+            "rxjs": "^7.8.1",
+            "string-width": "^4.2.3",
+            "strip-ansi": "^6.0.1",
+            "wrap-ansi": "^6.2.0"
+          }
         },
         "less": {
           "version": "4.2.0",
@@ -12442,6 +12551,14 @@
             "tslib": "^2.3.0"
           }
         },
+        "magic-string": {
+          "version": "0.30.7",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
+          "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+          "requires": {
+            "@jridgewell/sourcemap-codec": "^1.4.15"
+          }
+        },
         "mime": {
           "version": "1.6.0",
           "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -12493,43 +12610,35 @@
             "tslib": "^2.1.0"
           }
         },
-        "schema-utils": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-          "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+        "sass": {
+          "version": "1.70.0",
+          "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+          "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
           "requires": {
-            "@types/json-schema": "^7.0.8",
-            "ajv": "^6.12.5",
-            "ajv-keywords": "^3.5.2"
-          },
-          "dependencies": {
-            "ajv": {
-              "version": "6.12.6",
-              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-              "requires": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-              }
-            },
-            "ajv-keywords": {
-              "version": "3.5.2",
-              "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
-              "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-              "requires": {}
-            }
+            "chokidar": ">=3.0.0 <4.0.0",
+            "immutable": "^4.0.0",
+            "source-map-js": ">=0.6.2 <2.0.0"
+          }
+        },
+        "terser": {
+          "version": "5.27.0",
+          "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
+          "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
+          "requires": {
+            "@jridgewell/source-map": "^0.3.3",
+            "acorn": "^8.8.2",
+            "commander": "^2.20.0",
+            "source-map-support": "~0.5.20"
           }
         },
         "vite": {
-          "version": "5.1.5",
-          "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz",
-          "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==",
+          "version": "5.0.12",
+          "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
+          "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
           "requires": {
             "esbuild": "^0.19.3",
             "fsevents": "~2.3.3",
-            "postcss": "^8.4.35",
+            "postcss": "^8.4.32",
             "rollup": "^4.2.0"
           },
           "dependencies": {
@@ -12574,48 +12683,54 @@
             "graceful-fs": "^4.1.2"
           }
         },
-        "webpack": {
-          "version": "5.90.3",
-          "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz",
-          "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==",
+        "wrap-ansi": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
           "requires": {
-            "@types/eslint-scope": "^3.7.3",
-            "@types/estree": "^1.0.5",
-            "@webassemblyjs/ast": "^1.11.5",
-            "@webassemblyjs/wasm-edit": "^1.11.5",
-            "@webassemblyjs/wasm-parser": "^1.11.5",
-            "acorn": "^8.7.1",
-            "acorn-import-assertions": "^1.9.0",
-            "browserslist": "^4.21.10",
-            "chrome-trace-event": "^1.0.2",
-            "enhanced-resolve": "^5.15.0",
-            "es-module-lexer": "^1.2.1",
-            "eslint-scope": "5.1.1",
-            "events": "^3.2.0",
-            "glob-to-regexp": "^0.4.1",
-            "graceful-fs": "^4.2.9",
-            "json-parse-even-better-errors": "^2.3.1",
-            "loader-runner": "^4.2.0",
-            "mime-types": "^2.1.27",
-            "neo-async": "^2.6.2",
-            "schema-utils": "^3.2.0",
-            "tapable": "^2.1.1",
-            "terser-webpack-plugin": "^5.3.10",
-            "watchpack": "^2.4.0",
-            "webpack-sources": "^3.2.3"
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
           }
         }
       }
     },
     "@angular-devkit/build-webpack": {
-      "version": "0.1703.2",
-      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.2.tgz",
-      "integrity": "sha512-w7rVFQcZK4iTCd/MLfQWIkDkwBOfAs++txNQyS9qYID8KvLs1V+oWYd2qDBRelRv1u3YtaCIS1pQx3GFKBC3OA==",
+      "version": "0.1702.3",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.3.tgz",
+      "integrity": "sha512-G9F2Ori8WxJtMvOQGxTdg7d+5aAO1IPeEtMiZwFPrw65Ey6Gvfm0h2+3FnQdzeKrZmGaTk5E6gffHXJJQfCnmQ==",
       "requires": {
-        "@angular-devkit/architect": "0.1703.2",
+        "@angular-devkit/architect": "0.1702.3",
         "rxjs": "7.8.1"
       },
       "dependencies": {
+        "@angular-devkit/architect": {
+          "version": "0.1702.3",
+          "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.3.tgz",
+          "integrity": "sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ==",
+          "requires": {
+            "@angular-devkit/core": "17.2.3",
+            "rxjs": "7.8.1"
+          }
+        },
+        "@angular-devkit/core": {
+          "version": "17.2.3",
+          "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.3.tgz",
+          "integrity": "sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w==",
+          "requires": {
+            "ajv": "8.12.0",
+            "ajv-formats": "2.1.1",
+            "jsonc-parser": "3.2.1",
+            "picomatch": "4.0.1",
+            "rxjs": "7.8.1",
+            "source-map": "0.7.4"
+          }
+        },
+        "picomatch": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz",
+          "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg=="
+        },
         "rxjs": {
           "version": "7.8.1",
           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
@@ -12623,6 +12738,11 @@
           "requires": {
             "tslib": "^2.1.0"
           }
+        },
+        "source-map": {
+          "version": "0.7.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+          "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
         }
       }
     },
@@ -12746,42 +12866,6 @@
         "semver": "^7.0.0",
         "tslib": "^2.3.0",
         "yargs": "^17.2.1"
-      },
-      "dependencies": {
-        "@babel/core": {
-          "version": "7.23.9",
-          "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
-          "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
-          "requires": {
-            "@ampproject/remapping": "^2.2.0",
-            "@babel/code-frame": "^7.23.5",
-            "@babel/generator": "^7.23.6",
-            "@babel/helper-compilation-targets": "^7.23.6",
-            "@babel/helper-module-transforms": "^7.23.3",
-            "@babel/helpers": "^7.23.9",
-            "@babel/parser": "^7.23.9",
-            "@babel/template": "^7.23.9",
-            "@babel/traverse": "^7.23.9",
-            "@babel/types": "^7.23.9",
-            "convert-source-map": "^2.0.0",
-            "debug": "^4.1.0",
-            "gensync": "^1.0.0-beta.2",
-            "json5": "^2.2.3",
-            "semver": "^6.3.1"
-          },
-          "dependencies": {
-            "convert-source-map": {
-              "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-              "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
-            },
-            "semver": {
-              "version": "6.3.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-              "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
-            }
-          }
-        }
       }
     },
     "@angular/core": {
@@ -12839,27 +12923,32 @@
       "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw=="
     },
     "@babel/core": {
-      "version": "7.22.9",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz",
-      "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
+      "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
       "requires": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.22.5",
-        "@babel/generator": "^7.22.9",
-        "@babel/helper-compilation-targets": "^7.22.9",
-        "@babel/helper-module-transforms": "^7.22.9",
-        "@babel/helpers": "^7.22.6",
-        "@babel/parser": "^7.22.7",
-        "@babel/template": "^7.22.5",
-        "@babel/traverse": "^7.22.8",
-        "@babel/types": "^7.22.5",
-        "convert-source-map": "^1.7.0",
+        "@babel/code-frame": "^7.23.5",
+        "@babel/generator": "^7.23.6",
+        "@babel/helper-compilation-targets": "^7.23.6",
+        "@babel/helper-module-transforms": "^7.23.3",
+        "@babel/helpers": "^7.23.9",
+        "@babel/parser": "^7.23.9",
+        "@babel/template": "^7.23.9",
+        "@babel/traverse": "^7.23.9",
+        "@babel/types": "^7.23.9",
+        "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.2",
+        "json5": "^2.2.3",
         "semver": "^6.3.1"
       },
       "dependencies": {
+        "convert-source-map": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+          "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+        },
         "semver": {
           "version": "6.3.1",
           "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -12914,9 +13003,9 @@
       }
     },
     "@babel/helper-create-class-features-plugin": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz",
-      "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz",
+      "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==",
       "requires": {
         "@babel/helper-annotate-as-pure": "^7.22.5",
         "@babel/helper-environment-visitor": "^7.22.20",
@@ -13339,9 +13428,9 @@
       }
     },
     "@babel/plugin-transform-block-scoping": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz",
-      "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz",
+      "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==",
       "requires": {
         "@babel/helper-plugin-utils": "^7.24.0"
       }
@@ -13356,11 +13445,11 @@
       }
     },
     "@babel/plugin-transform-class-static-block": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz",
-      "integrity": "sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz",
+      "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==",
       "requires": {
-        "@babel/helper-create-class-features-plugin": "^7.24.1",
+        "@babel/helper-create-class-features-plugin": "^7.24.4",
         "@babel/helper-plugin-utils": "^7.24.0",
         "@babel/plugin-syntax-class-static-block": "^7.14.5"
       }
@@ -13661,12 +13750,12 @@
       }
     },
     "@babel/plugin-transform-runtime": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz",
-      "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz",
+      "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==",
       "requires": {
         "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-plugin-utils": "^7.24.0",
+        "@babel/helper-plugin-utils": "^7.22.5",
         "babel-plugin-polyfill-corejs2": "^0.4.8",
         "babel-plugin-polyfill-corejs3": "^0.9.0",
         "babel-plugin-polyfill-regenerator": "^0.5.5",
@@ -13757,13 +13846,13 @@
       }
     },
     "@babel/preset-env": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz",
-      "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz",
+      "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==",
       "requires": {
         "@babel/compat-data": "^7.23.5",
         "@babel/helper-compilation-targets": "^7.23.6",
-        "@babel/helper-plugin-utils": "^7.24.0",
+        "@babel/helper-plugin-utils": "^7.22.5",
         "@babel/helper-validator-option": "^7.23.5",
         "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3",
         "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3",
@@ -13816,7 +13905,7 @@
         "@babel/plugin-transform-new-target": "^7.23.3",
         "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4",
         "@babel/plugin-transform-numeric-separator": "^7.23.4",
-        "@babel/plugin-transform-object-rest-spread": "^7.24.0",
+        "@babel/plugin-transform-object-rest-spread": "^7.23.4",
         "@babel/plugin-transform-object-super": "^7.23.3",
         "@babel/plugin-transform-optional-catch-binding": "^7.23.4",
         "@babel/plugin-transform-optional-chaining": "^7.23.4",
@@ -13866,9 +13955,9 @@
       "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
     },
     "@babel/runtime": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
-      "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
+      "version": "7.23.9",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
+      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
       "requires": {
         "regenerator-runtime": "^0.14.0"
       }
@@ -13941,143 +14030,148 @@
       "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
     },
     "@esbuild/aix-ppc64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz",
-      "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz",
+      "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==",
       "optional": true
     },
     "@esbuild/android-arm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz",
-      "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz",
+      "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==",
       "optional": true
     },
     "@esbuild/android-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz",
-      "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz",
+      "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==",
       "optional": true
     },
     "@esbuild/android-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz",
-      "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz",
+      "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==",
       "optional": true
     },
     "@esbuild/darwin-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz",
-      "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz",
+      "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==",
       "optional": true
     },
     "@esbuild/darwin-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz",
-      "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz",
+      "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==",
       "optional": true
     },
     "@esbuild/freebsd-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz",
-      "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz",
+      "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==",
       "optional": true
     },
     "@esbuild/freebsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz",
-      "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz",
+      "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==",
       "optional": true
     },
     "@esbuild/linux-arm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz",
-      "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz",
+      "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==",
       "optional": true
     },
     "@esbuild/linux-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz",
-      "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz",
+      "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==",
       "optional": true
     },
     "@esbuild/linux-ia32": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz",
-      "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz",
+      "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==",
       "optional": true
     },
     "@esbuild/linux-loong64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz",
-      "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz",
+      "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==",
       "optional": true
     },
     "@esbuild/linux-mips64el": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz",
-      "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz",
+      "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==",
       "optional": true
     },
     "@esbuild/linux-ppc64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz",
-      "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz",
+      "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==",
       "optional": true
     },
     "@esbuild/linux-riscv64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz",
-      "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz",
+      "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==",
       "optional": true
     },
     "@esbuild/linux-s390x": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz",
-      "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz",
+      "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==",
       "optional": true
     },
     "@esbuild/linux-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz",
-      "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz",
+      "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==",
       "optional": true
     },
     "@esbuild/netbsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz",
-      "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz",
+      "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==",
       "optional": true
     },
     "@esbuild/openbsd-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz",
-      "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz",
+      "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==",
       "optional": true
     },
     "@esbuild/sunos-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz",
-      "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz",
+      "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==",
       "optional": true
     },
     "@esbuild/win32-arm64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz",
-      "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz",
+      "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==",
       "optional": true
     },
     "@esbuild/win32-ia32": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz",
-      "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz",
+      "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==",
       "optional": true
     },
     "@esbuild/win32-x64": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz",
-      "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz",
+      "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==",
       "optional": true
     },
+    "@fastify/busboy": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="
+    },
     "@fortawesome/fontawesome-free": {
       "version": "5.15.4",
       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
@@ -14209,9 +14303,9 @@
       }
     },
     "@leichtgewicht/ip-codec": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
-      "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+      "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="
     },
     "@ljharb/through": {
       "version": "2.3.13",
@@ -14221,12 +14315,6 @@
         "call-bind": "^1.0.7"
       }
     },
-    "@ngtools/webpack": {
-      "version": "17.3.2",
-      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.2.tgz",
-      "integrity": "sha512-E8zejFF4aJ8l2XcF+GgnE/1IqsZepnPT1xzulLB4LXtjVuXLFLoF9xkHQwxs7cJWWZsxd/SlNsCIcn/ezrYBcQ==",
-      "requires": {}
-    },
     "@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -14750,9 +14838,9 @@
       }
     },
     "@types/express-serve-static-core": {
-      "version": "4.17.43",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
-      "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
+      "version": "4.19.0",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz",
+      "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==",
       "requires": {
         "@types/node": "*",
         "@types/qs": "*",
@@ -14853,13 +14941,13 @@
       }
     },
     "@types/serve-static": {
-      "version": "1.15.5",
-      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
-      "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+      "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
       "requires": {
         "@types/http-errors": "*",
-        "@types/mime": "*",
-        "@types/node": "*"
+        "@types/node": "*",
+        "@types/send": "*"
       }
     },
     "@types/sockjs": {
@@ -15181,12 +15269,12 @@
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
     },
     "autoprefixer": {
-      "version": "10.4.18",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
-      "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
+      "version": "10.4.17",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
+      "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
       "requires": {
-        "browserslist": "^4.23.0",
-        "caniuse-lite": "^1.0.30001591",
+        "browserslist": "^4.22.2",
+        "caniuse-lite": "^1.0.30001578",
         "fraction.js": "^4.3.7",
         "normalize-range": "^0.1.2",
         "picocolors": "^1.0.0",
@@ -15827,9 +15915,9 @@
       "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
     },
     "critters": {
-      "version": "0.0.22",
-      "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz",
-      "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==",
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.20.tgz",
+      "integrity": "sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==",
       "requires": {
         "chalk": "^4.1.0",
         "css-select": "^5.1.0",
@@ -15837,7 +15925,7 @@
         "domhandler": "^5.0.2",
         "htmlparser2": "^8.0.2",
         "postcss": "^8.4.23",
-        "postcss-media-query-parser": "^0.2.3"
+        "pretty-bytes": "^5.3.0"
       },
       "dependencies": {
         "ansi-styles": {
@@ -16224,40 +16312,40 @@
       "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="
     },
     "esbuild": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz",
-      "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==",
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz",
+      "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==",
       "optional": true,
       "requires": {
-        "@esbuild/aix-ppc64": "0.20.1",
-        "@esbuild/android-arm": "0.20.1",
-        "@esbuild/android-arm64": "0.20.1",
-        "@esbuild/android-x64": "0.20.1",
-        "@esbuild/darwin-arm64": "0.20.1",
-        "@esbuild/darwin-x64": "0.20.1",
-        "@esbuild/freebsd-arm64": "0.20.1",
-        "@esbuild/freebsd-x64": "0.20.1",
-        "@esbuild/linux-arm": "0.20.1",
-        "@esbuild/linux-arm64": "0.20.1",
-        "@esbuild/linux-ia32": "0.20.1",
-        "@esbuild/linux-loong64": "0.20.1",
-        "@esbuild/linux-mips64el": "0.20.1",
-        "@esbuild/linux-ppc64": "0.20.1",
-        "@esbuild/linux-riscv64": "0.20.1",
-        "@esbuild/linux-s390x": "0.20.1",
-        "@esbuild/linux-x64": "0.20.1",
-        "@esbuild/netbsd-x64": "0.20.1",
-        "@esbuild/openbsd-x64": "0.20.1",
-        "@esbuild/sunos-x64": "0.20.1",
-        "@esbuild/win32-arm64": "0.20.1",
-        "@esbuild/win32-ia32": "0.20.1",
-        "@esbuild/win32-x64": "0.20.1"
+        "@esbuild/aix-ppc64": "0.20.0",
+        "@esbuild/android-arm": "0.20.0",
+        "@esbuild/android-arm64": "0.20.0",
+        "@esbuild/android-x64": "0.20.0",
+        "@esbuild/darwin-arm64": "0.20.0",
+        "@esbuild/darwin-x64": "0.20.0",
+        "@esbuild/freebsd-arm64": "0.20.0",
+        "@esbuild/freebsd-x64": "0.20.0",
+        "@esbuild/linux-arm": "0.20.0",
+        "@esbuild/linux-arm64": "0.20.0",
+        "@esbuild/linux-ia32": "0.20.0",
+        "@esbuild/linux-loong64": "0.20.0",
+        "@esbuild/linux-mips64el": "0.20.0",
+        "@esbuild/linux-ppc64": "0.20.0",
+        "@esbuild/linux-riscv64": "0.20.0",
+        "@esbuild/linux-s390x": "0.20.0",
+        "@esbuild/linux-x64": "0.20.0",
+        "@esbuild/netbsd-x64": "0.20.0",
+        "@esbuild/openbsd-x64": "0.20.0",
+        "@esbuild/sunos-x64": "0.20.0",
+        "@esbuild/win32-arm64": "0.20.0",
+        "@esbuild/win32-ia32": "0.20.0",
+        "@esbuild/win32-x64": "0.20.0"
       }
     },
     "esbuild-wasm": {
-      "version": "0.20.1",
-      "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz",
-      "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A=="
+      "version": "0.20.0",
+      "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.0.tgz",
+      "integrity": "sha512-Lc9KeQCg1Zf8kCtfDXgy29rx0x8dOuhDWbkP76Wc64q7ctOOc1Zv1C39AxiE+y4N6ONyXtJk4HKpM7jlU7/jSA=="
     },
     "escalade": {
       "version": "3.1.2",
@@ -16920,6 +17008,7 @@
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
       "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
+      "dev": true,
       "requires": {
         "agent-base": "^7.0.2",
         "debug": "4"
@@ -17046,6 +17135,7 @@
       "version": "9.2.15",
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz",
       "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==",
+      "dev": true,
       "requires": {
         "@ljharb/through": "^2.3.12",
         "ansi-escapes": "^4.3.2",
@@ -17068,6 +17158,7 @@
           "version": "4.3.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
           "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
           "requires": {
             "color-convert": "^2.0.1"
           }
@@ -17075,12 +17166,14 @@
         "chalk": {
           "version": "5.3.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
-          "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
+          "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+          "dev": true
         },
         "color-convert": {
           "version": "2.0.1",
           "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
           "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
           "requires": {
             "color-name": "~1.1.4"
           }
@@ -17088,12 +17181,14 @@
         "color-name": {
           "version": "1.1.4",
           "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
         },
         "rxjs": {
           "version": "7.8.1",
           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+          "dev": true,
           "requires": {
             "tslib": "^2.1.0"
           }
@@ -17102,6 +17197,7 @@
           "version": "6.2.0",
           "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
           "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+          "dev": true,
           "requires": {
             "ansi-styles": "^4.0.0",
             "string-width": "^4.1.0",
@@ -17749,6 +17845,7 @@
       "version": "0.30.8",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
       "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "dev": true,
       "requires": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       }
@@ -17862,9 +17959,9 @@
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
     },
     "mini-css-extract-plugin": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz",
-      "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz",
+      "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==",
       "requires": {
         "schema-utils": "^4.0.0",
         "tapable": "^2.2.1"
@@ -18655,9 +18752,9 @@
       "optional": true
     },
     "piscina": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz",
-      "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.3.1.tgz",
+      "integrity": "sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==",
       "requires": {
         "nice-napi": "^1.0.2"
       }
@@ -18721,20 +18818,15 @@
       }
     },
     "postcss-loader": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
-      "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz",
+      "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==",
       "requires": {
         "cosmiconfig": "^9.0.0",
         "jiti": "^1.20.0",
         "semver": "^7.5.4"
       }
     },
-    "postcss-media-query-parser": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
-      "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="
-    },
     "postcss-modules-extract-imports": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
@@ -18781,6 +18873,11 @@
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
     },
+    "pretty-bytes": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
+    },
     "proc-log": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
@@ -19159,6 +19256,8 @@
       "version": "1.71.1",
       "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
       "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
+      "optional": true,
+      "peer": true,
       "requires": {
         "chokidar": ">=3.0.0 <4.0.0",
         "immutable": "^4.0.0",
@@ -19166,9 +19265,9 @@
       }
     },
     "sass-loader": {
-      "version": "14.1.1",
-      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
-      "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz",
+      "integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==",
       "requires": {
         "neo-async": "^2.6.2"
       }
@@ -19757,17 +19856,6 @@
         }
       }
     },
-    "terser": {
-      "version": "5.29.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
-      "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
-      "requires": {
-        "@jridgewell/source-map": "^0.3.3",
-        "acorn": "^8.8.2",
-        "commander": "^2.20.0",
-        "source-map-support": "~0.5.20"
-      }
-    },
     "terser-webpack-plugin": {
       "version": "5.3.10",
       "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
@@ -19948,9 +20036,12 @@
       "devOptional": true
     },
     "undici": {
-      "version": "6.7.1",
-      "resolved": "https://registry.npmjs.org/undici/-/undici-6.7.1.tgz",
-      "integrity": "sha512-+Wtb9bAQw6HYWzCnxrPTMVEV3Q1QjYanI0E4q02ehReMuquQdLTEFEYbfs7hcImVYKcQkWSwT6buEmSVIiDDtQ=="
+      "version": "6.6.2",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz",
+      "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==",
+      "requires": {
+        "@fastify/busboy": "^2.0.0"
+      }
     },
     "undici-types": {
       "version": "5.26.5",
@@ -20083,7 +20174,6 @@
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
       "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
-      "peer": true,
       "requires": {
         "glob-to-regexp": "^0.4.1",
         "graceful-fs": "^4.1.2"
@@ -20106,19 +20196,18 @@
       }
     },
     "webpack": {
-      "version": "5.88.2",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
-      "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
-      "peer": true,
+      "version": "5.90.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
+      "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
       "requires": {
         "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^1.0.0",
+        "@types/estree": "^1.0.5",
         "@webassemblyjs/ast": "^1.11.5",
         "@webassemblyjs/wasm-edit": "^1.11.5",
         "@webassemblyjs/wasm-parser": "^1.11.5",
         "acorn": "^8.7.1",
         "acorn-import-assertions": "^1.9.0",
-        "browserslist": "^4.14.5",
+        "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
         "enhanced-resolve": "^5.15.0",
         "es-module-lexer": "^1.2.1",
@@ -20132,7 +20221,7 @@
         "neo-async": "^2.6.2",
         "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.3.7",
+        "terser-webpack-plugin": "^5.3.10",
         "watchpack": "^2.4.0",
         "webpack-sources": "^3.2.3"
       },
@@ -20141,7 +20230,6 @@
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
           "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-          "peer": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "fast-json-stable-stringify": "^2.0.0",
@@ -20153,20 +20241,17 @@
           "version": "3.5.2",
           "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
           "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-          "peer": true,
           "requires": {}
         },
         "json-schema-traverse": {
           "version": "0.4.1",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "peer": true
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
         },
         "schema-utils": {
           "version": "3.3.0",
           "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
           "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-          "peer": true,
           "requires": {
             "@types/json-schema": "^7.0.8",
             "ajv": "^6.12.5",
@@ -20176,9 +20261,9 @@
       }
     },
     "webpack-dev-middleware": {
-      "version": "6.1.2",
-      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz",
-      "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==",
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz",
+      "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==",
       "requires": {
         "colorette": "^2.0.10",
         "memfs": "^3.4.12",
diff --git a/package.json b/package.json
index 057b7c7..8c32c93 100644
--- a/package.json
+++ b/package.json
@@ -19,14 +19,14 @@
     "@angular/platform-browser": "^17.3.1",
     "@angular/platform-browser-dynamic": "^17.3.1",
     "@angular/router": "^17.3.1",
+    "@fortawesome/fontawesome-free": "^5.15.4",
     "@types/chrome": "^0.0.263",
     "rxjs": "~7.4.0",
     "tslib": "^2.3.0",
-    "zone.js": "~0.14.4",
-    "@fortawesome/fontawesome-free": "^5.15.4"
+    "zone.js": "~0.14.4"
   },
   "devDependencies": {
-    "@angular-devkit/build-angular": "^17.3.2",
+    "@angular-devkit/build-angular": "^17.2.3",
     "@angular/cli": "^17.3.2",
     "@angular/compiler-cli": "^17.3.1",
     "@types/jasmine": "~3.10.0",
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 806a9a2..6a8f331 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -12,15 +12,11 @@
 
   <final-stage *ngIf="!showOverview"
     [actions]="actions"
+    [finalStates] = "finalStates"
     [mainAttributes]="mainAttributes" 
     [finalStateTarget]="finalStateTarget"
     [savedAttributes]="savedAttributes"
-    [saveAttributeHandler]="saveAttributeHandler"
-    [isAttributeSavedHandler]="isAttributeSavedHandler"
-    [deleteFinalStateTargetHandler] = "deleteFinalStateTargetHandler"
-    [addFinalStateHandler] = "addFinalStateHandler"
-    [deleteFinalStateHandler] = "deleteFinalStateHandler"
     [unMarkFinalStateHandler]="unMarkFinalStateHandler"
   >
   </final-stage>
-</div>
\ No newline at end of file
+</div>
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 464870b..5bc8b6b 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -8,7 +8,6 @@ import {
   saveMarkFinalState,
   saveRecording
 } from './utils/storage.util';
-import {Message} from "./model/message.interface";
 import {Result} from "./model/result.interface";
 import {FinalStateTarget} from "./model/final-state-target.interface";
 import {ObjectKeys} from "./model/object-keys.interface";
@@ -24,100 +23,22 @@ export class AppComponent implements OnInit {
   recording: boolean = false;
   showOverview: boolean = true;
 
-  // START: MARK FINAL STATE
+  // data used and mofified in final state - here so it could be shared if necessary
   finalStates: Result[] = [];
   finalStateTarget?: FinalStateTarget = undefined;
   savedAttributes: ObjectKeys = {};
-
-  // array of attributes of final state which are always saved when the showed up
   mainAttributes: string[] = ['id', 'class', 'name'];
-  //protected readonly Object = Object; // just for the template
-  // END: MARK FINAL STATE
 
   constructor(private ref: ChangeDetectorRef){}
 
   ngOnInit(): void {
-    // START: MARK FINAL STATE
-    chrome.runtime.onMessage.addListener((message: Message) => {
-      if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
-        console.log("Final state target:", message)
-        this.finalStateTarget = message.finalStateClickTarget;
-        this.savedAttributes = {};
-        this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
-      }
-    });
-    
-    // END: MARK FINAL STATE
     chrome.storage.onChanged.addListener(() => {
+      console.log("Get data");
       this.getData();
     });
     this.getData();
   }
 
-  // START: MARK FINAL STATE
-  /**
-   * Deletes the clicked final state target and sets all variables to default values.
-   */
-  public deleteFinalStateTargetHandler(){
-    const xPath = this.finalStateTarget!.xPath;
-    this.finalStateTarget = undefined;
-    this.savedAttributes = {};
-
-    this.ref.detectChanges();
-    const message: Message = {
-      xPath: xPath
-    }
-    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
-      chrome.tabs.sendMessage(tabs[0].id!, message);
-    })
-  }
-
-  public saveAttributeHandler(event: Event, attribute: string, value: string){
-    if((event.target as HTMLInputElement).checked){
-      console.log(`Saving attribute ${attribute}=${value}`)
-      this.savedAttributes[attribute] = value;
-    } else {
-      console.log(`Deleting attribute ${attribute}=${value}`)
-      delete this.savedAttributes[attribute];
-    }
-    console.log(this.savedAttributes)
-  }
-
-  isAttributeSavedHandler(attribute: string): boolean {
-    return this.savedAttributes.hasOwnProperty(attribute);
-  }
-  
-  public addFinalStateHandler(){
-     // Call saveAttribute for each mandatory attribute
-    Object.keys(this.finalStateTarget!.attributes).forEach(attribute => {
-      if (this.mainAttributes.includes(attribute)) {
-        const mockEvent = { target: { checked: true } } as any;
-        this.saveAttributeHandler(mockEvent, attribute, this.finalStateTarget!.attributes[attribute]);
-      }
-    });
-
-    const element: HtmlElement = {
-      tagName: this.finalStateTarget!.tagName,
-      globalAttributes: {},
-      elementAttributes: {},
-      xPath: this.finalStateTarget!.xPath
-    };
-
-    const finalState: Result = {
-      element: element,
-      selectedAttributes: this.savedAttributes
-    };
-    this.finalStates.push(finalState);
-    saveFinalStates(this.finalStates);
-    this.deleteFinalStateTargetHandler();
-  }
-
-  public deleteFinalStateHandler = (index: number) => {
-    this.finalStates.splice(index, 1);
-    saveFinalStates(this.finalStates);
-  }
-  // END: MARK FINAL STATE
-
   /**
    * Loads all data needed by the component.
    */
@@ -128,11 +49,12 @@ export class AppComponent implements OnInit {
     await getRecording().then(recording => {
       this.recording = recording;
     });
-    // START: MARK FINAL STATE
+
+    // for final state
     await getFinalStates().then(finalStates => {
       this.finalStates = finalStates;
     });
-    // END: MARK FINAL STATE
+    
     this.ref.detectChanges();
     // we need to automatically detect changes, because chrome.storage.onChanged.addListener
     // is outside of the Angular scope
@@ -156,7 +78,12 @@ export class AppComponent implements OnInit {
     saveActions(this.actions);
   }
 
-  // START: MARK FINAL STATE
+  toggleView() {
+    this.showOverview = !this.showOverview;
+  }
+
+
+  // mark final state code
   public markFinalStateHandler = () => {
     saveMarkFinalState(true);
     this.toggleView();
@@ -166,9 +93,5 @@ export class AppComponent implements OnInit {
     //saveMarkFinalState(false);
     this.toggleView();
   }
-  // END: MARK FINAL STATE
-
-  toggleView() {
-    this.showOverview = !this.showOverview;
-  }
+  
 }
diff --git a/src/app/view/components/custom-button/custom-button.component.css b/src/app/view/components/custom-button/custom-button.component.css
index 63732bf..a969591 100644
--- a/src/app/view/components/custom-button/custom-button.component.css
+++ b/src/app/view/components/custom-button/custom-button.component.css
@@ -111,13 +111,13 @@
 
 
 .edit-button {
-    background-color: #999; /* Example color */
-    color: #fff; /* Example text color */
+    background-color: #999;
+    color: #fff;
 
 }
 
 .edit-button:hover {
-    background-color: #aaa; /* Example hover color */
+    background-color: #aaa;
 }
 
 
diff --git a/src/app/view/components/final-state/final-state.component.css b/src/app/view/components/final-state/final-state.component.css
index 83ffecf..bb73a8e 100644
--- a/src/app/view/components/final-state/final-state.component.css
+++ b/src/app/view/components/final-state/final-state.component.css
@@ -21,7 +21,7 @@
 }
 
 .first-component p {
-    margin: 0; /* or padding: 0; */
+    margin: 0;
 }
 
 .second-component {
@@ -33,7 +33,7 @@
 }
 
 .second-component p {
-    margin: 0; /* or padding: 0; */
+    margin: 0;
 }
 
 .third-component {
@@ -46,5 +46,5 @@
 
 .main-attributes {
     color:  #909090;
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
 }
\ No newline at end of file
diff --git a/src/app/view/components/final-state/final-state.component.html b/src/app/view/components/final-state/final-state.component.html
index 6f6df1c..cdd1d13 100644
--- a/src/app/view/components/final-state/final-state.component.html
+++ b/src/app/view/components/final-state/final-state.component.html
@@ -1,5 +1,3 @@
-
-
 <div class="container">
     <div class="component first-component">
         <p><b>{{finalState.element.tagName}}</b></p>
diff --git a/src/app/view/components/final-state/final-state.component.ts b/src/app/view/components/final-state/final-state.component.ts
index c676af0..b3c4327 100644
--- a/src/app/view/components/final-state/final-state.component.ts
+++ b/src/app/view/components/final-state/final-state.component.ts
@@ -1,9 +1,6 @@
 import { Component, Input } from '@angular/core';
-
-import { Action } from 'src/app/model/action.interface';
 import { Result } from 'src/app/model/result.interface';
-import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
-import { ObjectKeys } from 'src/app/model/object-keys.interface';
+
 
 @Component({
   selector: 'final-state',
diff --git a/src/app/view/pages/final-stage/final-stage.component.css b/src/app/view/pages/final-stage/final-stage.component.css
index d386906..13f4250 100644
--- a/src/app/view/pages/final-stage/final-stage.component.css
+++ b/src/app/view/pages/final-stage/final-stage.component.css
@@ -1,14 +1,10 @@
 .fs_container {
     background-color: rgb(232, 232, 232);
-    margin: 10px auto; /* Adjust the margin to center horizontally */
-    padding: 10px; /* Optionally add padding */
-    /*display: flex; /* Use flexbox for layout */
-    /*justify-content: center; /* Center horizontally */
-    /*align-items: center; /* Center vertically */
+    margin: 10px auto;
+    padding: 10px;
    
 }
 
-
 .custom-checkbox {
     appearance: none;
     width: 1.3em;
@@ -18,55 +14,55 @@
     vertical-align: middle;
     cursor: pointer;
     background-color: rgb(128, 128, 128);
-    padding: 10px; /* Remove padding */
-    display: flex; /* Use flexbox for centering */
-    justify-content: center; /* Center horizontally */
-    align-items: center; /* Center vertically */
+    padding: 10px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
 }
 
 .custom-checkbox:checked::after  {
-    content: '\f00c'; /* Font Awesome checkmark icon */
-    font-family: 'Font Awesome 5 Free'; /* Specify Font Awesome font family */
-    font-weight: 900; /* Adjust weight if necessary */
+    content: '\f00c';
+    font-family: 'Font Awesome 5 Free';
+    font-weight: 900;
     font-size: 1em;
     color: white;
 }
     
 .checkbox-container {
-    display: flex; /* Use flexbox for layout */
-    align-items: center; /* Align items vertically */
-    margin-bottom: 5px; /* Add margin between checkboxes */
-    flex-wrap: nowrap; /* Prevent wrapping of items */
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+    flex-wrap: nowrap;
 }
 
 .checkbox-container label {
-    margin-left: 5px; /* Add space between checkbox and text */
+    margin-left: 5px;
 }
 
 
 .button-container {
-    width: 100%; /* Ensures the container spans the full width */
+    width: 100%;
   }
 
 .return-fs-button {
     width: 100%;
-    background-color: #ffffff; /* Button background color */
-    color: #808080; /* Grey text color */
-    border: 1px solid #808080; /* Grey border */
-    padding: 10px; /* Adjust padding as needed */
+    background-color: #ffffff;
+    color: #808080;
+    border: 1px solid #808080;
+    padding: 10px;
     cursor: pointer;
-    box-sizing: border-box; /* Include padding and border in width */
-    text-decoration: none; /* Remove default link underline */
-    display: block; /* Ensures the button occupies the entire line */
+    box-sizing: border-box;
+    text-decoration: none;
+    display: block;
     text-align: right;
 }
 
 .return-fs-button:hover {
-    background-color: #f0f0f0; /* Lighter background color on hover */
+    background-color: #f0f0f0;
 }
 
 .return-fs-button:active {
-    background-color: #d9d9d9; /* Darker background color when button is active */
+    background-color: #d9d9d9;
 }  
 
 .attribute-name {
@@ -74,9 +70,9 @@
 }
 
 .attribute-value {
-    color: #333333; /* Darker grey color */
-    text-decoration: underline; /* Underline */
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
+    color: #333333;
+    text-decoration: underline;
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
     margin-left: 5px;
 }
 
@@ -88,5 +84,28 @@
 
 .main-attributes {
     color:  #909090;
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* Shadow around the value */
+    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); 
+}
+
+.container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100vh;
+}
+
+.content-container {
+    text-align: center;
+}
+
+.instruction {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 10px; 
+}
+  
+.waiting {
+    font-size: 16px;
+    color: #888;
 }
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index d291b6e..e5dcc47 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -1,147 +1,165 @@
 <div class="button-container">
-
-<custom-button
-            buttonText="Not a final state, go back"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-arrow-left"
-            additionalClasses="return-fs-button"
-            (click)="unMarkFinalStateHandler()"
-    >
-    </custom-button>
+  <custom-button
+              buttonText="Not a final state, go back"
+              size="S"
+              iconType="fontawesome"
+              icon="fa fa-arrow-left"
+              additionalClasses="return-fs-button"
+              (click)="unMarkFinalStateHandler()"
+      >
+      </custom-button>
 </div>
-<h3>How to recognize final state?</h3>
-<!-- <div id="scenario-tracker-content"> -->
-  <!-- START: MARK FINAL STATE -->
-  <ng-container *ngIf="finalStateTarget">  
-    <div class="fs_container">
-    <h3> Selected element: {{finalStateTarget.tagName}} </h3>
 
-    <!-- not chooseable attributes - default -->
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <ng-container *ngIf="mainAttributes.includes(attribute)">
-        <div class="checkbox-container">
-          <input type="checkbox" class="custom-checkbox" checked style="display: none;" 
+<ng-container *ngIf="!(!isMarkFinalState && finalStates && finalStates.length == 0)">
+  <div class="content-container">
+    <h2>How to recognize final state?</h2>
+  </div>
+</ng-container>
+
+
+<ng-container *ngIf="(!isMarkFinalState && finalStates && finalStates.length == 0); else elseBlock">
+  <div class="container content-container">
+    <p class="instruction">Click on the element you want to select</p>
+    <p class="waiting">Waiting...</p> 
+  </div>
+</ng-container>
+ 
+
+<ng-template #elseBlock>
+<ng-container *ngIf="!isMarkFinalState && finalStates && finalStates.length > 0">
+  <div class="content-container">
+    <p style="font-weight: bold;">Click on the next element you want to select</p>
+  </div>
+</ng-container>
+</ng-template>
+
+<ng-container *ngIf="finalStateTarget">  
+      <div class="fs_container">
+      <h3> Selected element: {{finalStateTarget.tagName}} </h3>
+
+      <!-- not chooseable attributes - default -->
+      <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
+        <ng-container *ngIf="mainAttributes.includes(attribute)">
+          <div class="checkbox-container">
+            <input type="checkbox" class="custom-checkbox" checked style="display: none;" 
+                (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
+            <label>
+              <span class="main-attributes main-attributes-name"> {{attribute}}: </span>
+              <span class="main-attributes">{{finalStateTarget.attributes[attribute]}} </span>
+            </label>
+          </div>
+        </ng-container>
+      </div>  
+
+      <ng-container *ngIf="finalStateTarget.text">
+        <h5>Text:</h5>
+        <p>{{finalStateTarget.text}}</p>
+      </ng-container>
+      <h4>Select characteristic properties</h4>
+      <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
+        <ng-container *ngIf="!mainAttributes.includes(attribute)">
+          <div class="checkbox-container">
+            <input type="checkbox" class="custom-checkbox" 
+              [checked]="isAttributeSavedHandler(attribute)" 
               (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
+            <label>
+              <span class="attribute-name"> {{attribute}} : </span>
+              <span class="attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+              <custom-button
+              buttonText=""
+              size="XS"
+              iconType="fontawesome"
+              icon="fa fa-edit"
+              additionalClasses="edit-button"
+              additionalStyles="margin-left: 3px;"
+              >
+              </custom-button>
+            </label>
+          </div>
+        </ng-container>
+      </div>
+      <h5>Styles</h5>
+      <!-- <h5>Explicitly set styles:</h5> -->
+      <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
+        <div class="checkbox-container">
+          <input type="checkbox" class="custom-checkbox"
+            [checked]="isAttributeSavedHandler('style.'+attribute)" 
+            (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.styles[attribute])">
           <label>
-            <span class="main-attributes main-attributes-name"> {{attribute}}: </span>
-            <span class="main-attributes">{{finalStateTarget.attributes[attribute]}} </span>
-          </label>
-        </div>
-      </ng-container>
-    </div>  
+            <span class="attribute-name"> {{attribute}}: </span>
+            <span class="attribute-value">{{finalStateTarget.styles[attribute]}} </span>
+            <custom-button
+              buttonText=""
+              size="XS"
+              iconType="fontawesome"
+              icon="fa fa-edit"
+              additionalClasses="edit-button"
+              additionalStyles="margin-left: 3px;"
+              >
+              </custom-button>
 
-    <ng-container *ngIf="finalStateTarget.text">
-      <h5>Text:</h5>
-      <p>{{finalStateTarget.text}}</p>
-    </ng-container>
-    <h4>Select characteristic properties</h4>
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
-      <ng-container *ngIf="!mainAttributes.includes(attribute)">
+        </label>
+        </div>
+      </div>
+      <!-- <h5>Computed styles:</h5> -->
+      <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">
         <div class="checkbox-container">
-          <input type="checkbox" class="custom-checkbox" 
-            [checked]="isAttributeSavedHandler(attribute)" 
-            (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
+        <input type="checkbox" class="custom-checkbox"
+          [checked]="isAttributeSavedHandler('style.'+attribute)"
+          (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.computedStyles[attribute])">
           <label>
-            <span class="attribute-name"> {{attribute}} : </span>
-            <span class="attribute-value">{{finalStateTarget.attributes[attribute]}} </span>
+            <span class="attribute-name"> {{attribute}}: </span>
+            <span class="attribute-value">{{finalStateTarget.computedStyles[attribute]}} </span> 
             <custom-button
-            buttonText=""
-            size="XS"
-            iconType="fontawesome"
-            icon="fa fa-edit"
-            additionalClasses="edit-button"
-            additionalStyles="margin-left: 3px;"
-            >
-            </custom-button>
-          </label>
+              buttonText=""
+              size="XS"
+              iconType="fontawesome"
+              icon="fa fa-edit"
+              additionalClasses="edit-button"
+              additionalStyles="margin-left: 3px;"
+              
+              >
+              </custom-button>
+        </label>
         </div>
-      </ng-container>
-    </div>
-    <h5>Styles</h5>
-    <!-- <h5>Explicitly set styles:</h5> -->
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.styles)">
-      <div class="checkbox-container">
-        <input type="checkbox" class="custom-checkbox"
-          [checked]="isAttributeSavedHandler('style.'+attribute)" 
-          (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.styles[attribute])">
-        <label>
-          <span class="attribute-name"> {{attribute}}: </span>
-          <span class="attribute-value">{{finalStateTarget.styles[attribute]}} </span>
-          <custom-button
-            buttonText=""
-            size="XS"
-            iconType="fontawesome"
-            icon="fa fa-edit"
-            additionalClasses="edit-button"
-            additionalStyles="margin-left: 3px;"
-            >
-            </custom-button>
-
-       </label>
       </div>
-    </div>
-    <!-- <h5>Computed styles:</h5> -->
-    <div *ngFor="let attribute of Object.keys(finalStateTarget.computedStyles)">
-      <div class="checkbox-container">
-      <input type="checkbox" class="custom-checkbox"
-        [checked]="isAttributeSavedHandler('style.'+attribute)"
-        (change)="saveAttributeHandler($event, 'style.'+attribute, finalStateTarget.computedStyles[attribute])">
-        <label>
-          <span class="attribute-name"> {{attribute}}: </span>
-          <span class="attribute-value">{{finalStateTarget.computedStyles[attribute]}} </span> 
-          <custom-button
-            buttonText=""
-            size="XS"
-            iconType="fontawesome"
-            icon="fa fa-edit"
-            additionalClasses="edit-button"
-            additionalStyles="margin-left: 3px;"
-            
-            >
-            </custom-button>
-       </label>
-       </div>
-    </div>
 
-    <span style="margin-top: 1em;">
-    <custom-button
-            buttonText="Cancel"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-trash"
-            additionalClasses="delete-button"
-            additionalStyles="width: 10em;"
-            (click)="deleteFinalStateTargetHandler()"
-    >
-    </custom-button>
-    <custom-button
-            buttonText="Add"
-            size="S"
-            iconType="fontawesome"
-            icon="fa fa-plus"
-            additionalClasses="success-button pl_fs"
-            additionalStyles="margin-left: 2em; width: 10em;"
-            (click)="addFinalStateHandler()"
+      <span style="margin-top: 1em;">
+      <custom-button
+              buttonText="Cancel"
+              size="S"
+              iconType="fontawesome"
+              icon="fa fa-trash"
+              additionalClasses="delete-button"
+              additionalStyles="width: 10em;"
+              (click)="deleteFinalStateTargetHandler()"
       >
-    </custom-button>
-    </span>
-  </div>
-  </ng-container>
-
-  <h3>Final states:</h3>
-  <div *ngFor="let finalState of finalStates; let i = index">
-    <final-state 
-        [index]="i"
-        [finalState]="finalState"
-        [deleteFinalState]="deleteFinalStateHandler()"
-        [mainAttributes]="mainAttributes"
+      </custom-button>
+      <custom-button
+              buttonText="Add"
+              size="S"
+              iconType="fontawesome"
+              icon="fa fa-plus"
+              additionalClasses="success-button pl_fs"
+              additionalStyles="margin-left: 2em; width: 10em;"
+              (click)="addFinalStateHandler()"
         >
-      </final-state>
-  </div>  
+      </custom-button>
+      </span>
+    </div>
+    </ng-container>
+ 
 
-  <!-- END: MARK FINAL STATE -->
-  <!--
-</div>
+<ng-container *ngIf="finalStates && finalStates.length > 0">
+  <h3>Final states:</h3>
+    <div *ngFor="let finalState of finalStates; let i = index">
+      <final-state 
+          [index]="i"
+          [finalState]="finalState"
+          [deleteFinalState]="deleteFinalStateHandler"
+          [mainAttributes]="mainAttributes"
+          >
+        </final-state>
+    </div> 
 
--->
\ No newline at end of file
+</ng-container>
\ No newline at end of file
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index 62525b3..20f2d2b 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -1,9 +1,23 @@
-import { Component, OnInit, Input } from '@angular/core';
+import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core';
 import { Action } from 'src/app/model/action.interface';
 import { Result } from 'src/app/model/result.interface';
 import { FinalStateTarget } from 'src/app/model/final-state-target.interface';
 import { ObjectKeys } from 'src/app/model/object-keys.interface';
 
+import { HtmlElement } from 'src/app/model/html-element.interface'; 
+
+import { Message } from 'src/app/model/message.interface'; 
+
+import {
+  getActions,
+  getFinalStates,
+  getRecording,
+  saveActions, saveFinalStates,
+  getMarkFinalState,
+  saveMarkFinalState,
+  saveRecording
+} from 'src/app/utils/storage.util';
+
 
 @Component({
   selector: 'final-stage',
@@ -15,45 +29,104 @@ export class FinalStageComponent implements OnInit {
     @Input() actions: Action[] = [];
     @Input() finalStates: Result[] = [];
     @Input() finalStateTarget?: FinalStateTarget = undefined;
-    //finalStateTarget?: FinalStateTarget = undefined;  //used for testing view
- 
     @Input() savedAttributes: ObjectKeys = {};
 
-
-    @Input() saveAttributeHandler!: Function;
-    @Input() isAttributeSavedHandler!: Function;
-    @Input() deleteFinalStateTargetHandler!: Function;
-    @Input() addFinalStateHandler!: Function;
-    @Input() deleteFinalStateHandler!: Function;
     @Input() unMarkFinalStateHandler!: Function;
     
     @Input() mainAttributes: string[] = [];
 
-    constructor(){}
+    constructor(private ref: ChangeDetectorRef){}
+
+    isMarkFinalState: boolean = false;
 
     protected readonly Object = Object; // just for the template
+    
     ngOnInit(): void {
-       // Initialize finalStateTarget to the default value for testing purposes
-      /* 
-      this.finalStateTarget = {
-        tagName: 'div', // Example tag name
-        text: 'Assault rifle-wielding standard trooper', // Example text
-        xPath: '/HTML[1]/BODY[1]/DIV[1]', // Example XPath
-        attributes: {
-          dir: 'ltr', // Example attribute
-          army: 'Republic Clone Army', // Example additional attribute
-          id: 'random-id', // Random id
-         class: 'random-class' // Random class
-        },
-        styles: {
-          outline: 'rgba(218, 172, 0, 0.8) solid 5px', // Example style
-          color: 'blue' // Another example style
-        },
-        computedStyles: {
-          fontFamily: 'Arial', // Example computed style
-          fontSize: '16px' // Another example computed style
+      console.log("Final stage ngOnInit()");
+      chrome.runtime.onMessage.addListener((message: Message) => {
+        if (message.finalStateClickTarget) {
+          this.isMarkFinalState = true;
+        } else {
+          // Otherwise, check getMarkFinalState
+            getMarkFinalState().then(markFinalState => {
+            this.isMarkFinalState = markFinalState;
+          });
+        }
+
+        if(message.finalStateClickTarget){ // in this component, we only listen for messages marking the final state
+          console.log("Final state target:", message)
+          this.finalStateTarget = message.finalStateClickTarget;
+          this.savedAttributes = {};
+          this.ref.detectChanges(); // chrome.runtime.onMessage is outside of Angular scope, so we need to detect changes manually
         }
-      };
-      */
+      });
+    }
+
+   /**
+   * Deletes the clicked final state target and sets all variables to default values.
+   */
+  public deleteFinalStateTargetHandler(){
+    const xPath = this.finalStateTarget!.xPath;
+    this.finalStateTarget = undefined;
+    this.savedAttributes = {};
+
+    this.ref.detectChanges();
+    const message: Message = {
+      xPath: xPath
+    }
+    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
+      chrome.tabs.sendMessage(tabs[0].id!, message);
+    })
+  }
+
+  public saveAttributeHandler(event: Event, attribute: string, value: string){
+    if((event.target as HTMLInputElement).checked){
+      console.log(`Saving attribute ${attribute}=${value}`)
+      this.savedAttributes[attribute] = value;
+    } else {
+      console.log(`Deleting attribute ${attribute}=${value}`)
+      delete this.savedAttributes[attribute];
     }
+    console.log(this.savedAttributes)
+  }
+
+  isAttributeSavedHandler(attribute: string): boolean {
+    return this.savedAttributes.hasOwnProperty(attribute);
+  }
+  
+  public addFinalStateHandler(){
+     // Call saveAttribute for each mandatory attribute
+    Object.keys(this.finalStateTarget!.attributes).forEach(attribute => {
+      if (this.mainAttributes.includes(attribute)) {
+        const mockEvent = { target: { checked: true } } as any;
+        this.saveAttributeHandler(mockEvent, attribute, this.finalStateTarget!.attributes[attribute]);
+      }
+    });
+
+    const element: HtmlElement = {
+      tagName: this.finalStateTarget!.tagName,
+      globalAttributes: {},
+      elementAttributes: {},
+      xPath: this.finalStateTarget!.xPath
+    };
+
+    const finalState: Result = {
+      element: element,
+      selectedAttributes: this.savedAttributes
+    };
+    this.finalStates.push(finalState);
+    saveFinalStates(this.finalStates);
+    this.deleteFinalStateTargetHandler();
+
+    saveMarkFinalState(true);
+
+    this.isMarkFinalState = false;
+  }
+
+  public deleteFinalStateHandler = (index: number) => {
+    this.finalStates.splice(index, 1);
+    saveFinalStates(this.finalStates);
+
+    this.isMarkFinalState = false;
+  }
 }
diff --git a/src/app/view/pages/overview/overview.component.html b/src/app/view/pages/overview/overview.component.html
index 81c20d0..285c1b8 100644
--- a/src/app/view/pages/overview/overview.component.html
+++ b/src/app/view/pages/overview/overview.component.html
@@ -51,7 +51,7 @@
   <captured-action [action]="action" [deleteActionHandler]="deleteActionHandler" [startTime]="startTime" [actionIndex]="i"></captured-action>
 </div>
 
-<!-- START: MARK FINAL STATE -->
+
 <custom-button
 buttonText="Mark final state"
 size="M"
@@ -60,5 +60,4 @@ icon="fa fa-solid fa-flag-checkered"
 additionalClasses="info-button"
 additionalStyles="margin-top: 2em; margin-left:1em;"
 (click)="markFinalStateHandler()">
-</custom-button>
-<!-- END: MARK FINAL STATE -->
\ No newline at end of file
+</custom-button>
\ No newline at end of file
diff --git a/src/app/view/pages/overview/overview.component.ts b/src/app/view/pages/overview/overview.component.ts
index b6ef2e6..8139376 100644
--- a/src/app/view/pages/overview/overview.component.ts
+++ b/src/app/view/pages/overview/overview.component.ts
@@ -15,16 +15,13 @@ export class OverviewComponent implements OnInit {
   @Input() deleteActionHandler!: Function;
   @Input() deleteAllActionsHandler!: Function;
   @Input() toggleRecordingHandler!: Function;
-  // START: MARK FINAL STATE
+
   @Input() markFinalStateHandler!: Function;
-  // END: MARK FINAL STATE
 
   readonly ActionEnum = ActionEnum
   
   startTime?: Date;
 
-  startTime?: Date;
-
   constructor() { }
 
   ngOnInit(): void {
-- 
GitLab


From 593a74309767f6d333ff66c36fe212a14bd60753 Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Sat, 6 Apr 2024 15:01:56 +0200
Subject: [PATCH 8/9] =?UTF-8?q?Re=20#11122=20-=20oprava=20ozna=C4=8Den?=
 =?UTF-8?q?=C3=AD=20zobrazen=C3=A9ho=20elementu?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- při return z final-stage do overview se zruší oznašení html elementu
- při zrušení elementu (cancel) lze zvolit jiný element
---
 src/app/app.component.ts                      |  4 +---
 .../final-stage/final-stage.component.html    |  2 +-
 .../final-stage/final-stage.component.ts      | 23 +++++++++++++------
 3 files changed, 18 insertions(+), 11 deletions(-)

diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 5bc8b6b..427072c 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -11,7 +11,6 @@ import {
 import {Result} from "./model/result.interface";
 import {FinalStateTarget} from "./model/final-state-target.interface";
 import {ObjectKeys} from "./model/object-keys.interface";
-import {HtmlElement} from "./model/html-element.interface";
 
 @Component({
   selector: 'app-root',
@@ -90,8 +89,7 @@ export class AppComponent implements OnInit {
   }
 
   public unMarkFinalStateHandler = () => {
-    //saveMarkFinalState(false);
+    saveMarkFinalState(false); // no longer mark another element
     this.toggleView();
   }
-  
 }
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index e5dcc47..9c75e0a 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -5,7 +5,7 @@
               iconType="fontawesome"
               icon="fa fa-arrow-left"
               additionalClasses="return-fs-button"
-              (click)="unMarkFinalStateHandler()"
+              (click)="returnHandler()"
       >
       </custom-button>
 </div>
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index 20f2d2b..1dff8db 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -9,13 +9,9 @@ import { HtmlElement } from 'src/app/model/html-element.interface';
 import { Message } from 'src/app/model/message.interface'; 
 
 import {
-  getActions,
-  getFinalStates,
-  getRecording,
-  saveActions, saveFinalStates,
+  saveFinalStates,
   getMarkFinalState,
   saveMarkFinalState,
-  saveRecording
 } from 'src/app/utils/storage.util';
 
 
@@ -31,8 +27,8 @@ export class FinalStageComponent implements OnInit {
     @Input() finalStateTarget?: FinalStateTarget = undefined;
     @Input() savedAttributes: ObjectKeys = {};
 
+    //@Input() unMarkFinalStateHandler!: Function;
     @Input() unMarkFinalStateHandler!: Function;
-    
     @Input() mainAttributes: string[] = [];
 
     constructor(private ref: ChangeDetectorRef){}
@@ -77,6 +73,10 @@ export class FinalStageComponent implements OnInit {
     chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
       chrome.tabs.sendMessage(tabs[0].id!, message);
     })
+
+    // called so we can mark another element
+    saveMarkFinalState(true);
+    this.isMarkFinalState = false;
   }
 
   public saveAttributeHandler(event: Event, attribute: string, value: string){
@@ -119,7 +119,6 @@ export class FinalStageComponent implements OnInit {
     this.deleteFinalStateTargetHandler();
 
     saveMarkFinalState(true);
-
     this.isMarkFinalState = false;
   }
 
@@ -129,4 +128,14 @@ export class FinalStageComponent implements OnInit {
 
     this.isMarkFinalState = false;
   }
+
+
+  public returnHandler()
+  {
+    // unmark selected object
+    if (this.finalStateTarget) {
+      this.deleteFinalStateTargetHandler();
+    }
+    this.unMarkFinalStateHandler();
+  }
 }
-- 
GitLab


From 1d39c42a4df7dcf21ab5c007cade35a6ef2d955f Mon Sep 17 00:00:00 2001
From: vondrp <vondrovic@centrum.cz>
Date: Sat, 6 Apr 2024 15:47:45 +0200
Subject: [PATCH 9/9] =?UTF-8?q?Re=20#11122=20:=20Odstranil=20text-shadow,?=
 =?UTF-8?q?=20mainAttributy=20p=C5=99idal=20do=20element=20atribut=C5=AF?=
 =?UTF-8?q?=20m=C3=ADsto=20selected=20atribut=C5=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/final-state/final-state.component.css     | 1 -
 .../components/final-state/final-state.component.html    | 9 ++-------
 .../view/components/final-state/final-state.component.ts | 1 -
 src/app/view/pages/final-stage/final-stage.component.css | 2 --
 .../view/pages/final-stage/final-stage.component.html    | 3 ---
 src/app/view/pages/final-stage/final-stage.component.ts  | 9 ++++++---
 6 files changed, 8 insertions(+), 17 deletions(-)

diff --git a/src/app/view/components/final-state/final-state.component.css b/src/app/view/components/final-state/final-state.component.css
index bb73a8e..0b79471 100644
--- a/src/app/view/components/final-state/final-state.component.css
+++ b/src/app/view/components/final-state/final-state.component.css
@@ -46,5 +46,4 @@
 
 .main-attributes {
     color:  #909090;
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
 }
\ No newline at end of file
diff --git a/src/app/view/components/final-state/final-state.component.html b/src/app/view/components/final-state/final-state.component.html
index cdd1d13..74912f0 100644
--- a/src/app/view/components/final-state/final-state.component.html
+++ b/src/app/view/components/final-state/final-state.component.html
@@ -2,19 +2,14 @@
     <div class="component first-component">
         <p><b>{{finalState.element.tagName}}</b></p>
         
-        <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
-            <ng-container *ngIf="mainAttributes.includes(attribute)">
-                <!-- show only first 3 -->
-                <p class="main-attributes S" *ngIf="j < 3">{{attribute}}: {{finalState.selectedAttributes[attribute]}}</p>
-            </ng-container>
+        <div *ngFor="let attribute of Object.keys(finalState.element.elementAttributes); let j = index">
+                <p class="main-attributes S" *ngIf="j < 3">{{attribute}}: {{finalState.element.elementAttributes[attribute]}}</p>
           </div>
     </div>
     <div class="component second-component">
         <div *ngFor="let attribute of Object.keys(finalState.selectedAttributes); let j = index">
             <!-- show only first 3 -->
-            <ng-container *ngIf="!mainAttributes.includes(attribute)">
                 <p *ngIf="j < 3"><span style="text-transform: capitalize;">{{attribute}}: </span> <b>{{finalState.selectedAttributes[attribute]}}</b></p>
-            </ng-container>
           </div>
     </div>    
     <div class="component third-component">
diff --git a/src/app/view/components/final-state/final-state.component.ts b/src/app/view/components/final-state/final-state.component.ts
index b3c4327..92e5d85 100644
--- a/src/app/view/components/final-state/final-state.component.ts
+++ b/src/app/view/components/final-state/final-state.component.ts
@@ -13,7 +13,6 @@ export class FinalStateComponent {
 
   @Input() deleteFinalState!: Function;
   @Input() index!: number;
-  @Input() mainAttributes: string[] = []
 
   protected readonly Object = Object; // just for the template
 }
diff --git a/src/app/view/pages/final-stage/final-stage.component.css b/src/app/view/pages/final-stage/final-stage.component.css
index 13f4250..460a05b 100644
--- a/src/app/view/pages/final-stage/final-stage.component.css
+++ b/src/app/view/pages/final-stage/final-stage.component.css
@@ -72,7 +72,6 @@
 .attribute-value {
     color: #333333;
     text-decoration: underline;
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
     margin-left: 5px;
 }
 
@@ -84,7 +83,6 @@
 
 .main-attributes {
     color:  #909090;
-    text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); 
 }
 
 .container {
diff --git a/src/app/view/pages/final-stage/final-stage.component.html b/src/app/view/pages/final-stage/final-stage.component.html
index 9c75e0a..c3f488e 100644
--- a/src/app/view/pages/final-stage/final-stage.component.html
+++ b/src/app/view/pages/final-stage/final-stage.component.html
@@ -41,8 +41,6 @@
       <div *ngFor="let attribute of Object.keys(finalStateTarget.attributes)">
         <ng-container *ngIf="mainAttributes.includes(attribute)">
           <div class="checkbox-container">
-            <input type="checkbox" class="custom-checkbox" checked style="display: none;" 
-                (change)="saveAttributeHandler($event, attribute, finalStateTarget.attributes[attribute])">
             <label>
               <span class="main-attributes main-attributes-name"> {{attribute}}: </span>
               <span class="main-attributes">{{finalStateTarget.attributes[attribute]}} </span>
@@ -157,7 +155,6 @@
           [index]="i"
           [finalState]="finalState"
           [deleteFinalState]="deleteFinalStateHandler"
-          [mainAttributes]="mainAttributes"
           >
         </final-state>
     </div> 
diff --git a/src/app/view/pages/final-stage/final-stage.component.ts b/src/app/view/pages/final-stage/final-stage.component.ts
index 1dff8db..1b0e29d 100644
--- a/src/app/view/pages/final-stage/final-stage.component.ts
+++ b/src/app/view/pages/final-stage/final-stage.component.ts
@@ -96,17 +96,20 @@ export class FinalStageComponent implements OnInit {
   
   public addFinalStateHandler(){
      // Call saveAttribute for each mandatory attribute
+     const elementAttributes: ObjectKeys = {} as ObjectKeys;
+
     Object.keys(this.finalStateTarget!.attributes).forEach(attribute => {
       if (this.mainAttributes.includes(attribute)) {
-        const mockEvent = { target: { checked: true } } as any;
-        this.saveAttributeHandler(mockEvent, attribute, this.finalStateTarget!.attributes[attribute]);
+        elementAttributes[attribute] = this.finalStateTarget!.attributes[attribute];
+        //const mockEvent = { target: { checked: true } } as any;
+        //this.saveAttributeHandler(mockEvent, attribute, this.finalStateTarget!.attributes[attribute]);
       }
     });
 
     const element: HtmlElement = {
       tagName: this.finalStateTarget!.tagName,
       globalAttributes: {},
-      elementAttributes: {},
+      elementAttributes: elementAttributes,
       xPath: this.finalStateTarget!.xPath
     };
 
-- 
GitLab