From ce13be188ebb67e29bda7dd24fd53be94ca90296 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:02:20 +0100 Subject: [PATCH 1/6] [OP-20346] Type query URL param helpers Types the query string builder input as an object and lets the decoded query props flow through their declared string types instead of re-annotating callback parameters as any. Types the query param listener's change stream as the checksum it emits. https://community.openproject.org/wp/OP-20346 --- .../components/wp-query/url-params-helper.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.ts b/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.ts index a0abb9618ddd..f6615029d708 100644 --- a/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.ts +++ b/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.ts @@ -113,7 +113,7 @@ export class UrlParamsHelperService { // copied more or less from angular buildUrl - public buildQueryString(params:any) { + public buildQueryString(params:object|null|undefined) { if (!params) { return undefined; } @@ -256,7 +256,7 @@ export class UrlParamsHelperService { const properties = JSON.parse(updateJson) as QueryProps; if (properties.c) { - queryData['columns[]'] = properties.c.map((column:any) => column); + queryData['columns[]'] = properties.c.map((column) => column); } if (properties.s) { queryData.showSums = properties.s; @@ -287,7 +287,7 @@ export class UrlParamsHelperService { } if (properties.hla) { - queryData['highlightedAttributes[]'] = properties.hla.map((column:any) => column); + queryData['highlightedAttributes[]'] = properties.hla.map((column) => column); } if (properties.hi !== undefined) { @@ -319,7 +319,7 @@ export class UrlParamsHelperService { // Sortation if (properties.t) { - queryData.sortBy = JSON.stringify(properties.t.split(',').map((sort:any) => sort.split(':'))); + queryData.sortBy = JSON.stringify(properties.t.split(',').map((sort) => sort.split(':'))); } if (properties.ts) { @@ -401,7 +401,7 @@ export class UrlParamsHelperService { private buildV3GetColumnsFromQueryResource(query:QueryResource):string[] { if (query.columns) { - return query.columns.map((column:any) => column.id || idFromLink(column.href)) as string[]; + return query.columns.map((column) => column.id || idFromLink(column.href)); } const links = query._links as { columns?:HalLink[] }; if (links.columns) { From 69565ea4274b44f9ace4f54a14c043808b7c9d66 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:02:21 +0100 Subject: [PATCH 2/6] [OP-20346] Type query form payloads Declares a QueryFormPayload interface for the raw JSON posted to the query form endpoint, with a link bag that accepts single links and link arrays such as sortBy. The graph widget's creation params now return that type so they still satisfy the typed parameter. https://community.openproject.org/wp/OP-20346 --- .../core/apiv3/endpoints/queries/apiv3-query-form.ts | 11 +++++++++-- .../configuration/wp-graph-configuration.ts | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-form.ts b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-form.ts index b7af457450fc..390167a11a68 100644 --- a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-form.ts +++ b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-form.ts @@ -34,6 +34,13 @@ import URI from 'urijs'; import { map, tap } from 'rxjs/operators'; import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator'; import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service'; +import { HalSourceLink } from 'core-app/features/hal/interfaces'; + +export interface QueryFormPayload { + [key:string]:unknown; + name?:string; + _links?:Record; +} export class ApiV3QueryForm extends ApiV3FormResource { @LazyInject() private queryFilters:QueryFiltersService; @@ -46,7 +53,7 @@ export class ApiV3QueryForm extends ApiV3FormResource { // We need a valid payload so that we // can check whether form saving is possible. // The query needs a name to be valid. - const payload:any = { + const payload:QueryFormPayload = { name: query.name || '!!!__O__o__O__!!!', }; @@ -75,7 +82,7 @@ export class ApiV3QueryForm extends ApiV3FormResource { * @param projectIdentifier * @param payload */ - public loadWithParams(params:Record, queryId:string|null|undefined, projectIdentifier:string|undefined|null, payload:any = {}):Observable<[QueryFormResource, QueryResource]> { + public loadWithParams(params:Record, queryId:string|null|undefined, projectIdentifier:string|undefined|null, payload:QueryFormPayload = {}):Observable<[QueryFormResource, QueryResource]> { // We need a valid payload so that we // can check whether form saving is possible. // The query needs a name to be valid. diff --git a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.ts b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.ts index 3a8679a93cd2..d03b9a8c89c4 100644 --- a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.ts +++ b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.ts @@ -29,6 +29,7 @@ import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { ChartOptions } from 'chart.js'; import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { QueryFormPayload } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-form'; export interface WpGraphQueryParams { id?:string; @@ -54,7 +55,7 @@ export class WpGraphConfiguration implements WpGraphConfiguration { this.chartType = this.chartType || 'bar'; } - public static queryCreationParams(i18n:I18nService, isPublic:boolean):unknown { + public static queryCreationParams(i18n:I18nService, isPublic:boolean):QueryFormPayload { return { public: isPublic, name: i18n.t('js.grid.widgets.work_packages_graph.title'), From fe09cbc33b4b72d8fb45d042d7fffb323bb954c8 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:02:51 +0100 Subject: [PATCH 3/6] [OP-20346] Cast APIv3 cache values to their type Replaces the any casts where cached APIv3 collections and the state cache hand HAL responses to their generic cache type. The casts go through unknown or the type parameter directly, keeping the existing assumption that the response matches the cached type. https://community.openproject.org/wp/OP-20346 --- frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts | 2 +- frontend/src/app/core/apiv3/cache/state-cache.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts b/frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts index a58a37bce57a..2ef0499cec09 100644 --- a/frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts +++ b/frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts @@ -62,7 +62,7 @@ export abstract class ApiV3Collection< if (response instanceof CollectionResource) { response.elements?.forEach(this.touch.bind(this)); } else if (response instanceof HalResource) { - this.touch(response as any); + this.touch(response as unknown as T); } }, ), diff --git a/frontend/src/app/core/apiv3/cache/state-cache.service.ts b/frontend/src/app/core/apiv3/cache/state-cache.service.ts index 5c0911988bf1..b22399e34e12 100644 --- a/frontend/src/app/core/apiv3/cache/state-cache.service.ts +++ b/frontend/src/app/core/apiv3/cache/state-cache.service.ts @@ -114,7 +114,7 @@ export class StateCacheService { * @param resource The value. */ public updateFor(resource:HasId):Promise { - return this.updateValue(resource.id!, resource as any); + return this.updateValue(resource.id!, resource as T); } /** From a8a148068f9e8dfe82b9e7fe5fbb674e6c41aaeb Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:03:15 +0100 Subject: [PATCH 4/6] [OP-20346] Type new work package breadcrumbs Declares the breadcrumb list as BreadcrumbItem[] instead of letting it evolve into any[], matching the work package view page. Asserts the current project name as present inside the identifier check, as that page already does. https://community.openproject.org/wp/OP-20346 --- .../components/wp-new/wp-new-full-view.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts index d8eef45e31e5..519f21655bbe 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts @@ -28,6 +28,7 @@ import { WorkPackageCreateComponent } from 'core-app/features/work-packages/components/wp-new/wp-create.component'; import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { BreadcrumbItem } from 'core-app/shared/components/breadcrumbs/op-breadcrumbs.component'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -39,11 +40,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; }) export class WorkPackageNewFullViewComponent extends WorkPackageCreateComponent { breadcrumbItems() { - const items = []; + const items:BreadcrumbItem[] = []; if (this.currentProjectService?.identifier) { items.push({ href: this.pathHelper.projectPath(this.currentProjectService.identifier), - text: this.currentProjectService.name, + text: this.currentProjectService.name!, }); } items.push({ From aeb5353edfb37dc095501cd53f5f84e56fb4e7ed Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:03:48 +0100 Subject: [PATCH 5/6] [OP-20346] Type board action query params Lets the board action query params infer their object type instead of declaring them as any, since addQuery only needs an object. Casts the synthetic unassigned user to HalResource once where it is declared, which is how every consumer already treats it. https://community.openproject.org/wp/OP-20346 --- .../board-actions/assignee/assignee-action.service.ts | 8 ++++---- .../boards/board/board-actions/board-action.service.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/features/boards/board/board-actions/assignee/assignee-action.service.ts b/frontend/src/app/features/boards/board/board-actions/assignee/assignee-action.service.ts index 9220516b2568..0af18a83a809 100644 --- a/frontend/src/app/features/boards/board/board-actions/assignee/assignee-action.service.ts +++ b/frontend/src/app/features/boards/board/board-actions/assignee/assignee-action.service.ts @@ -59,17 +59,17 @@ export class BoardAssigneeActionService extends CachedBoardActionService { image = imagePath('board_creation_modal/assignees.svg'); - readonly unassignedUser:any = { + readonly unassignedUser = { id: null, href: null, name: this.I18n.t('js.filter.noneElement'), - }; + } as unknown as HalResource; /** * Add a single action query */ addColumnWithActionAttribute(board:Board, value:HalResource):Promise { - const params:any = { + const params = { name: value.name, }; @@ -148,7 +148,7 @@ export class BoardAssigneeActionService extends CachedBoardActionService { .available_assignees .get() .pipe( - map((collection:CollectionResource) => [this.unassignedUser].concat(collection.elements) as HalResource[]), + map((collection:CollectionResource) => [this.unassignedUser].concat(collection.elements)), ); } } diff --git a/frontend/src/app/features/boards/board/board-actions/board-action.service.ts b/frontend/src/app/features/boards/board/board-actions/board-action.service.ts index 96358f6d84d0..7a9610bbfbd9 100644 --- a/frontend/src/app/features/boards/board/board-actions/board-action.service.ts +++ b/frontend/src/app/features/boards/board/board-actions/board-action.service.ts @@ -178,7 +178,7 @@ export abstract class BoardActionService { * Add a single action query */ addColumnWithActionAttribute(board:Board, value:HalResource):Promise { - const params:any = { + const params = { name: value.name, }; From 7898745041de90b5d4d0f9b8af7154fe0fa55f62 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Fri, 2 Oct 2026 17:58:46 +0100 Subject: [PATCH 6/6] [OP-20346] Constrain cached resource updates Requires updateFor values to satisfy both the cached type and HasId, removing the unchecked cast. Keeps the cache itself unconstrained because relation caches store maps without an id. https://community.openproject.org/wp/OP-20346 --- frontend/src/app/core/apiv3/cache/state-cache.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/core/apiv3/cache/state-cache.service.ts b/frontend/src/app/core/apiv3/cache/state-cache.service.ts index b22399e34e12..c5c26bb25ef2 100644 --- a/frontend/src/app/core/apiv3/cache/state-cache.service.ts +++ b/frontend/src/app/core/apiv3/cache/state-cache.service.ts @@ -113,8 +113,8 @@ export class StateCacheService { * * @param resource The value. */ - public updateFor(resource:HasId):Promise { - return this.updateValue(resource.id!, resource as T); + public updateFor(resource:T & HasId):Promise { + return this.updateValue(resource.id!, resource); } /**