UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

209 lines (205 loc) 9.22 kB
import * as i0 from '@angular/core'; import { signal, inject, Injectable } from '@angular/core'; import { cloneDeep, isEqual } from 'lodash-es'; import { BehaviorSubject } from 'rxjs'; import { InventoryService } from '@c8y/client'; /** * Manages dashboard-level state with dirty tracking and persistence capabilities. * * This service provides a centralized state management system for context-aware dashboards. * It tracks the original (default) state from the dashboard configuration, maintains a * working copy of the state, and determines when changes need to be saved based on deep * equality comparison. * * **Key responsibilities:** * - Extract and store the dashboard's default state on selection * - Maintain a mutable working state (globalState) that can be updated * - Track whether current state differs from default (dirty tracking) * - Enable/disable save functionality based on state changes * - Persist state changes back to the dashboard via inventory API * * @typeParam StateType - The type of state object managed by this service * * @example * ```ts * // Dashboard with global time context state * interface TimeContextState { * dateFrom: string; * dateTo: string; * interval: string; * } * * const stateService = inject(ContextDashboardStateService<TimeContextState>); * * // Set dashboard and extract its default state * stateService.setSelectedDashboard(dashboard); * * // Update state (triggers dirty tracking) * stateService.updateGlobalState({ dateFrom: '2024-01-01' }); * * // Check if save is needed * stateService.isSaveDisabled.subscribe(disabled => { * if (!disabled) { * // Save button enabled - state has changed * } * }); * ``` */ class ContextDashboardStateService { constructor() { /** Currently selected dashboard object with its embedded state */ this.selectedDashboard = signal(null, ...(ngDevMode ? [{ debugName: "selectedDashboard" }] : [])); /** Immutable snapshot of the dashboard's original state, used for comparison */ this.dashboardDefaultState = signal(null, ...(ngDevMode ? [{ debugName: "dashboardDefaultState" }] : [])); /** Current working state that can be modified, separate from the default state */ this.globalState = signal(null, ...(ngDevMode ? [{ debugName: "globalState" }] : [])); /** Observable for dashboard selection changes (legacy support) */ this.selected$ = new BehaviorSubject(null); /** Observable indicating whether save should be disabled (true when no changes detected) */ this.isSaveDisabled = new BehaviorSubject(true); /** Observable emitting the dashboard object after successful save operations */ this.dashboardSaved = new BehaviorSubject(null); this.inventory = inject(InventoryService); } /** * Selects a dashboard and initializes state management for it. * * This method performs the complete initialization workflow when a dashboard is selected: * 1. Extracts the dashboard's embedded state from c8y_Dashboard.dashboardState * 2. Creates an immutable snapshot as the default state (for dirty tracking) * 3. Creates a working copy as the current global state (for modifications) * 4. Resets the save button state to disabled (no changes yet) * * **Deep cloning strategy:** * Both default state and global state are deep cloned to ensure complete isolation. * This prevents accidental mutations from affecting the comparison baseline. * * **Null handling:** * When null/undefined is passed, all state is cleared and signals/observables are reset. * * @param dashboard - Dashboard object containing state at c8y_Dashboard.dashboardState, * or null to clear the selection */ setSelectedDashboard(dashboard) { if (!dashboard) { this.selected$.next(dashboard); this.dashboardDefaultState.set(null); this.globalState.set(null); this.selectedDashboard.set(null); return; } const defaultState = this.extractStateFromDashboard(dashboard); const initialDefaultState = cloneDeep(defaultState) ?? null; this.dashboardDefaultState.set(initialDefaultState); this.globalState.set(initialDefaultState ? cloneDeep(initialDefaultState) : null); this.selectedDashboard.set(dashboard); this.selected$.next(dashboard); this.isSaveDisabled.next(true); } /** * Extracts state from the standard dashboard structure. * * Retrieves the state object stored at the conventional path: * `dashboard.c8y_Dashboard.dashboardState` * * @param dashboard - Dashboard object with potential state at c8y_Dashboard.dashboardState * @returns Extracted state object, or null if dashboard is null or has no state */ extractStateFromDashboard(dashboard) { return dashboard?.c8y_Dashboard?.dashboardState ?? null; } /** * Merges partial state updates into the current working state with dirty tracking. * * This method applies partial updates to the global state using a shallow merge strategy, * then performs deep equality comparison against the default state to determine if save * should be enabled. * * @param newState - Partial state object with properties to merge into current state * * @example * ```ts * // Initial state: { dateFrom: '2024-01-01', dateTo: '2024-01-02' } * stateService.updateGlobalState({ dateFrom: '2024-02-01' }); * // Result: { dateFrom: '2024-02-01', dateTo: '2024-01-02' } * // isSaveDisabled = false (changed from default) * ``` */ updateGlobalState(newState) { const currentState = this.globalState(); if (currentState === null && Object.keys(newState).length === 0) { return; } const updatedState = { ...(currentState ? structuredClone(currentState) : {}), ...structuredClone(newState) }; if (!isEqual(currentState, updatedState)) { this.globalState.set(updatedState); const defaultState = this.dashboardDefaultState(); this.isSaveDisabled.next(isEqual(defaultState, updatedState)); } } /** * Returns a deep clone of the current working state. * * The returned object is a deep copy, so modifications won't affect the internal state. * Use this when you need to read state without risking accidental mutations. * * @returns Deep cloned copy of current state, or null if no state exists */ getGlobalState() { const state = this.globalState(); return structuredClone(state); } /** * Reverts the working state back to the dashboard's original default state. * * This effectively discards all changes made since the dashboard was selected, * resetting to the state that was extracted from c8y_Dashboard.dashboardState. * Also disables the save button since state now matches the default. * * **Use case:** * Called when user clicks "Cancel" or "Reset" to discard unsaved changes. */ resetGlobalState() { const defaultState = this.dashboardDefaultState(); this.globalState.set(defaultState ? defaultState : null); this.isSaveDisabled.next(true); } /** * Persists state changes to the dashboard via the Cumulocity inventory API. * * Updates the dashboard's c8y_Dashboard.dashboardState property with the provided * state object and saves it to the backend. Creates the c8y_Dashboard object if * it doesn't exist. * * **Note:** This method doesn't automatically update the local signals. After a * successful save, you should emit to dashboardSaved to notify listeners. * * @param mo - Managed object (dashboard) to update * @param state - State object to persist to c8y_Dashboard.dashboardState * @returns Promise resolving to the updated dashboard object from the API * @throws Error if the inventory update fails */ async saveDashboardState(mo, state) { if (!('c8y_Dashboard' in mo)) { mo.c8y_Dashboard = {}; } mo.c8y_Dashboard.dashboardState = state; return await this.inventory.update(mo); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ContextDashboardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ContextDashboardStateService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ContextDashboardStateService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Generated bundle index. Do not edit. */ export { ContextDashboardStateService }; //# sourceMappingURL=c8y-ngx-components-context-dashboard-state.mjs.map