From a3c9ee36ff8b57d754d74547c9692c4a38360562 Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Thu, 9 Jul 2026 12:39:58 +0200 Subject: [PATCH 1/3] feat: replace CpsDialogConfig class with interface + InjectionToken to reduce bundle size --- api-generator/api-generator.js | 31 +++++++- .../src/app/api-data/cps-cron-validation.json | 2 +- .../src/app/api-data/cps-dialog.json | 10 +++ .../src/app/api-data/cps-icon.json | 10 +++ .../src/app/api-data/cps-radio-group.json | 10 +++ .../src/app/api-data/types_map.json | 37 +++++++++ .../component-docs-viewer.component.html | 35 +++++++++ .../component-docs-viewer.component.ts | 3 +- .../dialog-content.component.ts | 10 +-- .../service-docs-viewer.component.html | 37 +++++++++ .../shared/api-type/api-type.component.scss | 5 +- .../src/app/pipes/detect-type.pipe.ts | 62 ++++++++------- .../components/cps-icon/cps-icon.component.ts | 1 + .../cps-radio-group.component.ts | 5 ++ .../cps-dialog/cps-dialog.service.spec.ts | 78 +++++++++---------- .../services/cps-dialog/cps-dialog.service.ts | 4 +- .../cps-confirmation.component.spec.ts | 7 +- .../cps-confirmation.component.ts | 10 ++- .../cps-dialog/cps-dialog.component.spec.ts | 9 ++- .../cps-dialog/cps-dialog.component.ts | 7 +- .../cps-dialog/utils/cps-dialog-config.ts | 24 +++++- 21 files changed, 305 insertions(+), 92 deletions(-) diff --git a/api-generator/api-generator.js b/api-generator/api-generator.js index 0c5b19a14..9596fa063 100644 --- a/api-generator/api-generator.js +++ b/api-generator/api-generator.js @@ -7,6 +7,27 @@ const outputPath = path.resolve( 'projects/composition/src/app/api-data/' ); +// Services aren't always documented on their own dedicated page — some are +// only ever embedded into a different component's page via the `[services]` +// input (e.g. CpsCronValidationService is only ever shown on /scheduler/api). +// Linking to `//api` in that case would 404, so only treat a +// service name as linkable if a matching top-level route actually exists. +const getKnownRoutes = () => { + const routingFile = path.resolve( + rootDir, + 'projects/composition/src/app/app-routing.module.ts' + ); + try { + const content = fs.readFileSync(routingFile, 'utf8'); + return new Set( + [...content.matchAll(/pathMatcher\(\s*'([^']+)'\s*\)/g)].map((m) => m[1]) + ); + } catch (_) { + return new Set(); + } +}; +const knownRoutes = getKnownRoutes(); + const staticMessages = { methods: "Defines methods that can be accessed by the component's reference.", emits: @@ -18,7 +39,8 @@ const staticMessages = { props: 'Defines the input properties of the component.', service: 'Defines the service used by the component.', enums: 'Defines enums used by the component or service.', - classes: 'Defines classes exposed by the component or service.' + classes: 'Defines classes exposed by the component or service.', + tokens: 'Injection tokens exposed by the component or service.' }; async function main() { @@ -191,6 +213,7 @@ async function main() { comment && comment.summary.map((s) => s.text || '').join(' ') }; + typesMap[componentName] = name.replace('cps-', ''); const component_props_group = component.groups.find( (g) => g.title === 'Props' @@ -469,6 +492,10 @@ async function main() { service.comment && service.comment.summary.map((s) => s.text || '').join(' ') }; + const serviceSlug = name.replace('cps-', ''); + if (knownRoutes.has(serviceSlug)) { + typesMap[service.name] = serviceSlug; + } const service_methods_group = service.groups.find( (g) => g.title === 'Method' ); @@ -510,7 +537,7 @@ async function main() { if (isProcessable(module_tokens_group)) { const tokens = { - description: 'Injection tokens exposed by the service.', + description: staticMessages.tokens, values: [] }; diff --git a/projects/composition/src/app/api-data/cps-cron-validation.json b/projects/composition/src/app/api-data/cps-cron-validation.json index 4d122635f..8f70b9d58 100644 --- a/projects/composition/src/app/api-data/cps-cron-validation.json +++ b/projects/composition/src/app/api-data/cps-cron-validation.json @@ -26,7 +26,7 @@ ] }, "tokens": { - "description": "Injection tokens exposed by the service.", + "description": "Injection tokens exposed by the component or service.", "values": [ { "name": "CPS_CRON_VALIDATION_SERVICE", diff --git a/projects/composition/src/app/api-data/cps-dialog.json b/projects/composition/src/app/api-data/cps-dialog.json index 1e72b5518..d2b947334 100644 --- a/projects/composition/src/app/api-data/cps-dialog.json +++ b/projects/composition/src/app/api-data/cps-dialog.json @@ -49,6 +49,16 @@ } ] }, + "tokens": { + "description": "Injection tokens exposed by the component or service.", + "values": [ + { + "name": "CPS_DIALOG_CONFIG", + "type": "InjectionToken>", + "description": "Injection token used to provide/inject a CpsDialogConfig value.\n\nThere is no default — it is provided per-dialog-instance by\n `CpsDialogService` , so it should only be injected from within a\ndialog's component tree." + } + ] + }, "interfaces": { "description": "Defines the custom interfaces used by the component or service.", "values": [ diff --git a/projects/composition/src/app/api-data/cps-icon.json b/projects/composition/src/app/api-data/cps-icon.json index 020a86b2a..60290d356 100644 --- a/projects/composition/src/app/api-data/cps-icon.json +++ b/projects/composition/src/app/api-data/cps-icon.json @@ -41,6 +41,16 @@ } } }, + "tokens": { + "description": "Injection tokens exposed by the component or service.", + "values": [ + { + "name": "ICONS_PATH", + "type": "InjectionToken", + "description": "Injection token that is used to provide the path to the icons." + } + ] + }, "types": { "description": "Defines the custom types used by the component or service.", "values": [ diff --git a/projects/composition/src/app/api-data/cps-radio-group.json b/projects/composition/src/app/api-data/cps-radio-group.json index 3aeb3fb70..90d9cfee2 100644 --- a/projects/composition/src/app/api-data/cps-radio-group.json +++ b/projects/composition/src/app/api-data/cps-radio-group.json @@ -236,6 +236,16 @@ } } }, + "tokens": { + "description": "Injection tokens exposed by the component or service.", + "values": [ + { + "name": "CPS_RADIO_GROUP", + "type": "InjectionToken", + "description": "Injection token used by child radio buttons to look up their parent\n `CpsRadioGroupComponent` ." + } + ] + }, "types": { "description": "Defines the custom types used by the component or service.", "values": [ diff --git a/projects/composition/src/app/api-data/types_map.json b/projects/composition/src/app/api-data/types_map.json index 56d0c694d..d186e26ce 100644 --- a/projects/composition/src/app/api-data/types_map.json +++ b/projects/composition/src/app/api-data/types_map.json @@ -1,39 +1,76 @@ { + "CpsAutocompleteComponent": "autocomplete", "CpsAutocompleteAppearanceType": "autocomplete", + "CpsButtonToggleComponent": "button-toggle", "CpsButtonToggleOption": "button-toggle", + "CpsButtonComponent": "button", + "CpsCheckboxComponent": "checkbox", + "CpsChipComponent": "chip", + "CpsDatepickerComponent": "datepicker", "CpsDatepickerAppearanceType": "datepicker", "CpsDatepickerDateFormat": "datepicker", + "CpsDividerComponent": "divider", "CpsDividerType": "divider", + "CpsExpansionPanelComponent": "expansion-panel", + "CpsFileUploadComponent": "file-upload", + "CpsIconComponent": "icon", "IconType": "icon", "iconSizeType": "icon", + "CpsInfoCircleComponent": "info-circle", + "CpsInputComponent": "input", "CpsInputAppearanceType": "input", + "CpsLoaderComponent": "loader", + "CpsMenuComponent": "menu", "CpsMenuItem": "menu", "CpsMenuAttachPosition": "menu", "CpsMenuHideReason": "menu", + "CpsPaginatorComponent": "paginator", + "CpsProgressCircularComponent": "progress-circular", + "CpsProgressLinearComponent": "progress-linear", + "CpsRadioButtonComponent": "radio-group", + "CpsRadioGroupComponent": "radio-group", "CpsRadioOption": "radio-group", + "CpsRadioComponent": "radio-group", + "CpsSchedulerComponent": "scheduler", + "CpsSelectComponent": "select", "CpsSelectAppearanceType": "select", + "CpsSidebarMenuComponent": "sidebar-menu", "CpsSidebarMenuItem": "sidebar-menu", + "CpsSwitchComponent": "switch", + "CpsTabGroupComponent": "tab-group", "CpsTabChangeEvent": "tab-group", "CpsTabsAnimationType": "tab-group", "CpsTabsAlignmentType": "tab-group", + "CpsTabComponent": "tab-group", "CpsColumnFilterCategoryOption": "table", "CpsColumnFilterType": "table", "CpsColumnFilterMatchMode": "table", + "CpsTableComponent": "table", "CpsTableExportFormat": "table", "CpsTableSize": "table", "CpsTableToolbarSize": "table", "CpsTableSortMode": "table", + "CpsTagComponent": "tag", + "CpsTextareaComponent": "textarea", + "CpsTimepickerComponent": "timepicker", "CpsTime": "timepicker", + "CpsTreeAutocompleteComponent": "tree-autocomplete", "CpsTreeAutocompleteAppearanceType": "tree-autocomplete", + "CpsTreeSelectComponent": "tree-select", "CpsTreeSelectAppearanceType": "tree-select", + "CpsTreeTableComponent": "tree-table", "CpsTreeTableSize": "tree-table", "CpsTreeTableToolbarSize": "tree-table", "CpsTreeTableSortMode": "tree-table", + "CpsBaseTreeDropdownComponent": "internal", + "CpsTooltipDirective": "tooltip", "CpsTooltipPosition": "tooltip", "CpsTooltipOpenOn": "tooltip", + "CpsDialogService": "dialog", "CpsDialogConfig": "dialog", "CpsDialogAutoFocusTarget": "dialog", "CpsDialogRef": "dialog", + "CpsNotificationService": "notification", "CpsNotificationConfig": "notification", "CpsNotificationAppearance": "notification", "CpsNotificationPosition": "notification" diff --git a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html index 5e67ec1d2..80370fe19 100644 --- a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html +++ b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html @@ -127,6 +127,41 @@

Types

} } + + @if ( + componentData.tokens && + componentData.tokens.values && + componentData.tokens.values.length > 0 + ) { +
+

Tokens

+

{{ componentData.tokens.description }}

+
+ + + + + + + + + + @for (token of componentData.tokens.values; track token.name) { + + + + + + } + +
NameTypeDescription
+ {{ token.name }} + + + {{ token.description }}
+
+
+ } @if ( componentData.interfaces && diff --git a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.ts b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.ts index 4330c7289..bf1e39312 100644 --- a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.ts +++ b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.ts @@ -5,7 +5,7 @@ import { InterfaceAPI, TypesAPI } from '../../models/component-api.model'; -import { ServiceAPI } from '../../models/service-api.model'; +import { ServiceAPI, TokensAPI } from '../../models/service-api.model'; import { CpsTabComponent, CpsTabGroupComponent, @@ -37,6 +37,7 @@ export class ComponentDocsViewerComponent extends ViewerComponent { types?: TypesAPI; interfaces?: InterfaceAPI; enums?: EnumsAPI; + tokens?: TokensAPI; }; @Input() services?: ServiceAPI[]; diff --git a/projects/composition/src/app/components/dialog-content/dialog-content.component.ts b/projects/composition/src/app/components/dialog-content/dialog-content.component.ts index 72f872be3..739d7f7af 100644 --- a/projects/composition/src/app/components/dialog-content/dialog-content.component.ts +++ b/projects/composition/src/app/components/dialog-content/dialog-content.component.ts @@ -1,9 +1,10 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { + CPS_DIALOG_CONFIG, CpsButtonComponent, - CpsDialogConfig, CpsDialogRef, - CpsIconComponent + CpsIconComponent, + type CpsDialogConfig } from 'cps-ui-kit'; @Component({ @@ -18,10 +19,9 @@ export class DialogContentComponent implements OnInit { closeDisabled = false; - // eslint-disable-next-line no-useless-constructor constructor( private _dialogRef: CpsDialogRef, - private _config: CpsDialogConfig + @Inject(CPS_DIALOG_CONFIG) private _config: CpsDialogConfig ) { this.info = this._config.data.info; this.icon = this._config.data.icon; diff --git a/projects/composition/src/app/components/service-docs-viewer/service-docs-viewer.component.html b/projects/composition/src/app/components/service-docs-viewer/service-docs-viewer.component.html index ed48477ad..39cd105e7 100644 --- a/projects/composition/src/app/components/service-docs-viewer/service-docs-viewer.component.html +++ b/projects/composition/src/app/components/service-docs-viewer/service-docs-viewer.component.html @@ -69,6 +69,43 @@

Methods

} + + @if ( + serviceData.tokens && + serviceData.tokens.values && + serviceData.tokens.values.length > 0 + ) { +
+

Tokens

+

{{ serviceData.tokens.description }}

+
+ + + + + + + + + + @for (token of serviceData.tokens.values; track token.name) { + + + + + + } + +
NameTypeDescription
+ {{ token.name }} + + + {{ token.description }}
+
+
+ } @if ( serviceData.types && diff --git a/projects/composition/src/app/components/shared/api-type/api-type.component.scss b/projects/composition/src/app/components/shared/api-type/api-type.component.scss index 4f3fff784..70583f9c5 100644 --- a/projects/composition/src/app/components/shared/api-type/api-type.component.scss +++ b/projects/composition/src/app/components/shared/api-type/api-type.component.scss @@ -9,7 +9,10 @@ flex-wrap: nowrap; display: inline-flex; align-items: center; - gap: 0.35rem; + + .type-sep { + margin-right: 0.35rem; + } } .type-part { diff --git a/projects/composition/src/app/pipes/detect-type.pipe.ts b/projects/composition/src/app/pipes/detect-type.pipe.ts index 4b4f46171..054fa14e8 100644 --- a/projects/composition/src/app/pipes/detect-type.pipe.ts +++ b/projects/composition/src/app/pipes/detect-type.pipe.ts @@ -29,6 +29,38 @@ function splitTopLevelUnion(value: string): string[] { return parts; } +// Finds known type names anywhere inside an arbitrarily nested type +// expression (e.g. the `CpsDialogConfig` inside `InjectionToken>`, +// or the `CpsCronValidationService` inside `InjectionToken`) +// and links just those identifiers, leaving the rest as plain text. +function linkifySegments( + text: string, + types: Record +): TypeSegment[] { + const identifierRe = /[A-Za-z_$][\w$]*/g; + const segments: TypeSegment[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = identifierRe.exec(text))) { + const identifier = match[0]; + if (identifier in types) { + if (match.index > lastIndex) { + segments.push({ text: text.slice(lastIndex, match.index) }); + } + segments.push({ + text: identifier, + route: `/${types[identifier]}/api`, + fragment: identifier + }); + lastIndex = identifierRe.lastIndex; + } + } + if (lastIndex < text.length) { + segments.push({ text: text.slice(lastIndex) }); + } + return segments.length > 0 ? segments : [{ text }]; +} + @Pipe({ name: 'detectType', pure: true }) export class DetectTypePipe implements PipeTransform { public transform(value: string, types: Record): TypeGroup[] { @@ -39,35 +71,7 @@ export class DetectTypePipe implements PipeTransform { const groups: TypeGroup[] = splitTopLevelUnion(base).map((part, i) => { const typeName = part.trim(); const hasSeparator = i > 0; - if (typeName in types) { - return { - hasSeparator, - segments: [ - { - text: typeName, - route: `/${types[typeName]}/api`, - fragment: typeName - } - ] - }; - } - const genericMatch = typeName.match(/^([^<]+<)([\w$]+)(>.*)$/); - if (genericMatch && genericMatch[2] in types) { - const innerType = genericMatch[2]; - return { - hasSeparator, - segments: [ - { text: genericMatch[1] }, - { - text: innerType, - route: `/${types[innerType]}/api`, - fragment: innerType - }, - { text: genericMatch[3] } - ] - }; - } - return { hasSeparator, segments: [{ text: typeName }] }; + return { hasSeparator, segments: linkifySegments(typeName, types) }; }); if (isArray && groups.length > 0) { diff --git a/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts b/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts index e5911db85..ce3215f32 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts @@ -16,6 +16,7 @@ import { convertSize } from '../../utils/internal/size-utils'; /** * Injection token that is used to provide the path to the icons. + * @group Tokens */ export const ICONS_PATH = new InjectionToken( 'Icons path for CpsIconComponent' diff --git a/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-group.component.ts b/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-group.component.ts index fdfb01e6b..067ba8506 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-group.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-group.component.ts @@ -34,6 +34,11 @@ export type CpsRadioOption = { tooltip?: string; }; +/** + * Injection token used by child radio buttons to look up their parent + * `CpsRadioGroupComponent`. + * @group Tokens + */ export const CPS_RADIO_GROUP = new InjectionToken( 'CpsRadioGroupComponent' ); diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts index eb4ea17cc..c6bab30b6 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts @@ -126,24 +126,24 @@ describe('CpsDialogService', () => { describe('open()', () => { it('should return a CpsDialogRef', () => { - const ref = service.open(TestContentComponent, new CpsDialogConfig()); + const ref = service.open(TestContentComponent, {}); expect(ref).toBeInstanceOf(CpsDialogRef); }); it('should add the ref to openDialogs', () => { - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); expect(service.openDialogs).toHaveLength(1); }); it('should set childComponentType on the dialog instance', () => { - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); expect(lastCreatedMockRef.instance.childComponentType).toBe( TestContentComponent ); }); it('should call ApplicationRef.attachView with the component host view', () => { - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); expect(appRef.attachView).toHaveBeenCalledWith( lastCreatedMockRef.hostView ); @@ -151,18 +151,18 @@ describe('CpsDialogService', () => { it('should append a DOM element to document.body', () => { const beforeCount = document.body.children.length; - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); expect(document.body.children.length).toBe(beforeCount + 1); }); it('should store the component ref in dialogComponentRefMap', () => { - const ref = service.open(TestContentComponent, new CpsDialogConfig()); + const ref = service.open(TestContentComponent, {}); expect(service.dialogComponentRefMap.get(ref)).toBe(lastCreatedMockRef); }); it('should open multiple dialogs independently', () => { - service.open(TestContentComponent, new CpsDialogConfig()); - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); + service.open(TestContentComponent, {}); expect(service.openDialogs).toHaveLength(2); }); @@ -171,99 +171,99 @@ describe('CpsDialogService', () => { CpsDialogRef.prototype, '_setContainerInstance' ); - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); expect(spySet).toHaveBeenCalledWith(lastCreatedMockRef.instance); }); }); describe('openConfirmationDialog()', () => { it('should return a CpsDialogRef', () => { - const ref = service.openConfirmationDialog(new CpsDialogConfig()); + const ref = service.openConfirmationDialog({}); expect(ref).toBeInstanceOf(CpsDialogRef); }); it('should set default headerTitle when not provided', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; service.openConfirmationDialog(config); expect(config.headerTitle).toBe('Confirm the action'); }); it('should not override headerTitle when already set', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; config.headerTitle = 'Custom Title'; service.openConfirmationDialog(config); expect(config.headerTitle).toBe('Custom Title'); }); it('should set default headerIcon to "warning"', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; service.openConfirmationDialog(config); expect(config.headerIcon).toBe('warning'); }); it('should not override headerIcon when already set', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; config.headerIcon = 'info'; service.openConfirmationDialog(config); expect(config.headerIcon).toBe('info'); }); it('should set default headerIconColor to "calm"', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; service.openConfirmationDialog(config); expect(config.headerIconColor).toBe('calm'); }); it('should not override headerIconColor when already set', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; config.headerIconColor = 'warn'; service.openConfirmationDialog(config); expect(config.headerIconColor).toBe('warn'); }); it('should set default minWidth to "25rem"', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; service.openConfirmationDialog(config); expect(config.minWidth).toBe('25rem'); }); it('should not override minWidth when already set', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; config.minWidth = '30rem'; service.openConfirmationDialog(config); expect(config.minWidth).toBe('30rem'); }); it('should set default maxWidth to "37.5rem"', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; service.openConfirmationDialog(config); expect(config.maxWidth).toBe('37.5rem'); }); it('should not override maxWidth when already set', () => { - const config = new CpsDialogConfig(); + const config: CpsDialogConfig = {}; config.maxWidth = '50rem'; service.openConfirmationDialog(config); expect(config.maxWidth).toBe('50rem'); }); it('should set childComponentType to CpsConfirmationComponent', () => { - service.openConfirmationDialog(new CpsDialogConfig()); + service.openConfirmationDialog({}); expect(lastCreatedMockRef.instance.childComponentType).toBe( CpsConfirmationComponent ); }); it('should add the ref to openDialogs', () => { - service.openConfirmationDialog(new CpsDialogConfig()); + service.openConfirmationDialog({}); expect(service.openDialogs).toHaveLength(1); }); }); describe('closeAll()', () => { it('should call close() on each open dialog', () => { - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - const ref2 = service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + const ref2 = service.open(TestContentComponent, {}); const closeSpy1 = jest.spyOn(ref1, 'close'); const closeSpy2 = jest.spyOn(ref2, 'close'); service.closeAll(); @@ -273,8 +273,8 @@ describe('CpsDialogService', () => { it('should call close() in reverse order', () => { const order: number[] = []; - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - const ref2 = service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + const ref2 = service.open(TestContentComponent, {}); jest.spyOn(ref1, 'close').mockImplementation(() => order.push(1)); jest.spyOn(ref2, 'close').mockImplementation(() => order.push(2)); service.closeAll(); @@ -282,8 +282,8 @@ describe('CpsDialogService', () => { }); it('should call destroy() instead of close() when force is true', () => { - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - const ref2 = service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + const ref2 = service.open(TestContentComponent, {}); const destroySpy1 = jest .spyOn(ref1, 'destroy') .mockImplementation(jest.fn()); @@ -306,28 +306,28 @@ describe('CpsDialogService', () => { describe('dialog cleanup on destroy signal', () => { it('should remove ref from openDialogs when onDestroy fires', () => { - const ref = service.open(TestContentComponent, new CpsDialogConfig()); + const ref = service.open(TestContentComponent, {}); expect(service.openDialogs).toHaveLength(1); ref.destroy(); expect(service.openDialogs).toHaveLength(0); }); it('should remove entry from dialogComponentRefMap when onDestroy fires', () => { - const ref = service.open(TestContentComponent, new CpsDialogConfig()); + const ref = service.open(TestContentComponent, {}); expect(service.dialogComponentRefMap.has(ref)).toBe(true); ref.destroy(); expect(service.dialogComponentRefMap.has(ref)).toBe(false); }); it('should call detachView on ApplicationRef when onDestroy fires', () => { - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); const capturedRef = lastCreatedMockRef; service.openDialogs[0].destroy(); expect(appRef.detachView).toHaveBeenCalledWith(capturedRef.hostView); }); it('should call destroy on the component ref when onDestroy fires', () => { - service.open(TestContentComponent, new CpsDialogConfig()); + service.open(TestContentComponent, {}); const capturedRef = lastCreatedMockRef; service.openDialogs[0].destroy(); expect(capturedRef.destroy).toHaveBeenCalled(); @@ -339,8 +339,8 @@ describe('CpsDialogService', () => { }); it('should only remove the destroyed ref from openDialogs', () => { - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + service.open(TestContentComponent, {}); ref1.destroy(); expect(service.openDialogs).toHaveLength(1); }); @@ -348,7 +348,7 @@ describe('CpsDialogService', () => { describe('onClose subscription', () => { it('should call close() on the dialog component instance when dialogRef.close() is called', () => { - const ref = service.open(TestContentComponent, new CpsDialogConfig()); + const ref = service.open(TestContentComponent, {}); const capturedRef = lastCreatedMockRef; ref.close(); expect(capturedRef.instance.close).toHaveBeenCalled(); @@ -357,8 +357,8 @@ describe('CpsDialogService', () => { describe('ngOnDestroy()', () => { it('should destroy all dialogs at this level', () => { - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - const ref2 = service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + const ref2 = service.open(TestContentComponent, {}); const destroySpy1 = jest .spyOn(ref1, 'destroy') .mockImplementation(jest.fn()); @@ -372,8 +372,8 @@ describe('CpsDialogService', () => { it('should destroy dialogs in reverse order', () => { const order: number[] = []; - const ref1 = service.open(TestContentComponent, new CpsDialogConfig()); - const ref2 = service.open(TestContentComponent, new CpsDialogConfig()); + const ref1 = service.open(TestContentComponent, {}); + const ref2 = service.open(TestContentComponent, {}); jest.spyOn(ref1, 'destroy').mockImplementation(() => order.push(1)); jest.spyOn(ref2, 'destroy').mockImplementation(() => order.push(2)); service.ngOnDestroy(); diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts index 1a141953a..f85f390fb 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts @@ -15,7 +15,7 @@ import { } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { CpsDialogRef } from './utils/cps-dialog-ref'; -import { CpsDialogConfig } from './utils/cps-dialog-config'; +import { CPS_DIALOG_CONFIG, CpsDialogConfig } from './utils/cps-dialog-config'; import { CpsDialogComponent } from './internal/components/cps-dialog/cps-dialog.component'; import { CpsConfirmationComponent } from './internal/components/cps-confirmation/cps-confirmation.component'; @@ -112,7 +112,7 @@ export class CpsDialogService implements OnDestroy { const componentRef = createComponent(CpsDialogComponent, { environmentInjector: createEnvironmentInjector( [ - { provide: CpsDialogConfig, useValue: config }, + { provide: CPS_DIALOG_CONFIG, useValue: config }, { provide: CpsDialogRef, useValue: dialogRef } ], this._environmentInjector diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.spec.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.spec.ts index 352bed11d..afb9d1b39 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.spec.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.spec.ts @@ -1,7 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CpsConfirmationComponent } from './cps-confirmation.component'; import { CpsDialogRef } from '../../../utils/cps-dialog-ref'; -import { CpsDialogConfig } from '../../../utils/cps-dialog-config'; +import { + CPS_DIALOG_CONFIG, + type CpsDialogConfig +} from '../../../utils/cps-dialog-config'; describe('CpsConfirmationComponent', () => { let component: CpsConfirmationComponent; @@ -17,7 +20,7 @@ describe('CpsConfirmationComponent', () => { imports: [CpsConfirmationComponent], providers: [ { provide: CpsDialogRef, useValue: mockDialogRef }, - { provide: CpsDialogConfig, useValue: mockDialogConfig } + { provide: CPS_DIALOG_CONFIG, useValue: mockDialogConfig } ] }); diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.ts index afea82add..976811c38 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-confirmation/cps-confirmation.component.ts @@ -1,7 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { CpsButtonComponent } from '../../../../../components/cps-button/cps-button.component'; import { CpsDialogRef } from '../../../utils/cps-dialog-ref'; -import { CpsDialogConfig } from '../../../utils/cps-dialog-config'; +import { + CPS_DIALOG_CONFIG, + type CpsDialogConfig +} from '../../../utils/cps-dialog-config'; @Component({ imports: [CpsButtonComponent], @@ -12,10 +15,9 @@ import { CpsDialogConfig } from '../../../utils/cps-dialog-config'; export class CpsConfirmationComponent { subtitle = ''; - // eslint-disable-next-line no-useless-constructor constructor( private _dialogRef: CpsDialogRef, - private _config: CpsDialogConfig + @Inject(CPS_DIALOG_CONFIG) private _config: CpsDialogConfig ) { this.subtitle = this._config.data?.subtitle; } diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.spec.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.spec.ts index 0e97e33b2..8dbb5be6c 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.spec.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.spec.ts @@ -9,7 +9,10 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { PrimeNG } from 'primeng/config'; import { DomHandler } from 'primeng/dom'; import { CpsDialogComponent } from './cps-dialog.component'; -import { CpsDialogConfig } from '../../../utils/cps-dialog-config'; +import { + CPS_DIALOG_CONFIG, + type CpsDialogConfig +} from '../../../utils/cps-dialog-config'; import { CpsDialogRef } from '../../../utils/cps-dialog-ref'; import { CPS_ROOT_FONT_SIZE_SERVICE } from '../../../../cps-root-font-size/cps-root-font-size.service'; @@ -39,13 +42,13 @@ describe('CpsDialogComponent', () => { componentInstance: null }; - config = Object.assign(new CpsDialogConfig(), configOverrides); + config = { ...configOverrides }; TestBed.configureTestingModule({ imports: [CpsDialogComponent, NoopAnimationsModule], providers: [ { provide: CpsDialogRef, useValue: mockDialogRef }, - { provide: CpsDialogConfig, useValue: config }, + { provide: CPS_DIALOG_CONFIG, useValue: config }, { provide: CPS_ROOT_FONT_SIZE_SERVICE, useValue: mockRootFontSizeService diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.ts index 8b9b324e7..1244909d5 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/internal/components/cps-dialog/cps-dialog.component.ts @@ -37,7 +37,10 @@ import { parseSize } from '../../../../../utils/internal/size-utils'; import { CpsDialogContentDirective } from '../../directives/cps-dialog-content.directive'; -import { CpsDialogConfig } from '../../../utils/cps-dialog-config'; +import { + CPS_DIALOG_CONFIG, + type CpsDialogConfig +} from '../../../utils/cps-dialog-config'; import { CpsDialogRef } from '../../../utils/cps-dialog-ref'; import { CpsButtonComponent } from '../../../../../components/cps-button/cps-button.component'; import { CpsInfoCircleComponent } from '../../../../../components/cps-info-circle/cps-info-circle.component'; @@ -239,7 +242,7 @@ export class CpsDialogComponent implements OnInit, AfterViewInit, OnDestroy { private _dialogRef: CpsDialogRef, private _cdRef: ChangeDetectorRef, public renderer: Renderer2, - public config: CpsDialogConfig, + @Inject(CPS_DIALOG_CONFIG) public config: CpsDialogConfig, public zone: NgZone, public primeNG: PrimeNG ) {} diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/utils/cps-dialog-config.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/utils/cps-dialog-config.ts index 3640c6e95..6783a3a8c 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/utils/cps-dialog-config.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/utils/cps-dialog-config.ts @@ -1,3 +1,4 @@ +import { InjectionToken } from '@angular/core'; import { CpsTooltipPosition } from '../../../directives/cps-tooltip/cps-tooltip.directive'; /** @@ -12,7 +13,7 @@ export type CpsDialogAutoFocusTarget = 'dialog' | 'first-tabbable'; * Configuration for the dialog service. * @group Interface */ -export class CpsDialogConfig { +export interface CpsDialogConfig { /** * An object to pass to the component loaded inside the Dialog. */ @@ -190,3 +191,24 @@ export class CpsDialogConfig { | 'bottom-left' | 'bottom-right'; } + +/** + * Injection token used to provide/inject a {@link CpsDialogConfig} value. + * + * There is no default — it is provided per-dialog-instance by + * `CpsDialogService`, so it should only be injected from within a + * dialog's component tree. + * + * @example + * ```typescript + * providers: [{ provide: CPS_DIALOG_CONFIG, useValue: myConfig }] + * ``` + * ```typescript + * constructor(@Inject(CPS_DIALOG_CONFIG) private config: CpsDialogConfig) {} + * ``` + * + * @group Tokens + */ +export const CPS_DIALOG_CONFIG = new InjectionToken( + 'CPS_DIALOG_CONFIG' +); From bf556dcfd8f4f264e66e31553d0e374a269764b7 Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Thu, 9 Jul 2026 12:43:13 +0200 Subject: [PATCH 2/3] add next-major --- .github/workflows/check_pr_title_cc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_pr_title_cc.yml b/.github/workflows/check_pr_title_cc.yml index 7a5fdb73f..65c25beb3 100644 --- a/.github/workflows/check_pr_title_cc.yml +++ b/.github/workflows/check_pr_title_cc.yml @@ -3,7 +3,7 @@ name: Check PR Title (Conventional Commits) on: pull_request: types: [opened, synchronize, reopened, edited, labeled, unlabeled] - branches: [master] + branches: [master, next-major] jobs: check-pr-title: From d2c5cfe92f0674c6e1410746ee30ef09ee703350 Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Thu, 9 Jul 2026 13:08:20 +0200 Subject: [PATCH 3/3] address feedback from copilot --- .../src/lib/services/cps-dialog/cps-dialog.service.spec.ts | 2 +- .../src/lib/services/cps-dialog/cps-dialog.service.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts index c6bab30b6..722c3b09d 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.spec.ts @@ -7,7 +7,7 @@ import { import { TestBed } from '@angular/core/testing'; import { Subject } from 'rxjs'; import { CpsDialogService } from './cps-dialog.service'; -import { CpsDialogConfig } from './utils/cps-dialog-config'; +import type { CpsDialogConfig } from './utils/cps-dialog-config'; import { CpsDialogRef } from './utils/cps-dialog-ref'; import { CpsConfirmationComponent } from './internal/components/cps-confirmation/cps-confirmation.component'; diff --git a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts index f85f390fb..25ea82e83 100644 --- a/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts +++ b/projects/cps-ui-kit/src/lib/services/cps-dialog/cps-dialog.service.ts @@ -15,7 +15,10 @@ import { } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { CpsDialogRef } from './utils/cps-dialog-ref'; -import { CPS_DIALOG_CONFIG, CpsDialogConfig } from './utils/cps-dialog-config'; +import { + CPS_DIALOG_CONFIG, + type CpsDialogConfig +} from './utils/cps-dialog-config'; import { CpsDialogComponent } from './internal/components/cps-dialog/cps-dialog.component'; import { CpsConfirmationComponent } from './internal/components/cps-confirmation/cps-confirmation.component';