From 789c5952ec659fedba01dca1c832fc065b06f1b3 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:36:28 +0100 Subject: [PATCH 1/7] [OP-20348] Type HTTP client param and header maps Types HTTPClientParamMap with the value shapes HttpParams accepts and HTTPClientHeaders with the shapes HttpHeaders accepts, instead of Record. Both maps feed Angular's HttpClient unchanged, so the narrower types only document what already worked. The BCF request service now takes its request payload as unknown, as the HAL resource service does, and casts it to a param map only on the GET branch. The query path helpers that forward loosely typed query params cast them at that boundary. https://community.openproject.org/wp/OP-20348 --- .../endpoints/queries/apiv3-queries-paths.ts | 5 +++-- .../endpoints/queries/apiv3-query-paths.ts | 3 ++- .../bim/bcf/api/bcf-api-request.service.ts | 18 +++++++++--------- .../topics/bcf-viewpoint-collection.paths.ts | 2 +- .../app/features/hal/http/http.interfaces.ts | 5 +++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-queries-paths.ts b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-queries-paths.ts index 71080464e1c1..f4bf59173663 100644 --- a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-queries-paths.ts +++ b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-queries-paths.ts @@ -38,6 +38,7 @@ import { CollectionResource } from 'core-app/features/hal/resources/collection-r import { ApiV3ListParameters, listParamsString } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service'; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper'; +import { HTTPClientParamMap } from 'core-app/features/hal/http/http.interfaces'; export class ApiV3QueriesPaths extends ApiV3ResourceCollection { @LazyInject() private queryFilters:QueryFiltersService; @@ -89,7 +90,7 @@ export class ApiV3QueriesPaths extends ApiV3ResourceCollection(path, queryData); + .get(path, queryData as HTTPClientParamMap); } /** @@ -101,7 +102,7 @@ export class ApiV3QueriesPaths extends ApiV3ResourceCollection( this.default.path, - params, + params as HTTPClientParamMap, ); } diff --git a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-paths.ts b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-paths.ts index e2fd8fb6b705..e8df6622aa5b 100644 --- a/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-paths.ts +++ b/frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-paths.ts @@ -36,6 +36,7 @@ import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorato import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service'; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper'; import { PaginationObject } from 'core-app/shared/components/table-pagination/pagination-service'; +import { HTTPClientParamMap } from 'core-app/features/hal/http/http.interfaces'; export class ApiV3QueryPaths extends ApiV3GettableResource { @LazyInject() private queryFilters:QueryFiltersService; @@ -52,7 +53,7 @@ export class ApiV3QueryPaths extends ApiV3GettableResource { */ public parameterised(params:object):Observable { return this.halResourceService - .get(this.path, params); + .get(this.path, params as HTTPClientParamMap); } /** diff --git a/frontend/src/app/features/bim/bcf/api/bcf-api-request.service.ts b/frontend/src/app/features/bim/bcf/api/bcf-api-request.service.ts index 5cdf27f3f69f..3c52b59043b7 100644 --- a/frontend/src/app/features/bim/bcf/api/bcf-api-request.service.ts +++ b/frontend/src/app/features/bim/bcf/api/bcf-api-request.service.ts @@ -70,7 +70,7 @@ export class BcfApiRequestService { responseType: 'json', }; - return this._request('get', path, config); + return this.sendRequest('get', path, config); } /** @@ -81,11 +81,11 @@ export class BcfApiRequestService { * @param data Request payload (URL params for get, JSON payload otherwise) * @param headers Request headers */ - public request(method:HTTPSupportedMethods, path:string, data:HTTPClientParamMap = {}, headers:HTTPClientHeaders = {}):Observable { + public request(method:HTTPSupportedMethods, path:string, data:unknown = {}, headers:HTTPClientHeaders = {}):Observable { // HttpClient requires us to create HttpParams instead of passing data for get // so forward to that method instead. if (method === 'get') { - return this.get(path, data, headers); + return this.get(path, data as HTTPClientParamMap, headers); } const config:HTTPClientOptions = { @@ -95,7 +95,7 @@ export class BcfApiRequestService { responseType: 'json', }; - return this._request(method, path, config); + return this.sendRequest(method, path, config); } /** @@ -107,12 +107,12 @@ export class BcfApiRequestService { * * @private */ - private _request(method:HTTPSupportedMethods, path:string, config:HTTPClientOptions):Observable { + private sendRequest(method:HTTPSupportedMethods, path:string, config:HTTPClientOptions):Observable { return this .http .request(method, path, config) .pipe( - map((response:any) => this.deserialize(response)), + map((response) => this.deserialize(response)), catchError((error:HttpErrorResponse) => { console.error(`Failed to ${method} ${path}: ${error.name}`); return throwError(error); @@ -124,11 +124,11 @@ export class BcfApiRequestService { * Deserialize the JSON data into the mapped resource class, if given. * @param data JSON API response. */ - protected deserialize(data:any):T { + protected deserialize(data:unknown):T { if (this.resourceClass) { const serializer = new TypedJSON(this.resourceClass); - return serializer.parse(data)!; + return serializer.parse(data) as T; } - return data; + return data as T; } } diff --git a/frontend/src/app/features/bim/bcf/api/topics/bcf-viewpoint-collection.paths.ts b/frontend/src/app/features/bim/bcf/api/topics/bcf-viewpoint-collection.paths.ts index 4880615e6fcb..b51de3e4df12 100644 --- a/frontend/src/app/features/bim/bcf/api/topics/bcf-viewpoint-collection.paths.ts +++ b/frontend/src/app/features/bim/bcf/api/topics/bcf-viewpoint-collection.paths.ts @@ -50,7 +50,7 @@ export class BcfTopicCollectionPath extends BcfResourceCollectionPath { + post(payload:unknown):Observable { return this .bcfTopicService .request( diff --git a/frontend/src/app/features/hal/http/http.interfaces.ts b/frontend/src/app/features/hal/http/http.interfaces.ts index c0432974a924..bd760862eb56 100644 --- a/frontend/src/app/features/hal/http/http.interfaces.ts +++ b/frontend/src/app/features/hal/http/http.interfaces.ts @@ -40,6 +40,7 @@ export interface HTTPClientOptions { responseType:'json'; } -export type HTTPClientParamMap = Record; -export type HTTPClientHeaders = HttpHeaders|HTTPClientParamMap; +export type HTTPClientParamValue = string|number|boolean|readonly (string|number|boolean)[]; +export type HTTPClientParamMap = Record; +export type HTTPClientHeaders = HttpHeaders|Record; export type HTTPClientParams = HttpParams|HTTPClientParamMap; From f0eb4eb47c332ec088c135842e7634391c677ede Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:36:29 +0100 Subject: [PATCH 2/7] [OP-20348] Type BCF API resources and gallery Types the BCF API path parser walk, the TypedJSON deserializer inputs and the viewpoint gallery images and actions with the types the libraries declare, instead of any. The parser still returns the resource it reaches and the deserializers still receive the raw date strings, so runtime behaviour is unchanged. https://community.openproject.org/wp/OP-20348 --- frontend/src/app/features/bim/bcf/api/bcf-api.service.ts | 8 ++++---- .../app/features/bim/bcf/api/topics/bcf-topic.resource.ts | 6 +++--- .../bcf-wp-attribute-group.component.ts | 8 ++++---- .../import-export-bcf/bcf-export-button.component.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/features/bim/bcf/api/bcf-api.service.ts b/frontend/src/app/features/bim/bcf/api/bcf-api.service.ts index 21cc945d65e1..0be855467895 100644 --- a/frontend/src/app/features/bim/bcf/api/bcf-api.service.ts +++ b/frontend/src/app/features/bim/bcf/api/bcf-api.service.ts @@ -58,22 +58,22 @@ export class BcfApiService { .split('/'); // Try to find a target collection or resource - let current:any = this; + let current = this as object; for (const pathOrId of parts) { if (pathOrId in current) { // Current has a member named like this URL part // descend into it - current = current[pathOrId]; + current = (current as Record)[pathOrId]; } else if (current instanceof BcfResourceCollectionPath) { // Otherwise, assume we're looking for an ID - current = current.id(pathOrId); + current = current.id(pathOrId) as object; } else { // Otherwise, return the current break; } } - return current === this ? undefined : current; + return current === this ? undefined : current as T; } } diff --git a/frontend/src/app/features/bim/bcf/api/topics/bcf-topic.resource.ts b/frontend/src/app/features/bim/bcf/api/topics/bcf-topic.resource.ts index 8608b72334f9..61d49454cd5c 100644 --- a/frontend/src/app/features/bim/bcf/api/topics/bcf-topic.resource.ts +++ b/frontend/src/app/features/bim/bcf/api/topics/bcf-topic.resource.ts @@ -64,13 +64,13 @@ export class BcfTopicResource { @jsonArrayMember(String) labels:string[]; - @jsonMember({ deserializer: (value) => moment(value), serializer: (timestamp:Moment) => timestamp.toISOString() }) + @jsonMember({ deserializer: (value:string) => moment(value), serializer: (timestamp:Moment) => timestamp.toISOString() }) creation_date:Moment; @jsonMember(String) creation_author:string; - @jsonMember({ deserializer: (value) => moment(value), serializer: (timestamp:Moment) => timestamp.toISOString() }) + @jsonMember({ deserializer: (value:string) => moment(value), serializer: (timestamp:Moment) => timestamp.toISOString() }) modified_date:Moment; @jsonMember(String, { preserveNull: true }) @@ -86,7 +86,7 @@ export class BcfTopicResource { description:string; @jsonMember({ - deserializer: (value) => moment(value), + deserializer: (value:string) => moment(value), serializer: (timestamp:Moment) => timestamp.format('YYYY-MM-DD'), }) due_date:Moment; diff --git a/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts b/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts index 241968ad46ab..893e22f19cbc 100644 --- a/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts +++ b/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts @@ -29,7 +29,7 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { UrlParamsService } from 'core-app/core/navigation/url-params.service'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; -import { NgxGalleryComponent, NgxGalleryOptions } from '@kolkov/ngx-gallery'; +import { NgxGalleryComponent, NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery'; import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { I18nService } from 'core-app/core/i18n/i18n.service'; @@ -135,7 +135,7 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements viewpoints:BcfViewpointItem[] = []; - galleryImages:any[] = []; + galleryImages:NgxGalleryImage[] = []; // Store whether viewing is allowed viewAllowed = false; @@ -279,7 +279,7 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements return [ { icon: 'icon-view-model', - onClick: (evt:any, index:number) => { + onClick: (evt:Event, index:number) => { this.showViewpoint(this.workPackage, index); this.gallery.preview.close(); }, @@ -287,7 +287,7 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements }, { icon: 'icon-delete', - onClick: (evt:any, index:number) => this.deleteViewpoint(this.workPackage, index), + onClick: (evt:Event, index:number) => this.deleteViewpoint(this.workPackage, index), titleText: this.text.delete_viewpoint, }, ]; diff --git a/frontend/src/app/features/bim/ifc_models/toolbar/import-export-bcf/bcf-export-button.component.ts b/frontend/src/app/features/bim/ifc_models/toolbar/import-export-bcf/bcf-export-button.component.ts index bcaf68a03bd9..0dadc3264f39 100644 --- a/frontend/src/app/features/bim/ifc_models/toolbar/import-export-bcf/bcf-export-button.component.ts +++ b/frontend/src/app/features/bim/ifc_models/toolbar/import-export-bcf/bcf-export-button.component.ts @@ -96,7 +96,7 @@ export class BcfExportButtonComponent extends UntilDestroyedMixin implements OnI }); } - public showDelayedExport(event:any) { + public showDelayedExport(event:Event) { this.requestExport(this.exportLink); event.preventDefault(); From 0b2c36d7697099b953e7123a8ca58baf12462887 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:38:02 +0100 Subject: [PATCH 3/7] [OP-20348] Type modal locals as unknown Declares the OpModalLocalsMap index signature as unknown instead of any, so every modal states the type of each local it reads. The modals that read locals now cast them at that boundary to the type their callers already pass. The date picker modal reads its resource through a small interface instead of eleven lint suppressions. Its formatter now stringifies the non-working-days flag itself; URLSearchParams did the same before, so the URL is unchanged. The macro and add-list modals infer their text maps instead of declaring them as any. https://community.openproject.org/wp/OP-20348 --- .../add-list-modal.component.ts | 4 +- .../tabs/highlighting-tab.component.ts | 2 +- .../wp-date-picker.modal.ts | 48 +++++++++---------- .../shared/components/modal/modal.types.ts | 2 +- .../child-pages-macro.modal.ts | 6 +-- .../code-block-macro.modal.ts | 2 +- .../wiki-include-page-macro.modal.ts | 4 +- .../wp-button-macro.modal.ts | 6 +-- 8 files changed, 36 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts index 61a34befe334..62ba3bc1d863 100644 --- a/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts @@ -97,7 +97,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { public warningText:string|undefined; - public text:any = { + public text = { title: this.I18n.t('js.boards.add_list'), button_add: this.I18n.t('js.button_add'), button_create: this.I18n.t('js.button_create'), @@ -118,7 +118,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { ngOnInit() { super.ngOnInit(); - this.board = this.locals.board; + this.board = this.locals.board as Board; this.active = new Set(this.locals.active as string[]); this.actionService = this.boardActions.get(this.board.actionAttribute!); this.autocompleterOptions.resource = this.actionService.resourceName.toLowerCase(); diff --git a/frontend/src/app/features/boards/board/configuration-modal/tabs/highlighting-tab.component.ts b/frontend/src/app/features/boards/board/configuration-modal/tabs/highlighting-tab.component.ts index 6af41e24b0a7..fa0d459955a6 100644 --- a/frontend/src/app/features/boards/board/configuration-modal/tabs/highlighting-tab.component.ts +++ b/frontend/src/app/features/boards/board/configuration-modal/tabs/highlighting-tab.component.ts @@ -73,7 +73,7 @@ export class BoardHighlightingTabComponent implements TabComponent, OnInit { } ngOnInit() { - this.board = this.locals.board; + this.board = this.locals.board as Board; this.highlightingMode = this.board.highlightingMode; this.updateMode(this.highlightingMode); } diff --git a/frontend/src/app/shared/components/datepicker/wp-date-picker-modal/wp-date-picker.modal.ts b/frontend/src/app/shared/components/datepicker/wp-date-picker-modal/wp-date-picker.modal.ts index 56149d1fd101..ca64bcd32573 100644 --- a/frontend/src/app/shared/components/datepicker/wp-date-picker-modal/wp-date-picker.modal.ts +++ b/frontend/src/app/shared/components/datepicker/wp-date-picker-modal/wp-date-picker.modal.ts @@ -31,6 +31,14 @@ import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/cor import { OpModalComponent } from 'core-app/shared/components/modal/modal.component'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +interface DatePickerModalResource { + id:string; + startDate:string|null; + dueDate:string|null; + duration:string|null; + includeNonWorkingDays:boolean|null; +} + @Component({ templateUrl: './wp-date-picker.modal.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -75,43 +83,33 @@ export class OpWpDatePickerModalComponent extends OpModalComponent implements On } public updateFrameSrc():void { + const resource = this.locals.resource as DatePickerModalResource; const url = new URL( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.pathHelper.workPackageDatepickerDialogContentPath(this.locals.resource.id as string), + this.pathHelper.workPackageDatepickerDialogContentPath(resource.id), window.location.origin, ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - url.searchParams.set('field', this.locals.name); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[initial][start_date]', this.nullAsEmptyStringFormatter(this.locals.resource.startDate)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[initial][due_date]', this.nullAsEmptyStringFormatter(this.locals.resource.dueDate)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[initial][duration]', this.nullAsEmptyStringFormatter(this.locals.resource.duration)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument - url.searchParams.set('work_package[initial][ignore_non_working_days]', this.nullAsEmptyStringFormatter(this.locals.resource.includeNonWorkingDays)); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[start_date]', this.nullAsEmptyStringFormatter(this.locals.resource.startDate)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[due_date]', this.nullAsEmptyStringFormatter(this.locals.resource.dueDate)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - url.searchParams.set('work_package[duration]', this.nullAsEmptyStringFormatter(this.locals.resource.duration)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument - url.searchParams.set('work_package[ignore_non_working_days]', this.nullAsEmptyStringFormatter(this.locals.resource.includeNonWorkingDays)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (this.locals.resource?.id === 'new' && this.locals.resource.startDate) { + url.searchParams.set('field', this.locals.name as string); + url.searchParams.set('work_package[initial][start_date]', this.nullAsEmptyStringFormatter(resource.startDate)); + url.searchParams.set('work_package[initial][due_date]', this.nullAsEmptyStringFormatter(resource.dueDate)); + url.searchParams.set('work_package[initial][duration]', this.nullAsEmptyStringFormatter(resource.duration)); + url.searchParams.set('work_package[initial][ignore_non_working_days]', this.nullAsEmptyStringFormatter(resource.includeNonWorkingDays)); + + url.searchParams.set('work_package[start_date]', this.nullAsEmptyStringFormatter(resource.startDate)); + url.searchParams.set('work_package[due_date]', this.nullAsEmptyStringFormatter(resource.dueDate)); + url.searchParams.set('work_package[duration]', this.nullAsEmptyStringFormatter(resource.duration)); + url.searchParams.set('work_package[ignore_non_working_days]', this.nullAsEmptyStringFormatter(resource.includeNonWorkingDays)); + if (resource.id === 'new' && resource.startDate) { url.searchParams.set('work_package[start_date_touched]', 'true'); } this.turboFrameSrc = url.toString(); } - private nullAsEmptyStringFormatter(value:null|undefined|string):string { + private nullAsEmptyStringFormatter(value:null|undefined|string|boolean):string { if (value === undefined || value === null) { return ''; } - return value; + return String(value); } } diff --git a/frontend/src/app/shared/components/modal/modal.types.ts b/frontend/src/app/shared/components/modal/modal.types.ts index 832bfa5414e6..992502a64029 100644 --- a/frontend/src/app/shared/components/modal/modal.types.ts +++ b/frontend/src/app/shared/components/modal/modal.types.ts @@ -30,5 +30,5 @@ import { OpModalService } from './modal.service'; export interface OpModalLocalsMap { service:OpModalService; - [key:string]:any; + [key:string]:unknown; } diff --git a/frontend/src/app/shared/components/modals/editor/macro-child-pages-modal/child-pages-macro.modal.ts b/frontend/src/app/shared/components/modals/editor/macro-child-pages-modal/child-pages-macro.modal.ts index d8990ed6458c..5aa4c99f2aa3 100644 --- a/frontend/src/app/shared/components/modals/editor/macro-child-pages-modal/child-pages-macro.modal.ts +++ b/frontend/src/app/shared/components/modals/editor/macro-child-pages-modal/child-pages-macro.modal.ts @@ -55,7 +55,7 @@ export class ChildPagesMacroModalComponent extends OpModalComponent implements A @ViewChild('selectedPageInput', { static: true }) selectedPageInput:ElementRef; - public text:any = { + public text = { title: this.I18n.t('js.editor.macro.child_pages.button'), hint: this.I18n.t('js.editor.macro.child_pages.hint'), page: this.I18n.t('js.editor.macro.child_pages.page'), @@ -68,8 +68,8 @@ export class ChildPagesMacroModalComponent extends OpModalComponent implements A constructor() { super(); - this.selectedPage = this.page = this.locals.page; - this.selectedIncludeParent = this.includeParent = this.locals.includeParent; + this.selectedPage = this.page = this.locals.page as string; + this.selectedIncludeParent = this.includeParent = this.locals.includeParent as boolean; // We could provide an autocompleter here to get correct page names } diff --git a/frontend/src/app/shared/components/modals/editor/macro-code-block-modal/code-block-macro.modal.ts b/frontend/src/app/shared/components/modals/editor/macro-code-block-modal/code-block-macro.modal.ts index 000562428ecb..1dda91bc2044 100644 --- a/frontend/src/app/shared/components/modals/editor/macro-code-block-modal/code-block-macro.modal.ts +++ b/frontend/src/app/shared/components/modals/editor/macro-code-block-modal/code-block-macro.modal.ts @@ -68,7 +68,7 @@ export class CodeBlockMacroModalComponent extends OpModalComponent implements Af readonly I18n = inject(I18nService); readonly codeMirrorLoader = inject(CodeMirrorLoaderService); - public text:any = { + public text = { title: this.I18n.t('js.editor.macro.code_block.title'), language: this.I18n.t('js.editor.macro.code_block.language'), language_hint: this.I18n.t('js.editor.macro.code_block.language_hint'), diff --git a/frontend/src/app/shared/components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts b/frontend/src/app/shared/components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts index ed02883395af..13ed41c4e21c 100644 --- a/frontend/src/app/shared/components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts +++ b/frontend/src/app/shared/components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal.ts @@ -51,7 +51,7 @@ export class WikiIncludePageMacroModalComponent extends OpModalComponent impleme @ViewChild('selectedPageInput', { static: true }) selectedPageInput:ElementRef; - public text:any = { + public text = { title: this.I18n.t('js.editor.macro.wiki_page_include.button'), hint: this.I18n.t('js.editor.macro.wiki_page_include.hint'), page: this.I18n.t('js.editor.macro.wiki_page_include.page'), @@ -63,7 +63,7 @@ export class WikiIncludePageMacroModalComponent extends OpModalComponent impleme constructor() { super(); - this.selectedPage = this.page = this.locals.page; + this.selectedPage = this.page = this.locals.page as string; // We could provide an autocompleter here to get correct page names } diff --git a/frontend/src/app/shared/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts b/frontend/src/app/shared/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts index 03a3e812ae19..ff6ad113bfbb 100644 --- a/frontend/src/app/shared/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts +++ b/frontend/src/app/shared/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts @@ -60,7 +60,7 @@ export class WpButtonMacroModalComponent extends OpModalComponent implements Aft @ViewChild('typeSelect', { static: true }) typeSelect:ElementRef; - public text:any = { + public text = { title: this.I18n.t('js.editor.macro.work_package_button.button'), none: this.I18n.t('js.label_none'), selected_type: this.I18n.t('js.editor.macro.work_package_button.type'), @@ -74,8 +74,8 @@ export class WpButtonMacroModalComponent extends OpModalComponent implements Aft constructor() { super(); - this.selectedType = this.type = this.locals.type; - this.classes = this.locals.classes; + this.selectedType = this.type = this.locals.type as string; + this.classes = this.locals.classes as string; this.buttonStyle = this.classes === 'button'; this From 523c656166702f714693c9c57f9250ea25b49067 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:39:45 +0100 Subject: [PATCH 4/7] [OP-20348] Type board list and version payloads Types the board list query params as an HTTP param map, the add-list handler's return value, the parsed board filter URL param and the board type label translation, instead of letting them fall back to any. The version payloads in the add-list modal and the version autocompleter are built as object literals instead of being assembled on an any-typed object, so their shape is inferred. The payload keys and values are unchanged. https://community.openproject.org/wp/OP-20348 --- .../add-list-modal/add-list-modal.component.ts | 13 ++++++------- .../boards/board/board-list/board-list.component.ts | 5 +++-- .../board-list-container.component.ts | 5 +++-- .../board-partitioned-page.component.ts | 3 ++- .../src/app/features/boards/board/board.service.ts | 2 +- .../version-autocompleter.component.ts | 13 ++++++------- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts index 62ba3bc1d863..89af46d9ef82 100644 --- a/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/features/boards/board/add-list-modal/add-list-modal.component.ts @@ -162,15 +162,14 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { } private getVersionPayload(name:string) { - const payload:any = {}; - payload.name = name; - payload._links = { - definingProject: { - href: this.apiV3Service.projects.id(this.currentProject.id!).path, + return { + name, + _links: { + definingProject: { + href: this.apiV3Service.projects.id(this.currentProject.id!).path, + }, }, }; - - return payload; } private warnIfNoOptions(values:unknown[]) { diff --git a/frontend/src/app/features/boards/board/board-list/board-list.component.ts b/frontend/src/app/features/boards/board/board-list/board-list.component.ts index 3b8162fe8d05..ea2f89520ccb 100644 --- a/frontend/src/app/features/boards/board/board-list/board-list.component.ts +++ b/frontend/src/app/features/boards/board/board-list/board-list.component.ts @@ -95,6 +95,7 @@ import { firstValueFrom } from 'rxjs'; import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { HTTPClientParamMap } from 'core-app/features/hal/http/http.interfaces'; export interface DisabledButtonPlaceholder { text:string; @@ -176,7 +177,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni private canAdd = firstValueFrom(this.wpInlineCreate.canAdd); - public columnsQueryProps:any; + public columnsQueryProps:HTTPClientParamMap; public get text() { return { @@ -480,7 +481,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni const existingFilters = (this.resource.options.filters || []) as ApiV3Filter[]; const newFilters = existingFilters.concat(filters); - const newColumnsQueryProps:any = { + const newColumnsQueryProps:HTTPClientParamMap = { 'columns[]': ['id', 'subject'], showHierarchies: false, pageSize: 500, diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts index 51e74727d837..0f7aa2cc6cbe 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts @@ -183,12 +183,12 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements this.saveBoard(board); } - addList(board:Board):any { + addList(board:Board):Promise|undefined { if (board.isFree) { return this.BoardList .addFreeQuery(board, { name: this.text.unnamedList }) .then((board) => this.Boards.save(board).toPromise()) - .catch((error) => this.showError(error)); + .catch((error) => this.showError(error as string)); } const active = this.getActionFiltersFromWidget(board); this.opModalService.show( @@ -196,6 +196,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements this.injector, { board, active }, ); + return undefined; } changeVisibilityOfList(board:Board, boardWidget:GridWidgetResource, visible:boolean) { diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts index 636492f6b2d1..2cad74ec1979 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts @@ -58,6 +58,7 @@ import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import * as Turbo from '@hotwired/turbo'; +import { ApiV3Filter } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; export function boardCardViewHandlerFactory(injector:Injector) { return new CardViewHandlerRegistry(injector); @@ -189,7 +190,7 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement this.editable = board.editable; this.selectedTitle = board.name; this.titleService.setFirstPart(board.name); - this.boardFilters.filters.putValue(queryProps ? JSON.parse(queryProps) : board.filters); + this.boardFilters.filters.putValue(queryProps ? JSON.parse(queryProps) as ApiV3Filter[] : board.filters); this.cdRef.detectChanges(); }); diff --git a/frontend/src/app/features/boards/board/board.service.ts b/frontend/src/app/features/boards/board/board.service.ts index d84b0c26034d..845028103d15 100644 --- a/frontend/src/app/features/boards/board/board.service.ts +++ b/frontend/src/app/features/boards/board/board.service.ts @@ -51,7 +51,7 @@ export class BoardService { private text = { unnamed_board: this.I18n.t('js.boards.label_unnamed_board'), action_board: (attr:string) => this.I18n.t('js.boards.board_type.action_by_attribute', - { attribute: this.I18n.t(`js.boards.board_type.action_type.${attr}`) }), + { attribute: this.I18n.t(`js.boards.board_type.action_type.${attr}`) }), unnamed_list: this.I18n.t('js.boards.label_unnamed_list'), }; diff --git a/frontend/src/app/shared/components/autocompleter/version-autocompleter/version-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/version-autocompleter/version-autocompleter.component.ts index 15ebaaf7c151..486cfc94429d 100644 --- a/frontend/src/app/shared/components/autocompleter/version-autocompleter/version-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/version-autocompleter/version-autocompleter.component.ts @@ -107,14 +107,13 @@ export class VersionAutocompleterComponent extends CreateAutocompleterComponent } private getVersionPayload(name:string) { - const payload:any = {}; - payload.name = name; - payload._links = { - definingProject: { - href: this.apiV3Service.projects.id(this.currentProject.id!).path, + return { + name, + _links: { + definingProject: { + href: this.apiV3Service.projects.id(this.currentProject.id!).path, + }, }, }; - - return payload; } } From a719aa9b73580ecc93aa03a32e766a87266a31f9 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:41:29 +0100 Subject: [PATCH 5/7] [OP-20348] Type work package graph datasets Types the graph dataset query props, the graph query params props and the embedded graph's label and count maps instead of using any. The graph configuration drops an interface that merged with its own class of the same name; the class already declares every member. The graph widget's configured handler drops the event argument it never read, and the graph modal's prepend token takes an unknown component type like its work package table counterpart. https://community.openproject.org/wp/OP-20348 --- .../widgets/wp-graph/wp-graph.component.html | 2 +- .../grids/widgets/wp-graph/wp-graph.component.ts | 2 +- .../wp-graph-configuration.modal.ts | 2 +- .../wp-graph-configuration.service.ts | 1 - .../configuration/wp-graph-configuration.ts | 11 ++--------- .../embedded/wp-embedded-graph.component.ts | 16 ++++++++-------- 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/shared/components/grids/widgets/wp-graph/wp-graph.component.html b/frontend/src/app/shared/components/grids/widgets/wp-graph/wp-graph.component.html index 2d2917cff4a8..07d812ba4a0a 100644 --- a/frontend/src/app/shared/components/grids/widgets/wp-graph/wp-graph.component.html +++ b/frontend/src/app/shared/components/grids/widgets/wp-graph/wp-graph.component.html @@ -5,7 +5,7 @@ + (onConfigured)="updateGraph()" /> { this.repaint(); diff --git a/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts b/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts index cee7178c858a..4eea1c8e3fbc 100644 --- a/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts +++ b/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts @@ -42,7 +42,7 @@ import { WpGraphConfigurationService } from 'core-app/shared/components/work-pac import { WpGraphConfiguration } from 'core-app/shared/components/work-package-graphs/configuration/wp-graph-configuration'; import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; -export const WpTableConfigurationModalPrependToken = new InjectionToken>('WpTableConfigurationModalPrependComponent'); +export const WpTableConfigurationModalPrependToken = new InjectionToken>('WpTableConfigurationModalPrependComponent'); @Component({ templateUrl: '../../../../features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.html', diff --git a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts index 8535b58f4d50..8f7842b1a39e 100644 --- a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts +++ b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts @@ -128,7 +128,6 @@ export class WpGraphConfigurationService { .apiv3Service .queries .find( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument { pageSize: 0, ...params.props }, params.id, this.projectIdentifier(), 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..5443c2e71137 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 @@ -32,18 +32,11 @@ import { I18nService } from 'core-app/core/i18n/i18n.service'; export interface WpGraphQueryParams { id?:string; - props?:any; + props?:object; name?:string; } -export interface WpGraphConfiguration { - queries:QueryResource[]; - queryParams:WpGraphQueryParams[]; - chartType:string; - chartOptions:ChartOptions; -} - -export class WpGraphConfiguration implements WpGraphConfiguration { +export class WpGraphConfiguration { public queries:QueryResource[] = []; constructor( diff --git a/frontend/src/app/shared/components/work-package-graphs/embedded/wp-embedded-graph.component.ts b/frontend/src/app/shared/components/work-package-graphs/embedded/wp-embedded-graph.component.ts index c219d3fdff97..eb340633d9a7 100644 --- a/frontend/src/app/shared/components/work-package-graphs/embedded/wp-embedded-graph.component.ts +++ b/frontend/src/app/shared/components/work-package-graphs/embedded/wp-embedded-graph.component.ts @@ -38,7 +38,7 @@ import { chartTypeLocaleKey } from './../chart-type'; export interface WorkPackageEmbeddedGraphDataset { label:string; - queryProps:any; + queryProps:unknown; queryId?:number|string; groups?:GroupObject[]; } @@ -106,24 +106,24 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges { } private updateChartData() { - let uniqLabels = Array.from(new Set(this.datasets.reduce((array, dataset) => { - const groups = (dataset.groups || []).map((group) => group.value) as any; + const uniqLabels = Array.from(new Set(this.datasets.reduce<(string|null)[]>((array, dataset) => { + const groups = (dataset.groups ?? []).map((group) => group.value as string|null); return array.concat(groups); - }, []))) as string[]; + }, []))); const labelCountMaps = this.datasets.map((dataset) => { - const countMap = (dataset.groups || []).reduce((hash, group) => ({ + const countMap = (dataset.groups ?? []).reduce>((hash, group) => ({ ...hash, [group.value]: group.count, }), {}); return { label: dataset.label, - data: uniqLabels.map((label) => countMap[label] || 0), + data: uniqLabels.map((label) => countMap[String(label)] || 0), }; }); - uniqLabels = uniqLabels.map((label) => { + const displayLabels = uniqLabels.map((label) => { if (label === null) { return this.i18n.t('js.placeholders.default'); } @@ -134,7 +134,7 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges { // keep the array in order to update the labels this.chartLabels.length = 0; - this.chartLabels.push(...uniqLabels); + this.chartLabels.push(...displayLabels); this.chartData.length = 0; this.chartData.push(...labelCountMaps); } From 98db856d956cb3b2fc1c01f4b56c790cbe2701bd Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:42:29 +0100 Subject: [PATCH 6/7] [OP-20348] Type grid DOM access and widget refs Types the placeholder's non-standard scrollIntoViewIfNeeded method, the dragged area element lookup, the widget component registrations and the custom text widget's changed resource instead of reaching them through any. The lookups keep asserting that the elements exist, as the any casts did. https://community.openproject.org/wp/OP-20348 --- .../src/app/shared/components/grids/grid/area.service.ts | 8 +++++--- .../shared/components/grids/grid/drag-and-drop.service.ts | 2 +- .../app/shared/components/grids/grid/grid.component.ts | 6 +++--- .../grids/widgets/custom-text/custom-text.component.ts | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/shared/components/grids/grid/area.service.ts b/frontend/src/app/shared/components/grids/grid/area.service.ts index df824687f6f8..a7b1ea8892f9 100644 --- a/frontend/src/app/shared/components/grids/grid/area.service.ts +++ b/frontend/src/app/shared/components/grids/grid/area.service.ts @@ -53,6 +53,8 @@ interface GridPatchPayload { [key:string]:unknown; } +type ScrollIntoViewIfNeededElement = HTMLElement&{ scrollIntoViewIfNeeded?:() => void }; + @Injectable() export class GridAreaService { private apiV3Service = inject(ApiV3Service); @@ -207,10 +209,10 @@ export class GridAreaService { // But as scrollIntoView will always readjust the viewport, the result would be an unbearable flicker // which causes e.g. dragging to be impossible. public scrollPlaceholderIntoView() { - const placeholder = document.querySelector('.grid--area.-placeholder'); + const placeholder = document.querySelector('.grid--area.-placeholder')!; - if ((placeholder as any).scrollIntoViewIfNeeded) { - setTimeout(() => (placeholder as any).scrollIntoViewIfNeeded()); + if (placeholder.scrollIntoViewIfNeeded) { + setTimeout(() => placeholder.scrollIntoViewIfNeeded!()); } } diff --git a/frontend/src/app/shared/components/grids/grid/drag-and-drop.service.ts b/frontend/src/app/shared/components/grids/grid/drag-and-drop.service.ts index 39c6f0cf4279..aa27a9c1301c 100644 --- a/frontend/src/app/shared/components/grids/grid/drag-and-drop.service.ts +++ b/frontend/src/app/shared/components/grids/grid/drag-and-drop.service.ts @@ -116,7 +116,7 @@ export class GridDragAndDropService implements OnDestroy { public start(area:GridWidgetArea) { this.placeholderArea = new GridWidgetArea(area.widget); // TODO find an angular way to do this that ideally does not require passing the element from the grid component - this.draggedHeight = (document as any).getElementById(area.guid).offsetHeight - 2; // border width * 2 + this.draggedHeight = document.getElementById(area.guid)!.offsetHeight - 2; // border width * 2 this.draggedArea = area; } diff --git a/frontend/src/app/shared/components/grids/grid/grid.component.ts b/frontend/src/app/shared/components/grids/grid/grid.component.ts index 4be2ce3f6508..cd4b3a206617 100644 --- a/frontend/src/app/shared/components/grids/grid/grid.component.ts +++ b/frontend/src/app/shared/components/grids/grid/grid.component.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, HostListener, Input, OnDestroy, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, HostListener, Input, OnDestroy, OnInit, Type, inject } from '@angular/core'; import { GridResource } from 'core-app/features/hal/resources/grid-resource'; import { DomSanitizer } from '@angular/platform-browser'; import { GridWidgetsService } from 'core-app/shared/components/grids/widgets/widgets.service'; @@ -46,7 +46,7 @@ import { WidgetChangeset } from 'core-app/shared/components/grids/widgets/widget export interface WidgetRegistration { identifier:string; title:string; - component:new (...args:any[]) => AbstractWidgetComponent; + component:Type; properties?:Record; } @@ -80,7 +80,7 @@ export class GridComponent implements OnDestroy, OnInit { readonly browserDetector = inject(BrowserDetector); readonly cdRef = inject(ChangeDetectorRef); - public uiWidgets:ComponentRef[] = []; + public uiWidgets:ComponentRef[] = []; public GRID_AREA_HEIGHT = 'auto'; diff --git a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts index fd2453663860..8cd01711af3f 100644 --- a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts @@ -35,6 +35,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { filter } from 'rxjs/operators'; import { GridAreaService } from 'core-app/shared/components/grids/grid/area.service'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { GridWidgetResource } from 'core-app/features/hal/resources/grid-widget-resource'; @Component({ templateUrl: './custom-text.component.html', @@ -83,7 +84,7 @@ export class WidgetCustomTextComponent extends AbstractWidgetComponent implement } ngOnChanges(changes:SimpleChanges):void { - if (changes.resource.currentValue.options.text.raw !== this.currentRawText) { + if (((changes.resource.currentValue as GridWidgetResource).options.text as HalResource).raw !== this.currentRawText) { this.setupVariables(); this.cdr.detectChanges(); From 6689447f086680672c70f2bd438aeef131facac4 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 29 Sep 2026 01:45:00 +0100 Subject: [PATCH 7/7] [OP-20348] Type remaining module long tail Types the model auth links, the dynamic state lookup, the date-time component inputs, the option list form callbacks, the principal custom fields, the remote field response, the create autocompleter model and the teleport template ref instead of using any. Error callbacks that hand HTTP errors to the toast service now declare them as HttpErrorResponse, which the service already accepts. The state lookup falls back with ?? instead of ||; the looked-up members are state objects or undefined, so the result is unchanged. https://community.openproject.org/wp/OP-20348 --- frontend/src/app/core/model-auth/model-auth.service.ts | 4 ++-- frontend/src/app/core/schemas/schema-cache.service.ts | 2 +- .../app/core/state/capabilities/capabilities.service.ts | 3 ++- frontend/src/app/core/states/states.service.ts | 2 +- .../editable-query-props/editable-query-props.component.ts | 2 +- .../user-preferences/state/user-preferences.service.ts | 7 ++++--- .../create-autocompleter/create-autocompleter.component.ts | 2 +- .../app/shared/components/date/op-date-time.component.ts | 6 +++--- .../editable-toolbar-title.component.ts | 2 +- .../shared/components/option-list/option-list.component.ts | 4 ++-- .../src/app/shared/components/principal/principal-types.ts | 2 +- .../remote-field-updater/remote-field-updater.component.ts | 2 +- .../time_entries/edit/trigger-actions-entry.component.ts | 3 ++- .../drop-modal/drop-modal-teleportation.service.ts | 2 +- 14 files changed, 23 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/core/model-auth/model-auth.service.ts b/frontend/src/app/core/model-auth/model-auth.service.ts index 0333565d8d0d..3a79ea3f3589 100644 --- a/frontend/src/app/core/model-auth/model-auth.service.ts +++ b/frontend/src/app/core/model-auth/model-auth.service.ts @@ -31,7 +31,7 @@ import { input } from '@openproject/reactivestates'; import { Observable } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -export type ModelLinks = Record; +export type ModelLinks = Record; export type ModelLinksHash = Record; @Injectable({ providedIn: 'root' }) @@ -46,7 +46,7 @@ export class AuthorisationService { }); } - public observeUntil(unsubscribe:Observable) { + public observeUntil(unsubscribe:Observable) { return this.links.values$().pipe(takeUntil(unsubscribe)); } diff --git a/frontend/src/app/core/schemas/schema-cache.service.ts b/frontend/src/app/core/schemas/schema-cache.service.ts index 973413d76e70..91bca36e6af8 100644 --- a/frontend/src/app/core/schemas/schema-cache.service.ts +++ b/frontend/src/app/core/schemas/schema-cache.service.ts @@ -129,7 +129,7 @@ export class SchemaCacheService extends StateCacheService { ); } - protected loadAll(hrefs:string[]):Promise { + protected loadAll(hrefs:string[]):Promise { return Promise.all(hrefs.map((href) => this.load(href))); } diff --git a/frontend/src/app/core/state/capabilities/capabilities.service.ts b/frontend/src/app/core/state/capabilities/capabilities.service.ts index 6e136fe4ee90..64f3f5ecf4d6 100644 --- a/frontend/src/app/core/state/capabilities/capabilities.service.ts +++ b/frontend/src/app/core/state/capabilities/capabilities.service.ts @@ -40,6 +40,7 @@ import { ResourceStore, ResourceStoreService, } from 'core-app/core/state/resource-store.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Injectable() export class CapabilitiesResourceService extends ResourceStoreService { @@ -59,7 +60,7 @@ export class CapabilitiesResourceService extends ResourceStoreService { + catchError((error:HttpErrorResponse) => { this.toastService.addError(error); throw error; }), diff --git a/frontend/src/app/core/states/states.service.ts b/frontend/src/app/core/states/states.service.ts index 8e42af9ef35d..de68c9f468fd 100644 --- a/frontend/src/app/core/states/states.service.ts +++ b/frontend/src/app/core/states/states.service.ts @@ -93,7 +93,7 @@ export class States extends StatesGroup { additional:Record> = {}; forType(stateName:string):MultiInputState { - let state = (this as any)[stateName] || this.additional[stateName]; + let state = (this as unknown as Record|undefined>)[stateName] ?? this.additional[stateName] as MultiInputState|undefined; if (!state) { state = this.additional[stateName] = multiInput(); diff --git a/frontend/src/app/features/admin/editable-query-props/editable-query-props.component.ts b/frontend/src/app/features/admin/editable-query-props/editable-query-props.component.ts index 82caea3d7282..7bbbb409d78a 100644 --- a/frontend/src/app/features/admin/editable-query-props/editable-query-props.component.ts +++ b/frontend/src/app/features/admin/editable-query-props/editable-query-props.component.ts @@ -76,7 +76,7 @@ export class EditableQueryPropsComponent implements OnInit { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return JSON.parse(this.queryProps); } catch (e) { - console.error(`Failed to parse query props from ${this.queryProps}: ${e}`); + console.error(`Failed to parse query props from ${this.queryProps}: ${String(e)}`); return {}; } })(); diff --git a/frontend/src/app/features/user-preferences/state/user-preferences.service.ts b/frontend/src/app/features/user-preferences/state/user-preferences.service.ts index 32bd94ce8b76..5b74b9b2c4c7 100644 --- a/frontend/src/app/features/user-preferences/state/user-preferences.service.ts +++ b/frontend/src/app/features/user-preferences/state/user-preferences.service.ts @@ -34,6 +34,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service'; import { IUserPreference } from 'core-app/features/user-preferences/state/user-preferences.model'; import { UserPreferencesStore } from 'core-app/features/user-preferences/state/user-preferences.store'; import { UserPreferencesQuery } from 'core-app/features/user-preferences/state/user-preferences.query'; +import { HttpErrorResponse } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class UserPreferencesService { @@ -50,8 +51,8 @@ export class UserPreferencesService { this.preferenceAPI(user) .get() .subscribe( - (prefs) => this.store.update(prefs), - (error) => this.toastService.addError(error), + (prefs) => { this.store.update(prefs); }, + (error:HttpErrorResponse) => this.toastService.addError(error), ) .add( () => this.store.setLoading(false), @@ -68,7 +69,7 @@ export class UserPreferencesService { this.store.update(prefs); this.toastService.addSuccess(this.I18n.t('js.notice_successful_update')); }, - (error) => this.toastService.addError(error), + (error:HttpErrorResponse) => this.toastService.addError(error), ) .add(() => this.store.setLoading(false)); } diff --git a/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts index 08077650eb73..486f26989afb 100644 --- a/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts @@ -62,7 +62,7 @@ export class CreateAutocompleterComponent extends UntilDestroyedMixin implements @Input() public resource:HalResource; - @Input() public model:any; + @Input() public model:unknown; @Input() public required = false; diff --git a/frontend/src/app/shared/components/date/op-date-time.component.ts b/frontend/src/app/shared/components/date/op-date-time.component.ts index 7052772da6bf..3958ed87e67e 100644 --- a/frontend/src/app/shared/components/date/op-date-time.component.ts +++ b/frontend/src/app/shared/components/date/op-date-time.component.ts @@ -47,11 +47,11 @@ import { TimezoneService } from 'core-app/core/datetime/timezone.service'; export class OpDateTimeComponent implements OnInit { readonly timezoneService = inject(TimezoneService); - @Input() dateTimeValue:any; + @Input() dateTimeValue:string; - public date:any; + public date:string; - public time:any; + public time:string; ngOnInit() { const c = this.timezoneService.formattedDatetimeComponents(this.dateTimeValue); diff --git a/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts b/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts index d3730c056d31..2637ed9558d1 100644 --- a/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts +++ b/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts @@ -116,7 +116,7 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges { ngOnChanges(changes:SimpleChanges):void { if (changes.inputTitle) { - this.selectedTitle = changes.inputTitle.currentValue; + this.selectedTitle = changes.inputTitle.currentValue as string; } if (changes.initialFocus && changes.initialFocus.firstChange && this.inputField!) { diff --git a/frontend/src/app/shared/components/option-list/option-list.component.ts b/frontend/src/app/shared/components/option-list/option-list.component.ts index d43ad4aa2413..d86ec57dba2e 100644 --- a/frontend/src/app/shared/components/option-list/option-list.component.ts +++ b/frontend/src/app/shared/components/option-list/option-list.component.ts @@ -92,11 +92,11 @@ export class OpOptionListComponent implements ControlValueAccessor { this.cdRef.markForCheck(); } - registerOnChange(fn:any) { + registerOnChange(fn:(_:IOpOptionListValue) => void) { this.onChange = fn; } - registerOnTouched(fn:any) { + registerOnTouched(fn:(_:IOpOptionListValue) => void) { this.onTouched = fn; } } diff --git a/frontend/src/app/shared/components/principal/principal-types.ts b/frontend/src/app/shared/components/principal/principal-types.ts index 34c100b3d0d5..1c6c2603edac 100644 --- a/frontend/src/app/shared/components/principal/principal-types.ts +++ b/frontend/src/app/shared/components/principal/principal-types.ts @@ -38,5 +38,5 @@ export type PrincipalLike = export interface PrincipalData { principal:PrincipalLike|null; - customFields:Record, + customFields:Record, } diff --git a/frontend/src/app/shared/components/remote-field-updater/remote-field-updater.component.ts b/frontend/src/app/shared/components/remote-field-updater/remote-field-updater.component.ts index 04da91526c67..44ec22156843 100644 --- a/frontend/src/app/shared/components/remote-field-updater/remote-field-updater.component.ts +++ b/frontend/src/app/shared/components/remote-field-updater/remote-field-updater.component.ts @@ -133,7 +133,7 @@ export class RemoteFieldUpdaterComponent implements OnInit, OnDestroy { this .request(params) .subscribe((response:object) => { - Object.entries(response).forEach(([selector, val]) => { + Object.entries(response as Record).forEach(([selector, val]) => { const element = document.getElementById(selector) as HTMLElement|HTMLInputElement; if (element instanceof HTMLInputElement) { diff --git a/frontend/src/app/shared/components/time_entries/edit/trigger-actions-entry.component.ts b/frontend/src/app/shared/components/time_entries/edit/trigger-actions-entry.component.ts index 9e287e91af00..b119fe942d28 100644 --- a/frontend/src/app/shared/components/time_entries/edit/trigger-actions-entry.component.ts +++ b/frontend/src/app/shared/components/time_entries/edit/trigger-actions-entry.component.ts @@ -37,6 +37,7 @@ import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-re import { Observable, switchMap } from 'rxjs'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'opce-time-entry-trigger-actions', @@ -114,7 +115,7 @@ export class TriggerActionsEntryComponent { ) .subscribe( () => window.location.reload(), - (error) => this.toastService.addError(error || this.text.error), + (error:HttpErrorResponse) => this.toastService.addError(error || this.text.error), ); } diff --git a/frontend/src/app/spot/components/drop-modal/drop-modal-teleportation.service.ts b/frontend/src/app/spot/components/drop-modal/drop-modal-teleportation.service.ts index ac395fcbe7ec..bb9c26b32cb0 100644 --- a/frontend/src/app/spot/components/drop-modal/drop-modal-teleportation.service.ts +++ b/frontend/src/app/spot/components/drop-modal/drop-modal-teleportation.service.ts @@ -36,7 +36,7 @@ import { } from 'rxjs'; import { distinctUntilChanged, filter, take } from 'rxjs/operators'; -export type TeleportInstance = TemplateRef; +export type TeleportInstance = TemplateRef; @Injectable({ providedIn: 'root' }) export class SpotDropModalTeleportationService {