diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index ee266a20e0dc..8e9338969001 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -218,14 +218,21 @@ export default defineConfig([ rules: { ...vitest.configs.recommended.rules, - // vitest expect(...) is always any - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - // Allow more than one class definitions per file (test components) 'max-classes-per-file': 'off', }, }, + { + files: ['**/*.spec.ts', '**/testing/**/*.ts', 'src/stimulus/test-helpers.ts', 'src/test-*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + }, + }, { // esbuild follows imports past the tsconfig exclude, so the import site // is the boundary keeping test helpers out of the production bundle. diff --git a/frontend/src/app/core/apiv3/cache/cachable-apiv3-resource.ts b/frontend/src/app/core/apiv3/cache/cachable-apiv3-resource.ts index 2e5f6f2ddc17..60ec84a759b2 100644 --- a/frontend/src/app/core/apiv3/cache/cachable-apiv3-resource.ts +++ b/frontend/src/app/core/apiv3/cache/cachable-apiv3-resource.ts @@ -36,6 +36,7 @@ import { } from 'rxjs/operators'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export abstract class ApiV3Resource extends ApiV3GettableResource { @@ -137,7 +138,7 @@ export abstract class ApiV3Resource switchMap((resource) => { if (resource.$links.schema) { return this.schemaCache - .requireAndStream(resource.$links.schema.href) + .requireAndStream((resource.$links.schema as CallableHalLink).href!) .pipe( take(1), mapTo(resource), @@ -145,7 +146,7 @@ export abstract class ApiV3Resource } return of(resource); }), - ) as any; // T does not extend HalResource for virtual endpoints such as board, thus we need to cast here + ) as unknown as Observable; // T does not extend HalResource for virtual endpoints such as board, thus we need to cast here } /** diff --git a/frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-form.ts b/frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-form.ts index 3283e5c0017e..7b82266a1447 100644 --- a/frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-form.ts +++ b/frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-form.ts @@ -31,6 +31,7 @@ import { SchemaResource } from 'core-app/features/hal/resources/schema-resource' import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper'; import { GridWidgetResource } from 'core-app/features/hal/resources/grid-widget-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { GridResource } from 'core-app/features/hal/resources/grid-resource'; export class ApiV3GridForm extends ApiV3FormResource { /** @@ -42,7 +43,7 @@ export class ApiV3GridForm extends ApiV3FormResource { */ public static extractPayload(resource:HalResource|object, schema:SchemaResource|null = null):object { if (resource instanceof HalResource && schema) { - const grid = resource; + const grid = resource as GridResource; const payload = HalPayloadHelper.extractPayloadFromSchema(grid, schema); // The widget only states the type of the widget resource but does not explain 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 138bcc194b2d..71080464e1c1 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 @@ -130,9 +130,9 @@ export class ApiV3QueriesPaths extends ApiV3ResourceCollection { if (query.starred) { - return query.unstar(); + return query.unstar!(); } - return query.star(); + return query.star!(); } /** diff --git a/frontend/src/app/core/apiv3/virtual/apiv3-boards-paths.ts b/frontend/src/app/core/apiv3/virtual/apiv3-boards-paths.ts index 8c63d64e2e66..05bc64cafde5 100644 --- a/frontend/src/app/core/apiv3/virtual/apiv3-boards-paths.ts +++ b/frontend/src/app/core/apiv3/virtual/apiv3-boards-paths.ts @@ -136,7 +136,7 @@ export class ApiV3BoardsPaths extends ApiV3Collection { switchMap((form) => this .apiRoot .grids - .post(form.payload.$source)), + .post(form.payload.$source as unknown as GridResource)), ); } } diff --git a/frontend/src/app/core/current-project/current-project.service.spec.ts b/frontend/src/app/core/current-project/current-project.service.spec.ts index 6db59e0f76ba..5661fe258745 100644 --- a/frontend/src/app/core/current-project/current-project.service.spec.ts +++ b/frontend/src/app/core/current-project/current-project.service.spec.ts @@ -46,7 +46,6 @@ describe('currentProject service', () => { providers: [ CurrentProjectService, PathHelperService, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment { provide: ApiV3Service, useValue: apiV3Stub }, ], }); diff --git a/frontend/src/app/core/global_search/input/global-search-input.component.spec.ts b/frontend/src/app/core/global_search/input/global-search-input.component.spec.ts index 4257977ec617..b03c9cefa117 100644 --- a/frontend/src/app/core/global_search/input/global-search-input.component.spec.ts +++ b/frontend/src/app/core/global_search/input/global-search-input.component.spec.ts @@ -27,6 +27,7 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalSource } from 'core-app/features/hal/interfaces'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { GlobalSearchInputComponent } from './global-search-input.component'; @@ -62,7 +63,7 @@ describe('GlobalSearchInputComponent#followItem', () => { // so followItem exercises the production displayId getter rather than a stub. function buildWorkPackage(source:{ id:number, displayId?:string }):WorkPackageResource { const item = Object.create(WorkPackageResource.prototype) as WorkPackageResource; - item.$source = source; + item.$source = source as unknown as HalSource; return item; } diff --git a/frontend/src/app/core/global_search/input/global-search-input.component.ts b/frontend/src/app/core/global_search/input/global-search-input.component.ts index 010d027feb66..bf2fbdbc2038 100644 --- a/frontend/src/app/core/global_search/input/global-search-input.component.ts +++ b/frontend/src/app/core/global_search/input/global-search-input.component.ts @@ -292,7 +292,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy { if (item instanceof WorkPackageResource) { window.location.href = this.wpPath(item.displayId); } else if (item) { - this.searchInScope(item.projectScope); + this.searchInScope((item as SearchOptionItem).projectScope); } } diff --git a/frontend/src/app/core/path-helper/apiv3-paths.ts b/frontend/src/app/core/path-helper/apiv3-paths.ts index ea34069217f7..75d1c35e3a40 100644 --- a/frontend/src/app/core/path-helper/apiv3-paths.ts +++ b/frontend/src/app/core/path-helper/apiv3-paths.ts @@ -28,7 +28,6 @@ import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class ApiV3Paths { readonly apiV3Base:string; @@ -96,7 +95,7 @@ export class ApiV3Paths { if (!workPackage.id || workPackage.id === 'new') { // that are members of that project: - filters.add('member', '=', [(workPackage.project as HalResource).id!]); + filters.add('member', '=', [workPackage.project.id!]); } else { // that are mentionable on the work package filters.add( diff --git a/frontend/src/app/core/schemas/schema-cache.service.ts b/frontend/src/app/core/schemas/schema-cache.service.ts index 990e68fa8f02..973413d76e70 100644 --- a/frontend/src/app/core/schemas/schema-cache.service.ts +++ b/frontend/src/app/core/schemas/schema-cache.service.ts @@ -37,6 +37,7 @@ import { HalResourceService } from 'core-app/features/hal/services/hal-resource. import { WorkPackageSchemaProxy } from 'core-app/features/hal/schemas/work-package-schema-proxy'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export const fallbackSchemaId = '__fallback'; @@ -82,8 +83,7 @@ export class SchemaCacheService extends StateCacheService { } public getSchemaHref(resource:HalResource):string|undefined { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return resource.$links.schema?.href as string|undefined; + return (resource.$links.schema as CallableHalLink|undefined)?.href as string|undefined; } /** diff --git a/frontend/src/app/core/state/attachments/attachments.service.ts b/frontend/src/app/core/state/attachments/attachments.service.ts index 86516f85d6b8..05283868fe5e 100644 --- a/frontend/src/app/core/state/attachments/attachments.service.ts +++ b/frontend/src/app/core/state/attachments/attachments.service.ts @@ -41,7 +41,6 @@ import { } from 'rxjs/operators'; import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { AttachmentsStore } from 'core-app/core/state/attachments/attachments.store'; @@ -58,6 +57,7 @@ import { import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator'; import isNewResource, { HAL_NEW_RESOURCE_ID } from 'core-app/features/hal/helpers/is-new-resource'; import waitForUploadsFinished from 'core-app/core/upload/wait-for-uploads-finished'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; @Injectable() export class AttachmentsResourceService extends ResourceStoreService { @@ -202,10 +202,10 @@ export class AttachmentsResourceService extends ResourceStoreService { - this.workPackage = wp; + .subscribe((wp) => { + void this.onWorkPackageChange(wp); + }); + } - if (!this.projectId) { - await this.initialize(this.workPackage); - } + private async onWorkPackageChange(wp:WorkPackageResource) { + this.workPackage = wp; - if (wp.bcfViewpoints) { - this.refreshViewpoints(wp.bcfViewpoints); - } - }); + if (!this.projectId) { + await this.initialize(this.workPackage); + } + + if (wp.bcfViewpoints) { + this.refreshViewpoints(wp.bcfViewpoints as unknown as HalLink[]); + } } async initialize(workPackage:WorkPackageResource) { diff --git a/frontend/src/app/features/bim/bcf/helper/viewpoints.service.ts b/frontend/src/app/features/bim/bcf/helper/viewpoints.service.ts index 04d046a4b086..3241813b3532 100644 --- a/frontend/src/app/features/bim/bcf/helper/viewpoints.service.ts +++ b/frontend/src/app/features/bim/bcf/helper/viewpoints.service.ts @@ -53,7 +53,7 @@ export class ViewpointsService { @LazyInject() apiV3Service:ApiV3Service; public getViewPointResource(workPackage:WorkPackageResource, index:number):BcfViewpointPaths { - const viewpointHref = (workPackage.bcfViewpoints as HalResource[])[index].href!; + const viewpointHref = workPackage.bcfViewpoints![index].href!; return this.bcfApi.parse(viewpointHref)!; } @@ -91,7 +91,7 @@ export class ViewpointsService { } public saveViewpoint$(workPackage:WorkPackageResource, viewpoint?:CreateBcfViewpointData):Observable { - const projectLink = (workPackage.project as HalResource).href; + const projectLink = workPackage.project.href; const wpProjectId = idFromLink(projectLink); const topicUUID$ = this.setBcfTopic$(workPackage); // Default to the current viewer's viewpoint @@ -137,7 +137,7 @@ export class ViewpointsService { private createBcfTopic$(workPackage:WorkPackageResource):Observable { const wpProjectId = idFromLink(workPackage.project.href); - const wpPayload = workPackage.convertBCF.payload; + const wpPayload = (workPackage.convertBCF as HalResource).payload; return this.bcfApi .projects.id(wpProjectId) diff --git a/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts b/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts index a8614c892f1c..73c979560f9c 100644 --- a/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts +++ b/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts @@ -41,7 +41,6 @@ import { IfcProjectDefinition, } from 'core-app/features/bim/ifc_models/pages/viewer/ifc-models-data.service'; import { BcfViewpointData, CreateBcfViewpointData } from 'core-app/features/bim/bcf/api/bcf-api.model'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { getMetaContent } from 'core-app/core/setup/globals/global-helpers'; @@ -256,7 +255,7 @@ export class IFCViewerService extends ViewerBridgeService { // no means of setting it from here. Hence we must make a hard transition to bcf details route of the // current work package. window.location.href = this.pathHelper.bimDetailsPath( - idFromLink((workPackage.project as HalResource).href), + idFromLink(workPackage.project.href), workPackage.id || '', index, ); diff --git a/frontend/src/app/features/hal/hal-link/hal-link.ts b/frontend/src/app/features/hal/hal-link/hal-link.ts index 7cf2cd0f7c5b..81f0d520bb84 100644 --- a/frontend/src/app/features/hal/hal-link/hal-link.ts +++ b/frontend/src/app/features/hal/hal-link/hal-link.ts @@ -39,7 +39,7 @@ export interface HalLinkInterface { method:HTTPSupportedMethods; title?:string; templated?:boolean; - payload?:any; + payload?:unknown; type?:string; identifier?:string; displayId?:string; @@ -50,18 +50,21 @@ export interface HalLinkSource { title:string; } -export interface CallableHalLink extends HalLinkInterface { - $link:this; +export interface CallableHalLink extends HalLinkInterface { + (data?:unknown, headers?:HTTPClientHeaders):Promise; + $link:HalLink; data?:Promise; } +export type HalLinkRequestMethod = (method:HTTPSupportedMethods, href:string, data?:unknown, headers?:HTTPClientHeaders) => Promise; + export class HalLink implements HalLinkInterface { - constructor(public requestMethod:(method:HTTPSupportedMethods, href:string, data:any, headers:any) => Promise, + constructor(public requestMethod:HalLinkRequestMethod, public href:string|null = null, public title = '', public method:HTTPSupportedMethods = 'get', public templated = false, - public payload?:any, + public payload?:unknown, public type = 'application/json', public identifier?:string, public displayId?:string) { @@ -72,7 +75,7 @@ export class HalLink implements HalLinkInterface { */ public static fromObject(halResourceService:HalResourceService, link:HalLinkInterface):HalLink { return new HalLink( - (method:HTTPSupportedMethods, href:string, data:object, headers:HTTPClientHeaders) => firstValueFrom(halResourceService.request(method, href, data, headers)), + (method, href, data, headers) => firstValueFrom(halResourceService.request(method, href, data, headers)), link.href, link.title, link.method, @@ -87,9 +90,8 @@ export class HalLink implements HalLinkInterface { /** * Fetch the resource. */ - public $fetch(...params:any[]):Promise { - const [data, headers] = params; - return this.requestMethod(this.method, this.href!, data, headers); + public $fetch(data?:unknown, headers?:HTTPClientHeaders):Promise { + return this.requestMethod(this.method, this.href!, data, headers) as Promise; } /** @@ -126,10 +128,10 @@ export class HalLink implements HalLinkInterface { * * @returns {CallableHalLink} */ - public $callable():CallableHalLink { - const linkFunc:any = (...params:any[]) => this.$fetch(...params); + public $callable():CallableHalLink { + const linkFunc = (data?:unknown, headers?:HTTPClientHeaders) => this.$fetch(data, headers); - Object.assign(linkFunc, { + return Object.assign(linkFunc, { $link: this, href: this.href, title: this.title, @@ -140,7 +142,5 @@ export class HalLink implements HalLinkInterface { identifier: this.identifier, displayId: this.displayId, }); - - return linkFunc; } } diff --git a/frontend/src/app/features/hal/helpers/hal-resource-builder.ts b/frontend/src/app/features/hal/helpers/hal-resource-builder.ts index 3544fe5740a0..29f3772c0fc2 100644 --- a/frontend/src/app/features/hal/helpers/hal-resource-builder.ts +++ b/frontend/src/app/features/hal/helpers/hal-resource-builder.ts @@ -28,10 +28,10 @@ import ObservableArray from 'observable-array'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { CallableHalLink, HalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; import { OpenprojectHalModuleHelpers } from 'core-app/features/hal/helpers/lazy-accessor'; -import { HalSource } from 'core-app/features/hal/interfaces'; +import { HalSource, HalSourceLink } from 'core-app/features/hal/interfaces'; export function cloneHalResourceCollection(values:T[]|undefined):T[] { if (values == null) { @@ -55,6 +55,14 @@ export function initializeHalProperties(halResourceServic setLinksAsProperties(); setEmbeddedAsProperties(); + function sourceLinks():Record { + return halResource.$source._links; + } + + function sourceEmbedded():Record|undefined { + return halResource.$source._embedded as Record|undefined; + } + function setSource() { if (!halResource.$source._links) { halResource.$source._links = {}; @@ -65,7 +73,7 @@ export function initializeHalProperties(halResourceServic } } - function asHalResource(value?:HalSource, loaded = true):HalResource|HalSource|undefined|null { + function asHalResource(value?:HalSource|null, loaded = true):HalResource|HalSource|undefined|null { if (value == null) { return value; } @@ -78,14 +86,14 @@ export function initializeHalProperties(halResourceServic } function proxyProperties() { - halResource.$embeddableKeys().forEach((property:any) => { + halResource.$embeddableKeys().forEach((property:string) => { Object.defineProperty(halResource, property, { get() { - const value = halResource.$source[property]; + const value = halResource.$source[property] as HalSource|undefined; return asHalResource(value, true); }, - set(value) { + set(value:unknown) { halResource.$source[property] = value; }, @@ -97,12 +105,13 @@ export function initializeHalProperties(halResourceServic function setLinksAsProperties() { halResource.$linkableKeys().forEach((linkName:string) => { - OpenprojectHalModuleHelpers.lazy(halResource, linkName, + OpenprojectHalModuleHelpers.lazy(halResource, linkName, () => { - const link:any = halResource.$links[linkName].$link || halResource.$links[linkName]; + const entry = halResource.$links[linkName]!; + const link = (entry as CallableHalLink).$link || entry; if (Array.isArray(link)) { - const items = link.map((item) => halResourceService.createLinkedResource(halResource, + const items = link.map((item:CallableHalLink) => halResourceService.createLinkedResource(halResource, linkName, item.$link)); const property:HalResource[] = new ObservableArray(...items).on('change', () => { @@ -112,7 +121,7 @@ export function initializeHalProperties(halResourceServic } }); - halResource.$source._links[linkName] = property.map((item) => item.$link); + sourceLinks()[linkName] = property.map((item) => item.$link); }); return property; @@ -128,92 +137,94 @@ export function initializeHalProperties(halResourceServic return null; }, - (val:any) => setter(val, linkName)); + (val) => setter(val, linkName)); }); } function setEmbeddedAsProperties() { - if (!halResource.$source._embedded) { + const embedded = sourceEmbedded(); + + if (!embedded) { return; } - Object.keys(halResource.$source._embedded).forEach((name) => { - OpenprojectHalModuleHelpers.lazy(halResource, + Object.keys(embedded).forEach((name) => { + OpenprojectHalModuleHelpers.lazy(halResource, name, () => halResource.$embedded[name], - (val:any) => setter(val, name)); + (val) => setter(val, name)); }); } - function setupProperty(name:string, callback:(element:any) => any) { - const instanceName = `$${name}`; - const sourceName = `_${name}`; - const sourceObj:any = halResource.$source[sourceName]; + function setupProperty(sourceName:'_links'|'_embedded', target:object, callback:(element:unknown) => unknown) { + const sourceObj = halResource.$source[sourceName]; if (typeof sourceObj === 'object' && sourceObj !== null) { Object.keys(sourceObj).forEach((propName) => { - OpenprojectHalModuleHelpers.lazy((halResource)[instanceName], + OpenprojectHalModuleHelpers.lazy(target, propName, - () => callback((sourceObj as any)[propName])); + () => callback((sourceObj as Record)[propName])); }); } } function setupLinks() { - setupProperty('links', + setupProperty('_links', + halResource.$links, (link) => { if (Array.isArray(link)) { - return link.map((l) => HalLink.fromObject(halResourceService, l).$callable()); + return (link as HalLinkInterface[]).map((l) => HalLink.fromObject(halResourceService, l).$callable()); } - return HalLink.fromObject(halResourceService, link).$callable(); + return HalLink.fromObject(halResourceService, link as HalLinkInterface).$callable(); }); } function setupEmbedded() { - setupProperty('embedded', (element:any) => { + setupProperty('_embedded', halResource.$embedded, (element) => { if (Array.isArray(element)) { - return element.map((source) => asHalResource(source, true)); + return (element as HalSource[]).map((source) => asHalResource(source, true)); } if (typeof element === 'object' && element !== null) { Object.entries(element as Record).forEach(([name, child]) => { if (child && (child._embedded || child._links)) { - OpenprojectHalModuleHelpers.lazy(element as any, + OpenprojectHalModuleHelpers.lazy(element, name, () => asHalResource(child, true)); } }); } - return asHalResource(element, true); + return asHalResource(element as HalSource|undefined, true); }); } - function setter(val:HalResource[]|HalResource|{ href?:string }, linkName:string) { + function setter(val:unknown, linkName:string):unknown { const isArray = Array.isArray(val); if (!val) { - halResource.$source._links[linkName] = { href: null }; + sourceLinks()[linkName] = { href: null }; } else if (isArray) { - halResource.$source._links[linkName] = (val).map((el:any) => ({ href: el.href })); + sourceLinks()[linkName] = (val as HalResource[]).map((el) => ({ href: el.href })); } else if (Object.hasOwn(val, '$link')) { const link = (val as HalResource).$link; if (link.href) { - halResource.$source._links[linkName] = link; + sourceLinks()[linkName] = link; } - } else if ('href' in val) { - halResource.$source._links[linkName] = { href: val.href }; + } else if ('href' in (val as { href?:string })) { + sourceLinks()[linkName] = { href: (val as { href?:string }).href }; } if (halResource.$embedded?.[linkName]) { halResource.$embedded[linkName] = val; + const embedded = sourceEmbedded()!; if (isArray) { - halResource.$source._embedded[linkName] = (val).map((el) => el.$source); + embedded[linkName] = (val as HalResource[]).map((el) => el.$source); } else { const source:unknown = (val as HalResource | undefined)?.$source; - (halResource.$source as { _embedded:Record })._embedded[linkName] = source === undefined ? val : source; + embedded[linkName] = source === undefined ? val : source; } } diff --git a/frontend/src/app/features/hal/helpers/lazy-accessor.ts b/frontend/src/app/features/hal/helpers/lazy-accessor.ts index f997696aff37..97e7beb09800 100644 --- a/frontend/src/app/features/hal/helpers/lazy-accessor.ts +++ b/frontend/src/app/features/hal/helpers/lazy-accessor.ts @@ -26,17 +26,15 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; - export namespace OpenprojectHalModuleHelpers { - export function lazy(obj:HalResource, + export function lazy(obj:object, property:string, - getter:() => any, - setter?:(value:any) => void):void { + getter:() => V, + setter?:(value:V) => V):void { if (typeof obj === 'object' && obj !== null) { let done = false; - let value:any; - const config:any = { + let value:V; + const config:PropertyDescriptor = { get() { if (!done) { value = getter(); @@ -51,7 +49,7 @@ export namespace OpenprojectHalModuleHelpers { }; if (setter) { - config.set = (val:any) => { + config.set = (val:V) => { value = setter(val); done = true; }; diff --git a/frontend/src/app/features/hal/http/http.interfaces.ts b/frontend/src/app/features/hal/http/http.interfaces.ts index 075e1cff96d4..c0432974a924 100644 --- a/frontend/src/app/features/hal/http/http.interfaces.ts +++ b/frontend/src/app/features/hal/http/http.interfaces.ts @@ -31,13 +31,13 @@ import { HttpHeaders, HttpParams } from '@angular/common/http'; export type HTTPSupportedMethods = 'get'|'post'|'put'|'patch'|'delete'; export interface HTTPClientOptions { - body?:any; + body?:unknown; headers?:HTTPClientHeaders; - observe?:any; + observe?:'body'; params?:HTTPClientParams; reportProgress?:boolean; withCredentials?:boolean; - responseType:any; + responseType:'json'; } export type HTTPClientParamMap = Record; diff --git a/frontend/src/app/features/hal/http/openproject-header-interceptor.ts b/frontend/src/app/features/hal/http/openproject-header-interceptor.ts index 085e306346c0..f3c0510fdc51 100644 --- a/frontend/src/app/features/hal/http/openproject-header-interceptor.ts +++ b/frontend/src/app/features/hal/http/openproject-header-interceptor.ts @@ -37,7 +37,7 @@ export const EXTERNAL_REQUEST_HEADER = 'X-External-Request'; @Injectable() export class OpenProjectHeaderInterceptor implements HttpInterceptor { - intercept(req:HttpRequest, next:HttpHandler):Observable> { + intercept(req:HttpRequest, next:HttpHandler):Observable> { const withCredentials = req.headers.get(EXTERNAL_REQUEST_HEADER) !== 'true'; if (withCredentials) { @@ -47,7 +47,7 @@ export class OpenProjectHeaderInterceptor implements HttpInterceptor { } } - private handleExternalRequest(req:HttpRequest, next:HttpHandler):Observable> { + private handleExternalRequest(req:HttpRequest, next:HttpHandler):Observable> { // Clone the request to add the new header const clonedRequest = req.clone({ withCredentials: false, @@ -57,7 +57,7 @@ export class OpenProjectHeaderInterceptor implements HttpInterceptor { return next.handle(clonedRequest); } - private handleAuthenticatedRequest(req:HttpRequest, next:HttpHandler):Observable> { + private handleAuthenticatedRequest(req:HttpRequest, next:HttpHandler):Observable> { const csrfToken = getMetaContent('csrf-token'); let newHeaders = req.headers.set('X-Requested-With', 'XMLHttpRequest'); diff --git a/frontend/src/app/features/hal/resources/activity-comment-resource.ts b/frontend/src/app/features/hal/resources/activity-comment-resource.ts index 04b35cb99bda..c91e408d1b03 100644 --- a/frontend/src/app/features/hal/resources/activity-comment-resource.ts +++ b/frontend/src/app/features/hal/resources/activity-comment-resource.ts @@ -26,7 +26,8 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceEmbedded, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { AttachmentCollectionResource } from './attachment-collection-resource'; @@ -36,15 +37,15 @@ interface ActivityCommentResourceEmbedded { workPackage:WorkPackageResource; } -interface ActivityCommentResourceLinks extends ActivityCommentResourceEmbedded { - addAttachment(attachment:HalResource):Promise; +interface ActivityCommentResourceLinks { + addAttachment:CallableHalLink; } class ActivityCommentBaseResource extends HalResource { - public $embedded:ActivityCommentResourceEmbedded; - public $links:ActivityCommentResourceLinks; + public $embedded:ActivityCommentResourceEmbedded & HalResourceEmbedded; + public $links:ActivityCommentResourceLinks & HalResourceLinks; } export const ActivityCommentResource = Attachable(ActivityCommentBaseResource); -export interface ActivityCommentResource extends ActivityCommentBaseResource, ActivityCommentResourceLinks {} +export interface ActivityCommentResource extends ActivityCommentBaseResource, ActivityCommentResourceLinks, ActivityCommentResourceEmbedded {} diff --git a/frontend/src/app/features/hal/resources/attachment-collection-resource.ts b/frontend/src/app/features/hal/resources/attachment-collection-resource.ts index 539ddd736fc7..e564b264531c 100644 --- a/frontend/src/app/features/hal/resources/attachment-collection-resource.ts +++ b/frontend/src/app/features/hal/resources/attachment-collection-resource.ts @@ -30,13 +30,11 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; export class AttachmentCollectionResource extends CollectionResource { - public $initialize(source:any) { + public elements:HalResource[]; + + public $initialize(source:unknown) { super.$initialize(source); this.elements = this.elements || []; } } - -export interface AttachmentCollectionResource { - elements:HalResource[]; -} diff --git a/frontend/src/app/features/hal/resources/custom-action-resource.ts b/frontend/src/app/features/hal/resources/custom-action-resource.ts index aa3db9504dee..0bae6b5046ca 100644 --- a/frontend/src/app/features/hal/resources/custom-action-resource.ts +++ b/frontend/src/app/features/hal/resources/custom-action-resource.ts @@ -28,17 +28,21 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export interface CustomActionResourceLinks { - self():Promise; - executeImmediately(payload:any):Promise; + self:CallableHalLink; + executeImmediately:CallableHalLink; } export interface CustomActionResourceEmbedded { description:string; } -export class CustomActionResource extends HalResource { -} +export class CustomActionResource extends HalResource implements CustomActionResourceLinks, CustomActionResourceEmbedded { + public self:CallableHalLink; + + public executeImmediately:CallableHalLink; -export interface CustomActionResource extends CustomActionResourceLinks, CustomActionResourceEmbedded {} + public description:string; +} diff --git a/frontend/src/app/features/hal/resources/error-resource.ts b/frontend/src/app/features/hal/resources/error-resource.ts index a07bf150118f..75d7a7593d2d 100644 --- a/frontend/src/app/features/hal/resources/error-resource.ts +++ b/frontend/src/app/features/hal/resources/error-resource.ts @@ -60,11 +60,11 @@ export interface IHalMultipleError extends IHalErrorBase { } export class ErrorResource extends HalResource { - public errors:any[]; + public errors:ErrorResource[]; public message:string; - public details:any; + public details:{ attribute:string }; public errorIdentifier:string; @@ -97,21 +97,21 @@ export class ErrorResource extends HalResource { let columns:ErrorResource[] = []; if (this.details) { - columns = [{ details: this.details as { attribute:string } } as ErrorResource]; + columns = [{ details: this.details } as ErrorResource]; } else if (this.errors) { - columns = this.errors as ErrorResource[]; + columns = this.errors; } return columns.map((resource:ErrorResource):string => { if (resource.errorIdentifier === v3ErrorIdentifierMultipleErrors) { return this.extractMultiError(resource)[0]; } - return (resource.details as { attribute:string }).attribute; + return resource.details.attribute; }).flat(); } public getMessagesPerAttribute():Record { - const perAttribute:any = {}; + const perAttribute:Record = {}; if (this.details) { perAttribute[this.details.attribute] = [this.message]; @@ -119,7 +119,7 @@ export class ErrorResource extends HalResource { this.errors?.forEach((error:ErrorResource) => { if (error.errorIdentifier === v3ErrorIdentifierMultipleErrors) { const [attribute, messages] = this.extractMultiError(error); - const current = perAttribute[attribute] || []; + const current = perAttribute[attribute] ?? []; perAttribute[attribute] = current.concat(messages); } else if (perAttribute[error.details.attribute]) { perAttribute[error.details.attribute].push(error.message); diff --git a/frontend/src/app/features/hal/resources/form-resource.ts b/frontend/src/app/features/hal/resources/form-resource.ts index 0cf1a5ecbe15..63a45e4a77c4 100644 --- a/frontend/src/app/features/hal/resources/form-resource.ts +++ b/frontend/src/app/features/hal/resources/form-resource.ts @@ -29,21 +29,34 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ErrorResource, v3ErrorIdentifierMultipleErrors } from 'core-app/features/hal/resources/error-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; + +export type FormSchemaResource = SchemaResource & Partial>; export interface FormResourceLinks { - commit(payload:any):Promise; + commit:CallableHalLink; } export interface FormResourceEmbedded { - schema:SchemaResource; + schema:FormSchemaResource; validationErrors:Record; + payload:HalResource; } -export class FormResource extends HalResource { - public schema:SchemaResource; +export class FormResource extends HalResource implements FormResourceEmbedded, FormResourceLinks { + public schema:FormSchemaResource; public validationErrors:Record; + public payload:HalResource; + + public commit:CallableHalLink; + + public configureForm?:HalResource; + + public customFields?:HalResource; + public getErrors():ErrorResource|null { const errors = Object.values(this.validationErrors ?? {}); const count = errors.length; @@ -64,5 +77,3 @@ export class FormResource extends HalResource { return resource; } } - -export interface FormResource extends FormResourceEmbedded, FormResourceLinks {} diff --git a/frontend/src/app/features/hal/resources/grid-resource.ts b/frontend/src/app/features/hal/resources/grid-resource.ts index ed129b70363a..37e585de07b9 100644 --- a/frontend/src/app/features/hal/resources/grid-resource.ts +++ b/frontend/src/app/features/hal/resources/grid-resource.ts @@ -26,17 +26,18 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { GridWidgetResource } from 'core-app/features/hal/resources/grid-widget-resource'; +import { AttachmentCollectionResource } from 'core-app/features/hal/resources/attachment-collection-resource'; import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; -import { IHalResourceLink } from 'core-app/core/state/hal-resource'; import idFromLink from '../helpers/id-from-link'; export interface GridResourceLinks { - update(payload:unknown):Promise; - updateImmediately(payload:unknown):Promise; - delete():Promise; - project:IHalResourceLink; + update:CallableHalLink; + updateImmediately:CallableHalLink; + delete:CallableHalLink; + project:CallableHalLink; } export class GridBaseResource extends HalResource { @@ -48,7 +49,9 @@ export class GridBaseResource extends HalResource { public columnCount:number; - public $links:GridResourceLinks; + public attachments:AttachmentCollectionResource; + + public $links:GridResourceLinks & HalResourceLinks; public get projectId():string|undefined { if (this.$links.project) { @@ -58,7 +61,7 @@ export class GridBaseResource extends HalResource { return undefined; } - public $initialize(source:any) { + public $initialize(source:unknown) { super.$initialize(source); this.widgets = this diff --git a/frontend/src/app/features/hal/resources/grid-widget-resource.ts b/frontend/src/app/features/hal/resources/grid-widget-resource.ts index be3350c2626a..da654144b39f 100644 --- a/frontend/src/app/features/hal/resources/grid-widget-resource.ts +++ b/frontend/src/app/features/hal/resources/grid-widget-resource.ts @@ -57,6 +57,8 @@ export class GridWidgetResource extends HalResource { public grid:GridResource; + public isNewWidget:boolean; + public get schema():SchemaResource { return this.halResource.createHalResource({ _type: 'Schema' }, true); } diff --git a/frontend/src/app/features/hal/resources/hal-resource.spec.ts b/frontend/src/app/features/hal/resources/hal-resource.spec.ts index 3efa51c88f84..7030bb772da4 100644 --- a/frontend/src/app/features/hal/resources/hal-resource.spec.ts +++ b/frontend/src/app/features/hal/resources/hal-resource.spec.ts @@ -34,7 +34,7 @@ import { States } from 'core-app/core/states/states.service'; import { of } from 'rxjs'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module'; -import { HalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; +import { CallableHalLink, HalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient, withInterceptorsFromDi, withXhr } from '@angular/common/http'; import type { Mock } from 'vitest'; @@ -135,7 +135,7 @@ describe('HalResource', () => { }); it('should not be loaded', () => { - expect(resource.someResource.$loaded).toBeFalsy(); + expect((resource.someResource as HalResource).$loaded).toBeFalsy(); }); }); }); @@ -393,7 +393,7 @@ describe('HalResource', () => { return null as any; }); - expect(() => resource.$links.beaver()).not.toThrow(Error); + expect(() => (resource.$links.beaver as CallableHalLink)()).not.toThrow(Error); }); it('should have a $links property with the keys of its source _links', () => { @@ -422,11 +422,11 @@ describe('HalResource', () => { }); it('should have a property, that is a loaded resource', () => { - expect(resource.resource.$loaded).toBeTruthy(); + expect((resource.resource as HalResource).$loaded).toBeTruthy(); }); it('should have an embedded resource, that is loaded', () => { - expect(resource.$embedded.resource.$loaded).toBeTruthy(); + expect((resource.$embedded.resource as HalResource).$loaded).toBeTruthy(); }); it('should have a property that is the resource', () => { @@ -445,7 +445,7 @@ describe('HalResource', () => { }); it('should set the property to that resource', () => { - expect(resource.resource.href).toEqual(link.href); + expect((resource.resource as HalResource).href).toEqual(link.href); }); }); @@ -466,8 +466,9 @@ describe('HalResource', () => { }, }; - first = resource.$embedded.resource.$embedded.first; - deep = resource.$embedded.resource.$embedded.first.$embedded.second; + const embedded = resource.$embedded.resource as HalResource; + first = embedded.$embedded.first; + deep = (embedded.$embedded.first as HalResource).$embedded.second; }); it('should create all nested resources recursively', () => { @@ -484,7 +485,7 @@ describe('HalResource', () => { describe('when creating a resource from a source with a linked array property', () => { const expectLengthsToBe = (length:any, update = 'update') => { it(`should ${update} the values of the resource`, () => { - expect(resource.values.length).toEqual(length); + expect((resource.values as unknown[]).length).toEqual(length); }); it(`should ${update} the source`, () => { @@ -492,7 +493,7 @@ describe('HalResource', () => { }); it(`should ${update} the $source property`, () => { - expect(resource.$source._links.values.length).toEqual(length); + expect((resource.$source._links.values as unknown as unknown[]).length).toEqual(length); }); }; @@ -518,28 +519,28 @@ describe('HalResource', () => { describe('when adding resources to the array', () => { beforeEach(() => { - resource.values.push(resource); + (resource.values as unknown[]).push(resource); }); expectLengthsToBe(3); }); describe('when adding arbitrary values to the array', () => { beforeEach(() => { - resource.values.push('something'); + (resource.values as unknown[]).push('something'); }); expectLengthsToBe(2, 'not update'); }); describe('when removing resources from the array', () => { beforeEach(() => { - resource.values.pop(); + (resource.values as unknown[]).pop(); }); expectLengthsToBe(1); }); describe('when each value is transformed', () => { beforeEach(() => { - resource = resource.values[0]; + resource = (resource.values as HalResource[])[0]; source = source._links.values[0]; }); @@ -577,8 +578,9 @@ describe('HalResource', () => { }); it('should transform the list elements', () => { - expect(resource.$embedded.elements[0].$isHal).toBeTruthy(); - expect(resource.$embedded.elements[1].$isHal).toBeTruthy(); + const elements = resource.$embedded.elements as HalResource[]; + expect(elements[0].$isHal).toBeTruthy(); + expect(elements[1].$isHal).toBeTruthy(); }); }); @@ -629,7 +631,7 @@ describe('HalResource', () => { it('should not be possible to override a link', () => { try { - resource.$links.action = 'foo'; + resource.$links.action = 'foo' as unknown as CallableHalLink; } catch (ignore) { /**/ @@ -689,7 +691,7 @@ describe('HalResource', () => { }); it('should be a resource, if the link method is "get"', () => { - expect(resource.property.$isHal).toBeTruthy(); + expect((resource.property as HalResource).$isHal).toBeTruthy(); }); describe('when a property is a resource', () => { @@ -698,12 +700,12 @@ describe('HalResource', () => { }); it('should not be loaded initially', () => { - expect(resource.property.$loaded).toBeFalsy(); - expect(resource.notLinked.$loaded).toBeTruthy(); + expect((resource.property as HalResource).$loaded).toBeFalsy(); + expect((resource.notLinked as HalResource).$loaded).toBeTruthy(); }); it('should be loaded, if the resource is embedded', () => { - expect(resource.embedded.$loaded).toBeTruthy(); + expect((resource.embedded as HalResource).$loaded).toBeTruthy(); }); it('should update the source when set', () => { @@ -731,7 +733,7 @@ describe('HalResource', () => { return false as any; }); - resource = resource.property; + resource = resource.property as HalResource; promise = resource.$load().then((result:HalResource) => { newResult = result; }); diff --git a/frontend/src/app/features/hal/resources/hal-resource.ts b/frontend/src/app/features/hal/resources/hal-resource.ts index 6294e91cbf75..7be0e963d27e 100644 --- a/frontend/src/app/features/hal/resources/hal-resource.ts +++ b/frontend/src/app/features/hal/resources/hal-resource.ts @@ -32,27 +32,30 @@ import { Injector } from '@angular/core'; import { States } from 'core-app/core/states/states.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator'; -import { HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; +import { CallableHalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; import { ICKEditorContext } from 'core-app/shared/components/editor/components/ckeditor/ckeditor.types'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { cloneDeep } from 'lodash-es'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; +import { HalSource } from 'core-app/features/hal/interfaces'; export type HalResourceClass = new( _injector:Injector, - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - _source:any, + _source:unknown, _$loaded:boolean, - _halInitializer:(_:T) => void, + _halInitializer:(_:HalResource) => void, _$halType:string, ) => T; +export interface HalResourceLinks { + self:CallableHalLink; + [name:string]:CallableHalLink|CallableHalLink[]|undefined; +} + +export type HalResourceEmbedded = Record; + export class HalResource { - // TODO this is the source of many issues in the frontend - // because it no longer properly type checks stuff - // Since 2019-10-21 I'm documenting what bugs this caused: - // https://community.openproject.com/wp/31462 - [attribute:string]:any; + [attribute:string]:unknown; // The API type reported from API public _type:string; @@ -87,23 +90,25 @@ export class HalResource { */ public constructor( public injector:Injector, - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - public $source:any, + $source:unknown, public $loaded:boolean, - public halInitializer:(halResource:any) => void, + public halInitializer:(halResource:HalResource) => void, $halType:string, ) { + this.$source = $source as HalSource; this.$halType = $halType; this.$initialize($source); } - public static getEmptyResource(self:{ href:string|null } = { href: null }):any { + public static getEmptyResource(self:{ href:string|null } = { href: null }):HalSource { return { _links: { self } }; } - public $links:any = {}; + public $source:HalSource; + + public $links = {} as HalResourceLinks; - public $embedded:any = {}; + public $embedded:HalResourceEmbedded = {}; public $self:Promise; @@ -114,8 +119,13 @@ export class HalResource { return match?.[1] ?? null; } - public $initialize(source:any) { - this.$source = source.$source || source; + public $initialize(source:unknown) { + const wrapped = (source as { $source?:HalSource }).$source; + if (wrapped) { + this.$source = wrapped; + } else { + this.$source = source as HalSource; + } this.halInitializer(this); } @@ -138,7 +148,7 @@ export class HalResource { */ public get id():string|null { if (this.$source.id) { - return this.$source.id.toString(); + return (this.$source.id as string|number).toString(); } const id = idFromLink(this.href); @@ -171,12 +181,12 @@ export class HalResource { * @returns A HalResource with the identitical copied source of other. */ public $copy(source:object = {}):T { - const clone:HalResourceClass = this.constructor as any; + const clone = this.constructor as HalResourceClass; return new clone(this.injector, merge(this.$plain(), source), this.$loaded, this.halInitializer, this.$halType); } - public $plain():any { + public $plain():HalSource { // Use a deep clone (not structuredClone) because $source may contain // HalResource instances (e.g. filter values), which carry functions and // injector state that structuredClone cannot clone (DataCloneError). @@ -223,7 +233,7 @@ export class HalResource { public previewPath():string|undefined { if (isNewResource(this) && this.project) { - return this.project.href; + return (this.project as { href:string }).href; } return undefined; @@ -269,7 +279,7 @@ export class HalResource { // Reset and load this resource this.$loaded = false; - this.$self = this.$links.self({}).then((source:any) => { + this.$self = this.$links.self({}).then((source:HalResource) => { this.$loaded = true; this.$initialize(source.$source); return this; diff --git a/frontend/src/app/features/hal/resources/meeting-resource.ts b/frontend/src/app/features/hal/resources/meeting-resource.ts index 4ee82f66b32e..7d573819c73b 100644 --- a/frontend/src/app/features/hal/resources/meeting-resource.ts +++ b/frontend/src/app/features/hal/resources/meeting-resource.ts @@ -26,17 +26,18 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; interface MeetingResourceLinks { - addAttachment(attachment:HalResource):Promise; + addAttachment:CallableHalLink; } class MeetingBaseResource extends HalResource { title:string; project:HalResource; - public $links:MeetingResourceLinks; + public $links:MeetingResourceLinks & HalResourceLinks; } export const MeetingResource = Attachable(MeetingBaseResource); diff --git a/frontend/src/app/features/hal/resources/membership-resource.ts b/frontend/src/app/features/hal/resources/membership-resource.ts index 1c7c5e98e798..b8fe2ed839c2 100644 --- a/frontend/src/app/features/hal/resources/membership-resource.ts +++ b/frontend/src/app/features/hal/resources/membership-resource.ts @@ -27,14 +27,15 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { RoleResource } from 'core-app/features/hal/resources/role-resource'; import { ProjectResource } from 'core-app/features/hal/resources/project-resource'; import Formattable = api.v3.Formattable; export interface MembershipResourceLinks { - update(payload:unknown):Promise; - updateImmediately(payload:unknown):Promise; - delete():Promise; + update:CallableHalLink; + updateImmediately:CallableHalLink; + delete:CallableHalLink; } export interface MembershipResourceEmbedded { @@ -44,7 +45,18 @@ export interface MembershipResourceEmbedded { notificationMessage:Formattable; } -export class MembershipResource extends HalResource { -} +export class MembershipResource extends HalResource implements MembershipResourceLinks, MembershipResourceEmbedded { + public update:CallableHalLink; + + public updateImmediately:CallableHalLink; + + public delete:CallableHalLink; + + public principal:HalResource; -export interface MembershipResource extends MembershipResourceLinks, MembershipResourceEmbedded {} + public roles:RoleResource[]; + + public project:ProjectResource; + + public notificationMessage:Formattable; +} diff --git a/frontend/src/app/features/hal/resources/mixins/attachable-mixin.ts b/frontend/src/app/features/hal/resources/mixins/attachable-mixin.ts index c4bc457f6d27..5a1c4be68022 100644 --- a/frontend/src/app/features/hal/resources/mixins/attachable-mixin.ts +++ b/frontend/src/app/features/hal/resources/mixins/attachable-mixin.ts @@ -45,7 +45,7 @@ export function Attachable>(Base:TBase) { * adding attachments is allowed. */ public get canAddAttachments():boolean { - return !!((this as HalResource).$links as unknown&{ addAttachment?:HalLink }).addAttachment || isNewResource(this); + return !!(this as HalResource).$links.addAttachment || isNewResource(this); } /** diff --git a/frontend/src/app/features/hal/resources/placeholder-user-resource.ts b/frontend/src/app/features/hal/resources/placeholder-user-resource.ts index 89ba7e275e6e..2d940528ab9a 100644 --- a/frontend/src/app/features/hal/resources/placeholder-user-resource.ts +++ b/frontend/src/app/features/hal/resources/placeholder-user-resource.ts @@ -38,7 +38,7 @@ export class PlaceholderUserResource extends HalResource { public showUser:HalResource; public get state():InputState { - return this.states.placeholderUsers.get(this.href!) as any; + return this.states.placeholderUsers.get(this.href!) as unknown as InputState; } public get showUserPath() { diff --git a/frontend/src/app/features/hal/resources/post-resource.ts b/frontend/src/app/features/hal/resources/post-resource.ts index 6a897e77ffe0..650876fb4add 100644 --- a/frontend/src/app/features/hal/resources/post-resource.ts +++ b/frontend/src/app/features/hal/resources/post-resource.ts @@ -26,15 +26,16 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; export interface PostResourceLinks { - addAttachment(attachment:HalResource):Promise; + addAttachment:CallableHalLink; } class PostBaseResource extends HalResource { - public $links:PostResourceLinks; + public $links:PostResourceLinks & HalResourceLinks; private attachmentsBackend = false; } diff --git a/frontend/src/app/features/hal/resources/project-resource.ts b/frontend/src/app/features/hal/resources/project-resource.ts index 6d3dec17be79..6a93edfda726 100644 --- a/frontend/src/app/features/hal/resources/project-resource.ts +++ b/frontend/src/app/features/hal/resources/project-resource.ts @@ -26,17 +26,17 @@ // See COPYRIGHT and LICENSE files for more details. //++ +import { InputState } from '@openproject/reactivestates'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ICKEditorContext } from 'core-app/shared/components/editor/components/ckeditor/ckeditor.types'; export class ProjectResource extends HalResource { public get identifier():string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return this.$source.identifier as string; } public get state() { - return this.states.projects.get(this.id!) as any; + return this.states.projects.get(this.id!) as unknown as InputState; } public getEditorContext(fieldName:string):ICKEditorContext { diff --git a/frontend/src/app/features/hal/resources/query-filter-instance-resource.ts b/frontend/src/app/features/hal/resources/query-filter-instance-resource.ts index fe3301b1f1f1..3b997ceda8de 100644 --- a/frontend/src/app/features/hal/resources/query-filter-instance-resource.ts +++ b/frontend/src/app/features/hal/resources/query-filter-instance-resource.ts @@ -31,6 +31,7 @@ import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { QueryOperatorResource } from 'core-app/features/hal/resources/query-operator-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { QueryFilterInstanceSchemaResource } from 'core-app/features/hal/resources/query-filter-instance-schema-resource'; import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource'; @@ -48,12 +49,12 @@ export class QueryFilterInstanceResource extends HalResource { @LazyInject(PathHelperService) pathHelper:PathHelperService; - public $initialize(source:any) { + public $initialize(source:unknown) { super.$initialize(source); this.$links.schema = { href: `${this.pathHelper.api.v3.apiV3Base}/queries/filter_instance_schemas/${idFromLink(this.filter.href)}`, - }; + } as unknown as CallableHalLink; } public get id():string { @@ -80,9 +81,9 @@ export class QueryFilterInstanceResource extends HalResource { if (this.memoizedCurrentSchemas[key] === undefined) { try { - this.memoizedCurrentSchemas[key] = this.schemaCache.of(this).resultingSchema(this.operator); + this.memoizedCurrentSchemas[key] = (this.schemaCache.of(this) as unknown as QueryFilterInstanceSchemaResource).resultingSchema(this.operator); } catch (e) { - console.error(`Failed to access filter schema${e}`); + console.error(`Failed to access filter schema${String(e)}`); } } @@ -99,7 +100,7 @@ export class QueryFilterInstanceResource extends HalResource { public isTemplated() { let flag = false; - (this.values as any[]).find((value:any) => { + (this.values as { href:string, toString():string }[]).find((value) => { const href:string = value?.href || value.toString() || ''; flag = href.includes('{id}'); }); diff --git a/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts b/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts index e77c433b0c47..5c39aedafe35 100644 --- a/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts +++ b/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts @@ -26,10 +26,10 @@ // See COPYRIGHT and LICENSE files for more details. //++ +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { merge } from 'lodash-es'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { QueryOperatorResource } from 'core-app/features/hal/resources/query-operator-resource'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; @@ -37,9 +37,9 @@ import { QueryFilterResource } from 'core-app/features/hal/resources/query-filte import { SchemaDependencyResource } from 'core-app/features/hal/resources/schema-dependency-resource'; import { SchemaAttributeObject } from 'core-app/features/hal/resources/schema-attribute-object'; -export interface QueryFilterInstanceSchemaResourceLinks { - self:HalLink; - filter:QueryFilterResource; +export interface QueryFilterInstanceSchemaResourceLinks extends HalResourceLinks { + self:CallableHalLink; + filter:CallableHalLink; } export class QueryFilterInstanceSchemaResource extends SchemaResource { @@ -67,18 +67,19 @@ export class QueryFilterInstanceSchemaResource extends SchemaResource { return this.filter.allowedValues[0]; } - public $initialize(source:any) { + public $initialize(source:unknown) { super.$initialize(source); - if (source._dependencies) { - this.dependency = new SchemaDependencyResource(this.injector, source._dependencies[0], true, this.halInitializer, 'SchemaDependency'); + const { _dependencies } = source as { _dependencies?:unknown[] }; + if (_dependencies) { + this.dependency = new SchemaDependencyResource(this.injector, _dependencies[0], true, this.halInitializer, 'SchemaDependency'); } } public getFilter():QueryFilterInstanceResource { const operator = (this.operator.allowedValues as HalResource[])[0]; const filter = (this.filter.allowedValues as HalResource[])[0]; - const source:any = { + const source:{ name:string, values?:unknown[], _links:Record } = { name: filter.name, _links: { filter: filter.$source._links.self, diff --git a/frontend/src/app/features/hal/resources/query-filter-resource.ts b/frontend/src/app/features/hal/resources/query-filter-resource.ts index 97bf5ac83c24..de9bec3b7643 100644 --- a/frontend/src/app/features/hal/resources/query-filter-resource.ts +++ b/frontend/src/app/features/hal/resources/query-filter-resource.ts @@ -27,20 +27,13 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { QueryFilterInstanceSchemaResource } from 'core-app/features/hal/resources/query-filter-instance-schema-resource'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; -export interface QueryFilterResourceEmbedded { - schema:QueryFilterInstanceSchemaResource; -} - export class QueryFilterResource extends HalResource { - public $embedded:QueryFilterResourceEmbedded; - - public values:any[]; + public values:unknown[]; public get id():string { - return this.$source.id || idFromLink(this.href); + return (this.$source.id as string) || idFromLink(this.href); } public set id(newId:string) { diff --git a/frontend/src/app/features/hal/resources/query-form-resource.ts b/frontend/src/app/features/hal/resources/query-form-resource.ts index 6ddaf84a5474..6c1107d9785d 100644 --- a/frontend/src/app/features/hal/resources/query-form-resource.ts +++ b/frontend/src/app/features/hal/resources/query-form-resource.ts @@ -27,19 +27,24 @@ //++ import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; -import { FormResource } from 'core-app/features/hal/resources/form-resource'; +import { FormResource, FormSchemaResource } from 'core-app/features/hal/resources/form-resource'; +import { HalResourceEmbedded } from 'core-app/features/hal/resources/hal-resource'; import { QueryFilterInstanceSchemaResource } from 'core-app/features/hal/resources/query-filter-instance-schema-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; -export interface QueryFormResourceEmbedded { +export interface QueryFormSchemaResource extends SchemaResource { + $embedded:HalResourceEmbedded & { filtersSchemas:CollectionResource }; +} + +export interface QueryFormResourceEmbedded extends HalResourceEmbedded { filtersSchemas:CollectionResource; - schema:SchemaResource; + schema:QueryFormSchemaResource; } export class QueryFormResource extends FormResource { public $embedded:QueryFormResourceEmbedded; - public schema:SchemaResource; + public schema:FormSchemaResource; public get filtersSchemas():QueryFilterInstanceSchemaResource[] { return this.$embedded.filtersSchemas.elements; diff --git a/frontend/src/app/features/hal/resources/query-operator-resource.ts b/frontend/src/app/features/hal/resources/query-operator-resource.ts index 990b2e80be24..56ebfac944d4 100644 --- a/frontend/src/app/features/hal/resources/query-operator-resource.ts +++ b/frontend/src/app/features/hal/resources/query-operator-resource.ts @@ -31,7 +31,7 @@ import idFromLink from 'core-app/features/hal/helpers/id-from-link'; export class QueryOperatorResource extends HalResource { public get id():string { - return this.$source.id || idFromLink(this.href); + return (this.$source.id as string) || idFromLink(this.href); } public set id(val:string) { diff --git a/frontend/src/app/features/hal/resources/query-resource.ts b/frontend/src/app/features/hal/resources/query-resource.ts index bf679dfd3beb..0e5891f2c295 100644 --- a/frontend/src/app/features/hal/resources/query-resource.ts +++ b/frontend/src/app/features/hal/resources/query-resource.ts @@ -27,7 +27,8 @@ //++ import { QueryColumn } from 'core-app/features/work-packages/components/wp-query/query-column'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalResource, HalResourceEmbedded } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { HighlightingMode } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting-mode.const'; import { QueryOrder } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-order'; import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource'; @@ -37,7 +38,7 @@ import { UserResource } from 'core-app/features/hal/resources/user-resource'; import { QuerySortByResource } from 'core-app/features/hal/resources/query-sort-by-resource'; import { QueryGroupByResource } from 'core-app/features/hal/resources/query-group-by-resource'; -export interface QueryResourceEmbedded { +export interface QueryResourceEmbedded extends HalResourceEmbedded { results:WorkPackageCollectionResource; columns:QueryColumn[]; groupBy:QueryGroupByResource|undefined; @@ -55,7 +56,14 @@ export interface TimelineLabels { farRight:string|null; } -export class QueryResource extends HalResource { +export interface QueryResourceLinks { + updateImmediately?:CallableHalLink; + icalUrl:CallableHalLink<{ icalUrl:{ href:string } }>; + star?:CallableHalLink; + unstar?:CallableHalLink; +} + +export class QueryResource extends HalResource implements QueryResourceLinks { public $embedded:QueryResourceEmbedded; public results:WorkPackageCollectionResource; @@ -106,7 +114,17 @@ export class QueryResource extends HalResource { public ordered_work_packages:QueryOrder; - public $initialize(source:any) { + public updatedAt:string; + + public updateImmediately?:CallableHalLink; + + public icalUrl:CallableHalLink<{ icalUrl:{ href:string } }>; + + public star?:CallableHalLink; + + public unstar?:CallableHalLink; + + public $initialize(source:unknown) { super.$initialize(source); this.filters = this @@ -120,10 +138,3 @@ export class QueryResource extends HalResource { )); } } - -export interface QueryResourceLinks { - updateImmediately?(attributes:any):Promise; - icalUrl(payload:unknown):Promise<{ icalUrl:{ href:string } }>; -} - -export interface QueryResource extends QueryResourceLinks {} diff --git a/frontend/src/app/features/hal/resources/query-sort-by-resource.ts b/frontend/src/app/features/hal/resources/query-sort-by-resource.ts index bc294187f880..cedb799501f3 100644 --- a/frontend/src/app/features/hal/resources/query-sort-by-resource.ts +++ b/frontend/src/app/features/hal/resources/query-sort-by-resource.ts @@ -27,12 +27,12 @@ //++ import { QueryColumn } from 'core-app/features/work-packages/components/wp-query/query-column'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalResource, HalResourceEmbedded } from 'core-app/features/hal/resources/hal-resource'; export const QUERY_SORT_BY_ASC = 'urn:openproject-org:api:v3:queries:directions:asc'; export const QUERY_SORT_BY_DESC = 'urn:openproject-org:api:v3:queries:directions:desc'; -export interface QuerySortByResourceEmbedded { +export interface QuerySortByResourceEmbedded extends HalResourceEmbedded { column:QueryColumn; direction:QuerySortByDirection; } diff --git a/frontend/src/app/features/hal/resources/relation-resource.ts b/frontend/src/app/features/hal/resources/relation-resource.ts index 277ce128490e..c7cb8247769f 100644 --- a/frontend/src/app/features/hal/resources/relation-resource.ts +++ b/frontend/src/app/features/hal/resources/relation-resource.ts @@ -26,17 +26,18 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; export interface RelationResourceLinks { - delete():Promise; + delete:CallableHalLink; - updateImmediately(payload:any):Promise; + updateImmediately:CallableHalLink; } -export class RelationResource extends HalResource { +export class RelationResource extends HalResource implements RelationResourceLinks { static RELATION_TYPES(includeParentChild = true):string[] { const types = [ 'relates', @@ -72,12 +73,16 @@ export class RelationResource extends HalResource { // Properties public description:string|null; - public type:any; + public type:string; public reverseType:string; // Links - public $links:RelationResourceLinks; + public $links:RelationResourceLinks & HalResourceLinks; + + public delete:CallableHalLink; + + public updateImmediately:CallableHalLink; public to:WorkPackageResource; @@ -127,13 +132,11 @@ export class RelationResource extends HalResource { return this.$links.updateImmediately({ description }); } - public updateType(type:any) { + public updateType(type:string) { return this.$links.updateImmediately({ type }); } } -export interface RelationResource extends RelationResourceLinks {} - export interface DenormalizedRelationData { target:WorkPackageResource; targetId:string; diff --git a/frontend/src/app/features/hal/resources/schema-dependency-resource.ts b/frontend/src/app/features/hal/resources/schema-dependency-resource.ts index ac18c341d6a7..9a0baf087ee4 100644 --- a/frontend/src/app/features/hal/resources/schema-dependency-resource.ts +++ b/frontend/src/app/features/hal/resources/schema-dependency-resource.ts @@ -29,9 +29,9 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class SchemaDependencyResource extends HalResource { - public dependencies:any; + public dependencies:Record; - public forValue(value:string):any { + public forValue(value:string):object { return this.dependencies[value]; } } diff --git a/frontend/src/app/features/hal/resources/schema-resource.ts b/frontend/src/app/features/hal/resources/schema-resource.ts index 9066845f0609..466197f595ff 100644 --- a/frontend/src/app/features/hal/resources/schema-resource.ts +++ b/frontend/src/app/features/hal/resources/schema-resource.ts @@ -27,16 +27,16 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { HalSource } from 'core-app/features/hal/interfaces'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; import { InputState } from '@openproject/reactivestates'; export class SchemaResource extends HalResource { public get state():InputState { - return this.states.schemas.get(this.href!) as any; + return this.states.schemas.get(this.href!) as unknown as InputState; } public get availableAttributes():string[] { - return Object.keys(this.$source as HalSource).filter((name) => !name.startsWith('_')); + return Object.keys(this.$source).filter((name) => !name.startsWith('_')); } // Find the attribute name with a matching (localized) name; @@ -44,7 +44,7 @@ export class SchemaResource extends HalResource { let match:string|null = null; for (const attribute of this.availableAttributes) { - const fieldSchema = this[attribute]; + const fieldSchema = this[attribute] as IOPFieldSchema|undefined; if (fieldSchema?.name === name) { match = attribute; break; diff --git a/frontend/src/app/features/hal/resources/share-resource.ts b/frontend/src/app/features/hal/resources/share-resource.ts index a787c1acfb0a..f1ba1a2d8fc2 100644 --- a/frontend/src/app/features/hal/resources/share-resource.ts +++ b/frontend/src/app/features/hal/resources/share-resource.ts @@ -37,7 +37,12 @@ export interface ShareResourceEmbedded { entity:HalResource; } -export class ShareResource extends HalResource { -} +export class ShareResource extends HalResource implements ShareResourceEmbedded { + public principal:HalResource; + + public roles:RoleResource[]; -export interface ShareResource extends ShareResourceEmbedded {} + public project:ProjectResource; + + public entity:HalResource; +} diff --git a/frontend/src/app/features/hal/resources/status-resource.ts b/frontend/src/app/features/hal/resources/status-resource.ts index 2c7152f81986..691ea8b81225 100644 --- a/frontend/src/app/features/hal/resources/status-resource.ts +++ b/frontend/src/app/features/hal/resources/status-resource.ts @@ -39,6 +39,6 @@ export class StatusResource extends HalResource { excludedFromTotals:boolean; public get state():InputState { - return this.states.statuses.get(this.href!) as any; + return this.states.statuses.get(this.href!) as unknown as InputState; } } diff --git a/frontend/src/app/features/hal/resources/time-entry-resource.ts b/frontend/src/app/features/hal/resources/time-entry-resource.ts index 9492744f1142..4c8bf804dbd1 100644 --- a/frontend/src/app/features/hal/resources/time-entry-resource.ts +++ b/frontend/src/app/features/hal/resources/time-entry-resource.ts @@ -27,6 +27,7 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { ProjectResource } from 'core-app/features/hal/resources/project-resource'; import { InputState } from '@openproject/reactivestates'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; @@ -48,6 +49,10 @@ export class TimeEntryResource extends HalResource { ongoing:boolean; + hours:string; + + delete:CallableHalLink; + public get state():InputState { return this.states.timeEntries.get(this.id!) as unknown as InputState; } @@ -60,10 +65,6 @@ export class TimeEntryResource extends HalResource { } } -export interface TimeEntryResource { - delete():Promise; -} - export function formatTimeEntryEntityName(entity:WorkPackageResource|MeetingResource):string { const displayId = entity.$link?.displayId; const formattedId = displayId ? formatWorkPackageId(displayId) : `#${idFromLink(entity.href)}`; diff --git a/frontend/src/app/features/hal/resources/type-resource.ts b/frontend/src/app/features/hal/resources/type-resource.ts index fddd3cdcbe18..4a7c728ef070 100644 --- a/frontend/src/app/features/hal/resources/type-resource.ts +++ b/frontend/src/app/features/hal/resources/type-resource.ts @@ -33,6 +33,6 @@ export class TypeResource extends HalResource { public color:string; public get state():InputState { - return this.states.types.get(this.href!) as any; + return this.states.types.get(this.href!) as unknown as InputState; } } diff --git a/frontend/src/app/features/hal/resources/user-resource.ts b/frontend/src/app/features/hal/resources/user-resource.ts index 5167a7e2abc7..8d7032a6a1e2 100644 --- a/frontend/src/app/features/hal/resources/user-resource.ts +++ b/frontend/src/app/features/hal/resources/user-resource.ts @@ -57,7 +57,7 @@ export class UserResource extends HalResource { } public get state():InputState { - return this.states.users.get(this.href!) as any; + return this.states.users.get(this.href!) as unknown as InputState; } public get showUserPath() { diff --git a/frontend/src/app/features/hal/resources/wiki-page-resource.ts b/frontend/src/app/features/hal/resources/wiki-page-resource.ts index f112f1e1525e..0578b7623d06 100644 --- a/frontend/src/app/features/hal/resources/wiki-page-resource.ts +++ b/frontend/src/app/features/hal/resources/wiki-page-resource.ts @@ -26,15 +26,16 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; export interface WikiPageResourceLinks { - addAttachment(attachment:HalResource):Promise; + addAttachment:CallableHalLink; } class WikiPageBaseResource extends HalResource { - public $links:WikiPageResourceLinks; + public $links:WikiPageResourceLinks & HalResourceLinks; } export const WikiPageResource = Attachable(WikiPageBaseResource); diff --git a/frontend/src/app/features/hal/resources/work-package-resource.spec.ts b/frontend/src/app/features/hal/resources/work-package-resource.spec.ts index c6a12c55e5e4..ac348cb3de2c 100644 --- a/frontend/src/app/features/hal/resources/work-package-resource.spec.ts +++ b/frontend/src/app/features/hal/resources/work-package-resource.spec.ts @@ -44,6 +44,7 @@ import { TimezoneService } from 'core-app/core/datetime/timezone.service'; import { AttachmentCollectionResource } from 'core-app/features/hal/resources/attachment-collection-resource'; import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { WeekdayService } from 'core-app/core/days/weekday.service'; import { of } from 'rxjs'; @@ -264,7 +265,7 @@ describe('WorkPackage', () => { }); it('when the work package has an `addAttachment` link', () => { - workPackage.$links.addAttachment = () => Promise.resolve(); + workPackage.$links.addAttachment = (() => Promise.resolve()) as unknown as CallableHalLink; expect(workPackage.canAddAttachments).toEqual(true); }); diff --git a/frontend/src/app/features/hal/resources/work-package-resource.ts b/frontend/src/app/features/hal/resources/work-package-resource.ts index 700c16f0b9d6..43a07f181eb1 100644 --- a/frontend/src/app/features/hal/resources/work-package-resource.ts +++ b/frontend/src/app/features/hal/resources/work-package-resource.ts @@ -41,7 +41,8 @@ import { import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { AttachmentCollectionResource } from 'core-app/features/hal/resources/attachment-collection-resource'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalResource, HalResourceEmbedded, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { TypeResource } from 'core-app/features/hal/resources/type-resource'; import { RelationResource } from 'core-app/features/hal/resources/relation-resource'; @@ -55,23 +56,23 @@ import { formatWorkPackageId } from 'core-app/shared/helpers/work-package-id-pat export interface WorkPackageResourceEmbedded { activities:CollectionResource; - assignee:HalResource|any; + assignee:HalResource|null; attachments:AttachmentCollectionResource; fileLinks?:CollectionResource; - author:HalResource|any; - availableWatchers:HalResource|any; - category:HalResource|any; + author:HalResource; + availableWatchers:HalResource; + category:HalResource|null; children:WorkPackageResource[]; parent:WorkPackageResource|null; - priority:HalResource|any; - project:HalResource|any; + priority:HalResource; + project:HalResource; relations:CollectionResource; - responsible:HalResource|any; - revisions:CollectionResource|any; - status:StatusResource|any; - timeEntries:HalResource[]|any[]; + responsible:HalResource|null; + revisions:CollectionResource; + status:StatusResource; + timeEntries:HalResource[]; type:TypeResource; - version:HalResource|any; + version:HalResource|null; watchers:CollectionResource; // For regular work packages startDate:string; @@ -82,46 +83,32 @@ export interface WorkPackageResourceEmbedded { scheduleManually:boolean; } -export interface WorkPackageResourceLinks extends WorkPackageResourceEmbedded { - addAttachment(attachment:HalResource):Promise; - - addChild(child:HalResource):Promise; - - addComment(comment:unknown, headers?:any):Promise; - - addRelation(relation:any):Promise|undefined; - - addWatcher(watcher:HalResource):Promise; - - changeParent(params:any):Promise; - - copy():Promise; - - delete():Promise; - - logTime():Promise; - - startTimer():Promise; - - move():Promise; - - removeWatcher():Promise; - - self():Promise; - - update(payload:any):Promise>; - - updateImmediately(payload:any):Promise; - - watch():Promise; +export interface WorkPackageResourceLinks { + addAttachment:CallableHalLink; + addChild:CallableHalLink; + addComment:CallableHalLink; + addRelation:CallableHalLink; + addWatcher:CallableHalLink; + changeParent:CallableHalLink; + copy:CallableHalLink; + delete:CallableHalLink; + logTime:CallableHalLink; + startTimer:CallableHalLink; + move:CallableHalLink; + removeWatcher:CallableHalLink; + update:CallableHalLink>; + updateImmediately:CallableHalLink; + watch:CallableHalLink; + unwatch:CallableHalLink; } -export interface WorkPackageLinksObject extends WorkPackageResourceLinks { - schema:HalResource; +export interface WorkPackageLinksObject extends HalResourceLinks, WorkPackageResourceLinks { + self:CallableHalLink; + schema:CallableHalLink; } export class WorkPackageBaseResource extends HalResource { - public $embedded:WorkPackageResourceEmbedded; + public $embedded:WorkPackageResourceEmbedded & HalResourceEmbedded; public $links:WorkPackageLinksObject; @@ -146,8 +133,8 @@ export class WorkPackageBaseResource extends HalResource { * bare hrefs). */ public get displayId():string { - return this.$source.displayId?.toString() - ?? this.$source._links?.self?.displayId?.toString() + return (this.$source.displayId as string|number|undefined)?.toString() + ?? (this.$source._links?.self as HalLinkInterface|undefined)?.displayId?.toString() ?? this.id?.toString() ?? ''; } @@ -167,7 +154,11 @@ export class WorkPackageBaseResource extends HalResource { public hasProjectAttributes:boolean; - public description:any; + public description:api.v3.Formattable; + + public configureForm?:HalResource; + + public bcfViewpoints?:HalResource[]; public activities:CollectionResource; @@ -211,8 +202,7 @@ export class WorkPackageBaseResource extends HalResource { * Return ": ()" if type and id are known. */ public subjectWithType(truncateSubject = 40):string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return `${this.type.name}: ${this.subjectWithId(truncateSubject)}`; + return `${(this.type as TypeResource).name}: ${this.subjectWithId(truncateSubject)}`; } /** @@ -257,33 +247,10 @@ export class WorkPackageBaseResource extends HalResource { return otherWorkPackage.parent?.$links.self.$link.href === this.$links.self.$link.href; } - /** - * Invalidate a set of linked resources of this work package. - * And inform the cache service about the work package update. - * - * Return a promise that returns the linked resources as properties. - * Return a rejected promise, if the resource is not a property of the work package. - */ - public updateLinkedResources(...resourceNames:string[]):Promise { - const resources:Record> = {}; - - resourceNames.forEach((name) => { - const linked = this[name]; - resources[name] = linked ? linked.$update() : Promise.reject(undefined); - }); - - const promise = Promise.all(Object.values(resources)); - promise.then(() => { - this.wpCacheService.touch(this.id!); - }); - - return promise; - } - - public $initialize(source:any) { + public $initialize(source:unknown) { super.$initialize(source); - const attachments:any = this.attachments || { $source: {}, elements: [] }; + const attachments:unknown = this.attachments || { $source: {}, elements: [] }; this.attachments = new AttachmentCollectionResource( this.injector, // Attachments MAY be an array if we're building from a form @@ -305,7 +272,7 @@ export class WorkPackageBaseResource extends HalResource { * Return the associated state to this HAL resource, if any. */ public get state():InputState { - return this.states.workPackages.get(this.id!) as any; + return this.states.workPackages.get(this.id!) as unknown as InputState; } /** @@ -316,10 +283,10 @@ export class WorkPackageBaseResource extends HalResource { // If there is a parent, its view has to be updated as well if (newValue.parent) { - this.apiV3Service.work_packages.id(newValue.parent).refresh(); + void this.apiV3Service.work_packages.id(newValue.parent as WorkPackageResource).refresh(); } - return this.apiV3Service.work_packages.cache.updateWorkPackage(newValue as any); + return this.apiV3Service.work_packages.cache.updateWorkPackage(newValue as unknown as WorkPackageResource); } } diff --git a/frontend/src/app/features/hal/resources/work-package-timestamp-resource.ts b/frontend/src/app/features/hal/resources/work-package-timestamp-resource.ts index 309c2c557786..63fdaf8de865 100644 --- a/frontend/src/app/features/hal/resources/work-package-timestamp-resource.ts +++ b/frontend/src/app/features/hal/resources/work-package-timestamp-resource.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; export interface IWorkPackageTimestampMeta { exists:boolean; @@ -43,15 +43,5 @@ export class IWorkPackageTimestamp extends HalResource { _meta:IWorkPackageTimestampMeta; - $links:{ - schema?:HalResource; - self:HalResource; - status?:HalResource; - assignee?:HalResource; - accountable?:HalResource; - project?:HalResource; - type?:HalResource; - priority?:HalResource; - version?:HalResource; - }; + $links:HalResourceLinks; } diff --git a/frontend/src/app/features/hal/resources/wp-collection-resource.ts b/frontend/src/app/features/hal/resources/wp-collection-resource.ts index a6df82c9a15b..d1d31884daab 100644 --- a/frontend/src/app/features/hal/resources/wp-collection-resource.ts +++ b/frontend/src/app/features/hal/resources/wp-collection-resource.ts @@ -30,16 +30,20 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { FormResource } from 'core-app/features/hal/resources/form-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export interface WorkPackageCollectionResourceEmbedded { elements:WorkPackageResource[]; groups:GroupObject[]; } -export class WorkPackageCollectionResource extends CollectionResource { +export class WorkPackageCollectionResource extends CollectionResource implements WorkPackageCollectionResourceEmbedded { public schemas:CollectionResource; - public createWorkPackage:any; + public createWorkPackage?:CallableHalLink; + + public customFields?:HalResource; public elements:WorkPackageResource[]; @@ -52,14 +56,12 @@ export class WorkPackageCollectionResource extends CollectionResource ({ href: element.href })); } else { payload._links[key] = { - href: (resource[key]?.href), + href: (resource[key] as HalResource|undefined)?.href, }; } } else { @@ -83,7 +84,7 @@ export class HalPayloadHelper { if (Array.isArray(resource[property])) { payload[property] = (resource[property] as HalResource[]).map((element) => { if (element instanceof HalResource) { - return this.extractPayloadFromSchema(element, element.currentSchema || element.schema); + return this.extractPayloadFromSchema(element, (element.currentSchema ?? element.schema) as SchemaResource); } return element; }); diff --git a/frontend/src/app/features/hal/schemas/schema-proxy.ts b/frontend/src/app/features/hal/schemas/schema-proxy.ts index 03fbce11685f..08d1ea9cfda4 100644 --- a/frontend/src/app/features/hal/schemas/schema-proxy.ts +++ b/frontend/src/app/features/hal/schemas/schema-proxy.ts @@ -50,16 +50,12 @@ export class SchemaProxy implements ProxyHandler { ) as ISchemaProxy; } - get(schema:SchemaResource, property:PropertyKey, receiver:any):any { + get(schema:SchemaResource, property:PropertyKey, receiver:unknown):unknown { switch (property) { - case 'ofProperty': { - return this.proxyMethod(this.ofProperty); - } - case 'isAttributeEditable': { - return this.proxyMethod(this.isAttributeEditable); - } + case 'ofProperty': + case 'isAttributeEditable': case 'mappedName': { - return this.proxyMethod(this.mappedName); + return this.proxyMethod(property); } case 'isEditable': { return this.isEditable; @@ -81,7 +77,7 @@ export class SchemaProxy implements ProxyHandler { * @param property the schema part is desired for */ public ofProperty(property:string):IFieldSchema|null { - const propertySchema = this.schema[this.mappedName(property)]; + const propertySchema = this.schema[this.mappedName(property)] as IFieldSchema|undefined; if (propertySchema) { return { ...propertySchema, writable: this.isEditable && propertySchema?.writable }; @@ -117,15 +113,7 @@ export class SchemaProxy implements ProxyHandler { return property; } - private proxyMethod(method:Function) { - const self = this; - - // Returning a Proxy here so that the call is bound - // to the SchemaProxy instance. - return new Proxy(method, { - apply(_, __, argumentsList) { - return method.apply(self, [argumentsList[0]]); - }, - }); + private proxyMethod(name:'ofProperty'|'isAttributeEditable'|'mappedName') { + return (property:string) => this[name](property); } } diff --git a/frontend/src/app/features/hal/schemas/work-package-schema-proxy.ts b/frontend/src/app/features/hal/schemas/work-package-schema-proxy.ts index 34f5cfd3ccd9..69a7fecde0b9 100644 --- a/frontend/src/app/features/hal/schemas/work-package-schema-proxy.ts +++ b/frontend/src/app/features/hal/schemas/work-package-schema-proxy.ts @@ -31,7 +31,7 @@ import { SchemaResource } from 'core-app/features/hal/resources/schema-resource' import { StatusResource } from 'core-app/features/hal/resources/status-resource'; export class WorkPackageSchemaProxy extends SchemaProxy { - get(schema:SchemaResource, property:PropertyKey, receiver:any):any { + get(schema:SchemaResource, property:PropertyKey, receiver:unknown):unknown { switch (property) { case 'isMilestone': { return this.isMilestone; diff --git a/frontend/src/app/features/hal/services/hal-resource-notification.service.ts b/frontend/src/app/features/hal/services/hal-resource-notification.service.ts index 2c82451913ba..130c22600725 100644 --- a/frontend/src/app/features/hal/services/hal-resource-notification.service.ts +++ b/frontend/src/app/features/hal/services/hal-resource-notification.service.ts @@ -29,7 +29,7 @@ import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; import { Injectable, Injector, inject } from '@angular/core'; import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service'; -import { ToastService } from 'core-app/shared/components/toaster/toast.service'; +import { IToast, ToastService } from 'core-app/shared/components/toaster/toast.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { HttpErrorResponse } from '@angular/common/http'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; @@ -52,9 +52,9 @@ export class HalResourceNotificationService { protected readonly schemaCache = inject(SchemaCacheService); public showSave(resource:HalResource, isCreate = false) { - const message:any = { + const message = { message: this.I18n.t(`js.notice_successful_${isCreate ? 'create' : 'update'}`), - }; + } as IToast; this.ToastService.addSuccess(message); } @@ -115,10 +115,10 @@ export class HalResourceNotificationService { return this.I18n.t('js.error.internal'); } - public retrieveError(response:unknown):ErrorResource|unknown { + public retrieveError(response:unknown):unknown { // we try to detect what we got, this may either be an HttpErrorResponse, // some older XHR response object or a string - let errorBody:any = response; + let errorBody:unknown = response; // Angular http response have an error body attribute if (response instanceof HttpErrorResponse) { @@ -127,19 +127,19 @@ export class HalResourceNotificationService { // Some older response may have a data attribute if ((response as { data?:{ _type?:string } } | null)?.data?._type === 'Error') { - errorBody = (response as any).data; + errorBody = (response as { data:unknown }).data; } - if (errorBody?._type === 'Error') { + if ((errorBody as { _type?:string }|null|undefined)?._type === 'Error') { return this.halResourceService.createHalResourceOfClass(ErrorResource, errorBody); } return errorBody; } - protected handleErrorResponse(errorResource:any, resource?:HalResource) { + protected handleErrorResponse(errorResource:unknown, resource?:HalResource) { if (errorResource instanceof HalError && resource) { - return this.showError(errorResource.resource, resource); + return this.showError(errorResource.resource!, resource); } if (!(errorResource instanceof ErrorResource)) { @@ -153,7 +153,7 @@ export class HalResourceNotificationService { return this.showApiErrorMessages(errorResource); } - public showError(errorResource:any, resource:HalResource) { + public showError(errorResource:ErrorResource, resource:HalResource) { this.showCustomError(errorResource, resource) || this.showApiErrorMessages(errorResource); } @@ -161,7 +161,7 @@ export class HalResourceNotificationService { let error = this.I18n.t('js.error.internal'); if (typeof (message) === 'string' || (message != null && Object.hasOwn(message, 'toString'))) { - error += ` ${(message as any).toString()}`; + error += ` ${(message as { toString():string }).toString()}`; } this.ToastService.addError(error); @@ -174,7 +174,7 @@ export class HalResourceNotificationService { )); } - protected showCustomError(errorResource:any, resource:HalResource) { + protected showCustomError(errorResource:ErrorResource, resource:HalResource) { if (errorResource.errorIdentifier === 'urn:openproject-org:api:v3:errors:PropertyFormatError') { const schema = this.schemaCache.of(resource).ofProperty(errorResource.details.attribute); const attributeName = schema.name; @@ -193,7 +193,7 @@ export class HalResourceNotificationService { return false; } - protected showApiErrorMessages(errorResource:any) { + protected showApiErrorMessages(errorResource:ErrorResource) { const messages = errorResource.errorMessages; if (messages.length > 1) { diff --git a/frontend/src/app/features/hal/services/hal-resource.service.ts b/frontend/src/app/features/hal/services/hal-resource.service.ts index abc6f772f698..c427d27b4ee4 100644 --- a/frontend/src/app/features/hal/services/hal-resource.service.ts +++ b/frontend/src/app/features/hal/services/hal-resource.service.ts @@ -49,7 +49,7 @@ import { HalError } from 'core-app/features/hal/services/hal-error'; import { getPaginatedCollections } from 'core-app/core/apiv3/helpers/get-paginated-results'; export interface HalResourceFactoryConfigInterface { - cls?:any; + cls?:HalResourceClass; attrTypes?:Record; } @@ -57,6 +57,10 @@ interface ErrorWithType { _type?:string; } +interface TypedHalSource { + _type:string; +} + @Injectable({ providedIn: 'root' }) export class HalResourceService { readonly injector = inject(Injector); @@ -147,7 +151,7 @@ export class HalResourceService { * @param headers * @returns {Promise} */ - public put(href:string, data?:any, headers?:HTTPClientHeaders):Observable { + public put(href:string, data?:unknown, headers?:HTTPClientHeaders):Observable { return this.request('put', href, data, headers); } @@ -159,7 +163,7 @@ export class HalResourceService { * @param headers * @returns {Promise} */ - public post(href:string, data?:any, headers?:HTTPClientHeaders):Observable { + public post(href:string, data?:unknown, headers?:HTTPClientHeaders):Observable { return this.request('post', href, data, headers); } @@ -171,7 +175,7 @@ export class HalResourceService { * @param headers * @returns {Promise} */ - public patch(href:string, data?:any, headers?:HTTPClientHeaders):Observable { + public patch(href:string, data?:unknown, headers?:HTTPClientHeaders):Observable { return this.request('patch', href, data, headers); } @@ -183,7 +187,7 @@ export class HalResourceService { * @param headers * @returns {Promise} */ - public delete(href:string, data?:any, headers?:HTTPClientHeaders):Observable { + public delete(href:string, data?:unknown, headers?:HTTPClientHeaders):Observable { return this.request('delete', href, data, headers); } @@ -215,16 +219,16 @@ export class HalResourceService { * @param source * @returns {HalResource} */ - public createHalResource(source:any, loaded = true):T { - source ??= HalResource.getEmptyResource(); + public createHalResource(source:unknown, loaded = true):T { + const halSource = (source ?? HalResource.getEmptyResource()) as TypedHalSource; - const type = source._type || 'HalResource'; - return this.createHalResourceOfType(type, source, loaded); + const type = halSource._type || 'HalResource'; + return this.createHalResourceOfType(type, halSource, loaded); } - public createHalResourceOfType(type:string, source:any, loaded = false) { + public createHalResourceOfType(type:string, source:unknown, loaded = false) { const resourceClass:HalResourceClass = this.getResourceClassOfType(type); - const initializer = (halResource:T) => initializeHalProperties(this, halResource); + const initializer = (halResource:HalResource) => initializeHalProperties(this, halResource); const resource = new resourceClass(this.injector, source, loaded, initializer, type); return resource; @@ -236,9 +240,9 @@ export class HalResourceService { * @param source * @param loaded */ - public createHalResourceOfClass(resourceClass:HalResourceClass, source:any, loaded = false) { - const initializer = (halResource:T) => initializeHalProperties(this, halResource); - const type = source._type || 'HalResource'; + public createHalResourceOfClass(resourceClass:HalResourceClass, source:unknown, loaded = false) { + const initializer = (halResource:HalResource) => initializeHalProperties(this, halResource); + const type = (source as TypedHalSource)._type || 'HalResource'; const resource = new resourceClass(this.injector, source, loaded, initializer, type); return resource; @@ -285,8 +289,11 @@ export class HalResourceService { * @returns {HalResource} */ protected getResourceClassOfType(type:string):HalResourceClass { - const config = this.config[type]; - return (config?.cls) ? config.cls : this.defaultClass as HalResourceClass; + const cls = this.config[type]?.cls; + if (cls) { + return cls as HalResourceClass; + } + return this.defaultClass as HalResourceClass; } /** @@ -313,7 +320,7 @@ export class HalResourceService { private createErrorObservable(error:HttpErrorResponse):Observable { let resource:ErrorResource|null = null; - const body = error.error as string|ErrorWithType|unknown; + const body:unknown = error.error; if (typeof body === 'object' && (body as ErrorWithType)?._type) { resource = this.createHalResource(error.error); } diff --git a/frontend/src/app/features/plugins/hook-service.spec.ts b/frontend/src/app/features/plugins/hook-service.spec.ts index 59d90d27796c..1be9dda6c7ef 100644 --- a/frontend/src/app/features/plugins/hook-service.spec.ts +++ b/frontend/src/app/features/plugins/hook-service.spec.ts @@ -156,4 +156,16 @@ describe('HookService', () => { shouldBehaveLikeResultWithElements(validId, 2); }); }); + + describe('known hooks', () => { + it('rejects callbacks and arguments that break the hook signature', () => { + // @ts-expect-error gridWidgets callbacks return widget registrations + service.register('gridWidgets', () => 123); + + // @ts-expect-error prependedAttributeGroups is called with a work package + service.call('prependedAttributeGroups', 'not a work package'); + + expect(service.call('gridWidgets')).toEqual([123]); + }); + }); }); diff --git a/frontend/src/app/features/plugins/hook-service.ts b/frontend/src/app/features/plugins/hook-service.ts index 7c5971862439..5c28c263b7e6 100644 --- a/frontend/src/app/features/plugins/hook-service.ts +++ b/frontend/src/app/features/plugins/hook-service.ts @@ -26,15 +26,43 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { Injectable } from '@angular/core'; +import { Injectable, Type } from '@angular/core'; +import type { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import type { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; +import type { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset'; +import type { GroupDescriptor } from 'core-app/features/work-packages/components/wp-single-view/wp-single-view.component'; +import type { WorkPackageAction } from 'core-app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service'; +import type { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset'; +import type { WidgetRegistration } from 'core-app/shared/components/grids/grid/grid.component'; + +type ResourceChangesetClass = new (...params:ConstructorParameters) => ResourceChangeset; + +export interface HookSignatures { + attributeGroupComponent:(group:GroupDescriptor, workPackage:WorkPackageResource) => Type|null; + gridWidgets:() => WidgetRegistration[]; + halResourceChangesetClass:(resource:HalResource) => ResourceChangesetClass|null; + prependedAttributeGroups:(workPackage:WorkPackageResource) => Type|undefined; + workPackageAttachmentListComponent:(workPackage:WorkPackageResource) => Type; + workPackageAttachmentUploadComponent:(workPackage:WorkPackageResource) => Type; + workPackageBulkContextMenu:() => WorkPackageAction; + workPackageNewInitialization:(change:WorkPackageChangeset) => void; + workPackageSingleContextMenu:() => WorkPackageAction; + workPackageTableContextMenu:() => WorkPackageAction; +} + +type HookCallback = (...params:never[]) => unknown; + +type CustomHookId = K extends keyof HookSignatures ? never : K; @Injectable({ providedIn: 'root', }) export class HookService { - private hooks:Record = {}; + private hooks:Record = {}; - public register(id:string, callback:Function) { + public register(id:K, callback:HookSignatures[K]):void; + public register(id:CustomHookId, callback:HookCallback):void; + public register(id:string, callback:HookCallback) { if (!callback) { return; } @@ -46,12 +74,18 @@ export class HookService { this.hooks[id].push(callback); } - public call(id:string, ...params:any[]):any[] { + public call( + id:K, + ...params:Parameters + ):NonNullable>[]; + + public call(id:CustomHookId, ...params:unknown[]):unknown[]; + public call(id:string, ...params:unknown[]):unknown[] { const results = []; if (this.hooks[id]) { - for (let x = 0; x < this.hooks[id].length; x++) { - const result = this.hooks[id][x](...params); + for (const hook of this.hooks[id] as ((...params:unknown[]) => unknown)[]) { + const result = hook(...params); if (result) { results.push(result); diff --git a/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts b/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts index 89be9c7c5a42..8e817cd19498 100644 --- a/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts +++ b/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts @@ -144,7 +144,7 @@ export class CalendarDragDropService { const startDate = moment(workPackage.startDate); const dueDate = moment(workPackage.dueDate); - const duration = Number(moment.duration(workPackage.duration).asDays().toFixed(0)); + const duration = Number(moment.duration(workPackage.duration as string).asDays().toFixed(0)); const diff = duration > 0 ? duration : dueDate.diff(startDate, 'days') + 1; return { diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index b475490883b3..abe0ae755224 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -122,6 +122,7 @@ import { personIconData, toDOMString, } from '@openproject/octicons-angular'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; export type TeamPlannerViewOptionKey = 'resourceTimelineWorkWeek'|'resourceTimelineWeek'|'resourceTimelineTwoWeeks'|'resourceTimelineFourWeeks'|'resourceTimelineEightWeeks'; export type TeamPlannerViewOptions = Record>>; @@ -740,7 +741,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, } isStatusClosed(workPackage:WorkPackageResource):boolean { - const status = this.statuses.find((el) => el.id === (workPackage.status as StatusResource).id); + const status = this.statuses.find((el) => el.id === workPackage.status.id); return status ? status.isClosed : false; } @@ -866,7 +867,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, await this.halEditing.save(changeset); } catch (e:unknown) { this.loading$?.complete(); - this.halNotification.showError((e as HalError).resource, changeset.projectedResource); + this.halNotification.showError((e as HalError).resource!, changeset.projectedResource); this.calendarDrag.handleDropError(changeset.projectedResource); info?.revert(); } @@ -874,8 +875,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, private eventResourceEditable(wp:WorkPackageResource):boolean { const schema = this.schemaCache.of(wp); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return !!schema.assignee?.writable && schema.isAttributeEditable('assignee'); + return !!(schema.assignee as IOPFieldSchema|undefined)?.writable && schema.isAttributeEditable('assignee'); } // Todo: Evaluate whether we really want to use that from a UI perspective ¯\_(ツ)_/¯ @@ -899,7 +899,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, return constraints; } - const assignables = projectAssignables[(wp.project as HalResource).href!]; + const assignables = projectAssignables[wp.project.href!]; if (assignables) { constraints.resourceIds = [...assignables]; } @@ -917,7 +917,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, } private wpAssignee(wp:WorkPackageResource):string { - return (wp.assignee as HalResource).href!; + return wp.assignee!.href!; } private toggleAddExistingPane():void { @@ -945,7 +945,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, .pipe( filter((el) => Object.keys(el).length > 0), take(1), - map((projectAssignables) => projectAssignables[(wp.project as HalResource).href!]), + map((projectAssignables) => projectAssignables[wp.project.href!]), withLatestFrom(this.principals$), ) .subscribe(([assignable, principals]) => { diff --git a/frontend/src/app/features/work-packages/components/filters/abstract-filter-date-time-value/abstract-filter-date-time-value.controller.ts b/frontend/src/app/features/work-packages/components/filters/abstract-filter-date-time-value/abstract-filter-date-time-value.controller.ts index ae7f5269166f..73385a4ed674 100644 --- a/frontend/src/app/features/work-packages/components/filters/abstract-filter-date-time-value/abstract-filter-date-time-value.controller.ts +++ b/frontend/src/app/features/work-packages/components/filters/abstract-filter-date-time-value/abstract-filter-date-time-value.controller.ts @@ -48,7 +48,7 @@ export abstract class AbstractDateTimeValueController extends UntilDestroyedMixi public abstract get upperBoundary():Moment|null; - public isoDateParser(data:any) { + public isoDateParser(data:string) { if (!this.timezoneService.isValidISODate(data)) { return ''; } @@ -56,7 +56,7 @@ export abstract class AbstractDateTimeValueController extends UntilDestroyedMixi return this.timezoneService.formattedISODateTime(d); } - public isoDateFormatter(data:any) { + public isoDateFormatter(data:string) { if (!this.timezoneService.isValidISODateTime(data)) { return ''; } diff --git a/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts index 4cf02fa6a918..f90237b2b607 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts @@ -27,7 +27,6 @@ //++ import { ChangeDetectionStrategy, Component, Input, HostBinding, OnInit, Output } from '@angular/core'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter'; import { Moment } from 'moment'; import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; @@ -56,12 +55,12 @@ export class FilterDateTimeValueComponent extends AbstractDateTimeValueControlle @Output() public filterChanged = new DebouncedEventEmitter(componentDestroyed(this)); - public get value():HalResource|string { - return this.filter.values[0]; + public get value():string { + return this.filter.values[0] as string; } public set value(val) { - this.filter.values = [val as string]; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + this.filter.values = [val]; this.filterChanged.emit(this.filter); } diff --git a/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts index f1c3b24ae45c..63d4f2404515 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts @@ -34,6 +34,7 @@ import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource'; +import { SchemaAttributeObject } from 'core-app/features/hal/resources/schema-attribute-object'; @Component({ selector: 'op-filter-integer-value', @@ -69,7 +70,7 @@ export class FilterIntegerValueComponent extends UntilDestroyedMixin { } public get unit() { - switch ((this.schema.filter.allowedValues as QueryFilterResource[])[0].id) { + switch (((this.schema.filter as SchemaAttributeObject).allowedValues as QueryFilterResource[])[0].id) { case 'startDate': case 'dueDate': case 'updatedAt': diff --git a/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.ts b/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.ts index 2563c1530892..d53f8cb42809 100644 --- a/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.ts @@ -36,6 +36,7 @@ import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource'; +import { QueryOperatorResource } from 'core-app/features/hal/resources/query-operator-resource'; import { WorkPackageViewBaselineService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-baseline.service'; @Component({ @@ -67,7 +68,7 @@ export class QueryFilterComponent implements OnInit { @Output() public deactivateFilter = new EventEmitter(); - public availableOperators:any; + public availableOperators:QueryOperatorResource[]; public showValuesInput = false; @@ -106,7 +107,7 @@ export class QueryFilterComponent implements OnInit { } ngOnInit() { - this.availableOperators = this.schemaCache.of(this.filter).availableOperators; + this.availableOperators = this.schemaCache.of(this.filter).availableOperators as QueryOperatorResource[]; this.showValuesInput = this.showValues(); this.baselineIncompatibleFilter = this.wpTableBaseline.isActive() && this.wpTableBaseline.isIncompatibleFilter(this.filter.id); } diff --git a/frontend/src/app/features/work-packages/components/filters/query-filters/query-filters.component.ts b/frontend/src/app/features/work-packages/components/filters/query-filters/query-filters.component.ts index c9925e076c6d..378f7ea9b7da 100644 --- a/frontend/src/app/features/work-packages/components/filters/query-filters/query-filters.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/query-filters/query-filters.component.ts @@ -168,8 +168,7 @@ export class QueryFiltersComponent extends UntilDestroyedMixin implements OnInit public isFilterAvailable(filter:QueryFilterResource):boolean { return (this.wpTableFilters.availableFilters.some((availableFilter) => availableFilter.id === filter.id) - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - && !(this.wpTableFilters.hidden.includes(filter.id) || filter.isTemplated())); + && !(this.wpTableFilters.hidden.includes(filter.id) || (filter as QueryFilterResource & { isTemplated():boolean }).isTemplated())); } public onOpen() { diff --git a/frontend/src/app/features/work-packages/components/filters/wp-filters/wp-filters.service.ts b/frontend/src/app/features/work-packages/components/filters/wp-filters/wp-filters.service.ts index 956f47e6cf5c..49fe198b4854 100644 --- a/frontend/src/app/features/work-packages/components/filters/wp-filters/wp-filters.service.ts +++ b/frontend/src/app/features/work-packages/components/filters/wp-filters/wp-filters.service.ts @@ -43,7 +43,7 @@ export class WorkPackageFiltersService { this.state.putValue(val); } - public observeUntil(unsubscribe:Observable) { + public observeUntil(unsubscribe:Observable) { return this.state.values$().pipe(takeUntil(unsubscribe)); } diff --git a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts index ddc040c1a534..ca953b2e49ab 100644 --- a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts @@ -93,8 +93,8 @@ export class WorkPackageBreadcrumbParentComponent { } this.isSaving = true; - this.wpRelationsHierarchy.changeParent(this.workPackage, newParentId) - .catch((error:any) => { + void this.wpRelationsHierarchy.changeParent(this.workPackage, newParentId) + .catch((error:unknown) => { this.notificationService.handleRawError(error, this.workPackage); }) .then(() => this.isSaving = false); // Behaves as .finally() @@ -108,6 +108,6 @@ export class WorkPackageBreadcrumbParentComponent { } public parentLink(parent:WorkPackageResource):string { - return this.pathHelper.genericWorkPackagePath(parent.project?.identifier, parent.displayId) + window.location.search; + return this.pathHelper.genericWorkPackagePath(parent.project?.identifier as string|null, parent.displayId) + window.location.search; } } diff --git a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb.component.ts b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb.component.ts index e5d41ed2cc9c..40f04ae46c2d 100644 --- a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb.component.ts @@ -64,7 +64,7 @@ export class WorkPackageBreadcrumbComponent { } public ancestorPath(ancestor:WorkPackageResource):string { - return this.pathHelper.genericWorkPackagePath(this.workPackage.project?.identifier, ancestor.displayId) + window.location.search; + return this.pathHelper.genericWorkPackagePath(this.workPackage.project?.identifier as string|null, ancestor.displayId) + window.location.search; } public updateActiveInput(val:boolean) { diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts index 125d642b2534..1e459846be76 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts @@ -59,8 +59,6 @@ export class WorkPackageCreateButtonComponent extends UntilDestroyedMixin implem projectIdentifier:string|null; - types:any; - text = { title: this.I18n.t('js.work_packages.create.title'), createWithDropdown: this.I18n.t('js.work_packages.create.button'), diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-status-button/wp-status-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-status-button/wp-status-button.component.ts index 0d81da63513f..9ec4718a04d8 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-status-button/wp-status-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-status-button/wp-status-button.component.ts @@ -73,7 +73,7 @@ export class WorkPackageStatusButtonComponent extends UntilDestroyedMixin implem this.workPackage = wp; if (this.workPackage.status) { - this.workPackage.status.$load(); + void this.workPackage.status.$load(); } this.cdRef.detectChanges(); diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.ts b/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.ts index 4fbcbdc7b6f4..01a5e874c7e4 100644 --- a/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.ts @@ -62,7 +62,6 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag import { isClickedWithModifier } from 'core-app/shared/helpers/link-handling/link-handling'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { TimezoneService } from 'core-app/core/datetime/timezone.service'; -import { StatusResource } from 'core-app/features/hal/resources/status-resource'; import { fromEvent, merge } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; @@ -76,6 +75,7 @@ import { } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; import { matchesRoutingId } from 'core-app/features/work-packages/helpers/work-package-id-resolvers'; import { UrlParamsService } from 'core-app/core/navigation/url-params.service'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -223,7 +223,7 @@ export class WorkPackageSingleCardComponent extends UntilDestroyedMixin implemen } cardTitle():string { - return `${this.workPackage.subject} (${(this.workPackage.status as StatusResource).name})`; + return `${this.workPackage.subject} (${this.workPackage.status.name})`; } public baselineIcon(workPackage:WorkPackageResource) { @@ -269,7 +269,7 @@ export class WorkPackageSingleCardComponent extends UntilDestroyedMixin implemen private cardHighlighting(wp:WorkPackageResource):string { if (['status', 'priority', 'type'].includes(this.highlightingMode)) { - return Highlighting.backgroundClass(this.highlightingMode, wp[this.highlightingMode].id); + return Highlighting.backgroundClass(this.highlightingMode, (wp[this.highlightingMode] as HalResource).id!); } return ''; } diff --git a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-full-view.component.ts b/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-full-view.component.ts index de58d6996110..73fd7435f6e2 100644 --- a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-full-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-full-view.component.ts @@ -28,6 +28,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { WorkPackageCopyController } from 'core-app/features/work-packages/components/wp-copy/wp-copy.controller'; +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 { WorkPackageCopyController } from 'core-app/features/work-packages/compo }) export class WorkPackageCopyFullViewComponent extends WorkPackageCopyController { 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({ @@ -54,7 +55,7 @@ export class WorkPackageCopyFullViewComponent extends WorkPackageCopyController href: this.pathHelper.projectWorkPackagePath(this.currentProjectService.identifier!, this.stateParams.copiedFromWorkPackageId!), text: this.newWorkPackage.subject, }); - items.push(I18n.t('js.button_duplicate')); + items.push(this.I18n.t('js.button_duplicate')); return items; } diff --git a/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.component.ts b/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.component.ts index 8b15c06e38f2..ccf1e85275ed 100644 --- a/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.component.ts @@ -62,7 +62,6 @@ export class WorkPackageSplitViewToolbarComponent implements OnInit { }; ngOnInit() { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access this.displayShareButton$ = this.currentUserService.hasCapabilities$('work_package_shares/index', this.workPackage.project.id); this.displayReminderButton$ = this.currentUserService.isLoggedInAndHasCapabalities$( 'work_packages/read', diff --git a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts index 95d28995ec97..a5e9ac91dc04 100644 --- a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts +++ b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts @@ -141,14 +141,14 @@ export class TableEditForm extends EditForm { } } - public requireVisible(fieldName:string):Promise { + public requireVisible(fieldName:string):Promise { // Ensure the query form is loaded before trying to set fields // as we require new columns to be present return this.wpListService .conditionallyLoadForm() - .then(() => { + .then(async () => { this.wpTableColumns.addColumn(fieldName); - return this.waitForContainer(fieldName); + await this.waitForContainer(fieldName); }); } diff --git a/frontend/src/app/features/work-packages/components/wp-edit-form/work-package-filter-values.spec.ts b/frontend/src/app/features/work-packages/components/wp-edit-form/work-package-filter-values.spec.ts index ba285f772873..63a442c0686f 100644 --- a/frontend/src/app/features/work-packages/components/wp-edit-form/work-package-filter-values.spec.ts +++ b/frontend/src/app/features/work-packages/components/wp-edit-form/work-package-filter-values.spec.ts @@ -99,7 +99,7 @@ describe('WorkPackageFilterValues', () => { injector = TestBed.inject(Injector); halResourceService = injector.get(HalResourceService); - resource = halResourceService.createHalResourceOfClass(WorkPackageResource, source, true); + resource = halResourceService.createHalResourceOfClass(WorkPackageResource, source, true) as unknown as WorkPackageResource; changeset = new WorkPackageChangeset(resource); const type1 = halResourceService.createHalResourceOfClass(TypeResource, { _type: 'Type', id: '1', _links: { self: { href: '/api/v3/types/1', name: 'Task' } } }); @@ -121,7 +121,7 @@ describe('WorkPackageFilterValues', () => { const version = halResourceService.createHalResourceOfClass( HalResource, { _type: 'Version', id: '42', _links: { self: { href: '/api/v3/versions/42', name: 'v1.0' } } }, - ) as HalResource; + ); filters.push({ id: 'version', diff --git a/frontend/src/app/features/work-packages/components/wp-edit/work-package-changeset.ts b/frontend/src/app/features/work-packages/components/wp-edit/work-package-changeset.ts index dd948d7ad67f..081c701d3d9f 100644 --- a/frontend/src/app/features/work-packages/components/wp-edit/work-package-changeset.ts +++ b/frontend/src/app/features/work-packages/components/wp-edit/work-package-changeset.ts @@ -28,12 +28,12 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset'; -import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; import { WorkPackageSchemaProxy } from 'core-app/features/hal/schemas/work-package-schema-proxy'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; export class WorkPackageChangeset extends ResourceChangeset { - public setValue(key:string, val:any) { + public setValue(key:string, val:unknown) { super.setValue(key, val); if (key === 'project' || key === 'type') { @@ -57,7 +57,6 @@ export class WorkPackageChangeset extends ResourceChangeset // Explicitly not send the subject, if the subject was not editable. // In this case a generated template is rendered in the subject and // must not get submitted. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (!this.schema.isAttributeEditable('subject')) { delete (payload as { subject?:string }).subject; } @@ -79,7 +78,7 @@ export class WorkPackageChangeset extends ResourceChangeset // Special handling for taking over the description and // the subject to the pristine resource. if (key === 'description' && isNewResource(this.pristineResource)) { - this.pristineResource.description = val; + this.pristineResource.description = val as api.v3.Formattable; return; } @@ -96,7 +95,7 @@ export class WorkPackageChangeset extends ResourceChangeset * If loaded, return the form schema, which provides better information on writable status * and contains available values. */ - public get schema():SchemaResource { + public get schema():ISchemaProxy { if (this.form$.hasValue()) { return WorkPackageSchemaProxy.create(super.schema, this.projectedResource); } 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 5290417138dc..5200bc39a52c 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 @@ -59,7 +59,7 @@ export class GroupSumsBuilder extends SingleRowBuilder { public renderColumns(sums:Record, tr:HTMLTableRowElement) { this.augmentedColumns.forEach((column, i:number) => { const td = document.createElement('td'); - const div = this.renderContent(sums, column.id, this.sumsSchema[column.id]); + const div = this.renderContent(sums, column.id, this.sumsSchema[column.id] as IFieldSchema); if (i === 0) { this.appendFirstLabel(div); diff --git a/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.service.ts b/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.service.ts index e86f00a7f2c6..cfcda31fa5a9 100644 --- a/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.service.ts @@ -51,7 +51,7 @@ export class WorkPackageInlineCreateService implements OnDestroy { /** * A separate reference pane for the inline create component */ - public readonly referenceComponentClass:ComponentType|null = null; + public readonly referenceComponentClass:ComponentType|null = null; /** * A related work package for the inline create context diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-list-checksum.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list-checksum.service.ts index 805ef8378c5a..a17899f6320f 100644 --- a/frontend/src/app/features/work-packages/components/wp-list/wp-list-checksum.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-list/wp-list-checksum.service.ts @@ -116,7 +116,7 @@ export class WorkPackagesListChecksumService { public executeIfOutdated(newId:string|null, newChecksum:string|null, - callback:Function) { + callback:() => void) { if (this.isUninitialized() || this.isOutdated(newId, newChecksum)) { this.set(newId, newChecksum); diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts index 024f8808e6c5..d36e48e7c699 100644 --- a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts @@ -55,6 +55,7 @@ import { WorkPackageStatesInitializationService } from './wp-states-initializati import { WorkPackagesListInvalidQueryService } from './wp-list-invalid-query.service'; import { WorkPackagesQueryViewService } from 'core-app/features/work-packages/components/wp-list/wp-query-view.service'; import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export interface QueryDefinition { queryParams:{ query_id?:string|null, query_props?:string|null }; @@ -121,7 +122,7 @@ export class WorkPackagesListService { .find(queryData, queryParams.query_id, projectIdentifier); return stream.pipe( - catchError((error) => { + catchError((error:ErrorResource) => { // Load a default query const queryProps = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps); return from(this.handleQueryLoadingError(error, queryProps, queryParams.query_id, projectIdentifier)); @@ -335,7 +336,7 @@ export class WorkPackagesListService { return this.save(query); } - public toggleStarred(query:QueryResource):Promise { + public toggleStarred(query:QueryResource):Promise { const promise = this .apiV3Service .queries @@ -365,8 +366,7 @@ export class WorkPackagesListService { .then((loaded) => this.conditionallyLoadForm(loaded)); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!currentForm || query.$links.update?.href !== currentForm.href) { + if (!currentForm || (query.$links.update as CallableHalLink|undefined)?.href !== currentForm.href) { return this.loadForm(query); } diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-states-initialization.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-states-initialization.service.ts index f72453dd1182..ee1dd6c8a333 100644 --- a/frontend/src/app/features/work-packages/components/wp-list/wp-states-initialization.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-list/wp-states-initialization.service.ts @@ -114,7 +114,7 @@ export class WorkPackageStatesInitializationService { * @param form */ public updateStatesFromForm(query:QueryResource, form:QueryFormResource) { - const schema:QuerySchemaResource = form.schema as any; + const schema = form.schema as QuerySchemaResource; schema.filtersSchemas.elements.forEach((schema) => { this.states.schemas.get(schema.href!).putValue(schema); diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-create.service.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-create.service.ts index 9e045c99e3a6..2795f8328f69 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-create.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-create.service.ts @@ -59,7 +59,7 @@ import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset'; import { AttachmentsResourceService } from 'core-app/core/state/attachments/attachments.service'; -import { AttachmentCollectionResource } from 'core-app/features/hal/resources/attachment-collection-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { HalSource } from 'core-app/features/hal/interfaces'; export const newWorkPackageHref = '/api/v3/work_packages/new'; @@ -358,7 +358,7 @@ export class WorkPackageCreateService extends UntilDestroyedMixin { 'Boolean', 'String', 'Text', - undefined].includes(schema.$source[attribute].type)) { + undefined].includes((schema.$source[attribute] as { type?:string }).type)) { links.push(attribute); } }); @@ -404,13 +404,13 @@ export class WorkPackageCreateService extends UntilDestroyedMixin { wp.__initialized_at = Date.now(); // Set update link to form - wp.update = wp.$links.update = form.$links.self; + wp.update = wp.$links.update = form.$links.self as CallableHalLink>; // Use POST /work_packages for saving link - wp.updateImmediately = (data:object) => firstValueFrom(this.apiV3Service.work_packages.post(data)); - wp.$links.updateImmediately = (data:object) => firstValueFrom(this.apiV3Service.work_packages.post(data)); + wp.updateImmediately = ((data:object) => firstValueFrom(this.apiV3Service.work_packages.post(data))) as CallableHalLink; + wp.$links.updateImmediately = ((data:object) => firstValueFrom(this.apiV3Service.work_packages.post(data))) as CallableHalLink; if (form.schema.$links.attachments) { - wp.$links.attachments = { elements: [] } as unknown as AttachmentCollectionResource; + wp.$links.attachments = { elements: [] } as unknown as CallableHalLink; } // We need to provide the schema to the cache so that it is available in the html form to e.g. determine diff --git a/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.spec.ts b/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.spec.ts index 133c917211ff..0742e265499a 100644 --- a/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.spec.ts +++ b/frontend/src/app/features/work-packages/components/wp-query/url-params-helper.spec.ts @@ -40,7 +40,6 @@ describe('UrlParamsHelper', () => { TestBed.configureTestingModule({ providers: [ UrlParamsHelperService, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment { provide: PaginationService, useValue: paginationStub }, ], }); 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 f43ce7d7c3ed..a0abb9618ddd 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 @@ -403,8 +403,9 @@ export class UrlParamsHelperService { if (query.columns) { return query.columns.map((column:any) => column.id || idFromLink(column.href)) as string[]; } - if (query._links.columns) { - return query._links.columns.map((column:HalLink) => idFromLink(column.href)) as string[]; + const links = query._links as { columns?:HalLink[] }; + if (links.columns) { + return links.columns.map((column:HalLink) => idFromLink(column.href)); } return []; @@ -450,7 +451,7 @@ export class UrlParamsHelperService { } public buildV3GetFilterIdFromFilter(filter:QueryFilterInstanceResource) { - const href = filter.filter ? filter.filter.href : filter._links.filter.href; + const href = filter.filter ? filter.filter.href : (filter._links as { filter:HalLink }).filter.href; return idFromLink(href as string); } @@ -466,13 +467,13 @@ export class UrlParamsHelperService { if (filter.operator) { return filter.operator.id || idFromLink(filter.operator.href); } - const { href } = filter._links.operator; + const { href } = (filter._links as { operator:HalLink }).operator; return idFromLink(href as string); } private buildV3GetSortByFromQuery(query:QueryResource) { - const sortBys = query.sortBy ? query.sortBy : query._links.sortBy; + const sortBys = query.sortBy ? query.sortBy : (query._links as { sortBy:QuerySortByResource[] }).sortBy; const sortByIds = sortBys.map((sort:QuerySortByResource) => { if (sort.id) { return sort.id; diff --git a/frontend/src/app/features/work-packages/components/wp-relations/embedded/children/wp-children-inline-create.service.ts b/frontend/src/app/features/work-packages/components/wp-relations/embedded/children/wp-children-inline-create.service.ts index 3afaaa5a1d38..9c30171b2ed5 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/embedded/children/wp-children-inline-create.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/embedded/children/wp-children-inline-create.service.ts @@ -78,7 +78,6 @@ export class WpChildrenInlineCreateService extends WorkPackageInlineCreateServic return of(false); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return this.canCreateWorkPackages(idFromLink(this.referenceTarget.project.href)); } diff --git a/frontend/src/app/features/work-packages/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts index 0b15da6ac83b..dc0457607412 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.ts @@ -107,7 +107,7 @@ export class WpRelationInlineAddExistingComponent { this.wpInlineCreate.newInlineWorkPackageReferenced.next(newRelationId); this.cancel(); }) - .catch((err:any) => { + .catch((err:unknown) => { this.notificationService.handleRawError(err, this.workPackage); this.isDisabled = false; this.cancel(); diff --git a/frontend/src/app/features/work-packages/components/wp-relations/embedded/relations/wp-relation-inline-create.service.ts b/frontend/src/app/features/work-packages/components/wp-relations/embedded/relations/wp-relation-inline-create.service.ts index 9501eb7d732b..ac3cc28a25b6 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/embedded/relations/wp-relation-inline-create.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/embedded/relations/wp-relation-inline-create.service.ts @@ -91,7 +91,6 @@ export class WpRelationInlineCreateService extends WorkPackageInlineCreateServic return of(false); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return this.canCreateWorkPackages(idFromLink(this.referenceTarget.project.href)); } diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts index 32d9f9195dbc..1d0d930f74c0 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts @@ -206,7 +206,7 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme this.userInputs.showRelationTypesForm = false; this.cdRef.detectChanges(); }) - .catch((error:any) => this.notificationService.handleRawError(error, this.workPackage)); + .catch((error:unknown) => this.notificationService.handleRawError(error, this.workPackage)); } public toggleUserDescriptionForm() { @@ -232,7 +232,7 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme this.notificationService.showSave(this.relatedWorkPackage); }) - .catch((err:any) => this.notificationService.handleRawError(err, + .catch((err:unknown) => this.notificationService.handleRawError(err, this.relatedWorkPackage)); } diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts index dbbc928fdf9a..09cb65e3fbd0 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts @@ -42,6 +42,7 @@ import { } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; import { TOpAutocompleterResource } from 'core-app/shared/components/autocompleter/op-autocompleter/typings'; import { repositionDropdownBugfix } from 'core-app/shared/components/autocompleter/op-autocompleter/autocompleter.helper'; +import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; export interface IWorkPackageAutocompleteItem extends WorkPackageResource { id:string, @@ -104,14 +105,12 @@ export class WorkPackageRelationsAutocompleteComponent extends OpAutocompleterCo } return from( - // Please address the disabled eslint rule when making major changes to this file. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - this.workPackage.availableRelationCandidates.$link.$fetch({ + (this.workPackage.availableRelationCandidates as { $link:HalLink }).$link.$fetch({ query, filters: JSON.stringify(this.createFilters()), type: this.filterCandidatesFor || this.selectedRelationType, sortBy: JSON.stringify([['exactMatch', 'desc'], ['updatedAt', 'desc']]), - }) as Promise, + }), ) .pipe( map((collection) => collection.elements), diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts index 10eeb145d9b4..9dbccd616cf3 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts @@ -70,7 +70,7 @@ export class WorkPackageRelationsHierarchyComponent extends UntilDestroyedMixin public canAddRelation:boolean; - public childrenQueryProps:any; + public childrenQueryProps:{ filters:string, 'columns[]':string[], showHierarchies:boolean }; public text = { parentHeadline: this.I18n.t('js.relations_hierarchy.parent_headline'), diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service.ts index 05025b708db8..961d668e5b80 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service.ts @@ -44,7 +44,7 @@ export class WorkPackageRelationsHierarchyService { public changeParent(workPackage:WorkPackageResource, parentId:string|null) { - const payload:any = { + const payload:{ lockVersion:number, _links?:{ parent:{ href:string|null } } } = { lockVersion: workPackage.lockVersion, }; diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.interfaces.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.interfaces.ts index 737cda907f31..6d88bb5e1c90 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.interfaces.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.interfaces.ts @@ -26,4 +26,4 @@ // See COPYRIGHT and LICENSE files for more details. //++ -export type RelatedWorkPackagesGroup = Record; +export type RelatedWorkPackagesGroup = Record; diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.service.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.service.ts index c1199878e412..b5a08ce1652a 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations.service.ts @@ -175,7 +175,7 @@ export class WorkPackageRelationsService extends StateCacheService) { + public updateRelation(relation:RelationResource, params:Record) { return relation.updateImmediately(params) .then((savedRelation:RelationResource) => { this.insertIntoStates(savedRelation); diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service.ts index f977a25d81d4..58ca518c14c3 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service.ts @@ -29,6 +29,7 @@ import { sortBy } from 'lodash-es'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { Injectable, inject } from '@angular/core'; import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { WorkPackageLinkedResourceCache } from 'core-app/features/work-packages/components/wp-single-view-tabs/wp-linked-resource-cache.service'; @@ -54,10 +55,10 @@ export class WorkPackagesActivityService extends WorkPackageLinkedResourceCache< * whose order depends on the 'commentsSortedInDescendingOrder' property. */ protected load(workPackage:WorkPackageResource):Promise { - const aggregated:any[] = []; const - promises:Promise[] = []; + const aggregated:HalResource[][] = []; const + promises:Promise[] = []; - const add = function (data:any) { + const add = function (data:CollectionResource) { aggregated.push(data.elements); }; @@ -69,7 +70,7 @@ export class WorkPackagesActivityService extends WorkPackageLinkedResourceCache< return Promise.all(promises).then(() => this.sortedActivityList(aggregated)); } - protected sortedActivityList(activities:HalResource[], attr = 'createdAt'):HalResource[] { + protected sortedActivityList(activities:HalResource[][], attr = 'createdAt'):HalResource[] { const sorted = sortBy(activities.flat(), attr); if (this.isReversed) { diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component.ts index 64c0ebab310c..cb25661bbf3a 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component.ts @@ -33,7 +33,6 @@ import { map } from 'rxjs/operators'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ProjectStoragesResourceService } from 'core-app/core/state/project-storages/project-storages.service'; import { IProjectStorage } from 'core-app/core/state/project-storages/project-storage.model'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; @@ -69,7 +68,7 @@ export class WorkPackageFilesTabComponent implements OnInit { showAttachments:boolean; ngOnInit():void { - const project = this.workPackage.project as HalResource; + const project = this.workPackage.project; if (project.id === null) { return; } diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.spec.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.spec.ts index a74513355449..bc8813b97e2c 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.spec.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.spec.ts @@ -44,10 +44,8 @@ describe('keepTab service', () => { TestBed.configureTestingModule({ providers: [ KeepTabService, - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ { provide: PathHelperService, useValue: pathHelper }, { provide: CurrentProjectService, useValue: currentProject }, - /* eslint-enable @typescript-eslint/no-unsafe-assignment */ ], }); diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts index f04964b1886d..c63b6f036779 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts @@ -28,7 +28,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, inject } from '@angular/core'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { UserResource } from 'core-app/features/hal/resources/user-resource'; +import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { @@ -41,6 +42,7 @@ import { import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; +import { IUserAutocompleteItem } from 'core-app/shared/components/autocompleter/user-autocompleter/user-autocompleter.component'; @Component({ templateUrl: './watchers-tab.html', @@ -78,7 +80,7 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme private element:HTMLElement; - public watching:any[] = []; + public watching:UserResource[] = []; public text = { loading: this.I18n.t('js.watchers.label_loading'), @@ -120,20 +122,20 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme } this.wpWatchersService.require(this.workPackage) - .then((watchers:HalResource[]) => { - this.watching = watchers; + .then((watchers) => { + this.watching = watchers as UserResource[]; this.cdRef.detectChanges(); }) - .catch((error:any) => { + .catch((error:ErrorResource) => { this.notificationService.showError(error, this.workPackage); }); } - public set loadingPromise(promise:Promise) { + public set loadingPromise(promise:Promise) { this.loadingIndicator.wpDetails.promise = promise; } - public addWatcher(user:any) { + public addWatcher(user:IUserAutocompleteItem) { this.loadingPromise = this.workPackage.addWatcher.$link.$fetch({ user: { href: user.href } }) .then(() => { // Forcefully reload the resource to update the watch/unwatch links @@ -149,13 +151,13 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme this.cdRef.detectChanges(); }) - .catch((error:any) => this.notificationService.showError(error, this.workPackage)); + .catch((error:ErrorResource) => this.notificationService.showError(error, this.workPackage)); } - public removeWatcher(watcher:any) { - this.workPackage.removeWatcher.$link.$prepare({ user_id: watcher.id })() + public removeWatcher(watcher:UserResource) { + this.workPackage.removeWatcher.$link.$prepare({ user_id: watcher.id! })() .then(() => { - this.watching = this.watching.filter((other:HalResource) => other.href !== watcher.href); + this.watching = this.watching.filter((other) => other.href !== watcher.href); // Forcefully reload the resource to update the watch/unwatch links // should the current user have been removed @@ -170,7 +172,7 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme this.cdRef.detectChanges(); }) - .catch((error:any) => this.notificationService.showError(error, this.workPackage)); + .catch((error:ErrorResource) => this.notificationService.showError(error, this.workPackage)); } public updateCounter() { diff --git a/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts index 24fdc530d35f..50ca464d9ca1 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts @@ -51,12 +51,13 @@ import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { debugLog } from 'core-app/shared/helpers/debug_output'; import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ProjectStoragesResourceService } from 'core-app/core/state/project-storages/project-storages.service'; import { IProjectStorage } from 'core-app/core/state/project-storages/project-storage.model'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { isSemanticWorkPackageId } from 'core-app/shared/helpers/work-package-id-pattern'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { HalSource } from 'core-app/features/hal/interfaces'; export interface FieldDescriptor { name:string; @@ -77,6 +78,21 @@ export interface GroupDescriptor { type:string; } +interface WorkPackageFormAttributeGroup { + _type:'WorkPackageFormAttributeGroup'; + name:string; + attributes:string[]; +} + +interface WorkPackageFormQueryGroup { + _type:'WorkPackageFormQueryGroup'; + name:string; + relationType?:string; + _embedded:{ query:HalSource }; +} + +type FormAttributeGroup = WorkPackageFormAttributeGroup|WorkPackageFormQueryGroup; + export interface ResourceContextChange { isNew:boolean; schema:string|null; @@ -182,7 +198,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen if (!resource.project) { this.projectContext = { matches: false, href: null, id: null }; } else { - const project = resource.project as unknown&{ href:string, id:string }; + const project = resource.project as { href:string, id:string }; const workPackageId = this.workPackage.id; if (!workPackageId) { throw new Error('work package id is invalid'); @@ -199,8 +215,11 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen this.updateWorkPackageCreationState(change); } - // eslint-disable-next-line no-underscore-dangle - this.groupedFields = this.rebuildGroupedFields(change, this.schema(resource)._attributeGroups) as GroupDescriptor[]; + this.groupedFields = this.rebuildGroupedFields( + change, + // eslint-disable-next-line no-underscore-dangle + this.schema(resource)._attributeGroups as FormAttributeGroup[]|undefined, + ) as GroupDescriptor[]; this.cdRef.detectChanges(); } @@ -208,13 +227,13 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen const resource = change.projectedResource; if (!this.currentProject.inProjectContext) { this.projectContext.field = this.getFields(change, ['project']); - this.workPackage.project = resource.project as HalResource; + this.workPackage.project = resource.project; } if (resource.project === null) { this.projectStorages.next([]); } else { - const project = resource.project as unknown&{ href:string, id:string }; + const project = resource.project as { href:string, id:string }; combineLatest([ this.projectsResourceService.requireEntity(project.href), this.projectStoragesService.requireCollection({ filters: [['projectId', '=', [project.id]]] }), @@ -299,12 +318,12 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen return this.element.getBoundingClientRect().width > 750; } - private rebuildGroupedFields(change:WorkPackageChangeset, attributeGroups:any) { + private rebuildGroupedFields(change:WorkPackageChangeset, attributeGroups:FormAttributeGroup[]|undefined) { if (!attributeGroups) { return []; } - return attributeGroups.map((group:any) => { + return attributeGroups.map((group) => { const groupId = this.getAttributesGroupId(group); if (group._type === 'WorkPackageFormAttributeGroup') { @@ -396,13 +415,13 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen const projectHref:string|null = workPackage.project?.href; if (schema.baseSchema) { - schemaHref = schema.baseSchema.href; + schemaHref = (schema.baseSchema as HalResource).href; } else { schemaHref = schema.href; } return { - isNew: workPackage.isNew, + isNew: workPackage.isNew as boolean, schema: schemaHref, project: projectHref, }; 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 c08972f9e94b..8fd6e7b97612 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 @@ -121,7 +121,7 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen public get availableHighlightedAttributes():HalResource[] { const { schema } = this.querySpace.queryForm.value!; - return schema.highlightedAttributes.allowedValues; + return schema.highlightedAttributes!.allowedValues as HalResource[]; } public onOpen(component:unknown) { 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 68d82621f843..15438eccfc9d 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 @@ -56,7 +56,7 @@ export class WorkPackageContextMenuHelperService { private wpViewIndent = inject(WorkPackageViewHierarchyIdentationService); private PathHelper = inject(PathHelperService); - private BULK_ACTIONS = [ + private BULK_ACTIONS:WorkPackageAction[] = [ { text: I18n.t('js.work_packages.bulk_actions.edit'), key: 'edit', diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts index 6804660bccea..27a385e7db65 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts @@ -48,6 +48,7 @@ import { timelineElementCssClass, timelineMarkerSelectionStartClass, } from '../wp-timeline'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; export interface CellDateMovement { // Target values to move work package to @@ -107,7 +108,7 @@ export class TimelineCellRenderer { public canMoveDates(wp:WorkPackageResource):boolean { const schema = this.schemaCache.of(wp); - return schema.startDate.writable && schema.dueDate.writable && schema.isAttributeEditable('startDate'); + return (schema.startDate as IOPFieldSchema).writable && (schema.dueDate as IOPFieldSchema).writable && schema.isAttributeEditable('startDate'); } public isEmpty(wp:WorkPackageResource) { 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 199c5bedf2b9..9ee0254d11a9 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 @@ -51,6 +51,7 @@ import { MouseDirection, } from './timeline-cell-renderer'; import { WorkPackageCellLabels } from './wp-timeline-cell-labels'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { public get type():string { @@ -64,7 +65,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { public canMoveDates(wp:WorkPackageResource) { const schema = this.schemaCache.of(wp); - return schema.date.writable && schema.isAttributeEditable('date'); + return (schema.date as IOPFieldSchema).writable && schema.isAttributeEditable('date'); } public displayPlaceholderUnderCursor(ev:MouseEvent, renderInfo:RenderInfo):HTMLElement { 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 835912bc7cfd..28bbe9fb2150 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 @@ -84,6 +84,7 @@ import { WeekdayService } from 'core-app/core/days/weekday.service'; import Mousetrap from 'mousetrap'; import { DayResourceService } from 'core-app/core/state/days/day.service'; import { IDay } from 'core-app/core/state/days/day.model'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -566,7 +567,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl const changedGroupId = groupIdFromIdentifier(groupIdentifier); const changedGroupType = groupTypeFromIdentifier(groupIdentifier); - const changedGroupTableWorkPackages = tableWorkPackages.filter((tableWorkPackage) => tableWorkPackage[changedGroupType].id === changedGroupId); + const changedGroupTableWorkPackages = tableWorkPackages.filter((tableWorkPackage) => (tableWorkPackage[changedGroupType] as HalResource).id === changedGroupId); const changedGroupWpsWithHeaderCells = changedGroupTableWorkPackages.filter((tableWorkPackage) => this.shouldBeShownInCollapsedGroupHeaders(tableWorkPackage) && (tableWorkPackage.date || tableWorkPackage.startDate)); const changedGroupWpsWithHeaderCellsIds = changedGroupWpsWithHeaderCells.map((workPackage) => workPackage.id!); diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function.ts b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function.ts index 997faec5b1c3..02518e6aa38e 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function.ts @@ -35,6 +35,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag import { AttachmentsResourceService } from 'core-app/core/state/attachments/attachments.service'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; import { IFileLink } from 'core-app/core/state/file-links/file-link.model'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export function workPackageFilesCount( workPackage:WorkPackageResource, @@ -43,7 +44,7 @@ export function workPackageFilesCount( const attachmentService = injector.get(AttachmentsResourceService); const http = injector.get(HttpClient); const attachmentsCollection = workPackage.$links.attachments - ? attachmentService.collection(workPackage.$links.attachments.href || '') + ? attachmentService.collection((workPackage.$links.attachments as CallableHalLink).href ?? '') : of([]); const totalFileLinks = workPackage.$links.fileLinks ? http.get>(href(workPackage)) @@ -60,5 +61,5 @@ function href(workPackage:WorkPackageResource):string { return ''; } - return `${workPackage.$links.fileLinks.href}?pageSize=0`; + return `${(workPackage.$links.fileLinks as CallableHalLink).href}?pageSize=0`; } diff --git a/frontend/src/app/features/work-packages/helpers/time-entries/time-entry-changeset.ts b/frontend/src/app/features/work-packages/helpers/time-entries/time-entry-changeset.ts index a5afb61907cc..20c70e67f317 100644 --- a/frontend/src/app/features/work-packages/helpers/time-entries/time-entry-changeset.ts +++ b/frontend/src/app/features/work-packages/helpers/time-entries/time-entry-changeset.ts @@ -30,7 +30,7 @@ import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/r import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource'; export class TimeEntryChangeset extends ResourceChangeset { - public setValue(key:string, val:any) { + public setValue(key:string, val:unknown) { super.setValue(key, val); // Update the form for fields that may alter the form itself @@ -40,7 +40,7 @@ export class TimeEntryChangeset extends ResourceChangeset { } protected buildPayloadFromChanges() { - const payload = super.buildPayloadFromChanges(); + const payload = super.buildPayloadFromChanges() as { _links:Record }; // we ignore the project and instead rely completely on the work package. delete payload._links.project; diff --git a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts index 755f6119a5a9..31528fe3b017 100644 --- a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts +++ b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts @@ -49,11 +49,12 @@ import { firstValueFrom } from 'rxjs'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import { UrlParamsService } from 'core-app/core/navigation/url-params.service'; +import { EventHandler } from 'ng-dynamic-component'; export interface DynamicComponentDefinition { - component:ComponentType; - inputs?:Record; - outputs?:Record; + component:ComponentType; + inputs?:Record; + outputs?:Record; } export interface ToolbarButtonComponentDefinition extends DynamicComponentDefinition { diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts index 4a42d7844ef2..4130932f0572 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts @@ -31,6 +31,7 @@ import { CurrentUserService } from 'core-app/core/current-user/current-user.serv import { TabDefinition } from 'core-app/shared/components/tabs/tab.interface'; import { RecentItemsService } from 'core-app/core/recent-items.service'; import { ProjectResource } from 'core-app/features/hal/resources/project-resource'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { WpSingleViewService } from 'core-app/features/work-packages/routing/wp-view-base/state/wp-single-view.service'; @@ -66,7 +67,7 @@ export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase imp public displayWatchButton = false; - public watchers:any; + public watchers:HalResource[]; public text = { fullView: { @@ -116,7 +117,7 @@ export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase imp // watchers if (wp.watchers) { - this.watchers = (wp.watchers as any).elements; + this.watchers = wp.watchers.elements; } } } diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts index bb3f70dcef6c..761ab58d86d8 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts @@ -52,7 +52,7 @@ export interface WorkPackageViewOutputs { stateLinkClicked:EventEmitter<{ workPackageId:string, requestedState:string }>; } -export const WorkPackageViewHandlerToken = new InjectionToken>('CardEventHandler'); +export const WorkPackageViewHandlerToken = new InjectionToken>('CardEventHandler'); /** * Abstract view handler registry for globally handling arbitrary event on the diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service.spec.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service.spec.ts index 2c336a81a827..19fc98658d05 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service.spec.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service.spec.ts @@ -85,7 +85,6 @@ describe('WorkPackageViewOrderService', () => { id: '123', _links: { self: { href: 'test' } }, } as Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-argument querySpace.query.putValue(mockQuery as any); }); @@ -103,12 +102,10 @@ describe('WorkPackageViewOrderService', () => { const order = ['1', '2', '3']; const wpId = '2'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.spyOn(service as any, 'update'); service.remove(order, wpId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((service as any).update).toHaveBeenCalledWith({ [wpId]: -1 }); }); }); diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-timeline.service.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-timeline.service.ts index d50efec1ff62..64232562a8af 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-timeline.service.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-timeline.service.ts @@ -105,7 +105,7 @@ export class WorkPackageViewTimelineService extends WorkPackageQueryStateService public getNormalizedLabels(workPackage:WorkPackageResource) { const labels:TimelineLabels = this.defaultLabels; - Object.entries(this.current.labels).forEach(([positionAsString, attribute]) => { + (Object.entries(this.current.labels) as [string, string|null][]).forEach(([positionAsString, attribute]) => { // RR: Lodash typings declare the position as string. However, it is save to cast // to `keyof TimelineLabels` because `this.current.labels` is of type TimelineLabels. const position:keyof TimelineLabels = positionAsString as keyof TimelineLabels; diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts index a096622e0114..e8e79e71621f 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts @@ -55,9 +55,9 @@ import { AttachmentsResourceService } from 'core-app/core/state/attachments/atta import { StoragesResourceService } from 'core-app/core/state/storages/storages.service'; import { FileLinksResourceService } from 'core-app/core/state/file-links/file-links.service'; import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; @Directive() export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { @@ -184,14 +184,14 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { .id(this.workPackage.project) .requireAndStream() .subscribe(() => { - this.projectIdentifier = this.workPackage.project.identifier; + this.projectIdentifier = this.workPackage.project.identifier as string; this.cdRef.detectChanges(); }); // lazy load the work package's project, needed when initializing // the work package resource from split view. this.projectsResourceService - .requireEntity((this.workPackage.$links.project as HalResource).href!) + .requireEntity((this.workPackage.$links.project as CallableHalLink).href!) .subscribe( () => {}, (error:HttpErrorResponse) => { @@ -210,7 +210,7 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { // Fetch attachments of current work package if (this.workPackage.$links.attachments) { - this.attachmentsResourceService.fetchCollection(this.workPackage.$links.attachments.href!).subscribe(); + this.attachmentsResourceService.fetchCollection((this.workPackage.$links.attachments as CallableHalLink).href!).subscribe(); } // Listen to tab changes to update the tab label @@ -229,8 +229,9 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { * Recompute the current tab focus label */ public updateFocusAnchorLabel(tabName:string):string { + const tab = this.i18n.t(`js.work_packages.tabs.${tabName}`); this.focusAnchorLabel = this.i18n.t('js.label_work_package_details_you_are_here', { - tab: this.i18n.t(`js.work_packages.tabs.${tabName}`), + tab, type: this.workPackage.type.name, subject: this.workPackage.subject, }); diff --git a/frontend/src/app/features/work-packages/services/notifications/work-package-notification.service.ts b/frontend/src/app/features/work-packages/services/notifications/work-package-notification.service.ts index 971f54b92934..c184c189a298 100644 --- a/frontend/src/app/features/work-packages/services/notifications/work-package-notification.service.ts +++ b/frontend/src/app/features/work-packages/services/notifications/work-package-notification.service.ts @@ -33,6 +33,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; +import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; @Injectable() export class WorkPackageNotificationService extends HalResourceNotificationService { @@ -48,7 +49,7 @@ export class WorkPackageNotificationService extends HalResourceNotificationServi this.ToastService.addSuccess(message); } - protected showCustomError(errorResource:any, resource:WorkPackageResource):boolean { + protected showCustomError(errorResource:ErrorResource, resource:WorkPackageResource):boolean { if (errorResource.errorIdentifier === 'urn:openproject-org:api:v3:errors:UpdateConflict') { // currently we do not have a programmatic way to show the primer flash messages // so we just do a request to the server to show it diff --git a/frontend/src/app/shared/components/autocompleter/time-entries-work-package-autocompleter/time-entries-work-package-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/time-entries-work-package-autocompleter/time-entries-work-package-autocompleter.component.ts index b545aa3cc1e4..df6c3ebd0f83 100644 --- a/frontend/src/app/shared/components/autocompleter/time-entries-work-package-autocompleter/time-entries-work-package-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/time-entries-work-package-autocompleter/time-entries-work-package-autocompleter.component.ts @@ -107,8 +107,8 @@ export class TimeEntriesWorkPackageAutocompleterComponent extends OpAutocomplete switchMap((collection:CollectionResource) => { this.recentWorkPackageIds = collection .elements - .filter((timeEntry) => timeEntry.workPackage?.href) - .map((timeEntry) => idFromLink(timeEntry.workPackage.href)) + .filter((timeEntry) => (timeEntry.workPackage as HalResource|undefined)?.href) + .map((timeEntry) => idFromLink((timeEntry.workPackage as HalResource).href)) .filter((v, i, a) => a.indexOf(v) === i); return this.loadWorkPackages(query); diff --git a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts index 2d8ee00d05d4..e44208cfd793 100644 --- a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts +++ b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts @@ -285,8 +285,7 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl } private setupAttachmentRemovalSignal(editor:ICKEditorInstance) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access - this.attachments = [...(this.halResource as HalResource).attachments.elements]; + this.attachments = [...this.halResource!.attachments.elements]; this .states @@ -300,8 +299,7 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl const presentIds = new Set(resource.attachments.elements.map((other:HalResource) => other.id)); const missingAttachments = this.attachments.filter((attachment:HalResource) => !presentIds.has(attachment.id)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - const removedUrls = missingAttachments.map((attachment) => attachment.downloadLocation.href); + const removedUrls = missingAttachments.map((attachment) => (attachment.downloadLocation as HalResource).href); if (removedUrls.length) { editor.model.fire('op:attachment-removed', removedUrls); diff --git a/frontend/src/app/shared/components/fields/changeset/resource-changeset.ts b/frontend/src/app/shared/components/fields/changeset/resource-changeset.ts index 19f06f5614cc..5b29d0646c9f 100644 --- a/frontend/src/app/shared/components/fields/changeset/resource-changeset.ts +++ b/frontend/src/app/shared/components/fields/changeset/resource-changeset.ts @@ -32,17 +32,17 @@ import { } from '@openproject/reactivestates'; import { cloneDeep } from 'lodash-es'; -import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; import { FormResource } from 'core-app/features/hal/resources/form-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ChangeMap, Changeset } from 'core-app/shared/components/fields/changeset/changeset'; import { IFieldSchema } from 'core-app/shared/components/fields/field.base'; import { debugLog } from 'core-app/shared/helpers/debug_output'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; -import { SchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; +import { ISchemaProxy, SchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; import { IHalOptionalTitledLink } from 'core-app/core/state/hal-resource'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { firstValueFrom } from 'rxjs'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export const PROXY_IDENTIFIER = '__is_changeset_proxy'; @@ -167,22 +167,19 @@ export class ResourceChangeset { protected updateForm():Promise { const payload = this.buildPayloadFromChanges(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!this.pristineResource.$links.update) { + const update = this.pristineResource.$links.update as CallableHalLink|undefined; + if (!update) { return Promise.reject(); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const promise = this.pristineResource - .$links - .update(payload) + const promise = update(payload) .then((form:FormResource) => { this.cache = {}; this.form$.putValue(form); this.setNewDefaults(form); this.push(); return form; - }) as Promise; + }); this.form$.putFromPromiseIfPristine(() => promise); return promise; @@ -349,7 +346,7 @@ export class ResourceChangeset { * If loaded, return the form schema, which provides better information on writable status * and contains available values. */ - public get schema():SchemaResource { + public get schema():ISchemaProxy { if (this.form$.hasValue()) { return SchemaProxy.create(this.form$.value!.schema, this.projectedResource); } @@ -423,7 +420,7 @@ export class ResourceChangeset { if (this.form$.value) { payload = cloneDeep((this.form$.value.payload as { $source:unknown }).$source) as typeof payload; } else { - payload = cloneDeep(this.pristineResource.$source) as typeof payload; + payload = cloneDeep(this.pristineResource.$source); } // Add attachments to be assigned. @@ -491,7 +488,7 @@ export class ResourceChangeset { * that we need to set. */ protected setNewDefaults(form:FormResource) { - Object.entries(form.payload as Record).forEach(([key, val]) => { + Object.entries(form.payload).forEach(([key, val]) => { const fieldSchema:IFieldSchema|null = this.schema.ofProperty(key); if (!fieldSchema?.writable && !fieldSchema?.required) { return; diff --git a/frontend/src/app/shared/components/fields/display/display-field-renderer.ts b/frontend/src/app/shared/components/fields/display/display-field-renderer.ts index e5cd49e77c40..1a1927b3acab 100644 --- a/frontend/src/app/shared/components/fields/display/display-field-renderer.ts +++ b/frontend/src/app/shared/components/fields/display/display-field-renderer.ts @@ -39,7 +39,7 @@ import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/r import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { LazyInject } from 'core-app/shared/helpers/angular/lazy-inject.decorator'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; -import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; import { HalResourceEditingService, } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; @@ -95,8 +95,7 @@ export class DisplayFieldRenderer { const span = document.createElement('span'); const schema = this.schema(resource, change); const attributeName = this.attributeName(requestedAttribute, schema); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const fieldSchema = schema.ofProperty(attributeName) as IFieldSchema; + const fieldSchema = schema.ofProperty(attributeName); // If the resource does not have that field, return an empty // span (e.g., for the table). @@ -171,17 +170,16 @@ export class DisplayFieldRenderer { } } - private isAttributeEditable(schema:SchemaResource, fieldName:string):boolean { + private isAttributeEditable(schema:ISchemaProxy, fieldName:string):boolean { // We need to handle start/due date cases like they were combined dates if (['startDate', 'dueDate', 'date'].includes(fieldName)) { fieldName = 'combinedDate'; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return schema.isAttributeEditable(fieldName) as boolean; + return schema.isAttributeEditable(fieldName); } - private getAriaLabel(field:DisplayField, schema:SchemaResource):string { + private getAriaLabel(field:DisplayField, schema:ISchemaProxy):string { let titleContent; const labelContent = this.getLabelContent(field); @@ -198,7 +196,6 @@ export class DisplayFieldRenderer { titleContent = labelContent; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (field.writable && !!schema.isAttributeEditable(field.name)) { return this.I18n.t('js.inplace.button_edit', { attribute: `${field.displayName} ${titleContent}` }); } @@ -219,22 +216,21 @@ export class DisplayFieldRenderer { * @param schema * @param attribute */ - private attributeName(attribute:string, schema:SchemaResource):string { + private attributeName(attribute:string, schema:ISchemaProxy):string { if (schema.mappedName) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return schema.mappedName(attribute) as string; + return schema.mappedName(attribute); } return attribute; } - private schema(resource:T, change:ResourceChangeset|null):SchemaResource { + private schema(resource:T, change:ResourceChangeset|null):ISchemaProxy { if (change) { return change.schema; } if (this.halEditing.typedState(resource).hasValue()) { - const val = this.halEditing.typedState(resource).value as { schema:SchemaResource }; + const val = this.halEditing.typedState(resource).value as { schema:ISchemaProxy }; return val.schema; } diff --git a/frontend/src/app/shared/components/fields/display/display-field.component.ts b/frontend/src/app/shared/components/fields/display/display-field.component.ts index f82da0b7f802..cc100381d2eb 100644 --- a/frontend/src/app/shared/components/fields/display/display-field.component.ts +++ b/frontend/src/app/shared/components/fields/display/display-field.component.ts @@ -33,7 +33,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { Constructor } from 'core-app/core/util-types'; import { DisplayField } from 'core-app/shared/components/fields/display/display-field.module'; -import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -99,10 +99,9 @@ export class DisplayFieldComponent implements OnInit { ); } - private attributeName(attribute:string, schema:SchemaResource):string { + private attributeName(attribute:string, schema:ISchemaProxy):string { if (schema.mappedName) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return schema.mappedName(attribute) as string; + return schema.mappedName(attribute); } return attribute; diff --git a/frontend/src/app/shared/components/fields/display/display-field.module.ts b/frontend/src/app/shared/components/fields/display/display-field.module.ts index 7a028488af1a..756081e12458 100644 --- a/frontend/src/app/shared/components/fields/display/display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/display-field.module.ts @@ -98,7 +98,7 @@ export class DisplayField extends Field { } public get valueString():string { - return this.value; + return this.value as string; } public get placeholder():string { diff --git a/frontend/src/app/shared/components/fields/display/field-types/combined-date-display.field.ts b/frontend/src/app/shared/components/fields/display/field-types/combined-date-display.field.ts index 5f9c143ad18a..40dea1e2ed80 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/combined-date-display.field.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/combined-date-display.field.ts @@ -71,13 +71,11 @@ export class CombinedDateDisplayField extends DateDisplayField { } private get startDate():string|null { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.resource.startDate; + return this.resource.startDate as string|null; } private get dueDate():string|null { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.resource.dueDate; + return this.resource.dueDate as string|null; } private renderSingleDate(field:'date'|'startDate'|'dueDate', element:HTMLElement):void { diff --git a/frontend/src/app/shared/components/fields/display/field-types/date-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/date-display-field.module.ts index 1e50be11aae5..c5bb368096cd 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/date-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/date-display-field.module.ts @@ -35,6 +35,7 @@ import { toDOMString, opAutoDateIconData, } from '@openproject/octicons-angular'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class DateDisplayField extends HighlightableDisplayField { @LazyInject() timezoneService:TimezoneService; @@ -51,12 +52,12 @@ export class DateDisplayField extends HighlightableDisplayField { // Highlight overdue tasks if (this.shouldHighlight && this.canOverdue && !!this.resource.status) { - const diff = this.timezoneService.daysFromToday(this.value); + const diff = this.timezoneService.daysFromToday(this.value as string); this .apiV3Service .statuses - .id(this.resource.status.id) + .id((this.resource.status as HalResource).id!) .get() .subscribe((status) => { if (!status.isClosed) { @@ -73,7 +74,7 @@ export class DateDisplayField extends HighlightableDisplayField { public get valueString() { if (this.value) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return this.timezoneService.formattedDate(this.value, this.context.options.dateFormat); + return this.timezoneService.formattedDate(this.value as string, this.context.options.dateFormat); } return ''; } diff --git a/frontend/src/app/shared/components/fields/display/field-types/datetime-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/datetime-display-field.module.ts index 625592aa0356..a4678950b362 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/datetime-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/datetime-display-field.module.ts @@ -35,7 +35,7 @@ export class DateTimeDisplayField extends DisplayField { public get valueString() { if (this.value) { - return this.timezoneService.formattedDatetime(this.value); + return this.timezoneService.formattedDatetime(this.value as string); } return ''; diff --git a/frontend/src/app/shared/components/fields/display/field-types/days-duration-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/days-duration-display-field.module.ts index fab4c11275f2..14d284024b3b 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/days-duration-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/days-duration-display-field.module.ts @@ -34,6 +34,6 @@ export class DaysDurationDisplayField extends DisplayField { @LazyInject() timezoneService:TimezoneService; public get valueString() { - return this.timezoneService.formattedDuration(this.value, 'days'); + return this.timezoneService.formattedDuration(this.value as string, 'days'); } } diff --git a/frontend/src/app/shared/components/fields/display/field-types/excluded-icon-helper.service.ts b/frontend/src/app/shared/components/fields/display/field-types/excluded-icon-helper.service.ts index a3c6da6c8532..a831d1a6059d 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/excluded-icon-helper.service.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/excluded-icon-helper.service.ts @@ -38,7 +38,7 @@ export class ExcludedIconHelperService { public addIconIfExcludedFromTotals(element:HTMLElement, resource:WorkPackageResource):void { if (resource?.status) { - this.apiV3Service.statuses.id(resource.status as StatusResource).get().subscribe( + this.apiV3Service.statuses.id(resource.status).get().subscribe( (status:StatusResource) => { if (status.excludedFromTotals) { this.addExcludedInfoIcon(element, status.name); diff --git a/frontend/src/app/shared/components/fields/display/field-types/float-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/float-display-field.module.ts index 59237f796f5e..f04a492f63fb 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/float-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/float-display-field.module.ts @@ -34,7 +34,7 @@ export class FloatDisplayField extends DisplayField { return ''; } - return this.value.toLocaleString( + return (this.value as number).toLocaleString( this.I18n.locale, { useGrouping: true, maximumFractionDigits: 20 }, ); diff --git a/frontend/src/app/shared/components/fields/display/field-types/formattable-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/formattable-display-field.module.ts index f228c5a5abc7..e64ea703268c 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/formattable-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/formattable-display-field.module.ts @@ -70,7 +70,7 @@ export class FormattableDisplayField extends DisplayField { if (!this.schema) { return null; } - const element = this.resource[this.name]; + const element = this.resource[this.name] as api.v3.Formattable|null|undefined; if (!(element?.html)) { return ''; } diff --git a/frontend/src/app/shared/components/fields/display/field-types/highlighted-resource-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/highlighted-resource-display-field.module.ts index a10ebef0f017..bf86f5bc4771 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/highlighted-resource-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/highlighted-resource-display-field.module.ts @@ -41,7 +41,7 @@ export class HighlightedResourceDisplayField extends HighlightableDisplayField { public get value() { if (this.schema) { - return this.attribute?.name; + return (this.attribute as HalResource|null|undefined)?.name; } return null; } diff --git a/frontend/src/app/shared/components/fields/display/field-types/hours-duration-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/hours-duration-display-field.module.ts index 1ab2a2af5c1a..e80b49a76e2e 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/hours-duration-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/hours-duration-display-field.module.ts @@ -34,6 +34,6 @@ export class HoursDurationDisplayField extends DisplayField { @LazyInject() timezoneService:TimezoneService; public get valueString() { - return this.timezoneService.formattedDuration(this.value, 'hour'); + return this.timezoneService.formattedDuration(this.value as string, 'hour'); } } diff --git a/frontend/src/app/shared/components/fields/display/field-types/integer-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/integer-display-field.module.ts index 3ebe7318bd6d..dfd63d27335c 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/integer-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/integer-display-field.module.ts @@ -30,7 +30,7 @@ import { DisplayField } from 'core-app/shared/components/fields/display/display- export class IntegerDisplayField extends DisplayField { public get value() { - return parseInt(this.resource[this.name]); + return parseInt(this.resource[this.name] as string); } public isEmpty():boolean { diff --git a/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts index 0c2fc4beaff8..2dfe9374052b 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts @@ -59,7 +59,7 @@ export class LinkedWorkPackageDisplayField extends WorkPackageDisplayField { const routingId = this.wpRoutingId; const link = this.uiStateBuilder.linkToShow( - this.wpId, + this.wpId!, this.text.linkTitle, this.valueString, routingId, diff --git a/frontend/src/app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module.ts index 314556f0b058..b4502155a323 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module.ts @@ -35,7 +35,7 @@ export class MultipleLinesUserFieldModule extends ResourcesDisplayField { @LazyInject() principalRenderer:PrincipalRendererService; public render(element:HTMLElement, displayText:string):void { - const values = this.attribute; + const values = this.attribute as UserResource[]; element.setAttribute('title', displayText); element.textContent = displayText; diff --git a/frontend/src/app/shared/components/fields/display/field-types/plain-formattable-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/plain-formattable-display-field.module.ts index 2632484f7ddc..6528c3da6fd7 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/plain-formattable-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/plain-formattable-display-field.module.ts @@ -33,7 +33,7 @@ export class PlainFormattableDisplayField extends DisplayField { if (!this.schema) { return null; } - const element = this.resource[this.name]; + const element = this.resource[this.name] as api.v3.Formattable|null|undefined; return element?.raw || ''; } diff --git a/frontend/src/app/shared/components/fields/display/field-types/project-status-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/project-status-display-field.module.ts index 112395ac7492..6065c66ea940 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/project-status-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/project-status-display-field.module.ts @@ -31,10 +31,11 @@ import { projectStatusCodeCssClass, projectStatusI18n, } from 'core-app/shared/components/fields/helpers/project-status-helper'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class ProjectStatusDisplayField extends DisplayField { public render(element:HTMLElement, displayText:string):void { - const code = this.value?.id; + const code = (this.value as HalResource|null|undefined)?.id; const bulb = document.createElement('span'); bulb.classList.add('project-status--bulb', projectStatusCodeCssClass(code)); diff --git a/frontend/src/app/shared/components/fields/display/field-types/render-hierarchy-item.ts b/frontend/src/app/shared/components/fields/display/field-types/render-hierarchy-item.ts index 33508daf5a9f..abb7b3974639 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/render-hierarchy-item.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/render-hierarchy-item.ts @@ -31,10 +31,10 @@ import { map } from 'rxjs/operators'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; export function renderHierarchyItem(item:HalResource, multiple = false):Observable { - const customFieldItemLinks = item.$links as { branch:() => HalResource[] }; - return from(customFieldItemLinks.branch()) + return from((item.$links.branch as CallableHalLink)()) .pipe( map((ancestors:CollectionResource) => spansFromAncestors(ancestors)), map((spans) => { diff --git a/frontend/src/app/shared/components/fields/display/field-types/resource-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/resource-display-field.module.ts index 0a92d640779c..813499878617 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/resource-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/resource-display-field.module.ts @@ -27,11 +27,12 @@ //++ import { DisplayField } from 'core-app/shared/components/fields/display/display-field.module'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class ResourceDisplayField extends DisplayField { public get value() { if (this.schema) { - return this.attribute?.name; + return (this.attribute as HalResource|null|undefined)?.name; } return null; } diff --git a/frontend/src/app/shared/components/fields/display/field-types/resources-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/resources-display-field.module.ts index cd9c76f0a915..e2fd6e295aaa 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/resources-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/resources-display-field.module.ts @@ -28,6 +28,7 @@ import { take } from 'lodash-es'; import { cssClassCustomOption, DisplayField } from 'core-app/shared/components/fields/display/display-field.module'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; export class ResourcesDisplayField extends DisplayField { public isEmpty():boolean { @@ -35,7 +36,7 @@ export class ResourcesDisplayField extends DisplayField { } public get stringValue():string[] { - const cf = this.resource[this.name]; + const cf = this.resource[this.name] as { elements?:HalResource[], map?:HalResource[]['map'], name?:string }|null|undefined; if (this.schema && cf) { if (cf.elements) { return cf.elements.map((e:any) => e.name); diff --git a/frontend/src/app/shared/components/fields/display/field-types/work-package-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/work-package-display-field.module.ts index 42c82e2e02eb..de92c385be37 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/work-package-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/work-package-display-field.module.ts @@ -36,7 +36,7 @@ export class WorkPackageDisplayField extends DisplayField { }; public get value() { - return this.resource[this.name]; + return this.resource[this.name] as WorkPackageResource; } public get title() { @@ -55,7 +55,7 @@ export class WorkPackageDisplayField extends DisplayField { return this.value.id; } - return this.value.href.match(/(\d+)$/)[0]; + return /(\d+)$/.exec(this.value.href!)![0]; } /** diff --git a/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts index d88cb6042d9f..55ef2fde4aed 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts @@ -46,8 +46,7 @@ export class WorkPackageIdDisplayField extends IdDisplayField { private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.keepTab, this.currentProject, this.pathHelper, this.urlParams); public get valueString():string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access - return this.resource.displayId ?? this.value?.toString() ?? ''; + return (this.resource.displayId as string|undefined) ?? (this.value as string|number|null|undefined)?.toString() ?? ''; } public render(element:HTMLElement, displayText:string):void { @@ -55,8 +54,7 @@ export class WorkPackageIdDisplayField extends IdDisplayField { return; } const link = this.uiStateBuilder.linkToShow( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.value, + this.value as string, displayText, displayText, this.valueString, diff --git a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts index 28b323d2f5f0..4cce85f3ae02 100644 --- a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts +++ b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts @@ -44,7 +44,6 @@ import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { HalError } from 'core-app/features/hal/services/hal-error'; import { FormResource } from 'core-app/features/hal/resources/form-resource'; import { HalResourceEditFieldHandler } from 'core-app/shared/components/fields/edit/field-handler/hal-resource-edit-field-handler'; -import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; export const activeFieldContainerClassName = 'inline-edit--active-field'; export const activeFieldClassName = 'inline-edit--field'; @@ -323,7 +322,7 @@ export abstract class EditForm { // field. Returning it synchronously lets the field activate without waiting on the // form request — required by Capybara specs whose activate! check has a tight // timeout, and by tests that intentionally disable AJAX before activating a field. - const cachedSchema = (this.change.schema as ISchemaProxy).ofProperty(fieldName); + const cachedSchema = this.change.schema.ofProperty(fieldName); if (cachedSchema) { // Still kick off the form load (or piggy-back on an in-flight one) so the form's // defaults, allowed values, and projected payload are populated for subsequent @@ -344,13 +343,13 @@ export abstract class EditForm { // then retry; if still missing, force a full reload once. return this.change.getForm() .then(():Promise => { - const fieldSchema:IFieldSchema|null = (this.change.schema as ISchemaProxy).ofProperty(fieldName); + const fieldSchema:IFieldSchema|null = this.change.schema.ofProperty(fieldName); if (fieldSchema) { return Promise.resolve(fieldSchema); } return this.change.getForm(true).then( - ():IFieldSchema|null => (this.change.schema as ISchemaProxy).ofProperty(fieldName), + ():IFieldSchema|null => this.change.schema.ofProperty(fieldName), ); }) .catch((error:unknown) => { diff --git a/frontend/src/app/shared/components/fields/edit/field-types/formattable-edit-field/formattable-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/formattable-edit-field/formattable-edit-field.component.ts index 42e980539bde..146c8cd613f6 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/formattable-edit-field/formattable-edit-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field-types/formattable-edit-field/formattable-edit-field.component.ts @@ -64,7 +64,6 @@ export class FormattableEditFieldComponent extends EditFieldComponent implements field: this.field.name, macros: 'none' as const, previewContext: this.previewContext, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access options: { rtl: this.schema.options?.rtl }, type: 'constrained', // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call diff --git a/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts index 3a7f3514cf4e..64bb3e567caa 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts @@ -199,7 +199,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements if (Array.isArray(allowedValues)) { this.setValues(allowedValues); } else if (this.schema.allowedValues) { - return (this.schema.allowedValues.$load() as Promise).then((values:CollectionResource) => { + return ((this.schema.allowedValues as HalResource).$load() as Promise).then((values:CollectionResource) => { // The select options of the project shall be sorted if (values.count > 0 && (values.elements[0] as any)._type === 'Project') { this.setValues(values.elements, true); diff --git a/frontend/src/app/shared/components/fields/edit/field-types/project-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/project-edit-field.component.ts index 2fc92e38dcfa..f579817dfb45 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/project-edit-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field-types/project-edit-field.component.ts @@ -38,6 +38,7 @@ import { HalResourceService } from 'core-app/features/hal/services/hal-resource. import { take, tap } from 'rxjs/operators'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { IAPIFilter } from 'core-app/shared/components/autocompleter/op-autocompleter/typings'; import { FilterOperator } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; @@ -102,7 +103,6 @@ export class ProjectEditFieldComponent extends EditFieldComponent implements OnI } private setUrl():void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.url = this.schema.allowedValues.$link.href as string; + this.url = (this.schema.allowedValues as CallableHalLink).$link.href!; } } diff --git a/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts index 7a30c6b1eadf..caadf3f5eac5 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts @@ -41,7 +41,8 @@ import { CollectionResource } from 'core-app/features/hal/resources/collection-r import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { HalResourceSortingService } from 'core-app/features/hal/services/hal-resource-sorting.service'; import { EditFieldComponent } from '../../edit-field.component'; -import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; +import { EventHandler } from 'ng-dynamic-component'; export interface ValueOption { name:string; @@ -66,7 +67,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn public appendTo:any = null; - public referenceOutputs:Record = { + public referenceOutputs:Record = { onCreate: (newElement:HalResource) => this.onCreate(newElement), onChange: (value:HalResource) => this.onChange(value), onAddNew: (value:HalResource) => this.onNewValueAdded(value), @@ -182,7 +183,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn protected loadAllowedValues(query?:string):Promise { // Cache the search without any params if (!query) { - const cacheKey = this.schema.allowedValues.$link.href; + const cacheKey = (this.schema.allowedValues as CallableHalLink).$link.href!; return this.change.cacheValue(cacheKey, this.fetchAllowedValueQuery.bind(this)); } @@ -190,8 +191,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn } protected fetchAllowedValueQuery(query?:string):Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const link = this.schema.allowedValues?.$link as HalLink|undefined; + const link = (this.schema.allowedValues as CallableHalLink|undefined)?.$link; // Race condition: Field was under edit but is no longer editable / values not loadable // which means the schema switched during the period it opened / updated after saved. @@ -199,7 +199,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn return new Promise(() => {}); } - return link.$fetch(this.allowedValuesFilter(query)) as Promise; + return link.$fetch(this.allowedValuesFilter(query)); } private addValue(val:HalResource) { diff --git a/frontend/src/app/shared/components/fields/edit/field-types/work-package-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/work-package-edit-field.component.ts index bd898c98d2db..0e6c9d7f4123 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/work-package-edit-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field-types/work-package-edit-field.component.ts @@ -28,6 +28,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { DebouncedRequestSwitchmap, errorNotificationHandler, @@ -72,11 +73,10 @@ export class WorkPackageEditFieldComponent extends SelectEditFieldComponent { protected fetchAllowedValueQuery(query?:string):Promise { if (this.name === 'parent') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - return this.schema.allowedValues.$link.$fetch({ + return (this.schema.allowedValues as CallableHalLink).$link.$fetch({ query, sortBy: '[["exactMatch","desc"],["updatedAt","desc"]]', - }) as Promise; + }); } return super.fetchAllowedValueQuery(query); diff --git a/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts b/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts index 108d00a843fb..46b6d370075a 100644 --- a/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts +++ b/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts @@ -46,7 +46,7 @@ import { States } from 'core-app/core/states/states.service'; import { debugLog } from '../../../../helpers/debug_output'; import { hasSelectionWithin } from '../../../../helpers/selection-helpers'; import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing-portal/edit-field-handler'; -import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; @Component({ selector: 'op-editable-attribute-field', @@ -151,7 +151,6 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme } public get isEditable():boolean { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call return !!(this.editForm && this.schema.isAttributeEditable(this.fieldName)); } @@ -246,7 +245,7 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme private get schema() { if (this.halEditing.typedState(this.resource).hasValue()) { - const val = this.halEditing.typedState(this.resource).value as { schema:SchemaResource }; + const val = this.halEditing.typedState(this.resource).value as { schema:ISchemaProxy }; return val.schema; } diff --git a/frontend/src/app/shared/components/fields/edit/services/hal-resource-editing.service.ts b/frontend/src/app/shared/components/fields/edit/services/hal-resource-editing.service.ts index 67e75b5ee726..95175d162bb4 100644 --- a/frontend/src/app/shared/components/fields/edit/services/hal-resource-editing.service.ts +++ b/frontend/src/app/shared/components/fields/edit/services/hal-resource-editing.service.ts @@ -40,6 +40,7 @@ import { HookService } from 'core-app/features/plugins/hook-service'; import { HalEventsService } from 'core-app/features/hal/services/hal-events.service'; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; class ChangesetStates extends StatesGroup { name = 'Changesets'; @@ -106,7 +107,7 @@ export class HalResourceEditingService extends StateCacheService>(change:T):Promise> { // Form the payload we're going to save const payload = await change.buildRequestPayload(); - const savedResource = await change.pristineResource.$links.updateImmediately(payload); + const savedResource = await (change.pristineResource.$links.updateImmediately as CallableHalLink)(payload) as V; // Initialize any potentially new HAL values savedResource.retainFrom(change.pristineResource); @@ -198,7 +199,7 @@ export class HalResourceEditingService extends StateCacheService(resource); } - if (Object.hasOwn(resource, 'lockVersion') && changeset.pristineResource.lockVersion < resource.lockVersion) { + if (Object.hasOwn(resource, 'lockVersion') && (changeset.pristineResource.lockVersion as number) < (resource.lockVersion as number)) { return this.edit(resource); } diff --git a/frontend/src/app/shared/components/fields/field.base.ts b/frontend/src/app/shared/components/fields/field.base.ts index d71ee7a17132..37d197eee7d0 100644 --- a/frontend/src/app/shared/components/fields/field.base.ts +++ b/frontend/src/app/shared/components/fields/field.base.ts @@ -28,15 +28,11 @@ import { DisplayFieldContext } from 'core-app/shared/components/fields/display/display-field.service'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; -export interface IFieldSchema { - type:string; - writable:boolean; - allowedValues?:any; - required?:boolean; - hasDefault:boolean; +export interface IFieldSchema extends Omit { name:string; - options?:any; + options?:IOPFieldSchema['options']; placeholder?:string; } @@ -75,7 +71,7 @@ export class Field extends UntilDestroyedMixin { return this.schema.hasDefault; } - public get options():boolean { + public get options():IFieldSchema['options'] { return this.schema.options; } diff --git a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text-edit-field.service.ts b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text-edit-field.service.ts index 1d1d7b67846e..59b37c2e4bab 100644 --- a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text-edit-field.service.ts +++ b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text-edit-field.service.ts @@ -37,7 +37,7 @@ import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/r import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { ICKEditorContext } from 'core-app/shared/components/editor/components/ckeditor/ckeditor.types'; import { GridResource } from 'core-app/features/hal/resources/grid-resource'; -import { HalSource } from 'core-app/features/hal/interfaces'; +import { HalSource, HalSourceLink } from 'core-app/features/hal/interfaces'; @Injectable() export class CustomTextEditFieldService extends EditFieldHandler { @@ -175,7 +175,7 @@ export class CustomTextEditFieldService extends EditFieldHandler { canAddAttachments: value.grid.canAddAttachments as boolean, _links: { addAttachment: grid.addAttachment as { href?:string }, - attachments: grid.attachments as { href?:string }, + attachments: grid.attachments, schema: { href: schemaHref, }, @@ -183,8 +183,7 @@ export class CustomTextEditFieldService extends EditFieldHandler { }; if (grid.prepareAttachment as { href?:string }) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - resourceSource._links.prepareAttachment = grid.prepareAttachment; + resourceSource._links.prepareAttachment = grid.prepareAttachment as HalSourceLink; } const resource = this.halResource.createHalResource(resourceSource, true); 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 a888f1f795f6..fd2453663860 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 @@ -149,7 +149,7 @@ export class WidgetCustomTextComponent extends AbstractWidgetComponent implement } private memorizeRawText() { - this.currentRawText = (this.resource.options.text as HalResource).raw; + this.currentRawText = (this.resource.options.text as HalResource).raw as string; } private memorizeCustomText() { diff --git a/frontend/src/app/shared/components/grids/widgets/documents/documents.component.ts b/frontend/src/app/shared/components/grids/widgets/documents/documents.component.ts index b36f6d1ce432..e08e7fb580bb 100644 --- a/frontend/src/app/shared/components/grids/widgets/documents/documents.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/documents/documents.component.ts @@ -81,11 +81,11 @@ export class WidgetDocumentsComponent extends AbstractWidgetComponent implements } public documentCreated(document:DocumentResource) { - return this.timezone.formattedDatetime(document.createdAt); + return this.timezone.formattedDatetime(document.createdAt as string); } public documentDescription(document:DocumentResource) { - return this.domSanitizer.sanitize(SecurityContext.HTML, document.description.html); + return this.domSanitizer.sanitize(SecurityContext.HTML, (document.description as { html:string }).html); } public get noEntries() { 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 b29124165e09..03a3e812ae19 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 @@ -85,7 +85,7 @@ export class WpButtonMacroModalComponent extends OpModalComponent implements Aft .form .post({}) .subscribe((form:FormResource) => { - this.availableTypes = form.schema.type.allowedValues; + this.availableTypes = form.schema.type!.allowedValues as TypeResource[]; this.cdRef.detectChanges(); }); } diff --git a/frontend/src/app/shared/components/modals/share-modal/query-sharing-form.component.ts b/frontend/src/app/shared/components/modals/share-modal/query-sharing-form.component.ts index 58efd78e4ef3..e1afed0d19dc 100644 --- a/frontend/src/app/shared/components/modals/share-modal/query-sharing-form.component.ts +++ b/frontend/src/app/shared/components/modals/share-modal/query-sharing-form.component.ts @@ -80,7 +80,7 @@ export class QuerySharingFormComponent { const form = this.querySpace.queryForm.value!; return this.authorisationService.can('query', 'updateImmediately') - && form.schema.public.writable; + && form.schema.public!.writable; } public updateStarred(val:boolean) { 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 a23d892f3a32..afb300cefb3d 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 @@ -350,7 +350,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements Aft { // Settings modal hidden: !this.query.results.customFields || this.hideTableOptions, - href: this.query.results.customFields?.href, + href: this.query.results.customFields?.href as string|undefined, linkText: this.query.results.customFields?.name, icon: 'icon-custom-fields', onClick: () => false, diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts index 44c786402169..d4f64e36ddc9 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts @@ -75,8 +75,7 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger implements .wpCreate .getEmptyForm(this.projectIdentifier) .then((form) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.buildItems(form.schema.type.allowedValues as TypeResource[]); + this.buildItems(form.schema.type!.allowedValues as TypeResource[]); this.opContextMenu.show(this, evt); }); } else { diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts index 17943fb47409..9f70b138fd86 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts @@ -64,7 +64,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger if (queryCustomFields) { this.items.push({ - href: queryCustomFields.href, + href: queryCustomFields.href as string|undefined, icon: 'icon-custom-fields', linkText: queryCustomFields.name, onClick: () => false, @@ -73,7 +73,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger if (configureFormLink) { this.items.push({ - href: configureFormLink.href, + href: configureFormLink.href as string|undefined, icon: 'icon-settings3', linkText: configureFormLink.name, onClick: () => false, diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts index f7ffb1373fbe..73d65ad5adda 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts @@ -36,7 +36,7 @@ import { } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; +import { StatusResource } from 'core-app/features/hal/resources/status-resource'; import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions'; @@ -48,6 +48,7 @@ import { } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { HalError } from 'core-app/features/hal/services/hal-error'; +import { IOPFieldSchema } from 'core-app/features/hal/interfaces'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector @@ -67,11 +68,11 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger { protected open(evt:Event) { const change = this.halEditing.changeFor(this.workPackage); - change.getForm().then((form:any) => { - const statuses = form.schema.status.allowedValues; + void change.getForm().then((form) => { + const statuses = form.schema.status!.allowedValues as HalResource[]; this.buildItems(statuses); - const { writable } = change.schema.status; + const { writable } = change.schema.status as IOPFieldSchema; if (!writable) { this.toastService.addError(this.I18n.t('js.work_packages.message_work_package_status_blocked')); } else { @@ -89,7 +90,7 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger { private updateStatus(status:HalResource) { const change = this.halEditing.changeFor(this.workPackage); - change.projectedResource.status = status; + change.projectedResource.status = status as StatusResource; if (!isNewResource(this.workPackage)) { this.halEditing @@ -98,12 +99,12 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger { this.workPackageNotificationService.showSave(this.workPackage); }) .catch((e:unknown) => { - this.workPackageNotificationService.showError((e as HalError).resource, change.projectedResource); + this.workPackageNotificationService.showError((e as HalError).resource!, change.projectedResource); }); } } - private buildItems(statuses:CollectionResource) { + private buildItems(statuses:HalResource[]) { this.items = statuses.map((status:HalResource) => ({ disabled: false, linkText: status.name, diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts index 4735c26d7e45..0fbdc6d73381 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts @@ -94,7 +94,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger } protected open(evt:Event) { - this.workPackage.project.$load().then(() => { + void this.workPackage.project.$load().then(() => { this.authorisationService.initModelAuth('work_package', this.workPackage.$links); const authorization = new WorkPackageAuthorization(this.workPackage, this.PathHelper); @@ -125,7 +125,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger break; case 'copy': if (this.workPackage.id) { - window.location.href = `${this.PathHelper.workPackageCopyPath(this.workPackage.project.identifier, this.workPackage.id)}`; + window.location.href = `${this.PathHelper.workPackageCopyPath(this.workPackage.project.identifier as string|null, this.workPackage.id)}`; } break; case 'delete': { @@ -211,7 +211,6 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger } protected buildItems(permittedActions:WorkPackageAction[]):OpContextMenuItem[] { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const configureFormLink = this.workPackage.configureForm; this.items = permittedActions.map((action:WorkPackageAction) => { @@ -243,7 +242,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger if (configureFormLink) { this.items.push( { - href: configureFormLink.href, + href: configureFormLink.href as string|undefined, icon: 'icon-settings3', linkText: I18n.t('js.button_configure-form'), onClick: () => false, diff --git a/frontend/src/app/shared/components/primer/dynamic-icon.directive.spec.ts b/frontend/src/app/shared/components/primer/dynamic-icon.directive.spec.ts index 6b601814637d..2999b557a663 100644 --- a/frontend/src/app/shared/components/primer/dynamic-icon.directive.spec.ts +++ b/frontend/src/app/shared/components/primer/dynamic-icon.directive.spec.ts @@ -26,8 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any */ - import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DynamicIconDirective } from './dynamic-icon.directive'; diff --git a/frontend/src/app/shared/components/primer/icon-button.component.spec.ts b/frontend/src/app/shared/components/primer/icon-button.component.spec.ts index 0f2720b8fbe8..362fc510ab12 100644 --- a/frontend/src/app/shared/components/primer/icon-button.component.spec.ts +++ b/frontend/src/app/shared/components/primer/icon-button.component.spec.ts @@ -26,8 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PrimerIconButtonComponent } from './icon-button.component'; diff --git a/frontend/src/app/shared/components/storages/storage/storage.component.ts b/frontend/src/app/shared/components/storages/storage/storage.component.ts index 9b03dafbc890..95a7da1f1ce8 100644 --- a/frontend/src/app/shared/components/storages/storage/storage.component.ts +++ b/frontend/src/app/shared/components/storages/storage/storage.component.ts @@ -62,7 +62,6 @@ import { OpModalService } from 'core-app/shared/components/modal/modal.service'; import { FilePickerModalComponent, } from 'core-app/shared/components/storages/file-picker-modal/file-picker-modal.component'; -import { IHalResourceLink } from 'core-app/core/state/hal-resource'; import { LocationPickerModalComponent, } from 'core-app/shared/components/storages/location-picker-modal/location-picker-modal.component'; @@ -93,6 +92,7 @@ import { import { IHalErrorBase, v3ErrorIdentifierMissingEnterpriseToken, } from 'core-app/features/hal/resources/error-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; @Component({ selector: 'op-storage', @@ -191,7 +191,7 @@ export class StorageComponent extends UntilDestroyedMixin implements OnInit, OnD return this.pathHelperService.fileLinksPath(); } - return (this.resource.$links as { addFileLink:IHalResourceLink }).addFileLink.href; + return (this.resource.$links.addFileLink as CallableHalLink).href!; } private onGlobalDragLeave:(_event:DragEvent) => void = (_event) => { @@ -533,7 +533,7 @@ export class StorageComponent extends UntilDestroyedMixin implements OnInit, OnD private fileLinkSelfLink(storage:IStorage):string { const fileLinks = (this.resource as WorkPackageResource).$links.fileLinks; - return `${fileLinks?.href}?pageSize=-1&filters=[{"storage":{"operator":"=","values":["${storage.id}"]}}]`; + return `${(fileLinks as CallableHalLink|undefined)?.href}?pageSize=-1&filters=[{"storage":{"operator":"=","values":["${storage.id}"]}}]`; } public onDropFiles(event:DragEvent):void { 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..2a271beacd56 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 @@ -112,9 +112,9 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges { }, []))) 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, + [String(group.value)]: group.count, }), {}); return { @@ -242,7 +242,7 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges { private setHeight() { if (this.chartType === 'horizontalBar' && this.datasets?.[0]) { - const labels:string[] = []; + const labels:(string|null)[] = []; this.datasets.forEach((d) => { d.groups!.forEach((g) => { if (!labels.includes(g.value)) { labels.push(g.value); diff --git a/frontend/src/stimulus/controllers/checkable.controller.spec.ts b/frontend/src/stimulus/controllers/checkable.controller.spec.ts index 473e2081b203..891a796b3b42 100644 --- a/frontend/src/stimulus/controllers/checkable.controller.spec.ts +++ b/frontend/src/stimulus/controllers/checkable.controller.spec.ts @@ -26,7 +26,7 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-empty-function */ import { ActionEvent } from '@hotwired/stimulus'; import CheckableController from './checkable.controller'; diff --git a/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.spec.ts b/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.spec.ts index 1690872b2587..0bc4003eb67c 100644 --- a/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.spec.ts +++ b/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.spec.ts @@ -26,8 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import PageController from './page.controller'; describe('Reporting PageController serialization', () => { diff --git a/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.ts b/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.ts index 73dd9d7267ca..743df61d9a50 100644 --- a/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/reporting/page.controller.ts @@ -207,7 +207,6 @@ export default class PageController extends Controller { jQuery.metadata = undefined; // Override the default texts to enable translations - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access jQuery.tablesorter.language = { sortAsc: I18n.t('js.sort.sorted_asc'), sortDesc: I18n.t('js.sort.sorted_dsc'), @@ -218,7 +217,6 @@ export default class PageController extends Controller { nextNone: I18n.t('js.sort.activate_no'), }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call jQuery('#sortable-table') .not('.tablesorter') .tablesorter({ diff --git a/frontend/src/stimulus/controllers/expandable-text.controller.spec.ts b/frontend/src/stimulus/controllers/expandable-text.controller.spec.ts index 69990742f869..35700ed355d2 100644 --- a/frontend/src/stimulus/controllers/expandable-text.controller.spec.ts +++ b/frontend/src/stimulus/controllers/expandable-text.controller.spec.ts @@ -26,8 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ - import ExpandableTextController from './expandable-text.controller'; import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test-helpers'; diff --git a/frontend/src/test-setup.ts b/frontend/src/test-setup.ts index 2c83fff6175c..824a3b4d93ae 100644 --- a/frontend/src/test-setup.ts +++ b/frontend/src/test-setup.ts @@ -43,7 +43,6 @@ afterEach(() => { vi.useRealTimers(); }); -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (window as any).global = window; window.I18n = new I18n(); @@ -51,9 +50,7 @@ window.I18n = new I18n(); // jsdom does not implement CSS.escape; production helpers (e.g. getMetaElement) // call it unconditionally. if (typeof CSS === 'undefined' || typeof CSS.escape !== 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).CSS = (globalThis as any).CSS || {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (globalThis as any).CSS.escape = (value:string) => String(value).replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`); } @@ -61,7 +58,6 @@ if (typeof CSS === 'undefined' || typeof CSS.escape !== 'function') { // constructor signature so static analysis resolving the global to this class // still accepts the callback every real call site passes. if (typeof (globalThis as any).ResizeObserver === 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).ResizeObserver = class { // eslint-disable-next-line @typescript-eslint/no-empty-function constructor(_callback:ResizeObserverCallback) {} @@ -73,7 +69,6 @@ if (typeof (globalThis as any).ResizeObserver === 'undefined') { // jsdom does not implement HTMLDialogElement.showModal/close. if (typeof HTMLDialogElement !== 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const proto = HTMLDialogElement.prototype as any; if (typeof proto.showModal !== 'function') { proto.showModal = function showModal() { this.open = true; }; diff --git a/frontend/src/turbo/turbo-navigation-patch.spec.ts b/frontend/src/turbo/turbo-navigation-patch.spec.ts index 432bf784fc7d..5f4a4593370d 100644 --- a/frontend/src/turbo/turbo-navigation-patch.spec.ts +++ b/frontend/src/turbo/turbo-navigation-patch.spec.ts @@ -26,7 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -/* eslint-disable @typescript-eslint/no-explicit-any */ import * as Turbo from '@hotwired/turbo'; import { applyTurboNavigationPatch } from './turbo-navigation-patch'; diff --git a/frontend/src/typings/open-project.typings.d.ts b/frontend/src/typings/open-project.typings.d.ts index cbffc738fbc8..7eaf7b4b28d6 100644 --- a/frontend/src/typings/open-project.typings.d.ts +++ b/frontend/src/typings/open-project.typings.d.ts @@ -38,24 +38,6 @@ declare namespace api { * API v3 */ namespace v3 { - interface Result { - _links:any; - _embedded:any; - _type:string; - } - - interface Collection extends Result { - total:number; - pageSize:number; - count:number; - offset:number; - groups:any; - totalSums:any; - } - - interface Duration extends String { - } - interface Formattable { format?:string; raw:string; @@ -69,20 +51,7 @@ declare namespace api { */ interface Function { - $link?:any; name:string; _type:string; } -declare let Factory:any; - -declare namespace op { - interface QueryParams { - offset?:number; - pageSize?:number; - filters?:any[]; - groupBy?:string; - showSums?:boolean; - sortBy?:any[]; - } -} diff --git a/frontend/src/typings/shims.d.ts b/frontend/src/typings/shims.d.ts index ef00c32df1f8..f223098f11c5 100644 --- a/frontend/src/typings/shims.d.ts +++ b/frontend/src/typings/shims.d.ts @@ -69,12 +69,12 @@ declare global { } interface JQuery { - tablesorter:any; + tablesorter(options:object):JQuery; } interface JQueryStatic { - metadata:any; - tablesorter:any; + metadata:unknown; + tablesorter:{ language:Record }; } } diff --git a/modules/budgets/frontend/module/hal/resources/budget-resource.ts b/modules/budgets/frontend/module/hal/resources/budget-resource.ts index e0d49fcb961b..d43b1ab5b339 100644 --- a/modules/budgets/frontend/module/hal/resources/budget-resource.ts +++ b/modules/budgets/frontend/module/hal/resources/budget-resource.ts @@ -26,15 +26,16 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from "core-app/features/hal/resources/hal-resource"; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { Attachable } from "core-app/features/hal/resources/mixins/attachable-mixin"; export interface BudgetResourceLinks { - addAttachment(attachment:HalResource):Promise; + addAttachment:CallableHalLink; } class BudgetBaseResource extends HalResource { - public $links:BudgetResourceLinks; + public $links:BudgetResourceLinks & HalResourceLinks; } export const BudgetResource = Attachable(BudgetBaseResource); diff --git a/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts b/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts index baa19c16d893..3cdf3fcb9aeb 100644 --- a/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts +++ b/modules/costs/frontend/module/wp-display/costs-by-type-display-field.module.ts @@ -30,6 +30,8 @@ import { DisplayField } from "core-app/shared/components/fields/display/display- import { IFieldSchema } from "core-app/shared/components/fields/field.base"; import { LazyInject } from "core-app/shared/helpers/angular/lazy-inject.decorator"; import { ApiV3Service } from "core-app/core/apiv3/api-v3.service"; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; interface ICostsByType { costObjectId:string; @@ -53,8 +55,8 @@ export class CostsByTypeDisplayField extends DisplayField { } protected loadIfNecessary() { - if (this.value && this.value.$loaded === false) { - this.value.$load().then(() => { + if (this.value && (this.value as HalResource).$loaded === false) { + (this.value as HalResource).$load().then(() => { if (this.resource.$source._type === 'WorkPackage') { this @@ -77,7 +79,7 @@ export class CostsByTypeDisplayField extends DisplayField { return; } - this.value.elements.forEach((val:ICostsByType, i:number) => { + (this.value as CollectionResource).elements.forEach((val:ICostsByType, i:number) => { if (this.resource.showCosts) { this.renderCostAsLink(val, element, i); } else { @@ -88,8 +90,8 @@ export class CostsByTypeDisplayField extends DisplayField { public isEmpty():boolean { return !this.value || - !this.value.elements || - this.value.elements.length === 0; + !(this.value as CollectionResource).elements || + (this.value as CollectionResource).elements.length === 0; } @@ -100,7 +102,7 @@ export class CostsByTypeDisplayField extends DisplayField { const showCosts = this.resource.showCosts; const link = document.createElement('a') as HTMLAnchorElement; - link.href = showCosts.href + '&unit=' + val.costType.id; + link.href = (showCosts as { href:string }).href + '&unit=' + val.costType.id; link.setAttribute('target', '_blank'); link.textContent = val.spentUnits + ' ' + val.costType.name; element.appendChild(link); @@ -119,7 +121,7 @@ export class CostsByTypeDisplayField extends DisplayField { } private addSeparator(element:HTMLElement, i:number) { - if (i < this.value.elements.length - 1) { + if (i < (this.value as CollectionResource).elements.length - 1) { const sep = document.createElement('span'); sep.textContent = ', '; diff --git a/modules/costs/frontend/module/wp-display/currency-display-field.module.ts b/modules/costs/frontend/module/wp-display/currency-display-field.module.ts index 319996ba8ee7..e7b0c5ca42c6 100644 --- a/modules/costs/frontend/module/wp-display/currency-display-field.module.ts +++ b/modules/costs/frontend/module/wp-display/currency-display-field.module.ts @@ -32,7 +32,7 @@ export class CurrencyDisplayField extends DisplayField { public isEmpty():boolean { return !this.value || - !parseFloat(this.value.match(/\d+/g)[0]); + !parseFloat((this.value as string).match(/\d+/g)![0]); } } diff --git a/modules/documents/frontend/module/hal/resources/document-resource.ts b/modules/documents/frontend/module/hal/resources/document-resource.ts index e11be7f691f0..32ad6a5af9c1 100644 --- a/modules/documents/frontend/module/hal/resources/document-resource.ts +++ b/modules/documents/frontend/module/hal/resources/document-resource.ts @@ -28,15 +28,16 @@ // This resource exists solely for the purpose of uploading attachments via the // WYSIWYIG editor. -import { HalResource } from "core-app/features/hal/resources/hal-resource"; +import { HalResource, HalResourceLinks } from 'core-app/features/hal/resources/hal-resource'; +import { CallableHalLink } from 'core-app/features/hal/hal-link/hal-link'; import { Attachable } from "core-app/features/hal/resources/mixins/attachable-mixin"; export interface DocumentResourceLinks { - addAttachment(attachment:HalResource):Promise; + addAttachment:CallableHalLink; } class DocumentBaseResource extends HalResource { - public $links:DocumentResourceLinks; + public $links:DocumentResourceLinks & HalResourceLinks; private attachmentsBackend = false; } diff --git a/modules/gitlab_integration/frontend/module/tab-issue/wp-gitlab-issue.service.ts b/modules/gitlab_integration/frontend/module/tab-issue/wp-gitlab-issue.service.ts index 6fe7d7771177..26696df75c9d 100644 --- a/modules/gitlab_integration/frontend/module/tab-issue/wp-gitlab-issue.service.ts +++ b/modules/gitlab_integration/frontend/module/tab-issue/wp-gitlab-issue.service.ts @@ -40,7 +40,7 @@ export class WorkPackagesGitlabIssueService extends WorkPackageLinkedResourceCac protected load(workPackage:WorkPackageResource):Promise { - return workPackage.gitlab_issues.$update().then((data:any) => { + return (workPackage.gitlab_issues as HalResource).$update().then((data:any) => { return this.sortList(data.elements); }); } diff --git a/modules/gitlab_integration/frontend/module/tab-mrs/wp-gitlab-mrs.service.ts b/modules/gitlab_integration/frontend/module/tab-mrs/wp-gitlab-mrs.service.ts index 7035c20a9c59..a661b5c501e8 100644 --- a/modules/gitlab_integration/frontend/module/tab-mrs/wp-gitlab-mrs.service.ts +++ b/modules/gitlab_integration/frontend/module/tab-mrs/wp-gitlab-mrs.service.ts @@ -40,7 +40,7 @@ export class WorkPackagesGitlabMrsService extends WorkPackageLinkedResourceCache protected load(workPackage:WorkPackageResource):Promise { - return workPackage.gitlab_merge_requests.$update().then((data:any) => { + return (workPackage.gitlab_merge_requests as HalResource).$update().then((data:any) => { return this.sortList(data.elements); }); }