Merge pull request #23256 from opf/fix/eslint-whitespace-errors

Fix auto-correctable eslint errors in `frontend/`
This commit is contained in:
Alexander Brandon Coles
2026-05-29 11:32:14 +02:00
committed by GitHub
67 changed files with 208 additions and 166 deletions
@@ -42,7 +42,7 @@ import {
states: OPENPROJECT_ROUTES,
useHash: false,
config: uiRouterConfiguration,
} as any),
}),
],
providers: [
FirstRouteService,
@@ -37,7 +37,7 @@ export function getMetaContent<T extends string|null>(
defaultValue?:T
):string|T {
const content = getMetaElement(name)?.content ?? defaultValue ?? '';
return content as string|T;
return content;
}
export function getMetaValue(name:string, key:string):string;
@@ -48,5 +48,5 @@ export function getMetaValue<T extends string|null>(
defaultValue?:T
):string|T {
const value = getMetaElement(name)?.dataset[key] ?? defaultValue ?? '';
return value as string|T;
return value;
}
@@ -195,7 +195,7 @@ export class AttachmentsResourceService extends ResourceStoreService<IAttachment
}
if (isNewResource(resource)) {
return this.configurationService.prepareAttachmentURL as string|null;
return this.configurationService.prepareAttachmentURL;
}
return null;
@@ -54,7 +54,7 @@ export class EditableQueryPropsComponent implements OnInit {
})();
this.externalQuery.show({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
currentQuery: queryProperties,
urlParams: this.urlParams,
callback: (queryProps:string) => {
@@ -42,7 +42,7 @@ import { IfcModelsDataService } from 'core-app/features/bim/ifc_models/pages/vie
aria-hidden="true"></span>
</a>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'op-bcf-manage-ifc-button',
@@ -184,7 +184,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
this.resource.isNewWidget = false;
// Set initial selection if split view open
const detailsMatch = window.location.pathname.match(/\/details\/(\d+)/);
const detailsMatch = /\/details\/(\d+)/.exec(window.location.pathname);
if (detailsMatch) {
this.wpViewSelectionService.initializeSelection([detailsMatch[1]]);
}
@@ -171,7 +171,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
let queryId:string|null = null;
if (this.urlParams.query_id) {
queryId = this.urlParams.query_id as string;
queryId = this.urlParams.query_id;
}
// We derive the necessary props in the following cases
// 1. We load a queryId with no props
@@ -199,7 +199,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
// There might also be a query_id but the settings persisted in it are overwritten by the props.
if (this.urlParams.query_props) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const oldQueryProps:Record<string, unknown> = JSON.parse(this.urlParams.query_props as string);
const oldQueryProps:Record<string, unknown> = JSON.parse(this.urlParams.query_props);
// Update the date period of the calendar in the filter
const newQueryProps = {
@@ -254,7 +254,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
}
public get initialView():string|undefined {
return this.urlParams.cview as string|undefined;
return this.urlParams.cview;
}
dateEditable(wp:WorkPackageResource):boolean {
@@ -425,7 +425,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
}
private get initialDate():string|undefined {
const date = this.urlParams.cdate as string|undefined;
const date = this.urlParams.cdate;
if (date) {
return this.timezoneService.formattedISODate(date);
}
@@ -239,6 +239,7 @@ export class TimeEntryCalendarComponent implements AfterViewInit, OnDestroy {
}
protected fetchTimeEntries(start:Moment, end:Moment):Promise<CollectionResource<TimeEntryResource>> {
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (!this.memoizedTimeEntries
|| this.memoizedTimeEntries.start.valueOf() !== start.valueOf()
|| this.memoizedTimeEntries.end.valueOf() !== end.valueOf()) {
@@ -217,7 +217,7 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
allDay: false,
className: 'fc-event-clickable op-wp-calendar--meeting-resource',
meeting,
} as EventInput;
};
});
successCallback(events);
@@ -262,7 +262,7 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
],
// DnD configuration
selectable: true,
select: this.handleDateClicked.bind(this) as unknown,
select: this.handleDateClicked.bind(this),
eventResizableFromStart: true,
editable: true,
displayEventTime: true,
@@ -538,7 +538,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
resources: skeletonResources,
resourceAreaWidth: this.isMobile ? '60px' : '180px',
resourceOrder: 'title',
select: this.handleDateClicked.bind(this) as unknown,
select: this.handleDateClicked.bind(this),
// DnD configuration
editable: true,
droppable: true,
@@ -60,13 +60,13 @@ export class FilterDateTimeValueComponent extends AbstractDateTimeValueControlle
return this.filter.values[0];
}
public get valueString() {
return this.filter.values[0].toString();
public set value(val) {
this.filter.values = [val as string]; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
this.filterChanged.emit(this.filter);
}
public set value(val) {
this.filter.values = [val as string];
this.filterChanged.emit(this.filter);
public get valueString() {
return this.filter.values[0].toString();
}
public get lowerBoundary():Moment|null {
@@ -135,12 +135,12 @@ describe('WorkPackageFilterValues', () => {
setupTestBed();
});
it('it should not apply the first value (Regression #30817)', (() => {
it('should not apply the first value (Regression #30817)', () => {
subject.applyDefaultsFromFilters(changeset);
expect(changeset.changedAttributes.length).toEqual(0);
expect(changeset.value<HalResource>('type').href).toEqual('/api/v3/types/1');
}));
});
});
describe('with the second type applied', () => {
@@ -158,12 +158,12 @@ describe('WorkPackageFilterValues', () => {
setupTestBed();
});
it('it should not keep the second value (Regression #30817)', (() => {
it('should not keep the second value (Regression #30817)', () => {
subject.applyDefaultsFromFilters(changeset);
expect(changeset.changedAttributes.length).toEqual(0);
expect(changeset.value<HalResource>('type').href).toEqual('/api/v3/types/2');
}));
});
});
});
});
@@ -50,7 +50,9 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
// Locate the details link from event
const target = evt.target as HTMLElement;
const element = target.closest(this.SELECTOR) as HTMLElement;
const element = target.closest<HTMLElement>(this.SELECTOR);
if (!element) { return true; }
const state = element.dataset.wpState;
const workPackageId = element.dataset.workPackageId;
@@ -66,7 +68,9 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
// not matter what other rows are (de-)selected below.
// Thus save that row for the details view button.
// Locate the row from event
const row = target.closest(`.${tableRowClassName}`) as HTMLElement;
const row = target.closest<HTMLElement>(`.${tableRowClassName}`);
if (!row) { return true; }
const classIdentifier = row.dataset.classIdentifier!;
const [index] = view.workPackageTable.findRenderedRow(classIdentifier);
@@ -187,7 +187,8 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
this.untilDestroyed(),
)
.subscribe((wp:WorkPackageResource) => {
if (this.currentWorkPackage && this.currentWorkPackage.__initialized_at === wp.__initialized_at) {
// eslint-disable-next-line no-underscore-dangle
if (this.currentWorkPackage?.__initialized_at === wp.__initialized_at) {
// Remove row and focus
this.resetRow();
@@ -36,23 +36,37 @@ import { SchemaResource } from 'core-app/features/hal/resources/schema-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';
import { QueryColumn } from '../wp-query/query-column';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { SchemaAttributeObject } from 'core-app/features/hal/resources/schema-attribute-object';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
interface QueryFormSchemaProperties {
columns:SchemaAttributeObject<QueryColumn>;
sortBy:SchemaAttributeObject<QuerySortByResource>;
groupBy:SchemaAttributeObject<QueryGroupByResource>;
}
type QueryFormSchema = SchemaResource & QueryFormSchemaProperties;
@Injectable()
export class WorkPackagesListInvalidQueryService {
protected halResourceService = inject(HalResourceService);
public restoreQuery(query:QueryResource, form:QueryFormResource) {
this.restoreFilters(query, form.payload, form.schema);
this.restoreColumns(query, form.payload, form.schema);
this.restoreSortBy(query, form.payload, form.schema);
this.restoreGroupBy(query, form.payload, form.schema);
this.restoreOtherProperties(query, form.payload);
const payload = form.payload as QueryResource;
const schema = form.schema as QueryFormSchema;
this.restoreFilters(query, payload, form.filtersSchemas);
this.restoreColumns(query, payload, schema);
this.restoreSortBy(query, payload, schema);
this.restoreGroupBy(query, payload, schema);
this.restoreOtherProperties(query, payload);
}
private restoreFilters(query:QueryResource, payload:QueryResource, querySchema:SchemaResource) {
let filters = _.map((payload.filters), (filter) => {
const filterInstanceSchema = _.find(querySchema.filtersSchemas.elements, (schema:QueryFilterInstanceSchemaResource) => (schema.filter.allowedValues as QueryFilterResource[])[0].href === filter.filter.href);
private restoreFilters(query:QueryResource, payload:QueryResource, filtersSchemas:QueryFilterInstanceSchemaResource[]) {
const filters = payload.filters.map((filter) => {
const filterInstanceSchema = filtersSchemas.find(
(schema) => (schema.filter.allowedValues as QueryFilterResource[])[0].href === filter.filter.href,
);
if (!filterInstanceSchema) {
return null;
@@ -60,56 +74,64 @@ export class WorkPackagesListInvalidQueryService {
const recreatedFilter = filterInstanceSchema.getFilter();
const operator = _.find(filterInstanceSchema.operator.allowedValues, (operator) => operator.href === filter.operator.href);
const operator = (filterInstanceSchema.operator.allowedValues as HalResource[]).find(
(op) => op.href === filter.operator.href,
);
if (operator) {
recreatedFilter.operator = operator;
recreatedFilter.operator = operator as typeof recreatedFilter.operator;
}
recreatedFilter.values.length = 0;
_.each(filter.values, (value) => recreatedFilter.values.push(value));
recreatedFilter.values = filter.values.slice();
return recreatedFilter;
});
filters = _.compact(filters);
}).filter((f):f is QueryFilterInstanceResource => f != null);
// clear filters while keeping reference
query.filters.length = 0;
_.each(filters, (filter) => query.filters.push(filter));
filters.forEach((filter) => query.filters.push(filter));
}
private restoreColumns(query:QueryResource, stubQuery:QueryResource, schema:SchemaResource) {
let columns = _.map(stubQuery.columns, (column) => _.find((schema.columns.allowedValues as QueryColumn[]), (candidate) => candidate.href === column.href));
columns = _.compact(columns);
private restoreColumns(query:QueryResource, stubQuery:QueryResource, schema:QueryFormSchema) {
const columns = stubQuery.columns
.map((column) => (schema.columns.allowedValues as QueryColumn[]).find((candidate) => candidate.href === column.href))
.filter((column):column is QueryColumn => column != null);
query.columns.length = 0;
_.each(columns, (column) => query.columns.push(column!));
columns.forEach((column) => query.columns.push(column));
}
private restoreSortBy(query:QueryResource, stubQuery:QueryResource, schema:SchemaResource) {
let sortBys = _.map((stubQuery.sortBy), (sortBy) => _.find((schema.sortBy.allowedValues as QuerySortByResource[]), (candidate) => candidate.href === sortBy.href)!);
sortBys = _.compact(sortBys);
private restoreSortBy(query:QueryResource, stubQuery:QueryResource, schema:QueryFormSchema) {
const sortBys = stubQuery.sortBy
.map((sortBy) => (schema.sortBy.allowedValues as QuerySortByResource[]).find((candidate) => candidate.href === sortBy.href))
.filter((sortBy):sortBy is QuerySortByResource => sortBy != null);
query.sortBy.length = 0;
_.each(sortBys, (sortBy) => query.sortBy.push(sortBy));
sortBys.forEach((sortBy) => query.sortBy.push(sortBy));
}
private restoreGroupBy(query:QueryResource, stubQuery:QueryResource, schema:SchemaResource) {
const groupBy = _.find((schema.groupBy.allowedValues as QueryGroupByResource[]), (candidate) => stubQuery.groupBy && stubQuery.groupBy.href === candidate.href) as any;
private restoreGroupBy(query:QueryResource, stubQuery:QueryResource, schema:QueryFormSchema) {
const groupBy = (schema.groupBy.allowedValues as QueryGroupByResource[]).find(
(candidate) => stubQuery.groupBy?.href === candidate.href,
);
query.groupBy = groupBy;
}
private restoreOtherProperties(query:QueryResource, stubQuery:QueryResource) {
_.without(Object.keys(stubQuery.$source), '_links', 'filters').forEach((property:any) => {
query[property] = stubQuery[property];
});
const source = stubQuery.$source as Record<string, unknown>;
const links = (source._links ?? {}) as Record<string, unknown>;
_.without(Object.keys(stubQuery.$source._links), 'columns', 'groupBy', 'sortBy').forEach((property:any) => {
query[property] = stubQuery[property];
});
Object.keys(source)
.filter((key) => key !== '_links' && key !== 'filters')
.forEach((property) => {
(query as Record<string, unknown>)[property] = (stubQuery as Record<string, unknown>)[property];
});
Object.keys(links)
.filter((key) => key !== 'columns' && key !== 'groupBy' && key !== 'sortBy')
.forEach((property) => {
(query as Record<string, unknown>)[property] = (stubQuery as Record<string, unknown>)[property];
});
}
}
@@ -366,7 +366,7 @@ export class WorkPackagesListService {
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!currentForm || !query.$links.update || query.$links.update.href !== currentForm.href) {
if (!currentForm || query.$links.update?.href !== currentForm.href) {
return this.loadForm(query);
}
@@ -89,7 +89,7 @@ export class WorkPackageStatesInitializationService {
const schema:QuerySchemaResource = form.schema as any;
_.each(schema.filtersSchemas.elements, (schema) => {
this.states.schemas.get(schema.href!).putValue(schema as any);
this.states.schemas.get(schema.href!).putValue(schema);
});
this.wpTableFilters.initializeFilters(query, schema);
@@ -59,7 +59,7 @@ import { HalResourceService } from 'core-app/features/hal/services/hal-resource.
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 { HalSource, HalSourceLink } from 'core-app/features/hal/interfaces';
import { HalSource } from 'core-app/features/hal/interfaces';
export const newWorkPackageHref = '/api/v3/work_packages/new';
@@ -371,7 +371,7 @@ export class WorkPackageCreateService extends UntilDestroyedMixin {
} else if (!value) {
payload._links[attribute] = { href: null };
} else {
payload._links[attribute] = value as unknown as HalSourceLink;
payload._links[attribute] = value;
}
delete payload[attribute];
});
@@ -373,7 +373,7 @@ export class UrlParamsHelperService {
queryData.sortBy = this.buildV3GetSortByFromQuery(query);
queryData.timestamps = query.timestamps.join(',');
return _.extend(additionalParams, queryData) as Partial<QueryRequestParams>;
return _.extend(additionalParams, queryData);
}
public queryFilterValueToParam(value:HalResource|string|boolean):string {
@@ -102,7 +102,6 @@ export class WorkPackageRelationsComponent extends UntilDestroyedMixin implement
const updateWorkPackage = !!form.dataset?.updateWorkPackage;
if (updateWorkPackage) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (event.detail?.success) {
// Update the work package
void this.apiV3Service
@@ -69,7 +69,7 @@ export class WpTableConfigurationRelationSelectorComponent implements OnInit {
private async initializeRelationFilters():Promise<void> {
await this.wpTableFilters.onReady();
this.availableRelationFilters = this.relationFiltersOf(this.wpTableFilters.availableFilters) as QueryFilterResource[];
this.availableRelationFilters = this.relationFiltersOf(this.wpTableFilters.availableFilters);
this.setSelectedRelationFilter();
this.cdRef.markForCheck();
}
@@ -89,7 +89,7 @@ export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewB
const query = this.querySpace.query.value!;
this.wpStatesInitialization.applyToQuery(query);
return this.urlParamsHelper.buildV3GetQueryFromQueryResource(query) as object;
return this.urlParamsHelper.buildV3GetQueryFromQueryResource(query);
}
public buildUrlParams() {
@@ -109,7 +109,7 @@ export class SortHeaderDirective extends UntilDestroyedMixin implements AfterVie
.subscribe(() => {
const latestSortElement = this.wpTableSortBy.current[0];
if (!latestSortElement || this.headerColumn.href !== latestSortElement.column.href) {
if (this.headerColumn.href !== latestSortElement?.column.href) {
this.currentSortDirection = null;
} else {
this.currentSortDirection = latestSortElement.direction;
@@ -33,7 +33,7 @@ export class OpUnlinkTableAction extends OpTableAction {
identifier,
title,
applicable,
onClick) as OpTableAction;
onClick);
}
public buildElement() {
@@ -83,8 +83,8 @@ export class TimelineCellRenderer {
}
public isEmpty(wp:WorkPackageResource) {
const start = moment(wp.startDate as any);
const due = moment(wp.dueDate as any);
const start = moment(wp.startDate);
const due = moment(wp.dueDate);
const noStartAndDueValues = _.isNaN(start.valueOf()) && _.isNaN(due.valueOf());
return noStartAndDueValues;
}
@@ -240,7 +240,9 @@ export class TimelineCellRenderer {
*/
public update(element:HTMLDivElement, labels:WorkPackageCellLabels|null, renderInfo:RenderInfo):boolean {
const { change } = renderInfo;
const bar = element.querySelector(`.${timelineBackgroundElementClass}`) as HTMLElement;
const bar = element.querySelector<HTMLElement>(`.${timelineBackgroundElementClass}`);
if (!bar) { return false; }
let start = moment(change.projectedResource.startDate);
let due = moment(change.projectedResource.dueDate);
@@ -30,7 +30,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
}
public isEmpty(wp:WorkPackageResource) {
const date = moment(wp.date as any);
const date = moment(wp.date);
return _.isNaN(date.valueOf());
}
@@ -131,7 +131,7 @@ export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin imple
)
.subscribe((list) => {
// ... make sure that the corresponding relations are loaded ...
const wps = _.compact(list.map((row) => row.workPackageId) as string[]);
const wps = _.compact(list.map((row) => row.workPackageId));
void this.wpRelations.requireAll(wps);
});
@@ -91,6 +91,6 @@ export class WorkPackageViewGroupByService extends WorkPackageQueryStateService<
public isCurrentlyGroupedBy(column:QueryColumn):boolean {
const cur = this.current;
return !!(cur && cur.id === column.id);
return cur?.id === column.id;
}
}
@@ -40,7 +40,7 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
}
public get current():WorkPackageViewHighlight {
const value = this.lastUpdatedState.getValueOr({ mode: 'inline' } as WorkPackageViewHighlight);
const value = this.lastUpdatedState.getValueOr({ mode: 'inline' } as WorkPackageViewHighlight); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
return this.filteredValue(value);
}
@@ -158,7 +158,7 @@ export class WorkPackageViewTimelineService extends WorkPackageQueryStateService
* @param update
*/
private modify(update:Partial<WorkPackageTimelineState>) {
this.update({ ...this.current, ...update } as WorkPackageTimelineState);
this.update({ ...this.current, ...update });
}
/**
@@ -33,7 +33,7 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
import { UrlParamsHelperService } from 'core-app/features/work-packages/components/wp-query/url-params-helper';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalDeletedEvent, HalEventsService } from 'core-app/features/hal/services/hal-events.service';
import { HalEventsService } from 'core-app/features/hal/services/hal-events.service';
import { States } from 'core-app/core/states/states.service';
import { resolveNumericId } from 'core-app/features/work-packages/helpers/work-package-id-resolvers';
@@ -69,7 +69,7 @@ export class WorkPackageService {
.then(() => {
this.toastService.addSuccess(this.text.successful_delete);
ids.forEach((id) => this.halEvents.push({ _type: 'WorkPackage', id }, { eventType: 'deleted' } as HalDeletedEvent));
ids.forEach((id) => this.halEvents.push({ _type: 'WorkPackage', id }, { eventType: 'deleted' }));
const routeWpId = this.$state.params.workPackageId as string;
const numericId = resolveNumericId(this.states, routeWpId);
@@ -12,7 +12,7 @@ describe('AttributeHelpTextModalService', () => {
let dialog:HTMLDialogElement|null;
beforeEach(() => {
fetchSpy = vi.spyOn(window, 'fetch') as unknown as Mock<Window['fetch']>;
fetchSpy = vi.spyOn(window, 'fetch'); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
});
beforeEach(async () => {
@@ -7,7 +7,6 @@ import { of, map } from 'rxjs';
import { NgSelectModule } from '@ng-select/ng-select';
import { OpAutocompleterComponent } from './op-autocompleter.component';
import { TOpAutocompleterResource } from './typings';
import { By } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
@@ -93,11 +92,11 @@ describe('autocompleter', () => {
}).compileComponents();
fixture = TestBed.createComponent(OpAutocompleterComponent);
getOptionsFnSpy = vi.fn().mockImplementation((searchTerm:string) => {
getOptionsFnSpy = vi.fn().mockImplementation((searchTerm:string) => { // eslint-disable-line @typescript-eslint/no-unsafe-assignment
return of(workPackagesStub).pipe(map((wps) => wps.filter((wp) => searchTerm !== '' && wp.subject.includes(searchTerm))));
}) as unknown as Mock;
});
fixture.componentInstance.resource = 'work_packages' as TOpAutocompleterResource;
fixture.componentInstance.resource = 'work_packages';
fixture.componentInstance.filters = [];
fixture.componentInstance.searchKey = 'typeahead';
fixture.componentInstance.appendTo = 'body';
@@ -124,7 +124,7 @@ export class TimeEntriesWorkPackageAutocompleterComponent extends OpAutocomplete
const base = this.filters ?? [];
const isRecent = this.mode === 'recent';
if (isRecent && this.recentWorkPackageIds.length > 0) {
return [...base, { name: 'id', operator: '=', values: this.recentWorkPackageIds } as IAPIFilter];
return [...base, { name: 'id', operator: '=', values: this.recentWorkPackageIds }];
}
return base;
@@ -264,7 +264,7 @@ export class OpBasicRangeDatePickerComponent implements OnInit, ControlValueAcce
private appendToBodyOrDialog():HTMLElement|undefined {
if (this.inDialog) {
return document.querySelector(`#${this.inDialog}`) as HTMLElement;
return document.querySelector<HTMLElement>(`#${this.inDialog}`)!;
}
return undefined;
@@ -236,7 +236,7 @@ export class OpBasicSingleDatePickerComponent implements ControlValueAccessor, O
private appendToBodyOrDialog():HTMLElement|undefined {
if (this.inDialog) {
return document.querySelector(`#${this.inDialog}`) as HTMLElement;
return document.querySelector<HTMLElement>(`#${this.inDialog}`)!;
}
return undefined;
@@ -58,8 +58,11 @@ export class CKEditorSetupService {
const editorClass = type === 'constrained' ? window.OPConstrainedEditor : window.OPClassicEditor;
wrapper.classList.add(`ckeditor-type-${type}`);
const toolbarWrapper = wrapper.querySelector('.document-editor__toolbar')!;
const contentWrapper = wrapper.querySelector('.document-editor__editable') as HTMLElement;
const toolbarWrapper = wrapper.querySelector<HTMLElement>('.document-editor__toolbar');
const contentWrapper = wrapper.querySelector<HTMLElement>('.document-editor__editable');
if (!toolbarWrapper || !contentWrapper) {
throw new Error('Missing CKEditor wrapper elements.');
}
const config = this.createConfig(context, initialData);
return this
@@ -91,27 +91,27 @@ export class DisplayFieldService extends AbstractFieldService<DisplayField, IDis
// We handle multi value fields differently in the single view context
const isCustomMultiLinesField = ['[]CustomOption'].includes(schema.type);
if (context.container === 'single-view' && isCustomMultiLinesField) {
return new MultipleLinesCustomOptionsDisplayField(fieldName, context) as DisplayField;
return new MultipleLinesCustomOptionsDisplayField(fieldName, context);
}
const isHierarchyItemsField = ['CustomField::Hierarchy::Item'].includes(schema.type);
if (context.container === 'single-view' && isHierarchyItemsField) {
return new HierarchyItemDisplayField(fieldName, context) as DisplayField;
return new HierarchyItemDisplayField(fieldName, context);
}
const isMultilineHierarchyItemsField = ['[]CustomField::Hierarchy::Item'].includes(schema.type);
if (context.container === 'single-view' && isMultilineHierarchyItemsField) {
return new MultipleLinesHierarchyItemDisplayField(fieldName, context) as DisplayField;
return new MultipleLinesHierarchyItemDisplayField(fieldName, context);
}
// Separate class seems not needed (merge with []CustomOption above?)
const isVersionMultiLinesField = ['[]Version'].includes(schema.type);
if (context.container === 'single-view' && isVersionMultiLinesField) {
return new MultipleLinesCustomOptionsDisplayField(fieldName, context) as DisplayField;
return new MultipleLinesCustomOptionsDisplayField(fieldName, context);
}
const isUserMultiLinesField = ['[]User'].includes(schema.type);
if (context.container === 'single-view' && isUserMultiLinesField) {
return new MultipleLinesUserFieldModule(fieldName, context) as DisplayField;
return new MultipleLinesUserFieldModule(fieldName, context);
}
// We handle progress differently in the timeline
@@ -7,7 +7,6 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
import { DisplayFieldContext } from 'core-app/shared/components/fields/display/display-field.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { IFieldSchema } from 'core-app/shared/components/fields/field.base';
import { Injector } from '@angular/core';
describe('WorkPackageIdDisplayField', () => {
let field:WorkPackageIdDisplayField;
@@ -37,7 +36,7 @@ describe('WorkPackageIdDisplayField', () => {
const mockInjector = {
get: (token:unknown, notFoundValue?:unknown) => serviceMap.get(token) ?? notFoundValue ?? {},
} as unknown as Injector;
};
field = new WorkPackageIdDisplayField('id', {
injector: mockInjector,
@@ -78,7 +78,7 @@ export class WorkPackageSpentTimeDisplayField extends WorkDisplayField {
// Link to the cost report having the work package filter preselected. No grouping.
const href = URI(
this.PathHelper.projectTimeEntriesPath(
project.identifier as string,
project.identifier,
),
)
.search(
@@ -30,7 +30,7 @@ import { ApplicationRef, Injector } from '@angular/core';
import { EditForm } from 'core-app/shared/components/fields/edit/edit-form/edit-form';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing-portal/edit-field-handler';
import { IFieldSchema } from 'core-app/shared/components/fields/field.base';
import { vi } from 'vitest';
class TestEditForm extends EditForm<HalResource> {
constructor(injector:Injector, private readonly requireVisibleSpy:(fieldName:string) => Promise<void>, private readonly activateFieldSpy:() => Promise<EditFieldHandler>, private readonly resetSpy:(fieldName:string, focus?:boolean) => void) {
@@ -58,7 +58,7 @@ describe('EditForm', () => {
it('does not require visibility twice for newly erroneous inactive fields', async () => {
const tick = vi.fn();
const requireVisible = vi.fn().mockResolvedValue(undefined);
const activateField = vi.fn().mockResolvedValue({} as EditFieldHandler);
const activateField = vi.fn().mockResolvedValue({});
const reset = vi.fn();
const injector = {
get: vi.fn().mockImplementation((token:unknown) => {
@@ -68,7 +68,7 @@ describe('EditForm', () => {
throw new Error(`Unexpected token: ${String(token)}`);
}),
} as unknown as Injector;
};
const form = new TestEditForm(injector, requireVisible, activateField, reset);
const change = {
@@ -77,7 +77,7 @@ describe('EditForm', () => {
ofProperty: vi.fn().mockReturnValue({
writable: true,
name: 'Foo',
} as IFieldSchema),
}),
},
getForm: vi.fn().mockResolvedValue(undefined),
};
@@ -98,7 +98,7 @@ export class HalResourceEditFieldHandler extends EditFieldHandler {
}
public focus(setClickOffset?:number) {
const target = this.element.querySelector('.inline-edit--field') as HTMLElement;
const target = this.element.querySelector<HTMLElement>('.inline-edit--field');
if (!target) {
debugLog(`Tried to focus on ${this.fieldName}, but element does not (yet) exist.`);
@@ -53,7 +53,7 @@ export class ProjectEditFieldComponent extends EditFieldComponent implements OnI
readonly http = inject(HttpClient);
readonly halResourceService = inject(HalResourceService);
isNew = isNewResource(this.resource);
isNew = isNewResource(this.resource as { id:string | null });
url:string;
@@ -84,9 +84,10 @@ export class ProjectEditFieldComponent extends EditFieldComponent implements OnI
{ name: 'active', operator: '=' as FilterOperator, values: ['t'] },
];
if (isNewResource(this.resource) && this.change.value('type')) {
const typeId = idFromLink((this.change.value('type') as { href:string }).href);
filters.push({ name: 'type_id', operator: '=' as FilterOperator, values: [typeId] });
const type = this.change.value<{ href:string }|null>('type');
if (isNewResource(this.resource as { id:string | null }) && type) {
const typeId = idFromLink(type.href);
filters.push({ name: 'type_id', operator: '=', values: [typeId] });
}
return filters;
@@ -27,7 +27,6 @@
//++
import { AfterViewInit, ChangeDetectorRef, Directive, ElementRef, EventEmitter, Input, OnDestroy, Output, inject } from '@angular/core';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalEventsService } from 'core-app/features/hal/services/hal-events.service';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
@@ -132,11 +131,11 @@ export class ModalWithTurboContentDirective implements AfterViewInit, OnDestroy
if (fetchResponse?.succeeded) {
this.halEvents.push(
this.resource as WorkPackageResource,
this.resource,
{ eventType: 'updated' },
);
void this.apiV3Service.work_packages.id(this.resource as WorkPackageResource).refresh();
void this.apiV3Service.work_packages.id(this.resource).refresh();
this.successfulUpdate.emit();
@@ -94,7 +94,7 @@ export class AttributeModelLoaderService {
shareReplay(1),
);
state.clearAndPutFromPromise(firstValueFrom(observable) as PromiseLike<HalResource>);
state.clearAndPutFromPromise(firstValueFrom(observable));
return observable;
}
@@ -399,7 +399,7 @@ export class GridAreaService {
}
public resetAreas(ignoredArea:GridWidgetArea|null = null) {
this.widgetAreas.filter((area) => !ignoredArea || area.guid !== ignoredArea.guid).forEach((area) => area.reset());
this.widgetAreas.filter((area) => area.guid !== ignoredArea?.guid).forEach((area) => area.reset());
this.numRows = this.resource.rowCount;
this.numColumns = this.resource.columnCount;
@@ -88,7 +88,7 @@ export class GridResizeService {
}
public isResized(area:GridWidgetArea) {
return this.resizedArea && this.resizedArea.guid === area.guid;
return this.resizedArea?.guid === area.guid;
}
public isPassive(area:GridWidgetArea) {
@@ -41,7 +41,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger
readonly halEditing = inject(HalResourceEditingService);
override readonly placement = 'bottom-end';
protected open(evt:Event) {
const wp = this.states.workPackages.get('new').value;
@@ -88,7 +88,7 @@ export class PersistentToggleComponent implements OnInit {
window.OpenProject.guardedLocalStorage(this.identifier, (!!isNowHidden).toString());
const targetNotification = this.targetNotification;
if (!targetNotification) return;
if (!targetNotification) return;
if (isNowHidden) {
slideUp(targetNotification, 400);
@@ -102,11 +102,11 @@ export class SearchableProjectListService {
// such as favorites or the preloaded projects (current project, selected projects)
// in a filtered view, it's legitimate for them to be missing, thus we skip extra fetching if a search text is present
if(!loadingEnabled || searchText.length > 0) {
return of([projects, false as boolean]);
return of([projects, false]);
}
return this.pipeConcatProjects(projects, this.preloadProjectIds.concat(favoriteIds))
.pipe(map((p) => [p, true as boolean]));
.pipe(map((p) => [p, true]));
}),
switchMap(([projects, enhancePreloadedProjects]:[IProject[],boolean]) => {
// These can be fetched in parallel to ancestors, since they share ancestors with preloadProjectIds entries and thus
@@ -299,7 +299,7 @@ export class SearchableProjectListService {
const listParent = findSearchableListParent(event.currentTarget as HTMLElement);
const focused = document.activeElement;
(listParent?.querySelector('.spot-list--item-action_active') as HTMLElement)?.click();
listParent?.querySelector<HTMLElement>('.spot-list--item-action_active')?.click();
(focused as HTMLElement)?.focus();
}
@@ -13,7 +13,7 @@ export class OpSearchHighlightDirective implements AfterViewChecked {
let el = this.elementRef.nativeElement as HTMLElement;
const highlightedElement = el.querySelector('.op-search-highlight');
if (!!highlightedElement && this.query && highlightedElement.innerHTML.toLocaleLowerCase() === this.query.toLocaleLowerCase()) {
if (!!highlightedElement && highlightedElement.innerHTML.toLocaleLowerCase() === this.query?.toLocaleLowerCase()) {
return;
}
@@ -118,7 +118,7 @@ describe('parseChronicDuration', () => {
});
/* The cecile case */
it('it parses 2h15 correctly to 2h 15 minutes even when the default unit is hours', () => {
it('parses 2h15 correctly to 2h 15 minutes even when the default unit is hours', () => {
expect(parseChronicDuration('2h15', { defaultUnit: 'hours' })).toBe(2 * 3600 + 15 * 60);
});
@@ -120,9 +120,9 @@ export class DomAutoscrollService {
public getElementsUnderPoint():HTMLElement[] {
const underPoint = [];
for (let i = 0; i < this.elements.length; i++) {
if (this.inside(this.point, this.elements[i])) {
underPoint.push(this.elements[i] as HTMLElement);
for (const element of this.elements) {
if (this.inside(this.point, element)) {
underPoint.push(element as HTMLElement); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
}
}
@@ -183,7 +183,7 @@ export class SpotDropModalComponent implements OnDestroy {
(this.focusGrabber.nativeElement as HTMLElement).focus();
}
private onGlobalClick = this.close.bind(this) as () => void;
private onGlobalClick = this.close.bind(this);
ngOnDestroy():void {
if (this.opened) {
@@ -117,9 +117,12 @@ export default class CustomFieldsController extends Controller {
addOption() {
const count = this.customOptionRowTargets.length;
const last = this.customOptionRowTargets[count - 1];
if (!last) { return false; }
const dup = last.cloneNode(true) as HTMLElement;
const input = dup.querySelector('.custom-option-value input') as HTMLInputElement;
const input = dup.querySelector<HTMLInputElement>('.custom-option-value input');
if (!input) { return false; }
input.setAttribute('name', `custom_field[custom_options_attributes][${count}][value]`);
input.setAttribute('id', `custom_field_custom_options_attributes_${count}_value`);
@@ -129,8 +132,9 @@ export default class CustomFieldsController extends Controller {
.querySelector('.custom-option-id')
?.remove();
const defaultValueCheckbox = dup.querySelector('input[type="checkbox"]') as HTMLInputElement;
const defaultValueHidden = dup.querySelector('input[type="hidden"]') as HTMLInputElement;
const defaultValueCheckbox = dup.querySelector<HTMLInputElement>('input[type="checkbox"]');
const defaultValueHidden = dup.querySelector<HTMLInputElement>('input[type="hidden"]');
if (!defaultValueCheckbox || !defaultValueHidden) { return false; }
defaultValueHidden.setAttribute('name', `custom_field[custom_options_attributes][${count}][default_value]`);
defaultValueHidden.removeAttribute('id');
@@ -85,10 +85,12 @@ export default class HierarchyItemController extends Controller {
if (event.dataTransfer) {
const origin = event.dataTransfer.getData('application/dragkey');
const originElement = document.querySelector(`[data-hierarchy-item-id='${origin}']`) as HTMLElement;
const originElement = document.querySelector<HTMLElement>(`[data-hierarchy-item-id='${origin}']`);
if (!originElement) { return; }
originElement.style.opacity = '1';
if (targetElement.dataset.hierarchyItemId === (originElement as HTMLElement).dataset.hierarchyItemId) {
if (targetElement.dataset.hierarchyItemId === originElement.dataset.hierarchyItemId) {
return;
}
@@ -83,8 +83,8 @@ export default class ProgressTrackingController extends Controller {
}
getSelectedMode() {
const checkedRadio = this.progressCalculationModeRadioGroupTarget.querySelector('input:checked') as HTMLInputElement;
return checkedRadio?.value || '';
const checkedRadio = this.progressCalculationModeRadioGroupTarget.querySelector<HTMLInputElement>('input:checked');
return checkedRadio?.value ?? '';
}
getWarningMessageHtml():string {
@@ -65,7 +65,7 @@ export default class RegistrationController extends Controller {
}
getSelectedOption() {
const checkedRadio = this.selfRegistrationRadioGroupTarget.querySelector('input[type="radio"]:checked') as HTMLInputElement;
return checkedRadio?.value || '';
const checkedRadio = this.selfRegistrationRadioGroupTarget.querySelector<HTMLInputElement>('input[type="radio"]:checked');
return checkedRadio?.value ?? '';
}
}
@@ -129,7 +129,8 @@ export default class BudgetSubformController extends Controller {
const row = this.element.querySelector(`#${row_identifier}`)!;
const body = new FormData();
body.append('element_id', row_identifier);
body.append('fixed_date', (document.querySelector('#budget_fixed_date') as HTMLInputElement).value);
const fixedDateInput = document.querySelector<HTMLInputElement>('#budget_fixed_date');
body.append('fixed_date', fixedDateInput?.value ?? '');
row.querySelectorAll('.budget-item-value').forEach((itemValue:HTMLInputElement|HTMLSelectElement) => {
body.append(itemValue.dataset.requestKey!, (itemValue.value || '0'));
@@ -36,12 +36,11 @@ export default class extends Controller {
}
hide(event:MouseEvent) {
const section = (event.target as HTMLElement).closest('.hide-section') as HTMLElement;
if (section) {
section.hidden = true;
}
const section = (event.target as HTMLElement).closest<HTMLElement>('.hide-section');
if (!section) { return; }
const name = (section as HTMLElement).dataset.name!;
section.hidden = true;
const name = section.dataset.name!;
this.toggleOption(name);
}
@@ -33,8 +33,7 @@ export default class MainMenuController extends Controller {
targetLi.querySelector<HTMLElement>('li > a, .tree-menu--title')?.focus();
const backArrow = targetLi.querySelector('.main-menu--arrow-left-to-project') as HTMLElement;
backArrow.focus();
targetLi.querySelector<HTMLElement>('.main-menu--arrow-left-to-project')?.focus();
this.markActive(targetLi.dataset.name!);
}
@@ -337,8 +337,10 @@ export default class MyTimeTrackingController extends Controller {
const colgroup = document.createElement('colgroup');
const col = document.createElement('col');
const otherCol = document.querySelector('.fc-scrollgrid-section-header .fc-col-header col') as HTMLElement;
col.style.width = otherCol?.style?.width;
const otherCol = document.querySelector<HTMLTableColElement>('.fc-scrollgrid-section-header .fc-col-header col');
if (otherCol) {
col.style.width = otherCol.style.width;
}
const tbody = document.createElement('tbody');
tbody.setAttribute('role', 'presentation');
@@ -213,12 +213,12 @@ export default class BulkSelectionController extends Controller {
});
}
private get selectedPermissions() {
private get selectedPermissions():string[] {
return this.selectedRoleButtons.map((button) => {
const label = button.querySelector('.Button-label')!;
return label.textContent;
}) as string[];
return label.textContent ?? '';
});
}
private get selectedRoleButtons() {
@@ -84,9 +84,9 @@ export default class SortByConfigController extends Controller {
this.parentForm = this.sortByFieldTarget.closest('form');
if (this.parentForm) {
this.pageTarget = this.parentForm.querySelector('input[data-sort-by-config-target="page"]')!;
if (this.pageTarget) {
const pageTarget = this.parentForm.querySelector<HTMLInputElement>('input[data-sort-by-config-target="page"]');
if (pageTarget) {
this.pageTarget = pageTarget;
this.parentForm.addEventListener('submit', this.onFormSubmit.bind(this));
}
}
@@ -116,7 +116,8 @@ export default class SortByConfigController extends Controller {
fieldChanged(event:Event):void {
const target = event.target as HTMLElement;
const row = target.closest('div[data-sort-by-config-target="inputRow"]') as HTMLElement;
const row = target.closest<HTMLElement>('div[data-sort-by-config-target="inputRow"]');
if (!row) { return; }
this.manageRow(row);
@@ -208,18 +209,20 @@ export default class SortByConfigController extends Controller {
}
getSelectedField(row:HTMLElement):string|null {
const selectedField = row.querySelector('select[name="sort_field"]') as HTMLSelectElement;
return selectedField?.value || null;
const selectedField = row.querySelector<HTMLSelectElement>('select[name="sort_field"]');
return selectedField?.value ?? null;
}
getSelectedDirection(row:HTMLElement):string|null {
const selectedSegment = row.querySelector('li.SegmentedControl-item--selected > button');
return selectedSegment?.getAttribute('data-direction') || null;
return selectedSegment?.getAttribute('data-direction') ?? null;
}
unsetField(row:HTMLElement):void {
const select = row.querySelector('select[name="sort_field"]') as HTMLSelectElement;
select.value = '';
const select = row.querySelector<HTMLSelectElement>('select[name="sort_field"]');
if (select) {
select.value = '';
}
}
unsetDirection(row:HTMLElement):void {
@@ -283,8 +286,8 @@ export default class SortByConfigController extends Controller {
disableSelectedFieldsForOtherSelects():void {
this.inputRowTargets.forEach((row) => {
const selectedFieldsInOtherRows = this.getAllSelectedFields(row);
const otherSelect = row.querySelector('select[name="sort_field"]')!;
otherSelect.querySelectorAll('option').forEach((option) => {
const otherSelect = row.querySelector<HTMLSelectElement>('select[name="sort_field"]');
otherSelect?.querySelectorAll('option').forEach((option) => {
option.disabled = selectedFieldsInOtherRows.includes(option.value);
});
});
@@ -534,14 +534,16 @@ export default class PreviewController extends DialogPreviewController {
}
private focusOnOpen() {
const banner = document.querySelector('.wp-datepicker--banner') as HTMLElement;
const banner = document.querySelector<HTMLElement>('.wp-datepicker--banner');
if (banner) {
banner.setAttribute('tabindex', '-1');
banner.focus();
} else {
const tabs = document.querySelector('.wp-datepicker-dialog--UnderlineNav') as HTMLElement;
tabs.setAttribute('tabindex', '-1');
tabs.focus();
const tabs = document.querySelector<HTMLElement>('.wp-datepicker-dialog--UnderlineNav');
if (tabs) {
tabs.setAttribute('tabindex', '-1');
tabs.focus();
}
}
}
}
@@ -10,10 +10,10 @@ export default class GeneratePdfController extends Controller {
const data = target.options[target.selectedIndex].dataset;
const template = target.options[target.selectedIndex].value;
const formControl = target.closest('.FormControl')!;
const captionElement = formControl.querySelector('.FormControl-caption') as HTMLElement;
const formControl = target.closest<HTMLElement>('.FormControl');
const captionElement = formControl?.querySelector<HTMLElement>('.FormControl-caption');
if (captionElement) {
captionElement.innerText = (data.caption || '');
captionElement.innerText = (data.caption ?? '');
}
this.inputGroupsTargets.forEach((inputGroup:HTMLElement) => {
if (inputGroup.dataset.template === template) {
+1 -1
View File
@@ -16,7 +16,7 @@ export function addTurboGlobalListeners() {
const runOnRenderAndLoad = () => {
// Add to content if warnings displayed
if (document.querySelector('.warning-bar--item')) {
const content = document.querySelector('#content') as HTMLElement;
const content = document.querySelector<HTMLElement>('#content');
if (content) {
content.style.marginBottom = '100px';
}