From 1dcd5166cb4af4a09502d6d0a08697ba3bf24cd0 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:27:58 +0100 Subject: [PATCH 1/5] [OP-20347] Type work package context menu actions Types the permitted action constants, the collected action lists and the bulk action link inputs as WorkPackageAction and WorkPackageResource instead of any. The bulk link keeps assuming that every bulk action carries an href, now as an explicit non-null assertion. The view context menu passes action links on as strings. https://community.openproject.org/wp/OP-20347 --- .../wp-context-menu-helper.service.ts | 14 +++++++------- .../wp-view-context-menu.directive.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service.ts b/frontend/src/app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service.ts index 15438eccfc9d..b7d6e63ad613 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service.ts @@ -83,8 +83,8 @@ export class WorkPackageContextMenuHelperService { }, ]; - public getPermittedActionLinks(workPackage:WorkPackageResource, permittedActionConstants:any, allowSplitScreenActions:boolean):WorkPackageAction[] { - const singularPermittedActions:any[] = []; + public getPermittedActionLinks(workPackage:WorkPackageResource, permittedActionConstants:WorkPackageAction[], allowSplitScreenActions:boolean):WorkPackageAction[] { + const singularPermittedActions:WorkPackageAction[] = []; let allowedActions = this.getAllowedActions(workPackage, permittedActionConstants); @@ -146,15 +146,15 @@ export class WorkPackageContextMenuHelperService { return bulkPermittedActions; } - public getBulkActionLink(action:any, workPackages:any) { + public getBulkActionLink(action:WorkPackageAction, workPackages:WorkPackageResource[]) { const workPackageIdParams = { - 'ids[]': workPackages.map((wp:any) => wp.id), + 'ids[]': workPackages.map((wp) => wp.id), }; const serializedIdParams = this.UrlParamsHelper.buildQueryString(workPackageIdParams); - const linkAndQueryString = action.href.split('?'); + const linkAndQueryString = action.href!.split('?'); const link = linkAndQueryString.shift(); - const queryParts = linkAndQueryString.concat(new Array(serializedIdParams)); + const queryParts = linkAndQueryString.concat(new Array(serializedIdParams!)); return `${link}?${queryParts.join('&')}`; } @@ -247,7 +247,7 @@ export class WorkPackageContextMenuHelperService { return allowedActions; } - public getPermittedActions(workPackages:WorkPackageResource[], permittedActionConstants:any, allowSplitScreenActions:boolean):WorkPackageAction[] { + public getPermittedActions(workPackages:WorkPackageResource[], permittedActionConstants:WorkPackageAction[], allowSplitScreenActions:boolean):WorkPackageAction[] { if (workPackages.length === 1) { return this.getPermittedActionLinks(workPackages[0], permittedActionConstants, allowSplitScreenActions); } diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts index 39f55a5d7343..6d1deb326a97 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts @@ -176,7 +176,7 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { void this.turboRequests.request(this.pathHelper.workPackagesBulkDeleteDialogPath(ids, backUrl), { method: 'GET' }); } - private editSelectedWorkPackages(link:any) { + private editSelectedWorkPackages(link:string) { const selected = this.getSelectedWorkPackages(); if (selected.length > 1) { @@ -184,7 +184,7 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { } } - private copySelectedWorkPackages(link:any) { + private copySelectedWorkPackages(link:string) { const selected = this.getSelectedWorkPackages(); if (selected.length > 1) { From 0a43e9f7e60fb5a59635279e406462c2a43d28ce Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:27:59 +0100 Subject: [PATCH 2/5] [OP-20347] Type context menu locals and handlers Types the context menu locals token with OpContextMenuLocalsMap and declares the service that OPContextMenuService injects into the locals, so the menu component no longer reads it through any. Drops unused any-typed click event parameters, types the permission action names as strings and reads the status form through the typed form schema. https://community.openproject.org/wp/OP-20347 --- .../handlers/op-columns-context-menu.directive.ts | 4 ++-- .../handlers/op-context-menu-trigger.directive.ts | 2 +- .../handlers/op-settings-dropdown-menu.directive.ts | 8 ++++---- .../handlers/wp-view-dropdown-menu.directive.ts | 6 +++--- .../op-context-menu/op-context-menu.component.ts | 4 ++-- .../components/op-context-menu/op-context-menu.types.ts | 4 +++- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts index 9f6d8c4ddee2..c2d6d2253cd8 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts @@ -109,7 +109,7 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger { hidden: !this.wpTableSortBy.isSortable(c), linkText: this.I18n.t('js.work_packages.query.sort_descending'), icon: 'icon-sort-descending', - onClick: (evt:any) => { + onClick: () => { if (this.wpTableSortBy.isManualSortingMode) { this.confirmDialog.confirm({ text: this.text.confirmDelete, @@ -128,7 +128,7 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger { hidden: !this.wpTableSortBy.isSortable(c), linkText: this.I18n.t('js.work_packages.query.sort_ascending'), icon: 'icon-sort-ascending', - onClick: (evt:any) => { + onClick: () => { if (this.wpTableSortBy.isManualSortingMode) { this.confirmDialog.confirm({ text: this.text.confirmDelete, diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts index cc2ede3aa3e3..aa60e62160be 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts @@ -69,7 +69,7 @@ export class OpContextMenuTrigger extends OpContextMenuHandler implements AfterV }); // Open with keyboard combination as well - Mousetrap(this.element).bind('shift+alt+f10', (evt:any) => { + Mousetrap(this.element).bind('shift+alt+f10', (evt) => { this.open(evt); }); } diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts index afb300cefb3d..29d8e5f34b0c 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts @@ -87,7 +87,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements Aft private form:QueryFormResource; - private loadingPromise:PromiseLike; + private loadingPromise:PromiseLike; override readonly placement = 'bottom-end'; @@ -133,11 +133,11 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements Aft } } - private allowQueryAction(event:Event, action:any) { + private allowQueryAction(event:Event, action:string) { return this.allowAction(event, 'query', action); } - private allowWorkPackageAction(event:Event, action:any) { + private allowWorkPackageAction(event:Event, action:string) { return this.allowAction(event, 'work_packages', action); } @@ -149,7 +149,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements Aft return false; } - private allowAction(event:Event, modelName:string, action:any) { + private allowAction(event:Event, modelName:string, action:string) { if (this.authorisationService.can(modelName, action)) { return true; } diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts index f11cd4929c20..287bd726c9bc 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts @@ -75,7 +75,7 @@ export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger { linkText: this.I18n.t('js.views.card'), title: this.I18n.t('js.button_show_cards'), icon: 'icon-view-card', - onClick: (evt:any) => { + onClick: () => { this.isOpen = false; this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayCardRepresentation); if (this.wpTableTimeline.isVisible) { @@ -95,7 +95,7 @@ export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger { linkText: this.I18n.t('js.views.list'), title: this.I18n.t('js.button_show_table'), icon: 'icon-view-list', - onClick: (evt:any) => { + onClick: () => { this.isOpen = false; this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayListRepresentation); if (this.wpTableTimeline.isVisible) { @@ -114,7 +114,7 @@ export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger { linkText: this.I18n.t('js.views.timeline'), title: this.I18n.t('js.button_show_gantt'), icon: 'icon-view-timeline', - onClick: (evt:any) => { + onClick: () => { this.isOpen = false; if (!this.wpTableTimeline.isVisible) { this.wpTableTimeline.toggle(); diff --git a/frontend/src/app/shared/components/op-context-menu/op-context-menu.component.ts b/frontend/src/app/shared/components/op-context-menu/op-context-menu.component.ts index 85031c8fdd0a..5ca3b0b997e8 100644 --- a/frontend/src/app/shared/components/op-context-menu/op-context-menu.component.ts +++ b/frontend/src/app/shared/components/op-context-menu/op-context-menu.component.ts @@ -51,7 +51,7 @@ export class OPContextMenuComponent { constructor() { this.items = this.locals.items.filter((item) => !item?.hidden); - this.service = this.locals.service; + this.service = this.locals.service!; } public handleClick(item:OpContextMenuItem, event:MouseEvent) { @@ -60,7 +60,7 @@ export class OPContextMenuComponent { } if (item.onClick!(event)) { - this.locals.service.close(); + this.locals.service!.close(); event.preventDefault(); event.stopPropagation(); return false; diff --git a/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts b/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts index 0669781d7ccf..030c8007b532 100644 --- a/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts +++ b/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts @@ -27,8 +27,9 @@ //++ import { InjectionToken } from '@angular/core'; +import type { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service'; -export const OpContextMenuLocalsToken = new InjectionToken('CONTEXT_MENU_LOCALS'); +export const OpContextMenuLocalsToken = new InjectionToken('CONTEXT_MENU_LOCALS'); export interface OpContextMenuItem { disabled?:boolean; @@ -49,6 +50,7 @@ export interface OpContextMenuLocalsMap { showAnchorRight?:boolean; contextMenuId?:string; label?:string; + service?:OPContextMenuService; /* eslint-disable @typescript-eslint/no-explicit-any */ [key:string]:any; } From f7edf0be84a3a941029eb982ba748f5ac4a4df63 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:27:59 +0100 Subject: [PATCH 3/5] [OP-20347] Type work package table configuration Types the configuration tab component classes with the CDK ComponentType, the external query configuration locals token and injector data with QueryConfigurationLocals, and the table component text labels with their actual shape. Replaces the remaining any annotations in the table configuration with unknown where the value is only passed along. Declares the embedded table refresh as void, matching the abstract WorkPackagesViewBase contract. It no longer returns its load promise, which no caller read; the loading indicator still receives it. https://community.openproject.org/wp/OP-20347 --- .../wp-table/configuration-modal/tab-portal-outlet.ts | 10 +++++----- .../tabs/highlighting-tab.component.ts | 2 +- .../wp-table-configuration.modal.ts | 4 ++-- .../wp-table/embedded/wp-embedded-base.component.ts | 3 +-- .../external-query-configuration.constants.ts | 3 ++- .../external-query-configuration.service.ts | 6 +++--- .../components/wp-table/wp-table-configuration.ts | 2 +- .../components/wp-table/wp-table.component.ts | 10 ++++++++-- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet.ts b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet.ts index bffed92cea59..7d3b4c3628b6 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet.ts @@ -30,7 +30,7 @@ * A PortalOutlet that lets multiple components live for the lifetime of the outlet, * allowing faster switching and persistent data. */ -import { ComponentPortal } from '@angular/cdk/portal'; +import { ComponentPortal, ComponentType } from '@angular/cdk/portal'; import { ApplicationRef, ComponentRef, @@ -41,7 +41,7 @@ import { import { TabDefinition } from 'core-app/shared/components/tabs/tab.interface'; export interface TabInterface extends TabDefinition { - componentClass:new(...args:any[]) => TabComponent; + componentClass:ComponentType; } export interface TabComponent { @@ -88,7 +88,7 @@ export class TabPortalOutlet { // At this point the component has been instantiated, so we move it to the location in the DOM // where we want it to be rendered. this.outletElement.innerHTML = ''; - this.outletElement.appendChild(this._getComponentRootNode(instance.componentRef)); + this.outletElement.appendChild(this.getComponentRootNode(instance.componentRef)); this.outletElement.dataset.tabName = tab.name; this.currentTab = instance; } @@ -144,7 +144,7 @@ export class TabPortalOutlet { } /** Gets the root HTMLElement for an instantiated component. */ - private _getComponentRootNode(componentRef:ComponentRef):HTMLElement { - return (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; + private getComponentRootNode(componentRef:ComponentRef):HTMLElement { + return (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; } } diff --git a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/highlighting-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/highlighting-tab.component.ts index 8fd6e7b97612..ae2bac991cb1 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/highlighting-tab.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/highlighting-tab.component.ts @@ -61,7 +61,7 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen public availableInlineHighlightedAttributes:HalResource[] = []; - public selectedAttributes:any[] = []; + public selectedAttributes:HalResource[] = []; public availableRowHighlightedAttributes:{ name:string; value:HighlightingMode }[] = []; diff --git a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts index 08356fb74132..87fbf2bb8b56 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts @@ -47,7 +47,7 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { QueryFormResource } from 'core-app/features/hal/resources/query-form-resource'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; -export const WpTableConfigurationModalPrependToken = new InjectionToken>('WpTableConfigurationModalPrependComponent'); +export const WpTableConfigurationModalPrependToken = new InjectionToken>('WpTableConfigurationModalPrependComponent'); @Component({ templateUrl: './wp-table-configuration.modal.html', @@ -107,7 +107,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme this.loadingIndicator.indicator('modal').promise = this.loadForm() .then(() => { - const initialTabName = this.locals.initialTab; + const initialTabName = this.locals.initialTab as string|undefined; const initialTab = this.availableTabs.find((el) => el.id === initialTabName); this.switchTo(initialTab || this.availableTabs[0]); this.cdRef.detectChanges(); diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts index 022b085130fb..89b9628bac37 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts @@ -114,7 +114,7 @@ export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewB this.cdRef.detectChanges(); } - public refresh(visible = true, firstPage = false):Promise { + public refresh(visible = true, firstPage = false):void { const query = this.querySpace.query.value!; const pagination = this.wpTablePagination.paginationObject; @@ -133,7 +133,6 @@ export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewB if (visible) { this.loadingIndicator = promise; } - return promise; } public get isInitialized() { diff --git a/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.constants.ts b/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.constants.ts index f9dc0e5b0c8a..d572e584e23e 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.constants.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.constants.ts @@ -27,5 +27,6 @@ //++ import { InjectionToken } from '@angular/core'; +import type { QueryConfigurationLocals } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.component'; -export const OpQueryConfigurationLocalsToken = new InjectionToken('OpQueryConfigurationLocalsToken'); +export const OpQueryConfigurationLocalsToken = new InjectionToken('OpQueryConfigurationLocalsToken'); diff --git a/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.service.ts b/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.service.ts index d327c700d22b..f78a9b001d91 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.service.ts @@ -27,7 +27,7 @@ //++ import { ApplicationRef, Injectable, Injector, inject } from '@angular/core'; -import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal'; +import { ComponentPortal, ComponentType, DomPortalOutlet } from '@angular/cdk/portal'; import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper'; import { ExternalQueryConfigurationComponent, @@ -35,7 +35,7 @@ import { } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.component'; import { OpQueryConfigurationLocalsToken } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.constants'; -export type Class = new(...args:any[]) => any; +export type Class = ComponentType; @Injectable() export class ExternalQueryConfigurationService { @@ -101,7 +101,7 @@ export class ExternalQueryConfigurationService { * This allows callers to pass data into the newly created modal. * */ - private injectorFor(data:any) { + private injectorFor(data:Partial) { // Pass the service because otherwise we're getting a cyclic dependency between the portal // host service and the bound portal data.service = this; diff --git a/frontend/src/app/features/work-packages/components/wp-table/wp-table-configuration.ts b/frontend/src/app/features/work-packages/components/wp-table/wp-table-configuration.ts index 2e4af5e64274..139239105fed 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/wp-table-configuration.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/wp-table-configuration.ts @@ -80,7 +80,7 @@ export class WorkPackageTableConfiguration { constructor(providedConfig:WorkPackageTableConfigurationObject) { Object.entries(providedConfig).forEach(([k, value]) => { const key = (k as keyof WorkPackageTableConfiguration); - (this as any)[key] = value; + (this as Record)[key] = value; }); } } diff --git a/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts b/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts index e28b4d8e8d9f..c6839c136f54 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts @@ -98,7 +98,7 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O private element:HTMLElement; - private scrollSyncUpdate:(timelineVisible:boolean) => any; + private scrollSyncUpdate:(timelineVisible:boolean) => void; private wpTableHoverSync:WpTableHoverSync; @@ -114,7 +114,13 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O public locale:string; - public text:any; + public text:{ + cancel:string; + noResults:{ title:string; description:string }; + limitedResults:(count:number, total:number) => string; + tableSummary:string; + tableSummaryHints:string; + }; public results:WorkPackageCollectionResource; From 95b2ffe51d4558078a124778fa344e7d4549518c Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:27:59 +0100 Subject: [PATCH 4/5] [OP-20347] Type timeline callbacks and cell map Resolves WorkPackageTimelineCell in the global IGroupCellsMap declaration through an import type, which the ambient file could not see before. Makes the timeline container's common pipe generic, types the selection mode callbacks as returning void, relation errors as unknown and milestone date moves as CellDateMovement. https://community.openproject.org/wp/OP-20347 --- .../timeline/cells/timeline-milestone-cell-renderer.ts | 4 ++-- .../timeline/container/wp-timeline-container.directive.ts | 8 ++++---- .../components/wp-table/timeline/wp-timeline.ts | 2 +- .../work-packages/components/wp-table/typings.d.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts index 9ee0254d11a9..997733d73d83 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts @@ -97,8 +97,8 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { */ public assignDateValues(change:WorkPackageChangeset, labels:WorkPackageCellLabels, - dates:any):void { - this.assignDate(change, 'date', dates.date); + dates:CellDateMovement):void { + this.assignDate(change, 'date', dates.date!); this.updateLabels(true, labels, change); } diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts index 28bbe9fb2150..283038324ac2 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts @@ -145,7 +145,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl private orderedRows:RenderedWorkPackage[] = []; get commonPipes() { - return (source:Observable) => source.pipe( + return (source:Observable) => source.pipe( this.untilDestroyed(), takeUntil(this.querySpace.stopAllSubscriptions), filter(() => this.initialized && this.wpTableTimeline.isVisible), @@ -300,7 +300,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl relationType: 'follows', }); }) - .catch((error:any) => this.notificationService.handleRawError(error, end)); + .catch((error:unknown) => this.notificationService.handleRawError(error, end)); }); } @@ -315,7 +315,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl relationType: 'precedes', }); }) - .catch((error:any) => this.notificationService.handleRawError(error, end)); + .catch((error:unknown) => this.notificationService.handleRawError(error, end)); }); } @@ -366,7 +366,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl this.refreshView(); } - private activateSelectionMode(start:string, callback:(wp:WorkPackageResource) => any) { + private activateSelectionMode(start:string, callback:(wp:WorkPackageResource) => void) { start = start.toString(); // old system bug: ID can be a 'number' this._viewParameters.activeSelectionMode = (wp:WorkPackageResource) => { diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/wp-timeline.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/wp-timeline.ts index 4f023524d5fc..fe29ee8c0ff5 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/timeline/wp-timeline.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/wp-timeline.ts @@ -85,7 +85,7 @@ export class TimelineViewParameters { settings:TimelineViewParametersSettings = new TimelineViewParametersSettings(); - activeSelectionMode:null | ((wp:WorkPackageResource) => any) = null; + activeSelectionMode:null | ((wp:WorkPackageResource) => void) = null; selectionModeStart:null | string = null; diff --git a/frontend/src/app/features/work-packages/components/wp-table/typings.d.ts b/frontend/src/app/features/work-packages/components/wp-table/typings.d.ts index 14f88ad78ebd..d7638acdfb5c 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/typings.d.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/typings.d.ts @@ -26,4 +26,4 @@ // See COPYRIGHT and LICENSE files for more details. //++ -type IGroupCellsMap = Record; +type IGroupCellsMap = Record; From 0c93dd36fcb212aa48b0cc067db012dc746cba0f Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:27:59 +0100 Subject: [PATCH 5/5] [OP-20347] Type fast table and card view handlers Types group sums with the GroupObject sums record and casts it at the display field boundary, where it has always been passed as a resource. Casts the still untyped group value locally, reads the card view handler token through a typed provider token, and stringifies the caught scroll error explicitly. https://community.openproject.org/wp/OP-20347 --- .../components/wp-card-view/wp-card-view.component.ts | 7 +++++-- .../builders/modes/grouped/group-sums-builder.ts | 7 ++++--- .../builders/modes/grouped/grouped-rows-helpers.ts | 4 ++-- .../wp-fast-table/handlers/table-handler-registry.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/wp-card-view.component.ts b/frontend/src/app/features/work-packages/components/wp-card-view/wp-card-view.component.ts index f1354ef43a6e..730ac214418a 100644 --- a/frontend/src/app/features/work-packages/components/wp-card-view/wp-card-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-card-view/wp-card-view.component.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Injector, Input, OnInit, Output, ViewChild, OnDestroy, inject } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Injector, Input, OnInit, Output, ViewChild, OnDestroy, inject, ProviderToken } from '@angular/core'; import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { WorkPackageInlineCreateService } from 'core-app/features/work-packages/components/wp-inline-create/wp-inline-create.service'; @@ -224,7 +224,10 @@ export class WorkPackageCardViewComponent extends UntilDestroyedMixin implements this.cardDragDrop.init(this); // Register event handlers for the cards - const registry = this.injector.get(WorkPackageViewHandlerToken, CardViewHandlerRegistry); + const registry = this.injector.get( + WorkPackageViewHandlerToken as ProviderToken, + CardViewHandlerRegistry, + ); if (registry instanceof CardViewHandlerRegistry) { registry.attachTo(this); } else { diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/group-sums-builder.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/group-sums-builder.ts index 5200bc39a52c..d441d1bf50d9 100644 --- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/group-sums-builder.ts +++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/group-sums-builder.ts @@ -35,6 +35,7 @@ import { DisplayFieldService } from 'core-app/shared/components/fields/display/d import { groupedRowClassName } from 'core-app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; import { GroupObject } from 'core-app/features/hal/resources/wp-collection-resource'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class GroupSumsBuilder extends SingleRowBuilder { @LazyInject() readonly querySpace:IsolatedQuerySpace; @@ -56,7 +57,7 @@ export class GroupSumsBuilder extends SingleRowBuilder { return tr; } - public renderColumns(sums:Record, tr:HTMLTableRowElement) { + public renderColumns(sums:GroupObject['sums'], tr:HTMLTableRowElement) { this.augmentedColumns.forEach((column, i:number) => { const td = document.createElement('td'); const div = this.renderContent(sums, column.id, this.sumsSchema[column.id] as IFieldSchema); @@ -85,7 +86,7 @@ export class GroupSumsBuilder extends SingleRowBuilder { return this.schemaCache.state(href).value!; } - private renderContent(sums:any, name:string, fieldSchema:IFieldSchema) { + private renderContent(sums:GroupObject['sums'], name:string, fieldSchema:IFieldSchema) { const div = document.createElement('div'); div.classList.add('wp-table--sum-container', name); @@ -96,7 +97,7 @@ export class GroupSumsBuilder extends SingleRowBuilder { } const field = this.displayFieldService.getField( - sums, + sums as unknown as HalResource, name, fieldSchema, { injector: this.injector, container: 'table', options: {} }, diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers.ts index 6a9841ffb61e..f02335e98cb2 100644 --- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers.ts +++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-helpers.ts @@ -29,7 +29,7 @@ import { GroupObject } from 'core-app/features/hal/resources/wp-collection-resource'; export function groupIdentifier(group:GroupObject) { - let value = group.value || 'nullValue'; + let value = (group.value as string) || 'nullValue'; if (group.href) { try { @@ -44,7 +44,7 @@ export function groupIdentifier(group:GroupObject) { } export function groupName(group:GroupObject) { - const { value } = group; + const value = group.value as string|null; if (value === null) { return '-'; } diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry.ts index d92eba6d0643..9311044fa9f8 100644 --- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry.ts +++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry.ts @@ -63,7 +63,7 @@ import { } from 'core-app/features/work-packages/components/wp-fast-table/handlers/state/sharing-transformer'; // noinspection JSUnusedLocalSymbols -type StateTransformers = new(injector:Injector, table:WorkPackageTable) => any; +type StateTransformers = new(injector:Injector, table:WorkPackageTable) => unknown; export interface TableEventComponent extends WorkPackageViewOutputs { // Reference to the fast table instance