@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 932 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-global-context.mjs","sources":["../../global-context/models/global-context.model.ts","../../global-context/models/auto-refresh.model.ts","../../global-context/models/interval-picker.model.ts","../../global-context/models/global-context-defaults.ts","../../global-context/models/control-flow.model.ts","../../global-context/models/aggregation.model.ts","../../global-context/models/constants.ts","../../global-context/utils/date-time-context.util.ts","../../global-context/utils/global-context-defaults.util.ts","../../global-context/services/state/global-context.service.ts","../../global-context/services/state/global-context-form.service.ts","../../global-context/services/state/global-context-event.service.ts","../../global-context/services/domain/widget-control.service.ts","../../global-context/services/domain/date-time-context-picker.service.ts","../../global-context/services/domain/widget-config-migration.service.ts","../../global-context/services/domain/global-context-validation.service.ts","../../global-context/services/domain/aggregation-validation.service.ts","../../global-context/services/domain/aggregation-picker.service.ts","../../global-context/services/infrastructure/global-context-utils.service.ts","../../global-context/features/aggregation/aggregation-picker/aggregation-picker.component.ts","../../global-context/features/aggregation/aggregation-picker/aggregation-picker.component.html","../../global-context/features/configuration/history-mode-configuration-controls/history-mode-configuration-controls.component.ts","../../global-context/features/configuration/history-mode-configuration-controls/history-mode-configuration-controls.component.html","../../global-context/features/time-context/interval-picker/interval-picker.component.ts","../../global-context/features/time-context/interval-picker/interval-picker.component.html","../../global-context/features/time-context/time-range-picker/date-time-context-picker.component.ts","../../global-context/features/time-context/time-range-picker/date-time-context-picker.component.html","../../global-context/features/configuration/live-mode-configuration-controls/live-mode-configuration-controls.component.ts","../../global-context/features/configuration/live-mode-configuration-controls/live-mode-configuration-controls.component.html","../../global-context/features/configuration/configuration-controls/configuration-controls.component.ts","../../global-context/features/configuration/configuration-collapse/configuration-collapse.component.ts","../../global-context/features/configuration/configuration-collapse/configuration-collapse.component.html","../../global-context/services/infrastructure/global-context-query.service.ts","../../global-context/features/aggregation/aggregation-display/aggregation-display.component.ts","../../global-context/features/aggregation/aggregation-display/aggregation-display.component.html","../../global-context/features/refresh/auto-refresh/auto-refresh-control.component.ts","../../global-context/features/refresh/auto-refresh/auto-refresh-control.component.html","../../global-context/features/time-context/time-range-display/time-range-display.component.ts","../../global-context/features/time-context/time-range-display/time-range-display.component.html","../../global-context/shared/context-controls/context-controls.component.ts","../../global-context/shared/context-controls/context-controls.component.html","../../global-context/core/global-context.component.ts","../../global-context/core/global-context.component.html","../../global-context/services/infrastructure/global-context-navigation.service.ts","../../global-context/core/widget-inline/inline-link-controls.component.ts","../../global-context/core/widget-inline/inline-link-helpers.ts","../../global-context/core/widget-inline/orchestrator/config-composer.util.ts","../../global-context/core/widget-inline/inline-refresh-helpers.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-aggregation.helper.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-context.helper.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-refresh.helper.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-state.helper.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-toggle.helper.ts","../../global-context/core/widget-inline/orchestrator/validation.util.ts","../../global-context/core/widget-inline/orchestrator/shared-mode-orchestrator.ts","../../global-context/core/widget-inline/orchestrator/history-mode-orchestrator.ts","../../global-context/core/widget-inline/orchestrator/live-mode-orchestrator.ts","../../global-context/core/widget-inline/orchestrator/global-context-inline-orchestrator.service.ts","../../global-context/core/global-context-inline.component.ts","../../global-context/core/global-context-inline.component.html","../../global-context/services/state/global-context-store.service.ts","../../global-context/core/global-context-config.component.ts","../../global-context/core/global-context-config.component.html","../../global-context/features/configuration/config-context-selector/config-context-selector.component.ts","../../global-context/core/global-context-widget-config.component.ts","../../global-context/core/index.ts","../../global-context/features/time-context/index.ts","../../global-context/features/refresh/realtime-control/realtime-control.component.ts","../../global-context/features/refresh/realtime-control/realtime-control.component.html","../../global-context/features/refresh/index.ts","../../global-context/features/configuration/index.ts","../../global-context/shared/preview-controls/preview-controls.component.ts","../../global-context/shared/preview-controls/preview-controls.component.html","../../global-context/shared/index.ts","../../global-context/integration/widget-controls/default-widget-template.ts","../../global-context/integration/widget-controls/widget-controls-factory.ts","../../global-context/integration/widget-controls/guards.ts","../../global-context/integration/widget-controls/update-helpers.ts","../../global-context/integration/widget-controls/widget-controls-presets.helpers.ts","../../global-context/integration/widget-controls/widget-controls-presets.ts","../../global-context/integration/widget-wrapper/global-context-widget-wrapper.component.ts","../../global-context/integration/presets/control-presets.ts","../../global-context/integration/config-mode-controls/config-mode-controls.component.ts","../../global-context/integration/context-controls/context-controls.component.ts","../../global-context/integration/link-buttons/link-buttons.component.ts","../../global-context/integration/global-context-connector/global-context-connector.component.ts","../../global-context/integration/local-controls/local-controls.component.ts","../../global-context/integration/index.ts","../../global-context/global-context.module.ts","../../global-context/c8y-ngx-components-global-context.ts"],"sourcesContent":["import { aggregationType } from '@c8y/client';\nimport { RefreshOption } from './auto-refresh.model';\nimport { LinkStatesMap } from './control-flow.model';\nimport { DateTimeContext } from './date-time-context-picker.model';\nimport { Interval, TimeInterval } from './interval-picker.model';\n\nexport const GLOBAL_CONTEXT_EVENTS = {\n STATE_CHANGE: 'GLOBAL_CONTEXT_STATE_CHANGE',\n REFRESH: 'REFRESH',\n REFRESH_OPTION_CHANGE: 'GLOBAL_CONTEXT_REFRESH_OPTION_CHANGE',\n UPDATE_GLOBAL_CONTEXT_LIVE: 'UPDATE_GLOBAL_CONTEXT_LIVE',\n UPDATE_GLOBAL_CONTEXT_HISTORY: 'UPDATE_GLOBAL_CONTEXT_HISTORY',\n INIT_GLOBAL_CONTEXT: 'INIT_GLOBAL_CONTEXT'\n} as const;\n\nexport enum GLOBAL_CONTEXT_DISPLAY_MODE {\n DASHBOARD = 'dashboard',\n CONFIG = 'config',\n VIEW_AND_CONFIG = 'view_and_config'\n}\n\nexport type GlobalContextKeys = (keyof GlobalContextState)[];\n\n/**\n * State object passed to widget state handlers\n */\nexport interface WidgetState {\n config?: GlobalContextState;\n inlineControlSettings?: Partial<GlobalContextSettings>;\n dashboardControlSettings?: Partial<GlobalContextSettings>;\n currentLinks?: LinkStatesMap;\n displayMode?: string;\n [key: string]: unknown;\n}\n\n/**\n * Result returned from widget state handlers\n */\nexport interface WidgetStateHandlerResult {\n inlineControlSettings: Partial<GlobalContextSettings>;\n dashboardControlSettings?: Partial<GlobalContextSettings>;\n links?: Partial<LinkStatesMap>;\n options?: { noAutoRefreshCounter?: boolean };\n}\n\nexport interface WidgetControls {\n supports: GlobalContextKeys;\n supportedModes?: RefreshOption[];\n options?: { noAutoRefreshCounter?: boolean };\n configSettings: {\n [mode in GLOBAL_CONTEXT_DISPLAY_MODE]?: {\n [option in RefreshOption]?: Partial<GlobalContextSettings>;\n };\n };\n settings: {\n [mode in GLOBAL_CONTEXT_DISPLAY_MODE]?: {\n [option in RefreshOption]?: Partial<GlobalContextSettings>;\n };\n };\n defaultLinks?: {\n [mode in GLOBAL_CONTEXT_DISPLAY_MODE]?: {\n [option in RefreshOption]?: LinkStatesMap;\n };\n };\n stateHandlers?: {\n [stateName: string]: (widgetState?: WidgetState) => WidgetStateHandlerResult;\n };\n}\n\n/** Settings to control which features are visible in the global context UI */\nexport interface GlobalContextSettings {\n showTimeContext: boolean;\n showAggregation: boolean;\n showAutoRefresh: boolean;\n showRefresh: boolean;\n showRefreshInterval: boolean;\n}\n\n/** Base interface for all global context events */\nexport interface GlobalContextEventBase<Type extends string, Payload> {\n type: Type;\n payload: Payload;\n timestamp?: number;\n}\n\n/**\n * Registry of all available global context events and their payloads\n *\n * Extend with new events by declaring module augmentation:\n * interface GlobalContextEventRegistry {\n * NEW_EVENT: boolean; // Add your event payload type\n * }\n */\nexport interface GlobalContextEventRegistry {\n [GLOBAL_CONTEXT_EVENTS.REFRESH]: {\n dateFrom?: string | Date;\n dateTo?: string | Date;\n interval?: TimeInterval;\n };\n [GLOBAL_CONTEXT_EVENTS.STATE_CHANGE]: Partial<GlobalContextState>;\n [GLOBAL_CONTEXT_EVENTS.REFRESH_OPTION_CHANGE]: RefreshOption;\n [GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_LIVE]: Partial<GlobalContextState>;\n [GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_HISTORY]: Partial<GlobalContextState>;\n [GLOBAL_CONTEXT_EVENTS.INIT_GLOBAL_CONTEXT]: void;\n}\n\n/** Union of all possible event type strings */\nexport type GlobalContextEventType = keyof GlobalContextEventRegistry;\n/** Union type of all possible global context events */\nexport type GlobalContextEventUnion = {\n [K in GlobalContextEventType]: GlobalContextEventBase<K, GlobalContextEventRegistry[K]>;\n}[GlobalContextEventType];\n\nexport enum DateContextQueryParamNames {\n DATE_CONTEXT_FROM = 'dateContextFrom',\n DATE_CONTEXT_TO = 'dateContextTo',\n DATE_CONTEXT_INTERVAL = 'dateContextInterval',\n DATE_CONTEXT_AGGREGATION = 'dateContextAggregation',\n DATE_CONTEXT_AUTO_REFRESH = 'globalContextAutoRefresh',\n DATE_CONTEXT_REFRESH_MODE = 'globalContextRefreshMode'\n}\n\ntype DateContextFromToQueryParams = {\n [DateContextQueryParamNames.DATE_CONTEXT_FROM]: string;\n [DateContextQueryParamNames.DATE_CONTEXT_TO]: string;\n [DateContextQueryParamNames.DATE_CONTEXT_INTERVAL]?: never;\n};\n\ntype DateContextIntervalQueryParams = {\n [DateContextQueryParamNames.DATE_CONTEXT_FROM]?: never;\n [DateContextQueryParamNames.DATE_CONTEXT_TO]?: never;\n [DateContextQueryParamNames.DATE_CONTEXT_INTERVAL]: Interval['id'];\n};\n\n/**\n * Input query params is an object representing all possible query params related to widget time context.\n * It can be provided by user typing them in browser URL address bar, so all of them should be considered.\n */\nexport type InputDateContextQueryParams = {\n [DateContextQueryParamNames.DATE_CONTEXT_FROM]?: string;\n [DateContextQueryParamNames.DATE_CONTEXT_TO]?: string;\n [DateContextQueryParamNames.DATE_CONTEXT_INTERVAL]?: Interval['id'];\n [DateContextQueryParamNames.DATE_CONTEXT_AGGREGATION]?: aggregationType;\n [DateContextQueryParamNames.DATE_CONTEXT_AUTO_REFRESH]?: boolean;\n [DateContextQueryParamNames.DATE_CONTEXT_REFRESH_MODE]?: string;\n};\n\n/**\n * Output query params is an object representing params that are applied to current URL in browser address bar.\n * These params are set programmatically.\n * Time context interval and time range described by date \"from\" and date \"to\" exclude each other.\n */\nexport type OutputDateContextQueryParams = (\n | DateContextFromToQueryParams\n | DateContextIntervalQueryParams\n) & {\n [DateContextQueryParamNames.DATE_CONTEXT_AGGREGATION]: aggregationType;\n};\n\nexport type GlobalContextDisplayMode = `${GLOBAL_CONTEXT_DISPLAY_MODE}`;\n\nexport const GLOBAL_CONTEXT_SOURCE = {\n WIDGET: 'widget',\n DASHBOARD: 'dashboard'\n} as const;\n\nexport type GlobalContextSource =\n (typeof GLOBAL_CONTEXT_SOURCE)[keyof typeof GLOBAL_CONTEXT_SOURCE];\n\n// TODO: This type should extend ContextWidgetConfig from @c8y/ngx-components/context-dashboard\n// but importing it creates a circular dependency (global-context <-> context-dashboard)\nexport interface GlobalContextState {\n dateTimeContext?: DateTimeContext;\n aggregation?: aggregationType | null;\n isAutoRefreshEnabled?: boolean;\n refreshInterval?: number;\n refreshOption?: RefreshOption;\n displayMode?: `${GLOBAL_CONTEXT_DISPLAY_MODE}`;\n source?: GlobalContextSource;\n eventSourceId?: string;\n /**\n * Flag indicating global context values have been applied.\n * For dashboard mode, widgets should wait for this flag before processing config.\n */\n isGlobalContextReady?: boolean;\n}\n\n/**\n * Interface for date context parameters passed to setDateContextQueryParams method.\n * Provides strong typing for all possible parameters.\n */\nexport interface DateContextParams {\n /** Time interval ID ('DAYS', 'HOURS', etc.) or 'custom' for date ranges */\n interval?: Interval['id'];\n /** Array containing [dateFrom, dateTo] strings or Date objects for custom date ranges */\n date?: (string | Date)[] | null;\n /** Data aggregation type ('HOURLY', 'DAILY', etc.) */\n aggregation?: aggregationType;\n /** Whether auto-refresh should be enabled */\n isAutoRefreshEnabled?: boolean;\n /** Refresh mode ('live' or 'history') */\n refreshOption?: RefreshOption;\n}\n\n/**\n * Result type for query parameter validation status.\n * Provides detailed validation information for debugging and UI state management.\n */\nexport interface ParameterValidationStatus {\n /** True if interval parameter is valid and selectable */\n interval: boolean;\n /** True if aggregation parameter is valid */\n aggregation: boolean;\n /** True if both dateFrom and dateTo form a valid date range */\n dateRange: boolean;\n /** Parsed boolean value, or undefined if invalid/missing */\n autoRefresh: boolean | undefined;\n}\n\nexport const WIDGET_DISPLAY_MODE = {\n INLINE: 'inline',\n CONFIG: 'config',\n PREVIEW: 'preview'\n} as const;\n\nexport type WidgetDisplayMode = (typeof WIDGET_DISPLAY_MODE)[keyof typeof WIDGET_DISPLAY_MODE];\n\nexport interface GlobalContextEvent {\n context: Partial<GlobalContextState>;\n diff: Partial<GlobalContextState>;\n}\n","export const REFRESH_OPTION = {\n LIVE: 'live',\n HISTORY: 'history'\n} as const;\n\nexport type RefreshOption = (typeof REFRESH_OPTION)[keyof typeof REFRESH_OPTION];\n","import { gettext } from '@c8y/ngx-components/gettext';\n\nconst todayDate = new Date();\n\nexport const TIME_SPAN_MS = {\n MINUTE: 1000 * 60,\n HOUR: 1000 * 60 * 60,\n DAY: 1000 * 60 * 60 * 24,\n WEEK: 1000 * 60 * 60 * 24 * 7,\n MONTH: todayDate.valueOf() - new Date(todayDate.setMonth(todayDate.getMonth() - 1)).valueOf()\n} as const;\n\nexport const TIME_INTERVAL = {\n NONE: 'none',\n MINUTES: 'minutes',\n HOURS: 'hours',\n DAYS: 'days',\n WEEKS: 'weeks',\n MONTHS: 'months',\n CUSTOM: 'custom'\n} as const;\n\nexport type TimeInterval = (typeof TIME_INTERVAL)[keyof typeof TIME_INTERVAL];\n\nexport type Interval = {\n id: 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'custom' | string;\n title: string;\n timespanInMs?: number;\n};\n\nexport type AlarmFilterInterval =\n | Interval\n | {\n id: 'none';\n title: string;\n timespanInMs?: number;\n };\n\nexport const INTERVALS: Interval[] = [\n {\n id: 'minutes',\n title: gettext('Last minute'),\n timespanInMs: TIME_SPAN_MS.MINUTE\n },\n {\n id: 'hours',\n title: gettext('Last hour'),\n timespanInMs: TIME_SPAN_MS.HOUR\n },\n {\n id: 'days',\n title: gettext('Last day'),\n timespanInMs: TIME_SPAN_MS.DAY\n },\n {\n id: 'weeks',\n title: gettext('Last week'),\n timespanInMs: TIME_SPAN_MS.WEEK\n },\n {\n id: 'months',\n title: gettext('Last month'),\n timespanInMs: TIME_SPAN_MS.MONTH\n },\n { id: 'custom', title: gettext('Custom') }\n];\n\nexport const INTERVAL_TITLES: Record<AlarmFilterInterval['id'], string> = {\n none: gettext('No date filter'),\n minutes: gettext('Last minute'),\n hours: gettext('Last hour'),\n days: gettext('Last day'),\n weeks: gettext('Last week'),\n months: gettext('Last month'),\n custom: gettext('Custom')\n};\n","import { REFRESH_OPTION } from './auto-refresh.model';\nimport { GLOBAL_CONTEXT_DISPLAY_MODE } from './global-context.model';\nimport { TIME_INTERVAL } from './interval-picker.model';\n\n/**\n * Default values for global context configuration\n * Single source of truth for all default values across the global context feature\n */\nexport const GLOBAL_CONTEXT_DEFAULTS = {\n /** Default refresh option - live mode */\n REFRESH_OPTION: REFRESH_OPTION.LIVE,\n\n /** Default auto-refresh state */\n IS_AUTO_REFRESH_ENABLED: true,\n\n /** Default refresh interval in milliseconds (5 seconds) */\n REFRESH_INTERVAL: 5000,\n\n /** Default aggregation type */\n AGGREGATION: null,\n\n /** Default display mode */\n DISPLAY_MODE: GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD,\n\n /** Default date range duration in milliseconds (1 hour) */\n DATE_RANGE_DURATION_MS: 60 * 60 * 1000,\n\n /** Default time interval for custom ranges */\n TIME_INTERVAL: TIME_INTERVAL.HOURS\n} as const;\n","import { gettext } from '@c8y/ngx-components/gettext';\nimport { GlobalContextSettings, GlobalContextState } from './global-context.model';\n\n/**\n * Define the mapping between GlobalContextState keys and their equivalent in GlobalContextSettings\n */\nexport const LINK_BTNS_CONFIG: {\n [K in Extract<\n keyof GlobalContextState,\n 'dateTimeContext' | 'aggregation' | 'isAutoRefreshEnabled'\n >]: {\n /** Form control name, same as global state key */\n formControlName: K;\n /** Setting key in GlobalContextSettings */\n settingKey: keyof GlobalContextSettings;\n /** User-friendly label for UI */\n label: string;\n /** Css Class for link button visualization */\n cssClass: string;\n icon: string;\n };\n} = {\n dateTimeContext: {\n formControlName: 'dateTimeContext',\n settingKey: 'showTimeContext',\n label: gettext('date/time'),\n cssClass: 'time-context',\n icon: 'calendar'\n },\n isAutoRefreshEnabled: {\n formControlName: 'isAutoRefreshEnabled',\n settingKey: 'showAutoRefresh',\n label: gettext('auto refresh'),\n cssClass: 'auto-refresh',\n icon: 'refresh'\n },\n aggregation: {\n formControlName: 'aggregation',\n settingKey: 'showAggregation',\n label: gettext('aggregation'),\n cssClass: 'aggregation',\n icon: 'input'\n }\n} as const;\n\n/**\n * Type for link toggle event\n */\nexport interface LinkToggleEvent {\n key: Extract<\n keyof GlobalContextState,\n 'dateTimeContext' | 'aggregation' | 'isAutoRefreshEnabled'\n >;\n isLinked: boolean;\n}\n\n/**\n * Type for link states map\n */\nexport type LinkStatesMap = Partial<\n Record<\n Extract<keyof GlobalContextState, 'dateTimeContext' | 'aggregation' | 'isAutoRefreshEnabled'>,\n boolean\n >\n>;\n\n/**\n * Type for control configs map\n */\nexport type ControlConfigsMap = Partial<\n Record<\n Extract<keyof GlobalContextState, 'dateTimeContext' | 'aggregation' | 'isAutoRefreshEnabled'>,\n {\n cssClass?: string;\n linkTooltip?: string;\n unlinkTooltip?: string;\n icon?: string;\n disabled?: boolean;\n disabledTooltip?: string;\n autoUnlinked?: boolean;\n }\n >\n>;\n","import { aggregationType } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TIME_SPAN_MS } from './interval-picker.model';\n\nexport type Aggregation = {\n id: aggregationType | null;\n title: string;\n};\n\nexport const AGGREGATIONS: Aggregation[] = [\n { id: null, title: gettext('None') },\n { id: aggregationType.MINUTELY, title: gettext('Minutely') },\n { id: aggregationType.HOURLY, title: gettext('Hourly') },\n { id: aggregationType.DAILY, title: gettext('Daily') }\n];\n\nexport const AGGREGATION_LIMITS = {\n MINUTELY_LIMIT: TIME_SPAN_MS.MINUTE * 10,\n HOURLY_LIMIT: TIME_SPAN_MS.DAY * 1,\n DAILY_LIMIT: TIME_SPAN_MS.DAY * 4\n};\n\nexport const AGGREGATION_ICON_TYPE = {\n UNDEFINED: 'line-chart',\n MINUTELY: 'hourglass',\n HOURLY: 'clock-o',\n DAILY: 'calendar-o'\n} as const;\n\nexport type AggregationIconType =\n (typeof AGGREGATION_ICON_TYPE)[keyof typeof AGGREGATION_ICON_TYPE];\n\nexport const AGGREGATION_ICONS: Record<aggregationType | 'undefined', AggregationIconType> = {\n undefined: AGGREGATION_ICON_TYPE.UNDEFINED,\n MINUTELY: AGGREGATION_ICON_TYPE.MINUTELY,\n HOURLY: AGGREGATION_ICON_TYPE.HOURLY,\n DAILY: AGGREGATION_ICON_TYPE.DAILY\n};\n\nexport const AGGREGATION_TEXTS: Record<\n aggregationType | 'undefined' | 'null' | 'disabled',\n string\n> = {\n disabled: gettext('No aggregation with realtime enabled'),\n undefined: gettext('No aggregation'),\n null: gettext('No aggregation'),\n MINUTELY: gettext('Minutely aggregation'),\n HOURLY: gettext('Hourly aggregation'),\n DAILY: gettext('Daily aggregation')\n};\n\nexport const AGGREGATION_VALUES = {\n none: 'NONE',\n minutely: aggregationType.MINUTELY,\n hourly: aggregationType.HOURLY,\n daily: aggregationType.DAILY\n} as const;\n\nexport const AGGREGATION_VALUES_ARR = [\n AGGREGATION_VALUES.none,\n AGGREGATION_VALUES.minutely,\n AGGREGATION_VALUES.hourly,\n AGGREGATION_VALUES.daily\n] as const;\n\nexport const AGGREGATION_LABELS = {\n NONE: AGGREGATIONS[0].title,\n [aggregationType.MINUTELY]: AGGREGATIONS[1].title,\n [aggregationType.HOURLY]: AGGREGATIONS[2].title,\n [aggregationType.DAILY]: AGGREGATIONS[3].title\n} as const;\n\n/**\n * Represents the available aggregation options.\n * Aggregation 'none' is not handled by our backend.\n */\nexport type AggregationOption = typeof AGGREGATION_VALUES.none | `${aggregationType}`;\n/**\n * Represents the status of aggregation options.\n * Used to determine which aggregation options should be disabled.\n */\nexport type AggregationOptionStatus = {\n [key in AggregationOption]?: boolean;\n};\n\nexport interface AggregationState {\n aggregationType: aggregationType;\n isDisabled: boolean;\n}\n","/**\n * Time duration constants (ms)\n */\nexport const TIME_DURATION = {\n /** One hour in milliseconds */\n ONE_HOUR_MS: 60 * 60 * 1000,\n /** One minute in milliseconds */\n ONE_MINUTE_MS: 60 * 1000,\n /** One second in milliseconds */\n ONE_SECOND_MS: 1000\n} as const;\n\n/**\n * Timing constants for debouncing and throttling\n */\nexport const TIMING = {\n /** Debounce time for form value changes (ms) */\n FORM_DEBOUNCE: 100,\n /** Throttle time for context changes (ms) */\n CONTEXT_THROTTLE: 200,\n /** Tooltip delay time (ms) */\n TOOLTIP_DELAY: 500,\n /** Default debounce time (ms) */\n DEFAULT_DEBOUNCE: 100\n} as const;\n\n/**\n * UI priority constants\n */\nexport const UI_PRIORITIES = {\n /** High priority for configuration controls */\n CONFIGURATION_PRIORITY: 1000\n} as const;\n","import { DateTimeContext } from '../models/date-time-context-picker.model';\nimport { INTERVALS, TIME_INTERVAL, TimeInterval } from '../models/interval-picker.model';\n\n/**\n * Central utility for all DateTimeContext operations.\n * Single source of truth for date validation, conversion, and context normalization.\n */\nexport class DateTimeContextUtil {\n /**\n * Converts a value to a Date object, handling various input types.\n * Allows JavaScript's natural type coercion for compatibility with existing tests.\n */\n static toDate(value: unknown): Date | null {\n // Handle undefined explicitly - new Date(undefined) is Invalid Date\n if (value === undefined) {\n return null;\n }\n\n // Already a Date object\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? null : value;\n }\n\n // Let JavaScript's natural type coercion handle all other cases\n // This allows null, false, true, numbers, strings to be handled like new Date() would\n const date = new Date(value as never);\n return isNaN(date.getTime()) ? null : date;\n }\n\n /**\n * Converts a date value to ISO string format.\n */\n static toIso(value: Date | string | null | undefined): string | null {\n const date = this.toDate(value);\n return date ? date.toISOString() : null;\n }\n\n /**\n * Normalizes an ISO string to remove milliseconds (sets to .000Z).\n * This ensures date comparisons ignore millisecond precision.\n *\n * @param isoString - ISO date string (e.g., \"2025-10-27T15:10:04.405Z\")\n * @returns ISO string with milliseconds zeroed (e.g., \"2025-10-27T15:10:04.000Z\")\n */\n static normalizeIsoToSeconds(isoString: string | null | undefined): string | null {\n if (!isoString || typeof isoString !== 'string') {\n return null;\n }\n\n // Parse the ISO string to a Date\n const date = this.toDate(isoString);\n if (!date) {\n return null;\n }\n\n // Set milliseconds to 0\n date.setMilliseconds(0);\n return date.toISOString();\n }\n\n /**\n * Validates if a single date value is valid.\n * Only accepts Date objects and valid date strings (not numbers, null, etc.)\n * This is the strict validation used for form inputs and user-facing APIs.\n */\n static isValidDate(value: unknown): boolean {\n // Only accept Date instances or strings\n if (value instanceof Date) {\n return !isNaN(value.getTime());\n }\n if (typeof value === 'string') {\n const date = new Date(value);\n return !isNaN(date.getTime());\n }\n return false;\n }\n\n /**\n * Validates if a value can be converted to a valid date.\n * Uses JavaScript's natural type coercion, accepting null, numbers, booleans, etc.\n * This is used internally for range validation.\n */\n static isCoercibleToDate(value: unknown): boolean {\n return this.toDate(value) !== null;\n }\n\n /**\n * Validates if a date range is valid (from < to).\n */\n static isValidRange(from: unknown, to: unknown): boolean {\n const fromDate = this.toDate(from);\n const toDate = this.toDate(to);\n\n // Both dates must be valid and fromDate must be before toDate\n return fromDate !== null && toDate !== null && fromDate < toDate;\n }\n\n /**\n * Validates if a date range is valid, allowing equal dates (from <= to).\n */\n static isValidRangeOrEqual(from: unknown, to: unknown): boolean {\n const fromDate = this.toDate(from);\n const toDate = this.toDate(to);\n\n // Both dates must be valid and fromDate must be before or equal to toDate\n return fromDate !== null && toDate !== null && fromDate <= toDate;\n }\n\n /**\n * Calculates date range from an interval.\n * Pure implementation based on INTERVALS configuration.\n */\n static dateRangeFromInterval(\n interval: TimeInterval,\n now: Date = new Date()\n ): [Date, Date] | null {\n // Special case: 'none' returns epoch to current time\n if (interval === 'none') {\n return [new Date(0), now];\n }\n\n // Special case: 'custom' has no default range\n if (interval === TIME_INTERVAL.CUSTOM) {\n return null;\n }\n\n // Find the interval configuration\n const intervalConfig = INTERVALS.find(({ id }) => id === interval);\n if (!intervalConfig || !intervalConfig.timespanInMs) {\n return null;\n }\n\n // Calculate date range: current time minus interval timespan\n const dateTo = new Date(now);\n const dateFrom = new Date(dateTo.valueOf() - intervalConfig.timespanInMs);\n return [dateFrom, dateTo];\n }\n\n /**\n * Ensures all dates in a DateTimeContext are ISO strings with milliseconds normalized to .000Z.\n */\n static ensureIsoContext(ctx: Partial<DateTimeContext>): DateTimeContext {\n const result: DateTimeContext = {\n interval: ctx.interval || TIME_INTERVAL.CUSTOM,\n dateFrom: null,\n dateTo: null\n };\n\n if (ctx.dateFrom) {\n const iso = this.toIso(ctx.dateFrom);\n result.dateFrom = this.normalizeIsoToSeconds(iso);\n }\n if (ctx.dateTo) {\n const iso = this.toIso(ctx.dateTo);\n result.dateTo = this.normalizeIsoToSeconds(iso);\n }\n\n return result;\n }\n\n /**\n * Normalizes a DateTimeContext for live mode.\n * - For non-custom intervals: recomputes range based on current time\n * - For custom interval: updates dateTo to current time\n */\n static normalizeForLive(ctx: Partial<DateTimeContext>, now: Date = new Date()): DateTimeContext {\n const interval = ctx.interval || TIME_INTERVAL.CUSTOM;\n\n if (interval !== TIME_INTERVAL.CUSTOM) {\n // Recompute range from interval\n const range = this.dateRangeFromInterval(interval, now);\n if (range) {\n return this.ensureIsoContext({\n interval,\n dateFrom: range[0],\n dateTo: range[1]\n });\n }\n }\n\n // For custom interval, keep dateFrom but update dateTo to now\n return this.ensureIsoContext({\n interval: TIME_INTERVAL.CUSTOM,\n dateFrom: ctx.dateFrom,\n dateTo: now\n });\n }\n\n /**\n * Normalizes a DateTimeContext for history mode.\n * Forces interval to CUSTOM and preserves explicit date range.\n */\n static normalizeForHistory(ctx: Partial<DateTimeContext>): DateTimeContext {\n return this.ensureIsoContext({\n interval: TIME_INTERVAL.CUSTOM,\n dateFrom: ctx.dateFrom,\n dateTo: ctx.dateTo\n });\n }\n\n /**\n * Type guard to check if a value is a valid DateTimeContext object.\n */\n static isValidDateTimeContext(context: unknown): context is DateTimeContext {\n if (!context || typeof context !== 'object') return false;\n\n const ctx = context as Record<string, unknown>;\n\n const hasDates = 'dateFrom' in ctx && 'dateTo' in ctx && 'interval' in ctx;\n if (!hasDates) return false;\n\n return (\n typeof ctx.interval === 'string' &&\n ctx.interval.length > 0 &&\n this.isValidDate(ctx.dateFrom) &&\n this.isValidDate(ctx.dateTo)\n );\n }\n\n /**\n * Checks if a partial DateTimeContext has all required fields.\n * Business logic: Allow single field updates or complete updates (all 3 fields),\n * but reject incomplete updates with 2 fields.\n */\n static isCompleteDateTimeContext(\n diff: Partial<{ dateTimeContext: Partial<DateTimeContext> }>\n ): boolean {\n // If no dateTimeContext in diff, it's considered complete\n if (!('dateTimeContext' in diff) || !diff.dateTimeContext) {\n return true;\n }\n\n const keys = Object.keys(diff.dateTimeContext);\n // Allow single field updates (e.g., just interval) or complete updates (all 3 fields)\n // Reject incomplete updates with 2 fields\n return keys.length === 1 || keys.length === 3;\n }\n\n /**\n * Formats a context for emission, ensuring ISO dates and filtering by supported fields.\n */\n static formatForEmission(\n context: Partial<{ dateTimeContext: Partial<DateTimeContext> }>,\n supports: string[]\n ): Partial<{ dateTimeContext: DateTimeContext }> {\n if (!supports.includes('dateTimeContext') || !context.dateTimeContext) {\n return {};\n }\n\n const dateTimeContext = context.dateTimeContext;\n\n // For live mode with custom interval, default dateTo to now if missing\n if (dateTimeContext.interval === TIME_INTERVAL.CUSTOM && !dateTimeContext.dateTo) {\n return {\n dateTimeContext: this.ensureIsoContext({\n ...dateTimeContext,\n dateTo: new Date()\n })\n };\n }\n\n return {\n dateTimeContext: this.ensureIsoContext(dateTimeContext)\n };\n }\n}\n","import { GLOBAL_CONTEXT_DEFAULTS } from '../models/global-context-defaults';\nimport type { DateTimeContext } from '../models/date-time-context-picker.model';\nimport type { GlobalContextState } from '../models/global-context.model';\nimport { DateTimeContextUtil } from './date-time-context.util';\n\n/**\n * Creates a complete default global context state\n *\n * @returns A complete GlobalContextState with all required properties set to sensible defaults\n */\nexport function createDefaultGlobalContextState(): GlobalContextState {\n const now = new Date();\n const dateFrom = new Date(now.getTime() - GLOBAL_CONTEXT_DEFAULTS.DATE_RANGE_DURATION_MS);\n\n return {\n dateTimeContext: {\n dateFrom: dateFrom.toISOString(),\n dateTo: now.toISOString(),\n interval: GLOBAL_CONTEXT_DEFAULTS.TIME_INTERVAL\n },\n refreshOption: GLOBAL_CONTEXT_DEFAULTS.REFRESH_OPTION,\n isAutoRefreshEnabled: GLOBAL_CONTEXT_DEFAULTS.IS_AUTO_REFRESH_ENABLED,\n refreshInterval: GLOBAL_CONTEXT_DEFAULTS.REFRESH_INTERVAL,\n aggregation: GLOBAL_CONTEXT_DEFAULTS.AGGREGATION,\n displayMode: GLOBAL_CONTEXT_DEFAULTS.DISPLAY_MODE\n };\n}\n\n/**\n * Fills missing or invalid properties in a partial global context state with defaults\n *\n * @param state - Partial global context state that may have missing or invalid values\n * @returns Complete GlobalContextState with all required properties\n */\nexport function fillMissingDefaults(state: Partial<GlobalContextState>): GlobalContextState {\n const defaults = createDefaultGlobalContextState();\n\n // Use central utility to ensure ISO formatting for dateTimeContext\n const dateTimeContext = state.dateTimeContext\n ? DateTimeContextUtil.ensureIsoContext({\n ...defaults.dateTimeContext,\n ...state.dateTimeContext,\n // Use defaults for invalid dates\n dateFrom: DateTimeContextUtil.isValidDate(state.dateTimeContext?.dateFrom)\n ? state.dateTimeContext.dateFrom\n : defaults.dateTimeContext.dateFrom,\n dateTo: DateTimeContextUtil.isValidDate(state.dateTimeContext?.dateTo)\n ? state.dateTimeContext.dateTo\n : defaults.dateTimeContext.dateTo,\n interval: state.dateTimeContext?.interval || defaults.dateTimeContext.interval\n })\n : defaults.dateTimeContext;\n\n return {\n ...defaults,\n ...state,\n dateTimeContext,\n // Always override refreshInterval to use the fixed 5s default\n // This ensures old dashboards with custom intervals are migrated to the new fixed interval\n refreshInterval: GLOBAL_CONTEXT_DEFAULTS.REFRESH_INTERVAL\n };\n}\n\n/**\n * Validates if a value is a valid date (either Date object or ISO string)\n * Delegates to central utility for consistency.\n *\n * @param value - Value to validate (string, Date, or undefined)\n * @returns true if the value represents a valid date\n */\nexport function isValidDateValue(value: string | Date | undefined): value is string | Date {\n return DateTimeContextUtil.isValidDate(value);\n}\n\n/**\n * Checks if a date time context object has all required valid properties\n * Delegates to central utility for consistency.\n *\n * @param context - DateTimeContext to validate\n * @returns true if the context is complete and valid\n */\nexport function isValidDateTimeContext(context: unknown): context is DateTimeContext {\n return DateTimeContextUtil.isValidDateTimeContext(context);\n}\n\n/**\n * Checks if a dateTimeContext diff object is complete (has all 3 required fields).\n * Delegates to central utility for consistency.\n *\n * @param diff - Partial state that may contain dateTimeContext\n * @returns true if dateTimeContext is absent, has only 1 field, or has all 3 fields; false if it has 2 fields (incomplete)\n */\nexport function isCompleteDateTimeContext(diff: Partial<GlobalContextState>): boolean {\n return DateTimeContextUtil.isCompleteDateTimeContext(diff);\n}\n\n/**\n * Formats dateTimeContext for emission, handling date conversions and defaults.\n * Delegates to central utility for consistency.\n *\n * @param context - Global context state to format\n * @param supports - Array of supported fields from widget template\n * @returns Formatted context with properly formatted dateTimeContext\n */\nexport function formatDateTimeContextForEmission(\n context: Partial<GlobalContextState>,\n supports: string[]\n): Partial<GlobalContextState> {\n const result = DateTimeContextUtil.formatForEmission(context, supports);\n\n // Merge back with original context to preserve other fields\n return {\n ...context,\n ...result\n };\n}\n","import { computed, Injectable, signal } from '@angular/core';\nimport { ViewContext } from '@c8y/ngx-components';\nimport { BehaviorSubject, distinctUntilChanged, map, Observable, shareReplay } from 'rxjs';\nimport { GlobalContextSettings, GlobalContextState } from '../../models/global-context.model';\nimport { createDefaultGlobalContextState } from '../../utils/global-context-defaults.util';\n\n/**\n * Interface representing a registered widget's global context settings with its unique identifier.\n */\ninterface RegisteredGlobalContextSettings extends Partial<GlobalContextSettings> {\n /** Unique identifier for the widget that registered these settings */\n readonly id: string;\n}\n\n/**\n * Default settings applied when no widgets have registered any settings.\n */\nconst DEFAULT_GLOBAL_CONTEXT_SETTINGS: GlobalContextSettings = {\n showTimeContext: false,\n showAggregation: false,\n showAutoRefresh: false,\n showRefresh: false,\n showRefreshInterval: false\n} as const;\n\n/**\n * GlobalContextService manages widget settings registration and loading states.\n *\n * This service is responsible for:\n * - Registering and managing widget-specific global context settings\n * - Consolidating settings from multiple widgets using OR logic\n * - Tracking loading states across multiple widgets\n * - Providing default configurations for the global context\n *\n * @example\n * ```typescript\n * // Register widget settings\n * globalContextService.register('widget-1', { showTimeContext: true });\n *\n * // Get consolidated settings from all widgets\n * globalContextService.getConsolidatedSettings().subscribe(settings => {\n * console.log(settings); // { showTimeContext: true, ... }\n * });\n *\n * // Track loading state\n * globalContextService.registerLoading('widget-1');\n * console.log(globalContextService.isLoading()); // true\n * ```\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class GlobalContextService {\n /**\n * Computed signal that indicates if any widgets are currently in a loading state.\n * Returns true if at least one widget has registered a loading state.\n */\n readonly isLoading = computed(() => this.loadingIds().size > 0);\n\n /** Target context for the global context service */\n protected readonly targetContext: ViewContext.Device | ViewContext.Group = ViewContext.Group;\n\n /** Observable stream of all registered widget settings */\n private readonly settings$ = new BehaviorSubject<readonly RegisteredGlobalContextSettings[]>([]);\n\n /** Signal storing unique identifiers of widgets currently in loading state */\n private readonly loadingIds = signal<Set<string>>(new Set());\n\n /**\n * Registers global context settings for a specific widget.\n * If settings for the same widget ID already exist, they will be updated.\n *\n * @param id - Unique identifier for the widget\n * @param settings - Partial global context settings to register\n *\n * @throws {Error} When id is empty or null\n *\n * @example\n * ```typescript\n * globalContextService.register('my-widget', {\n * showTimeContext: true,\n * showAutoRefresh: false\n * });\n * ```\n */\n register(id: string, settings: Partial<GlobalContextSettings>): void {\n this.validateWidgetId(id);\n\n const registeredSettings: RegisteredGlobalContextSettings = {\n ...settings,\n id\n };\n\n const currentSettings = this.settings$.value;\n const existingIndex = this.findSettingsIndex(id, currentSettings);\n\n if (existingIndex >= 0) {\n this.updateExistingSettings(existingIndex, registeredSettings, currentSettings);\n } else {\n this.addNewSettings(registeredSettings, currentSettings);\n }\n }\n\n /**\n * Retrieves all currently registered widget settings.\n *\n * @returns Array of all registered settings with their widget IDs\n */\n getSettings(): readonly RegisteredGlobalContextSettings[] {\n return this.settings$.value;\n }\n\n /**\n * Removes all registered widget settings.\n * This will cause the consolidated settings to return to default values.\n */\n resetSettings(): void {\n this.settings$.next([]);\n }\n\n /**\n * Unregisters settings for a specific widget.\n *\n * @param id - Unique identifier of the widget to unregister\n *\n * @example\n * ```typescript\n * globalContextService.unregister('my-widget');\n * ```\n */\n unregister(id: string): void {\n if (!id) return; // Silently ignore invalid IDs\n\n const currentSettings = this.settings$.value;\n const filteredSettings = currentSettings.filter(s => s.id !== id);\n\n if (filteredSettings.length !== currentSettings.length) {\n this.settings$.next(filteredSettings);\n }\n }\n\n /**\n * Returns an observable of consolidated global context settings.\n *\n * Settings are consolidated using OR logic - if any widget requires a setting to be shown,\n * it will be included in the consolidated result. The observable emits distinct values only\n * and is shared among all subscribers for optimal performance.\n *\n * @returns Observable that emits consolidated settings whenever they change\n *\n * @example\n * ```typescript\n * globalContextService.getConsolidatedSettings().subscribe(settings => {\n * if (settings.showTimeContext) {\n * // Show time context controls\n * }\n * });\n * ```\n */\n getConsolidatedSettings(): Observable<GlobalContextSettings> {\n return this.settings$.pipe(\n map(settingsArray => this.consolidateSettings(settingsArray)),\n distinctUntilChanged(this.areSettingsEqual),\n shareReplay(1)\n );\n }\n\n /**\n * Returns the default global context settings.\n * These settings are used when no widgets have registered any settings.\n *\n * @returns Default settings object with all options disabled\n */\n getDefaultSettings(): GlobalContextSettings {\n return { ...DEFAULT_GLOBAL_CONTEXT_SETTINGS };\n }\n\n /**\n * Registers a loading state for a specific widget.\n * This will cause the `isLoading` computed signal to return true.\n *\n * @param id - Unique identifier of the widget in loading state\n *\n * @example\n * ```typescript\n * globalContextService.registerLoading('my-widget');\n * console.log(globalContextService.isLoading()); // true\n * ```\n */\n registerLoading(id: string): void {\n if (!id) return; // Silently ignore invalid IDs\n\n this.loadingIds.update(ids => {\n const newIds = new Set(ids);\n newIds.add(id);\n return newIds;\n });\n }\n\n /**\n * Unregisters a loading state for a specific widget.\n * When no widgets are in loading state, `isLoading` will return false.\n *\n * @param id - Unique identifier of the widget to remove from loading state\n *\n * @example\n * ```typescript\n * globalContextService.unregisterLoading('my-widget');\n * ```\n */\n unregisterLoading(id: string): void {\n if (!id) return; // Silently ignore invalid IDs\n\n this.loadingIds.update(ids => {\n const newIds = new Set(ids);\n newIds.delete(id);\n return newIds;\n });\n }\n\n /**\n * Returns the default global context state.\n * This state is used as the initial configuration for new global context instances.\n *\n * @returns Default global context state with live refresh enabled and 1-minute time range\n *\n * @example\n * ```typescript\n * const defaultState = globalContextService.getDefaultState();\n * console.log(defaultState.refreshOption); // 'live'\n * ```\n */\n getDefaultState(): GlobalContextState {\n return createDefaultGlobalContextState();\n }\n\n /**\n * Validates that a widget ID is not empty or null.\n *\n * @param id - Widget ID to validate\n * @throws {Error} When ID is invalid\n */\n private validateWidgetId(id: string): void {\n if (!id || id.trim().length === 0) {\n throw new Error('Widget ID cannot be empty or null');\n }\n }\n\n /**\n * Finds the index of settings for a given widget ID.\n *\n * @param id - Widget ID to search for\n * @param settings - Array of settings to search in\n * @returns Index of the settings or -1 if not found\n */\n private findSettingsIndex(\n id: string,\n settings: readonly RegisteredGlobalContextSettings[]\n ): number {\n return settings.findIndex(s => s.id === id);\n }\n\n /**\n * Updates existing settings for a widget.\n *\n * @param index - Index of the settings to update\n * @param newSettings - New settings to apply\n * @param currentSettings - Current settings array\n */\n private updateExistingSettings(\n index: number,\n newSettings: RegisteredGlobalContextSettings,\n currentSettings: readonly RegisteredGlobalContextSettings[]\n ): void {\n const updatedSettings = [...currentSettings];\n updatedSettings[index] = newSettings;\n this.settings$.next(updatedSettings);\n }\n\n /**\n * Adds new settings to the settings array.\n *\n * @param newSettings - Settings to add\n * @param currentSettings - Current settings array\n */\n private addNewSettings(\n newSettings: RegisteredGlobalContextSettings,\n currentSettings: readonly RegisteredGlobalContextSettings[]\n ): void {\n this.settings$.next([...currentSettings, newSettings]);\n }\n\n /**\n * Consolidates multiple widget settings using OR logic.\n *\n * @param settingsArray - Array of widget settings to consolidate\n * @returns Consolidated settings object\n */\n private consolidateSettings(\n settingsArray: readonly RegisteredGlobalContextSettings[]\n ): GlobalContextSettings {\n return settingsArray.reduce(\n (consolidated, current) => ({\n showTimeContext: consolidated.showTimeContext || !!current.showTimeContext,\n showAggregation: consolidated.showAggregation || !!current.showAggregation,\n showAutoRefresh: consolidated.showAutoRefresh || !!current.showAutoRefresh,\n showRefresh: consolidated.showRefresh || !!current.showRefresh,\n showRefreshInterval: consolidated.showRefreshInterval || !!current.showRefreshInterval\n }),\n { ...DEFAULT_GLOBAL_CONTEXT_SETTINGS }\n );\n }\n\n /**\n * Compares two settings objects for deep equality.\n *\n * @param prev - Previous settings\n * @param curr - Current settings\n * @returns True if settings are equal, false otherwise\n */\n private areSettingsEqual(prev: GlobalContextSettings, curr: GlobalContextSettings): boolean {\n return (\n prev.showTimeContext === curr.showTimeContext &&\n prev.showAggregation === curr.showAggregation &&\n prev.showAutoRefresh === curr.showAutoRefresh &&\n prev.showRefresh === curr.showRefresh &&\n prev.showRefreshInterval === curr.showRefreshInterval\n );\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { FormBuilder, type FormGroup } from '@angular/forms';\nimport { omit, pick } from 'lodash-es';\nimport { GLOBAL_CONTEXT_DEFAULTS } from '../../models/global-context-defaults';\nimport type { GlobalContextState } from '../../models/global-context.model';\nimport { createDefaultGlobalContextState } from '../../utils/global-context-defaults.util';\n\n/**\n * Service responsible for building and managing form groups for global context configuration.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class GlobalContextFormService {\n private readonly fb = inject(FormBuilder);\n\n /**\n * Creates a FormGroup for global context configuration.\n * @param config Optional existing configuration to initialize form values\n * @param defaults Optional override values for specific controls\n * @returns A fully configured FormGroup\n */\n buildForm(\n config: Partial<GlobalContextState> = {},\n defaults?: Partial<GlobalContextState>,\n skipProp?: Array<keyof GlobalContextState>\n ) {\n // Get base default values\n const defaultValues = this.getDefaultValues();\n\n // Extract valid config values\n const validConfigKeys = Object.keys(defaultValues) as Array<keyof GlobalContextState>;\n const configValues = pick(config, validConfigKeys) as Partial<GlobalContextState>;\n\n // Merge configurations with appropriate precedence\n const mergedValues = structuredClone(this.mergeValues(defaultValues, configValues, defaults));\n const filtered = omit(mergedValues, skipProp) as Partial<GlobalContextState>;\n\n return skipProp ? this.fb.group(filtered) : this.fb.group(mergedValues);\n }\n\n