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/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/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/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..1d57837e33f7 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 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..f76723b91cf7 100644 --- a/frontend/src/app/features/hal/resources/form-resource.ts +++ b/frontend/src/app/features/hal/resources/form-resource.ts @@ -29,21 +29,32 @@ 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 getErrors():ErrorResource|null { const errors = Object.values(this.validationErrors ?? {}); const count = errors.length; @@ -64,5 +75,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/hal-resource.spec.ts b/frontend/src/app/features/hal/resources/hal-resource.spec.ts index 3efa51c88f84..baa907079f70 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'; @@ -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', () => { @@ -426,7 +426,7 @@ describe('HalResource', () => { }); 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', () => { @@ -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', () => { @@ -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); }); }; @@ -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) { /**/ diff --git a/frontend/src/app/features/hal/resources/hal-resource.ts b/frontend/src/app/features/hal/resources/hal-resource.ts index 6294e91cbf75..c678d6d503f9 100644 --- a/frontend/src/app/features/hal/resources/hal-resource.ts +++ b/frontend/src/app/features/hal/resources/hal-resource.ts @@ -32,21 +32,28 @@ 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 @@ -87,23 +94,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 +123,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 +152,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 +185,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). @@ -269,7 +283,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..90210741d102 100644 --- a/frontend/src/app/features/hal/resources/query-form-resource.ts +++ b/frontend/src/app/features/hal/resources/query-form-resource.ts @@ -28,12 +28,17 @@ import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { FormResource } 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 { 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..3406d0269bf5 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,31 @@ 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; } -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 +132,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 +153,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; @@ -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..eab07b4dccea 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,8 +56,6 @@ export class WorkPackageCollectionResource extends CollectionResource ({ href: element.href })); } else { payload._links[key] = { - href: (resource[key]?.href), + href: (resource[key] as HalResource|undefined)?.href, }; } } else { 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/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..5d1db60a4504 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 @@ -740,7 +740,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 +866,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(); } @@ -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/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..d085ddb54e5c 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'; @@ -223,7 +222,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) { 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/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..310f02cf2869 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,7 +28,7 @@ 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'; @@ -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-list/wp-list.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts index 024f8808e6c5..de5a53c4c696 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 }; @@ -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-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-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/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-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/wp-single-view.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts index 24fdc530d35f..9e453b3c9eb3 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,7 +51,6 @@ 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'; @@ -208,7 +207,7 @@ 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) { 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/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..aadad6577512 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 { @@ -191,7 +191,7 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { // 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 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/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/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/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 34ef43e5c462..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,7 @@ 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 { @@ -183,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)); } @@ -191,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. @@ -200,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/services/hal-resource-editing.service.ts b/frontend/src/app/shared/components/fields/edit/services/hal-resource-editing.service.ts index 67e75b5ee726..da9e543abd08 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); 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..87f2222b2399 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 @@ -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, }, 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/wp-create-settings-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts index 17943fb47409..4ca70334471c 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 @@ -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..fb4f4d2097b6 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,6 +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 { StatusResource } from 'core-app/features/hal/resources/status-resource'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { Highlighting @@ -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,7 +99,7 @@ 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); }); } } 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..2da4106139d1 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); @@ -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/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/typings/open-project.typings.d.ts b/frontend/src/typings/open-project.typings.d.ts index e5afbccd2d39..7eaf7b4b28d6 100644 --- a/frontend/src/typings/open-project.typings.d.ts +++ b/frontend/src/typings/open-project.typings.d.ts @@ -51,7 +51,6 @@ declare namespace api { */ interface Function { - $link?:any; name:string; _type:string; } 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/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; }