UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

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