[OP-19542] Migrate lodash iteration to native ES6

Iteration and collection helpers move from the global `_` to native
Array and Object methods. Object-keyed collections become
`Object.entries`/`Object.values` so lodash's value/key iteration is
preserved, and a handful of nullable receivers gain the explicit guards
that `_` applied implicitly. The global `_` stays until the remaining
buckets land.

Adds a vitest spec for `ApiV3FilterBuilder` exercising the migrated
object iteration.

https://community.openproject.org/wp/OP-19542
This commit is contained in:
Alexander Brandon Coles
2026-06-13 18:34:59 +01:00
parent 8d1cbe8000
commit 34ff07ee2b
75 changed files with 292 additions and 171 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ export class StateCacheService<T> {
auditTime(250),
map(() => {
const mapped:T[] = [];
_.each(this.multiState.getValueOr({}), (state:State<T>) => {
Object.values(this.multiState.getValueOr({})).forEach((state:State<T>) => {
if (state.value) {
mapped.push(state.value);
}
@@ -60,7 +60,7 @@ export class ApiV3RelationsPaths extends ApiV3ResourceCollection<RelationResourc
);
}
const validIds = _.filter(workPackageIds, (id) => /\d+/.test(id));
const validIds = workPackageIds.filter((id) => /\d+/.test(id));
if (validIds.length === 0) {
return from([]);
@@ -72,9 +72,9 @@ export class ApiV3WorkPackagesPaths extends ApiV3Collection<WorkPackageResource,
this
.loadCollectionsFor(_.uniq(ids))
.then((pagedResults:WorkPackageCollectionResource[]) => {
_.each(pagedResults, (results) => {
pagedResults.forEach((results) => {
if (results.schemas) {
_.each(results.schemas.elements, (schema:SchemaResource) => {
results.schemas.elements.forEach((schema:SchemaResource) => {
this.states.schemas.get(schema.href!).putValue(schema);
});
}
@@ -229,7 +229,7 @@ export function initializeUiRouterListeners(injector:Injector) {
const projectIdentifier = toParams.projectPath as string || currentProject.identifier;
if (hasProjectRoutes && !toParams.projects && projectIdentifier) {
const newParams = _.clone(toParams);
_.assign(newParams, { projectPath: projectIdentifier, projects: 'projects' });
Object.assign(newParams, { projectPath: projectIdentifier, projects: 'projects' });
return $state.target(toState, newParams, { location: 'replace' });
}
@@ -19,7 +19,7 @@ export class BoardActionsRegistryService {
}
public available():ITileViewEntry[] {
return _.map(this.mapping, (service:BoardActionService, attribute:string) => ({
return Object.entries(this.mapping).map(([attribute, service]) => ({
attribute,
text: service.localizedName,
icon: '',
@@ -239,7 +239,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
const { filterName } = service;
const idFilterName = `${filterName}_id`;
const options = widget.options as unknown as BoardWidgetOption;
const instance = _.find(options.filters, (f) => !!f[filterName] || !!f[idFilterName]);
const instance = options.filters.find((f) => !!f[filterName] || !!f[idFilterName]);
if (instance) {
return ((instance[filterName] || instance[idFilterName])?.values[0] || null) as unknown as string|null;
@@ -103,7 +103,7 @@ export class HalLink implements HalLinkInterface {
}
let href = _.clone(this.href) || '';
_.each(templateValues, (value:string, key:string) => {
Object.entries(templateValues).forEach(([key, value]) => {
const regexp = new RegExp(`{${key}}`);
href = href.replace(regexp, value);
});
@@ -129,7 +129,7 @@ export class HalLink implements HalLinkInterface {
public $callable():CallableHalLink {
const linkFunc:any = (...params:any[]) => this.$fetch(...params);
_.extend(linkFunc, {
Object.assign(linkFunc, {
$link: this,
href: this.href,
title: this.title,
@@ -148,7 +148,7 @@ export function initializeHalProperties<T extends HalResource>(halResourceServic
}
if (_.isObject(element)) {
_.each(element, (child:any, name:string) => {
Object.entries(element as Record<string, HalSource>).forEach(([name, child]) => {
if (child && (child._embedded || child._links)) {
OpenprojectHalModuleHelpers.lazy(element as any,
name,
@@ -116,7 +116,7 @@ export class ErrorResource extends HalResource {
if (this.details) {
perAttribute[this.details.attribute] = [this.message];
} else {
_.forEach(this.errors, (error:any) => {
this.errors?.forEach((error:ErrorResource) => {
if (error.errorIdentifier === v3ErrorIdentifierMultipleErrors) {
const [attribute, messages] = this.extractMultiError(error);
const current = perAttribute[attribute] || [];
@@ -45,7 +45,7 @@ export class FormResource<T = HalResource> extends HalResource {
public validationErrors:Record<string, ErrorResource>;
public getErrors():ErrorResource|null {
const errors = _.values(this.validationErrors);
const errors = Object.values(this.validationErrors);
const count = errors.length;
if (count === 0) {
@@ -94,7 +94,7 @@ export class QueryFilterInstanceResource extends HalResource {
}
public findOperator(operatorSymbol:string):QueryOperatorResource|undefined {
return _.find(this.schemaCache.of(this).availableOperators, (operator:QueryOperatorResource) => operator.id === operatorSymbol) as QueryOperatorResource|undefined;
return (this.schemaCache.of(this).availableOperators as QueryOperatorResource[]).find((operator:QueryOperatorResource) => operator.id === operatorSymbol);
}
public isTemplated() {
@@ -118,7 +118,8 @@ export class QueryFilterInstanceSchemaResource extends SchemaResource {
}
private definesAllowedValues() {
return _.some(this._dependencies[0].dependencies,
(dependency:any) => dependency.values?._links?.allowedValues);
interface FilterDependency { values?:{ _links?:{ allowedValues?:unknown } } }
const dependencies = (this._dependencies as { dependencies:FilterDependency[] }[])[0].dependencies;
return dependencies.some((dependency:FilterDependency) => !!dependency.values?._links?.allowedValues);
}
}
@@ -110,7 +110,7 @@ export class RelationResource extends HalResource {
* @return {boolean}
*/
public isInvolved(wpId:string) {
return _.values(this.ids).includes(wpId.toString());
return Object.values(this.ids).includes(wpId.toString());
}
/**
@@ -27,6 +27,7 @@
//++
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalSource } from 'core-app/features/hal/interfaces';
import { InputState } from '@openproject/reactivestates';
export class SchemaResource extends HalResource {
@@ -35,7 +36,7 @@ export class SchemaResource extends HalResource {
}
public get availableAttributes():string[] {
return _.keys(this.$source).filter((name) => !name.startsWith('_'));
return Object.keys(this.$source as HalSource).filter((name) => !name.startsWith('_'));
}
// Find the attribute name with a matching (localized) name;
@@ -266,7 +266,7 @@ describe('WorkPackage', () => {
});
it('when the work package has an `addAttachment` link', () => {
workPackage.$links.addAttachment = _.noop as any;
workPackage.$links.addAttachment = () => Promise.resolve();
expect(workPackage.canAddAttachments).toEqual(true);
});
@@ -264,7 +264,7 @@ export class WorkPackageBaseResource extends HalResource {
resources[name] = linked ? linked.$update() : Promise.reject(undefined);
});
const promise = Promise.all(_.values(resources));
const promise = Promise.all(Object.values(resources));
promise.then(() => {
this.wpCacheService.touch(this.id!);
});
@@ -56,7 +56,7 @@ export class HalPayloadHelper {
* @param schema The associated schema to determine writable state of attributes
*/
static extractPayloadFromSchema<T extends HalResource = HalResource>(resource:T, schema:SchemaResource) {
const payload:any = {
const payload:{ _links:Record<string, unknown> } & Record<string, unknown> = {
_links: {},
};
@@ -66,7 +66,7 @@ export class HalPayloadHelper {
if (schema.hasOwnProperty(key) && schema[key]?.writable) {
if (resource.$links[key]) {
if (Array.isArray(resource[key])) {
payload._links[key] = _.map(resource[key], (element) => ({ href: (element as HalResource).href }));
payload._links[key] = (resource[key] as HalResource[]).map((element) => ({ href: element.href }));
} else {
payload._links[key] = {
href: (resource[key]?.href),
@@ -78,10 +78,10 @@ export class HalPayloadHelper {
}
}
_.each(nonLinkProperties, (property) => {
nonLinkProperties.forEach((property) => {
if (resource.hasOwnProperty(property) || resource[property]) {
if (Array.isArray(resource[property])) {
payload[property] = _.map(resource[property], (element:any) => {
payload[property] = (resource[property] as HalResource[]).map((element) => {
if (element instanceof HalResource) {
return this.extractPayloadFromSchema(element, element.currentSchema || element.schema);
}
@@ -162,8 +162,9 @@ export class HalResourceNotificationService {
public showGeneralError(message?:unknown) {
let error = this.I18n.t('js.error.internal');
const hasOwnToString = message != null && Object.prototype.hasOwnProperty.call(message, 'toString');
if (typeof (message) === 'string' || _.has(message, 'toString')) {
if (typeof (message) === 'string' || hasOwnToString) {
error += ` ${(message as any).toString()}`;
}
@@ -222,6 +222,6 @@ const halResourceDefaultConfig:Record<string, HalResourceFactoryConfigInterface>
export function initializeHalResourceConfig(halResourceService:HalResourceService) {
return () => {
_.each(halResourceDefaultConfig, (value, key) => halResourceService.registerResource(key, value));
Object.entries(halResourceDefaultConfig).forEach(([key, value]) => halResourceService.registerResource(key, value));
};
}
@@ -135,7 +135,7 @@ export class QueryFiltersComponent extends UntilDestroyedMixin implements OnInit
}
public get isSecondSpacerVisible():boolean {
const hasSearch = !!_.find(this.filters, (f) => f.id === 'search');
const hasSearch = !!this.filters.find((f) => f.id === 'search');
const hasAvailableFilter = !!this.filters.find((f) => f.id !== 'search' && this.isFilterAvailable(f));
return hasSearch && hasAvailableFilter;
@@ -226,7 +226,7 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
}
public dateChange(values:string[]):void {
if (_.every(values, validDate)) {
if (values.every(validDate)) {
this.selectedDates = values;
}
}
@@ -12,12 +12,12 @@ export class WorkPackageCardViewService {
return `wp-row-${wp.id}`;
}
public get renderedCards() {
public get renderedCards():RenderedWorkPackage[] {
return this.querySpace.tableRendered.getValueOr([]);
}
public findRenderedCard(classIdentifier:string):number {
const index = _.findIndex(this.renderedCards, (card) => card.classIdentifier === classIdentifier);
const index = this.renderedCards.findIndex((card) => card.classIdentifier === classIdentifier);
return index;
}
@@ -87,7 +87,7 @@ export class TableEditForm extends EditForm<WorkPackageResource> {
}
destroy() {
_.each(this.activeFields, (field) => {
Object.values(this.activeFields).forEach((field) => {
field.deactivate(false);
});
this.resourceSubscription.unsubscribe();
@@ -28,7 +28,7 @@ export class WorkPackageFilterValues {
) {}
applyDefaultsFromFilters(change:WorkPackageChangeset|Record<string, unknown>):void {
_.each(this.filters, (filter) => {
this.filters.forEach((filter) => {
// Exclude filters specified in constructor
if (this.excluded.includes(filter.id)) {
return;
@@ -42,7 +42,7 @@ export class WorkPackageFilterValues {
if (operator !== '=') return;
const currentProjectId = this.currentProject.id;
const projectFilter = _.find(filter.values, (resource:HalResource|string) => {
const projectFilter = filter.values.find((resource:HalResource|string) => {
const href = (resource instanceof HalResource) ? resource.href : resource;
const hrefParts = href?.split('/');
return hrefParts?.[hrefParts.length - 1] === currentProjectId;
@@ -70,7 +70,7 @@ export class GroupedRenderPass extends PlainRenderPass {
* The API sadly doesn't provide us with the information which group a WP belongs to.
*/
private matchingGroup(workPackage:WorkPackageResource) {
return _.find(this.groups, (group:GroupObject) => {
return this.groups.find((group:GroupObject) => {
let property = workPackage[groupByProperty(group)];
// explicitly check for undefined as `false` (bool) is a valid value.
if (property === undefined) {
@@ -79,14 +79,14 @@ export class GroupedRenderPass extends PlainRenderPass {
// If the property is a multi-value
// Compare the href's of all resources with the ones in valueLink
if (_.isArray(property)) {
if (Array.isArray(property)) {
return this.matchesMultiValue(property as HalResource[], group);
}
/// / If it's a linked resource, compare the href,
/// / which is an array of links the resource offers
if (property?.href) {
return !!_.find(group._links.valueLink, (l:any):any => property.href === l.href);
return !!group._links.valueLink.find((l) => (property as HalResource).href === l.href);
}
// Otherwise, fall back to simple value comparison.
@@ -108,7 +108,7 @@ export class GroupedRenderPass extends PlainRenderPass {
return false;
}
const joinedOrderedHrefs = (objects:any[]) => _.map(objects, (object) => object.href).sort().join(', ');
const joinedOrderedHrefs = (objects:{ href:string }[]) => objects.map((object) => object.href).sort().join(', ');
return _.isEqualWith(
property,
@@ -51,7 +51,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
this.hierarchies = this.wpTableHierarchies.current;
_.each(this.workPackageTable.originalRowIndex, (row) => {
Object.values(this.workPackageTable.originalRowIndex).forEach((row) => {
row.object.getAncestors().forEach((ancestor:WorkPackageResource) => {
this.parentsWithVisibleChildren[ancestor.id!] = true;
});
@@ -208,7 +208,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
});
// Insert this row to parent
const parent = _.last(ancestors);
const parent = ancestors.at(-1);
this.insertUnderParent(row, parent!);
}
@@ -85,7 +85,7 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {
}
const ancestors = workPackage.getAncestors();
if (_.isArray(ancestors)) {
if (Array.isArray(ancestors)) {
ancestors.forEach((ancestor) => {
rowClasses.push(hierarchyGroupClass(ancestor.id!));
@@ -67,7 +67,7 @@ export class RelationsRenderPass {
// If the work package has no relations, ignore
const { workPackage } = row;
const state = this.wpRelations.state(workPackage.id!);
if (!state.hasValue() || _.size(state.value) === 0) {
if (!state.hasValue() || Object.keys(state.value ?? {}).length === 0) {
return;
}
@@ -73,7 +73,7 @@ export class HierarchyTransformer {
const collapsed:Record<number, boolean> = {};
// Hide all collapsed hierarchies
_.each(state.collapsed, (isCollapsed:boolean, wpId:string) => {
Object.entries(state.collapsed).forEach(([wpId, isCollapsed]) => {
// Toggle the root style
document.querySelector(`.${hierarchyRootClass(wpId)} .wp-table--hierarchy-indicator`)?.classList.toggle(indicatorCollapsedClass, isCollapsed);
@@ -69,7 +69,7 @@ export class SelectionTransformer {
context.querySelectorAll(`.${tableRowClassName}.${checkedClassName}`).forEach((el) => el.classList.remove(checkedClassName));
_.each(state.selected, (selected:boolean, workPackageId:any) => {
Object.entries(state.selected).forEach(([workPackageId, selected]) => {
context.querySelectorAll(`.${tableRowClassName}[data-work-package-id="${workPackageId}"]`).forEach((el) => {
el.classList.toggle(checkedClassName, selected);
});
@@ -62,18 +62,18 @@ export class WorkPackageTable {
) {
}
public get renderedRows() {
public get renderedRows():RenderedWorkPackage[] {
return this.querySpace.tableRendered.getValueOr([]);
}
public findRenderedRow(classIdentifier:string):[number, RenderedWorkPackage] {
const index = _.findIndex(this.renderedRows, (row) => row.classIdentifier === classIdentifier);
const index = this.renderedRows.findIndex((row) => row.classIdentifier === classIdentifier);
return [index, this.renderedRows[index]];
}
public get rowBuilder():RowsBuilder {
return _.find(this.builders, (builder:RowsBuilder) => builder.isApplicable(this))!;
return this.builders.find((builder:RowsBuilder) => builder.isApplicable(this))!;
}
/**
@@ -142,7 +142,7 @@ export class WorkPackageTable {
return;
}
_.each(pass.renderedOrder, (row) => {
pass.renderedOrder.forEach((row) => {
if (row.workPackage?.id === workPackage.id!) {
debugLog(`Refreshing rendered row ${row.classIdentifier}`);
row.workPackage = workPackage;
@@ -17,7 +17,7 @@ export class WorkPackageTableEditingContext {
public forms:Record<string, TableEditForm> = {};
public reset() {
_.each(this.forms, (form) => form.destroy());
Object.values(this.forms).forEach((form) => form.destroy());
this.forms = {};
}
@@ -88,7 +88,7 @@ export class WorkPackageStatesInitializationService {
public updateStatesFromForm(query:QueryResource, form:QueryFormResource) {
const schema:QuerySchemaResource = form.schema as any;
_.each(schema.filtersSchemas.elements, (schema) => {
schema.filtersSchemas.elements.forEach((schema) => {
this.states.schemas.get(schema.href!).putValue(schema);
});
@@ -107,7 +107,7 @@ export class WorkPackageStatesInitializationService {
this.querySpace.tableRendered.clear('Clearing rendered data before upgrading query space');
if (results.schemas) {
_.each(results.schemas.elements, (schema:SchemaResource) => {
results.schemas.elements.forEach((schema:SchemaResource) => {
this.states.schemas.get(schema.href!).putValue(schema);
});
}
@@ -19,8 +19,8 @@ export class QueryFiltersService {
* from the schema
*/
private getFilterSchema(filter:QueryFilterInstanceResource, form:QueryFormResource):QueryFilterInstanceSchemaResource|undefined {
const available = form.$embedded.schema.filtersSchemas.elements;
return _.find(available, (schema) => schema.allowedFilterValue.href === filter.filter.href);
const available = form.$embedded.schema.filtersSchemas.elements as QueryFilterInstanceSchemaResource[];
return available.find((schema) => schema.allowedFilterValue.href === filter.filter.href);
}
/**
@@ -112,20 +112,16 @@ export class UrlParamsHelperService {
}
const parts:string[] = [];
_.each(params, (value, key) => {
Object.entries(params as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (!Array.isArray(value)) {
value = [value];
}
const values:unknown[] = Array.isArray(value) ? value as unknown[] : [value];
_.each(value, (v) => {
if (v !== null && typeof v === 'object') {
v = JSON.stringify(v);
}
values.forEach((v) => {
const encoded = (v !== null && typeof v === 'object') ? JSON.stringify(v) : v as string;
parts.push(`${encodeURIComponent(key)}=${
encodeURIComponent(v)}`);
encodeURIComponent(encoded)}`);
});
});
@@ -303,7 +299,7 @@ export class UrlParamsHelperService {
// the array check is only there for backwards compatibility reasons.
// Nowadays, it will always be an array;
const vs = Array.isArray(urlFilter.v) ? urlFilter.v : [urlFilter.v];
_.extend(attributes, { values: vs });
Object.assign(attributes, { values: vs });
}
const filterData:any = {};
filterData[urlFilter.n] = attributes;
@@ -373,7 +369,7 @@ export class UrlParamsHelperService {
queryData.sortBy = this.buildV3GetSortByFromQuery(query);
queryData.timestamps = query.timestamps.join(',');
return _.extend(additionalParams, queryData);
return Object.assign(additionalParams, queryData);
}
public queryFilterValueToParam(value:HalResource|string|boolean):string {
@@ -411,8 +407,8 @@ export class UrlParamsHelperService {
const newFilters = filters.map((filter:QueryFilterInstanceResource) => {
const id = this.buildV3GetFilterIdFromFilter(filter);
const operator = this.buildV3GetOperatorIdFromFilter(filter);
const values = this.buildV3GetValuesFromFilter(filter).map((value) => {
_.each(replacements, (val:string, key:string) => {
const values = this.buildV3GetValuesFromFilter(filter).map((value:string) => {
Object.entries(replacements).forEach(([key, val]:[string, string]) => {
value = value.replace(`{${key}}`, val);
});
@@ -454,9 +450,9 @@ export class UrlParamsHelperService {
public buildV3GetValuesFromFilter(filter:QueryFilterInstanceResource|QueryFilterResource) {
if (filter.values) {
return _.map(filter.values, (v:any) => this.queryFilterValueToParam(v));
return filter.values.map((v:any) => this.queryFilterValueToParam(v));
}
return _.map(filter._links.values, (v:any) => idFromLink(v.href as string));
return filter._links.values.map((v:any) => idFromLink(v.href as string));
}
private buildV3GetOperatorIdFromFilter(filter:QueryFilterInstanceResource) {
@@ -40,8 +40,8 @@ export class WorkPackageRelationsCountComponent extends UntilDestroyedMixin impl
]).pipe(
this.untilDestroyed(),
).subscribe(([relations, workPackage]) => {
const relationCount = _.size(relations);
const childrenCount = _.size(workPackage.children);
const relationCount = Object.keys(relations).length;
const childrenCount = (workPackage.children ?? []).length;
this.count = relationCount + childrenCount;
this.cdRef.markForCheck();
@@ -81,8 +81,7 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme
this.userInputs.newRelationText = this.relation.description || '';
this.availableRelationTypes = RelationResource.LOCALIZED_RELATION_TYPES(false);
this.selectedRelationType = _.find(this.availableRelationTypes,
{ name: this.relation.normalizedType(this.workPackage) })!;
this.selectedRelationType = this.availableRelationTypes.find((relationType) => relationType.name === this.relation.normalizedType(this.workPackage))!;
this
.apiV3Service
@@ -112,7 +112,7 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
return;
}
return _.find(relations, (relation:RelationResource) => {
return Object.values(relations).find((relation:RelationResource) => {
const denormalized = relation.denormalized(from);
// Check that
// 1. the denormalized relation points at "to"
@@ -187,7 +187,7 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
* @param relation
*/
private insertIntoStates(relation:RelationResource) {
_.values(relation.ids).forEach((wpId) => {
Object.values(relation.ids).forEach((wpId) => {
this.multiState.get(wpId).doModify((value:RelationsStateValue) => {
value[relation.id!] = relation;
return value;
@@ -204,7 +204,7 @@ export class WorkPackageRelationsService extends StateCacheService<RelationsStat
* @param relation
*/
private removeFromStates(relation:RelationResource) {
_.values(relation.ids).forEach((wpId) => {
Object.values(relation.ids).forEach((wpId) => {
this.multiState.get(wpId).doModify((value:RelationsStateValue) => {
delete value[relation.id!];
return value;
@@ -42,7 +42,7 @@ export class TabPortalOutlet {
}
public get activeComponents():TabComponent[] {
const tabs = _.values(this.activeTabs);
const tabs = Object.values(this.activeTabs);
return tabs.map((tab:ActiveTabInterface) => tab.componentRef.instance);
}
@@ -78,7 +78,7 @@ export class TabPortalOutlet {
*/
dispose():void {
// Dispose all active tabs
_.each(this.activeTabs, (active) => active.dispose());
Object.values(this.activeTabs).forEach((active) => active.dispose());
// Remove outlet element
if (this.outletElement.parentNode != null) {
@@ -65,7 +65,7 @@ export class WpTableConfigurationDisplaySettingsTabComponent implements TabCompo
public updateGroup(href:string) {
this.displayMode = 'grouped';
this.currentGroup = _.find(this.availableGroups, (group) => group.href === href) || null;
this.currentGroup = this.availableGroups.find((group) => group.href === href) ?? null;
}
ngOnInit() {
@@ -94,7 +94,7 @@ export class WpTableConfigurationSortByTabComponent implements TabComponent, OnI
this.getManualSortingOption();
_.each(this.wpTableSortBy.current, (sort) => {
this.wpTableSortBy.current.forEach((sort) => {
if (!sort.column.href!.endsWith('/parent')) {
this.sortationObjects.push(
new SortModalObject({ name: sort.column.name, href: sort.column.href },
@@ -113,7 +113,7 @@ export class WpTableConfigurationSortByTabComponent implements TabComponent, OnI
}
public updateSelection(sort:SortModalObject, selected:string | null) {
sort.column = _.find(this.allColumns, (column) => column.href === selected) || this.emptyColumn;
sort.column = this.allColumns.find((column) => column.href === selected) ?? this.emptyColumn;
this.updateUsedColumns();
}
@@ -130,7 +130,7 @@ export class WpTableConfigurationSortByTabComponent implements TabComponent, OnI
}
private getMatchingSort(column:string, direction:string) {
return _.find(this.wpTableSortBy.available, (sort) => sort.column.href === column && sort.direction.href === direction);
return this.wpTableSortBy.available.find((sort) => sort.column.href === column && sort.direction.href === direction);
}
private fillUpSortElements() {
@@ -77,7 +77,7 @@ export class WpTableConfigurationRelationSelectorComponent implements OnInit {
private setSelectedRelationFilter():void {
const currentRelationFilters:QueryFilterInstanceResource[] = this.relationFiltersOf(this.wpTableFilters.current) as QueryFilterInstanceResource[];
if (currentRelationFilters.length > 0) {
this.selectedRelationFilter = _.find(this.availableRelationFilters, { id: currentRelationFilters[0].id })!;
this.selectedRelationFilter = this.availableRelationFilters.find((relationFilter) => relationFilter.id === currentRelationFilters[0].id)!;
} else {
this.selectedRelationFilter = this.availableRelationFilters[0];
}
@@ -97,7 +97,7 @@ export class WpTableConfigurationRelationSelectorComponent implements OnInit {
}
private relationFiltersOf(filters:QueryFilterResource[]|QueryFilterInstanceResource[]):QueryFilterResource[]|QueryFilterInstanceResource[] {
return _.filter(filters, (filter:QueryFilterResource|QueryFilterInstanceResource) => _.includes(this.relationFilterIds, filter.id));
return filters.filter((filter:QueryFilterResource|QueryFilterInstanceResource) => this.relationFilterIds.includes(filter.id));
}
private addFilterToCurrentState(filter:QueryFilterResource):void {
@@ -110,7 +110,7 @@ export class WpTableConfigurationRelationSelectorComponent implements OnInit {
}
private getOperatorForId(filter:QueryFilterResource, id:string):QueryOperatorResource {
return _.find(this.schemaCache.of(filter).availableOperators, { id }) as QueryOperatorResource;
return (this.schemaCache.of(filter).availableOperators as QueryOperatorResource[]).find((operator:QueryOperatorResource) => operator.id === id)!;
}
public compareRelationFilters(f1:undefined|QueryFilterResource, f2:undefined|QueryFilterResource):boolean {
@@ -98,7 +98,7 @@ export class WorkPackageContextMenuHelperService {
allowedActions = allowedActions.concat(this.getAllowedRelationActions(workPackage, allowSplitScreenActions));
_.each(allowedActions, (allowedAction) => {
allowedActions.forEach((allowedAction) => {
singularPermittedActions.push({
key: allowedAction.key,
text: allowedAction.text,
@@ -123,12 +123,12 @@ export class WorkPackageContextMenuHelperService {
return link;
}
public getIntersectOfPermittedActions(workPackages:any) {
const bulkPermittedActions:any = [];
const possibleActions = this.BULK_ACTIONS.concat(this.HookService.call('workPackageBulkContextMenu'));
const permittedActions = _.filter(possibleActions, (action:any) => _.every(workPackages, (workPackage:WorkPackageResource) => this.isActionAllowed(workPackage, action)));
public getIntersectOfPermittedActions(workPackages:WorkPackageResource[]) {
const bulkPermittedActions:WorkPackageAction[] = [];
const possibleActions:WorkPackageAction[] = this.BULK_ACTIONS.concat(this.HookService.call('workPackageBulkContextMenu'));
const permittedActions = possibleActions.filter((action) => workPackages.every((workPackage:WorkPackageResource) => this.isActionAllowed(workPackage, action)));
_.each(permittedActions, (permittedAction:any) => {
permittedActions.forEach((permittedAction) => {
bulkPermittedActions.push({
key: permittedAction.key,
text: permittedAction.text,
@@ -154,20 +154,20 @@ export class WorkPackageContextMenuHelperService {
}
private isActionAllowed(workPackage:WorkPackageResource, action:WorkPackageAction):boolean {
return _.filter(this.getAllowedActions(workPackage, [action]), (a) => a === action).length >= 1;
return this.getAllowedActions(workPackage, [action]).filter((a) => a === action).length >= 1;
}
private getAllowedActions(workPackage:WorkPackageResource, actions:WorkPackageAction[]):WorkPackageAction[] {
const allowedActions:WorkPackageAction[] = [];
_.each(actions, (action) => {
actions.forEach((action) => {
if (action.link && workPackage[action.link] !== undefined) {
action.text = action.text || I18n.t(`js.button_${action.key}`);
allowedActions.push(action);
}
});
_.each(this.HookService.call('workPackageTableContextMenu'), (action:WorkPackageAction) => {
this.HookService.call('workPackageTableContextMenu').forEach((action:WorkPackageAction) => {
if (workPackage[action.link!] !== undefined) {
const index = action.indexBy ? action.indexBy(allowedActions) : allowedActions.length;
allowedActions.splice(index, 0, action);
@@ -462,7 +462,7 @@ export class TimelineCellRenderer {
// ensure minimum width
if (!_.isNaN(start.valueOf()) || !_.isNaN(due.valueOf())) {
const minWidth = _.max([renderInfo.viewParams.pixelPerDay, 2]);
const minWidth = Math.max(renderInfo.viewParams.pixelPerDay, 2);
element.style.minWidth = `${minWidth}px`;
}
}
@@ -268,7 +268,7 @@ export function registerWorkPackageMouseHandler(this:void,
.save<WorkPackageResource, WorkPackageChangeset>(change)
.then((result) => {
notificationService.showSave(result.resource);
const ids = _.map(querySpace.tableRendered.value, (row) => row.workPackageId);
const ids = (querySpace.tableRendered.value ?? []).map((row) => row.workPackageId);
return apiv3Service
.work_packages
.filterUpdatedSince(ids, updatedAt)
@@ -59,7 +59,7 @@ export class WorkPackageTimelineCellsRenderer {
}
public getCellsFor(wpId:string):WorkPackageTimelineCell[] {
return _.filter(this.cells, (cell) => cell.workPackageId === wpId) || [];
return Object.values(this.cells).filter((cell) => cell.workPackageId === wpId) || [];
}
/**
@@ -70,11 +70,11 @@ export class WorkPackageTimelineCellsRenderer {
this.synchronizeCells();
// Update all cells
_.each(this.cells, (cell) => this.refreshSingleCell(cell));
Object.values(this.cells).forEach((cell) => this.refreshSingleCell(cell));
}
public refreshCellsFor(wpId:string) {
_.each(this.getCellsFor(wpId), (cell) => this.refreshSingleCell(cell));
this.getCellsFor(wpId).forEach((cell) => this.refreshSingleCell(cell));
}
public refreshSingleCell(cell:WorkPackageTimelineCell, isDuplicatedCell?:boolean, withAlternativeLabels?:boolean) {
@@ -95,7 +95,7 @@ export class WorkPackageTimelineCellsRenderer {
const currentlyActive:string[] = Object.keys(this.cells);
const newCells:string[] = [];
_.each(this.wpTimeline.workPackageIdOrder, (renderedRow:RenderedWorkPackage) => {
this.wpTimeline.workPackageIdOrder.forEach((renderedRow:RenderedWorkPackage) => {
const wpId = renderedRow.workPackageId;
// Ignore extra rows not tied to a work package
@@ -264,7 +264,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
// Update all cells
this.cellsRenderer.refreshAllCells();
_.each(this.renderers, (cb, key) => {
Object.entries(this.renderers).forEach(([key, cb]) => {
debugLog(`Refreshing timeline member ${key}`);
cb(this._viewParameters);
});
@@ -485,7 +485,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
const pixelPerDay = getPixelPerDayForZoomLevel(zoomLevel);
const visibleDays = timelineWidthInPx / pixelPerDay;
if (visibleDays >= daysSpan || zoomLevel === _.last(zoomLevelOrder)) {
if (visibleDays >= daysSpan || zoomLevel === zoomLevelOrder.at(-1)) {
// Zoom level is enough
const previousZoomLevel = this._viewParameters.settings.zoomLevel;
@@ -166,8 +166,8 @@ export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin imple
}
this.removeRelationElementsForWorkPackage(workPackageId);
const relations = _.values(workPackageWithRelation);
const relationsList = _.values(relations);
const relations = Object.values(workPackageWithRelation);
const relationsList = Object.values(relations);
relationsList.forEach((relation) => {
if (!(relation.type === 'precedes'
|| relation.type === 'follows')) {
@@ -196,7 +196,7 @@ export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin imple
}
private renderElements() {
const wpIdsWithRelations:string[] = _.keys(this.workPackagesWithRelations);
const wpIdsWithRelations:string[] = Object.keys(this.workPackagesWithRelations);
this.renderWorkPackagesRelations(wpIdsWithRelations);
}
@@ -163,11 +163,8 @@ export function getTimeSlicesForHeader(vp:TimelineViewParameters,
const firstRest:[Moment, Moment] = rest.splice(0, 1)[0];
const lastRest:[Moment, Moment] = rest.pop()!;
const inViewportAndBoundaries = _.concat(
[firstRest].filter((e) => !_.isNil(e)),
inViewport,
[lastRest].filter((e) => !_.isNil(e)),
);
const inViewportAndBoundaries = [firstRest].filter((e) => e != null)
.concat(inViewport, [lastRest].filter((e) => e != null));
return {
inViewportAndBoundaries,
@@ -78,7 +78,7 @@ export class WorkPackageTableConfiguration {
public filterButtonText:string = I18n.t('js.button_filter');
constructor(providedConfig:WorkPackageTableConfigurationObject) {
_.each(providedConfig, (value, k) => {
Object.entries(providedConfig).forEach(([k, value]) => {
const key = (k as keyof WorkPackageTableConfiguration);
(this as any)[key] = value;
});
@@ -147,8 +147,8 @@ export class WorkPackageViewAdditionalElementsService {
*/
private getInvolvedWorkPackages(states:RelationsStateValue[]) {
const ids:string[] = [];
_.each(states, (relations:RelationsStateValue) => {
_.each(relations, (resource:RelationResource) => {
states.forEach((relations:RelationsStateValue) => {
Object.values(relations).forEach((resource:RelationResource) => {
ids.push(resource.ids.from, resource.ids.to);
});
});
@@ -79,21 +79,21 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
queryColumnTypes.RELATION_OF_TYPE,
queryColumnTypes.RELATION_TO_TYPE,
];
return !!_.find(this.getColumns(), (c) => relationColumns.includes(c._type));
return !!this.getColumns().find((c) => relationColumns.includes(c._type));
}
/**
* Returns whether the current set of columns include child relations
*/
public hasChildRelationsColumn() {
return !!_.find(this.getColumns(), (c) => c._type === queryColumnTypes.RELATION_CHILD);
return !!this.getColumns().find((c) => c._type === queryColumnTypes.RELATION_CHILD);
}
/**
* Returns whether the current set of columns include shares
*/
public hasShareColumn() {
return !!_.find(this.getColumns(), (c) => c.id === sharedUserColumn.id);
return !!this.getColumns().find((c) => c.id === sharedUserColumn.id);
}
/**
@@ -108,7 +108,7 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
* Return the index of the given column or -1 if it is not contained.
*/
public index(id:string):number {
return _.findIndex(this.getColumns(), (column) => column.id === id);
return this.getColumns().findIndex((column) => column.id === id);
}
/**
@@ -116,7 +116,7 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
* @param id
*/
public findById(id:string):QueryColumn|undefined {
return _.find(this.getColumns(), (column) => column.id === id);
return this.getColumns().find((column) => column.id === id);
}
/**
@@ -174,7 +174,7 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
}
public setColumnsById(columnIds:string[]) {
const mapped = columnIds.map((id) => _.find(this.all, (c) => c.id === id));
const mapped = columnIds.map((id) => this.all.find((c) => c.id === id));
this.setColumns(_.compact(mapped));
}
@@ -225,7 +225,7 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
}
if (this.index(id) === -1) {
const newColumn = _.find(this.all, (column) => column.id === id);
const newColumn = this.all.find((column) => column.id === id);
if (!newColumn) {
throw new Error('Column with provided name is not found');
@@ -153,9 +153,7 @@ export class WorkPackageViewFiltersService extends WorkPackageQueryStateService<
public instantiate(filterOrId:QueryFilterResource|string):QueryFilterInstanceResource {
const id = (filterOrId instanceof QueryFilterResource) ? filterOrId.id : filterOrId;
const schema = _.find(
this.availableSchemas,
(schema) => (schema.filter.allowedValues as HalResource)[0].id === id,
const schema = this.availableSchemas.find((schema) => (schema.filter.allowedValues as HalResource[])[0].id === id,
)!;
return schema.getFilter();
@@ -201,7 +199,7 @@ export class WorkPackageViewFiltersService extends WorkPackageQueryStateService<
* @param filters
*/
public isComplete(filters:QueryFilterInstanceResource[]):boolean {
return _.every(filters, (filter) => filter.isCompletelyDefined());
return filters.every((filter) => filter.isCompletelyDefined());
}
/**
@@ -247,7 +245,7 @@ export class WorkPackageViewFiltersService extends WorkPackageQueryStateService<
* @param id Identifier of the filter
*/
public findIndex(id:string):number {
return _.findIndex(this.current, (f) => f.id === id);
return this.current.findIndex((f) => f.id === id);
}
public applyToQuery(query:QueryResource):boolean {
@@ -58,7 +58,7 @@ export class WorkPackageViewGroupByService extends WorkPackageQueryStateService<
}
public isGroupable(column:QueryColumn):boolean {
return !!_.find(this.available, (candidate) => candidate.id === column.id);
return !!this.available.find((candidate) => candidate.id === column.id);
}
public disable() {
@@ -66,7 +66,7 @@ export class WorkPackageViewGroupByService extends WorkPackageQueryStateService<
}
public setBy(column:QueryColumn) {
const groupBy = _.find(this.available, (candidate) => candidate.id === column.id);
const groupBy = this.available.find((candidate) => candidate.id === column.id);
if (groupBy) {
this.update(groupBy);
@@ -36,7 +36,7 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
}
// 3. Is name in selected attributes ?
return !!_.find(this.current.selectedAttributes, (attr:HalResource) => attr.id === name);
return !!this.current.selectedAttributes?.find((attr:HalResource) => attr.id === name);
}
public get current():WorkPackageViewHighlight {
@@ -89,10 +89,8 @@ workPackage:WorkPackageResource,
const type = this.relationColumnType(column);
if (type !== null) {
_.each(
this.relationsForColumn(workPackage, relations, column),
(relation) => eachCallback(relation, column, type),
);
this.relationsForColumn(workPackage, relations, column)
.forEach((relation:RelationResource) => eachCallback(relation, column, type));
}
}
@@ -114,7 +112,7 @@ this.relationsForColumn(workPackage, relations, column),
if (type === 'toType') {
const typeHref = (column as TypeRelationQueryColumn).type.href;
return _.filter(relations, (relation:RelationResource) => {
return Object.values(relations).filter((relation:RelationResource) => {
const denormalized = relation.denormalized(workPackage);
const target = this.apiV3Service.work_packages.cache.state(denormalized.targetId).value;
@@ -126,7 +124,7 @@ this.relationsForColumn(workPackage, relations, column),
if (type === 'ofType') {
const { relationType } = column as RelationQueryColumn;
return _.filter(relations, (relation:RelationResource) => relation.denormalized(workPackage).relationType === relationType);
return Object.values(relations).filter((relation:RelationResource) => relation.denormalized(workPackage).relationType === relationType);
}
return [];
@@ -73,7 +73,7 @@ export class WorkPackageViewSelectionService extends WorkPackageViewBaseService<
public getSelectedWorkPackageIds():string[] {
const selected:string[] = [];
_.each(this.current?.selected, (isSelected:boolean, wpId:string) => {
Object.entries(this.current?.selected ?? {}).forEach(([wpId, isSelected]) => {
if (isSelected) {
selected.push(wpId);
}
@@ -97,7 +97,7 @@ export class WorkPackageViewSelectionService extends WorkPackageViewBaseService<
* Return the number of selected rows.
*/
public get selectionCount():number {
return _.size(this.current?.selected);
return Object.keys(this.current?.selected ?? {}).length;
}
/**
@@ -73,9 +73,7 @@ export class WorkPackageViewSortByService extends WorkPackageQueryStateService<Q
}
public isSortable(column:QueryColumn):boolean {
return !!_.find(
this.available,
(candidate) => candidate.column.href === column.href,
return !!this.available.find((candidate) => candidate.column.href === column.href,
);
}
@@ -96,9 +94,7 @@ export class WorkPackageViewSortByService extends WorkPackageQueryStateService<Q
}
public findAvailableDirection(column:QueryColumn, direction:string):QuerySortByResource | undefined {
return _.find(
this.available,
(candidate) => (candidate.column.href === column.href
return this.available.find((candidate) => (candidate.column.href === column.href
&& candidate.direction.href === direction),
);
}
@@ -146,6 +142,6 @@ export class WorkPackageViewSortByService extends WorkPackageQueryStateService<Q
}
private get manualSortObject() {
return _.find(this.available, (sort) => sort.column.href!.endsWith('/manualSorting'));
return this.available.find((sort) => sort.column.href!.endsWith('/manualSorting'));
}
}
@@ -104,7 +104,7 @@ export class WorkPackageViewTimelineService extends WorkPackageQueryStateService
public getNormalizedLabels(workPackage:WorkPackageResource) {
const labels:TimelineLabels = this.defaultLabels;
_.each(this.current.labels, (attribute:string | null, positionAsString:string) => {
Object.entries(this.current.labels).forEach(([positionAsString, attribute]) => {
// RR: Lodash typings declare the position as string. However, it is save to cast
// to `keyof TimelineLabels` because `this.current.labels` is of type TimelineLabels.
const position:keyof TimelineLabels = positionAsString as keyof TimelineLabels;
@@ -76,7 +76,7 @@ export class DatePicker {
}
});
const mergedOptions = _.extend({}, this.defaultOptions, options);
const mergedOptions = Object.assign({}, this.defaultOptions, options);
let datePickerInstances:Instance|Instance[];
if (this.datepickerTarget) {
@@ -21,7 +21,7 @@ export class Changeset {
* @returns {string[]}
*/
public get changed():string[] {
return _.keys(this.changes);
return Object.keys(this.changes);
}
/**
@@ -219,7 +219,7 @@ export class ResourceChangeset<T extends HalResource = HalResource> {
public get changes():Record<string, unknown> {
const changes:Record<string, unknown> = {};
_.each(this.changeset.all, (item, key) => {
Object.entries(this.changeset.all).forEach(([key, item]) => {
changes[key] = item.to;
});
@@ -389,7 +389,7 @@ export class ResourceChangeset<T extends HalResource = HalResource> {
reference = this.form$.value.payload.$source;
}
_.each(this.changeset.all, (val:ChangeItem, key:string) => {
Object.entries(this.changeset.all).forEach(([key, val]) => {
if (!this.schema.isAttributeEditable(key)) {
debugLog(`Trying to write ${key} but is not writable in schema`);
return;
@@ -495,7 +495,7 @@ export class ResourceChangeset<T extends HalResource = HalResource> {
* that we need to set.
*/
protected setNewDefaults(form:FormResource) {
_.each(form.payload, (val:unknown, key:string) => {
Object.entries(form.payload as Record<string, unknown>).forEach(([key, val]) => {
const fieldSchema:IFieldSchema|null = this.schema.ofProperty(key);
if (!fieldSchema?.writable && !fieldSchema?.required) {
return;
@@ -208,7 +208,7 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
public register(field:EditableAttributeFieldComponent) {
this.fields[field.fieldName] = field;
this.registeredFields.putValue(_.keys(this.fields));
this.registeredFields.putValue(Object.keys(this.fields));
const shouldActivate = (this.editMode && !this.skipField(field) || this.activeFields[field.fieldName]);
@@ -228,7 +228,7 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
}
public start() {
_.each(this.fields, (ctrl) => this.activate(ctrl.fieldName));
Object.values(this.fields).forEach((ctrl) => { void this.activate(ctrl.fieldName); });
}
protected focusOnFirstError():void {
@@ -151,7 +151,7 @@ export abstract class EditForm<T extends HalResource = HalResource> {
return this.change.getForm().then((form:FormResource) => {
const activateFields:Promise<unknown>[] = [];
_.each(form.validationErrors, (_:ErrorResource, key:string) => {
Object.entries(form.validationErrors).forEach(([key]) => {
if (key === 'id') {
return;
}
@@ -183,10 +183,10 @@ export abstract class EditForm<T extends HalResource = HalResource> {
this.errorsPerAttribute = {};
// Notify all fields of upcoming save
const openFields = _.keys(this.activeFields);
const openFields = Object.keys(this.activeFields);
// Call onSubmit handlers
await Promise.all(_.map(this.activeFields, (handler:EditFieldHandler) => handler.onSubmit()));
await Promise.all(Object.values(this.activeFields).map((handler:EditFieldHandler) => handler.onSubmit()));
return new Promise<T>((resolve, reject) => {
this.halEditing.save<T, ResourceChangeset<T>>(this.change)
@@ -231,7 +231,7 @@ export abstract class EditForm<T extends HalResource = HalResource> {
*/
public closeEditFields(fields:string[]|'all' = 'all', resetChange = true) {
if (fields === 'all') {
fields = _.keys(this.activeFields);
fields = Object.keys(this.activeFields);
}
fields.forEach((name:string) => {
@@ -124,7 +124,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
public set selectedOption(val:ValueOption[]) {
this._selectedOption = val;
const mapper = (val:ValueOption) => {
const option = _.find(this.availableOptions, (o) => o.href === val.href) || this.nullOption;
const option = (this.availableOptions as ValueOption[]).find((o) => o.href === val.href) ?? this.nullOption;
// Special case 'null' value, which angular
// only understands in ng-options as an empty string.
@@ -161,10 +161,10 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
}
private findValueOption(option?:HalResource):ValueOption {
let result;
let result:ValueOption|undefined;
if (option) {
result = _.find(this.availableOptions, (valueOption) => valueOption.href === option.href)!;
result = (this.availableOptions as ValueOption[]).find((valueOption) => valueOption.href === option.href)!;
}
return result || this.nullOption;
@@ -225,10 +225,12 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
private checkCurrentValueValidity() {
if (this.value) {
const resourceValue = (this.resource as Record<string, HalResource[]|HalResource>)[this.name];
const values = Array.isArray(resourceValue) ? resourceValue : [resourceValue];
this.currentValueInvalid = !!(
// (If value AND)
// MultiSelect AND there is no value which href is not in the options hrefs
(!_.some(this.value, (value:HalResource) => _.some(this.availableOptions, (option) => (option.href === value.href))))
(!values.some((value:HalResource) => (this.availableOptions as ValueOption[]).some((option) => (option.href === value.href))))
);
} else {
// If no value but required
@@ -42,7 +42,7 @@ export class SelectAutocompleterRegisterService {
}
public getAutocompleterOfAttribute(attribute:string) {
const assignment = _.find(this._fields, (field) => field.attribute === attribute);
const assignment = this._fields.find((field) => field.attribute === attribute);
return assignment ? assignment.component : undefined;
}
}
@@ -86,11 +86,11 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn
public get selectedOption() {
const href = this.value ? this.value.href : null;
return _.find(this.availableOptions, (o) => o.href === href)!;
return (this.availableOptions as ValueOption[]).find((o) => o.href === href)!;
}
public set selectedOption(val:ValueOption|HalResource) {
const option = _.find(this.availableOptions, (o) => o.href === val.href);
const option = (this.availableOptions as ValueOption[]).find((o) => o.href === val.href);
// Special case 'null' value, which angular
// only understands in ng-options as an empty string.
@@ -217,7 +217,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn
public get currentValueInvalid():boolean {
return !!(
(this.value && !_.some(this.availableOptions, (option:HalResource) => (option.href === this.value.href)))
(this.value && !this.availableOptions.some((option:HalResource) => (option.href === (this.value as HalResource).href)))
|| (!this.value && this.schema.required)
);
}
@@ -298,8 +298,8 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn
return {};
}
private getEmptyOption():undefined {
return _.find(this.availableOptions, (el) => el.name === this.text.placeholder);
private getEmptyOption():ValueOption|undefined {
return (this.availableOptions as ValueOption[]).find((el) => el.name === this.text.placeholder);
}
private syncUrlParamsOnChangeIfNeeded(fieldName:string, editMode?:boolean) {
@@ -44,7 +44,7 @@ export class GridWidgetsService {
private buildWidgets() {
let registeredWidgets:WidgetRegistration[] = this.buildDefaultWidgets();
_.each(this.Hook.call('gridWidgets'), (registration:WidgetRegistration[]) => {
this.Hook.call('gridWidgets').forEach((registration:WidgetRegistration[]) => {
registeredWidgets = registeredWidgets.concat(registration);
});
@@ -153,7 +153,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
actions = this.addTimerAction(actions);
// Splice plugin actions onto the core actions
_.each(this.getPermittedPluginActions(authorization), (action:WorkPackageAction) => {
this.getPermittedPluginActions(authorization).forEach((action:WorkPackageAction) => {
const index = action.indexBy ? action.indexBy(actions) : actions.length;
actions.splice(index, 0, action);
});
@@ -132,7 +132,7 @@ export class RemoteFieldUpdaterComponent implements OnInit, OnDestroy {
this
.request(params)
.subscribe((response:object) => {
_.each(response, (val:string, selector:string) => {
Object.entries(response).forEach(([selector, val]) => {
const element = document.getElementById(selector) as HTMLElement|HTMLInputElement;
if (element instanceof HTMLInputElement) {
@@ -193,12 +193,12 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges {
}
public get chartDescription():string {
const chartDataDescriptions = _.map(this.chartLabels, (label, index) => {
const chartDataDescriptions = this.chartLabels.map((label, index) => {
if (this.chartData.length === 1) {
const allCount = this.chartData[0].data[index];
return `${allCount} ${label}`;
}
const labelCounts = _.map(this.chartData, (dataset) => `${dataset.data[index]} ${dataset.label}`);
const labelCounts = this.chartData.map((dataset) => `${dataset.data[index]} ${dataset.label}`);
return `${label}: ${labelCounts.join(', ')}`;
});
@@ -84,7 +84,7 @@ export class KeyboardShortcutService {
public register():void {
void this.configurationService.initialize().then(() => {
if (!this.configurationService.disableKeyboardShortcuts()) {
_.each(this.shortcuts, (action:() => void, key:string) => Mousetrap.bind(key, action));
Object.entries(this.shortcuts).forEach(([key, action]) => Mousetrap.bind(key, action));
}
});
}
@@ -0,0 +1,132 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++
import { describe, expect, it } from 'vitest';
import {
ApiV3FilterBuilder,
FalseValue,
TrueValue,
} from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
// Exercises the object-iteration helpers migrated off lodash (Object.entries
// over the filter map): the `filters` getter and `toFilterObject`.
describe('ApiV3FilterBuilder', () => {
describe('add() and the filters getter', () => {
it('serialises each entry of the filter map to a single-key object', () => {
const builder = new ApiV3FilterBuilder()
.add('status', '=', ['1'])
.add('subject', '~', ['foo']);
expect(builder.filters).toEqual([
{ status: { operator: '=', values: ['1'] } },
{ subject: { operator: '~', values: ['foo'] } },
]);
});
it('preserves insertion order of the filter map', () => {
const builder = new ApiV3FilterBuilder()
.add('c', '=', ['3'])
.add('a', '=', ['1'])
.add('b', '=', ['2']);
expect(builder.filters.map((f) => Object.keys(f)[0])).toEqual(['c', 'a', 'b']);
});
it('maps boolean filter values to the lodash truthy/falsy sentinels', () => {
const builder = new ApiV3FilterBuilder()
.add('open', '=', true)
.add('closed', '=', false);
expect(builder.filters).toEqual([
{ open: { operator: '=', values: TrueValue } },
{ closed: { operator: '=', values: FalseValue } },
]);
});
it('returns an empty array when no filters were added', () => {
expect(new ApiV3FilterBuilder().filters).toEqual([]);
});
});
describe('toFilterObject', () => {
it('flattens an array of single-key filters into one map', () => {
const map = ApiV3FilterBuilder.toFilterObject([
{ status: { operator: '=', values: ['1'] } },
{ subject: { operator: '~', values: ['foo'] } },
]);
expect(map).toEqual({
status: { operator: '=', values: ['1'] },
subject: { operator: '~', values: ['foo'] },
});
});
it('iterates every key of a multi-key filter item', () => {
const map = ApiV3FilterBuilder.toFilterObject([
{
status: { operator: '=', values: ['1'] },
subject: { operator: '~', values: ['foo'] },
},
]);
expect(Object.keys(map)).toEqual(['status', 'subject']);
});
it('lets a later duplicate key overwrite an earlier one', () => {
const map = ApiV3FilterBuilder.toFilterObject([
{ status: { operator: '=', values: ['1'] } },
{ status: { operator: '!', values: ['2'] } },
]);
expect(map.status).toEqual({ operator: '!', values: ['2'] });
});
});
describe('round trips', () => {
it('fromFilterObject reconstructs an equivalent builder', () => {
const original = new ApiV3FilterBuilder()
.add('status', '=', ['1'])
.add('subject', '~', ['foo']);
const rebuilt = ApiV3FilterBuilder.fromFilterObject(
ApiV3FilterBuilder.toFilterObject(original.filters),
);
expect(rebuilt.filters).toEqual(original.filters);
});
it('clone produces an independent copy', () => {
const original = new ApiV3FilterBuilder().add('status', '=', ['1']);
const copy = original.clone();
copy.add('subject', '~', ['foo']);
expect(original.filters).toHaveLength(1);
expect(copy.filters).toHaveLength(2);
});
});
});
@@ -83,7 +83,7 @@ export class ApiV3FilterBuilder {
const map:ApiV3FilterObject = {};
filters.forEach((item:ApiV3Filter) => {
_.each(item, (val:ApiV3FilterValue, filter:string) => {
Object.entries(item).forEach(([filter, val]) => {
map[filter] = val;
});
});
@@ -129,7 +129,7 @@ export class ApiV3FilterBuilder {
public get filters():ApiV3Filter[] {
const filters:ApiV3Filter[] = [];
_.each(this.filterMap, (val:ApiV3FilterValue, filter:string) => {
Object.entries(this.filterMap).forEach(([filter, val]) => {
filters.push({ [filter]: val });
});
@@ -69,7 +69,7 @@ export class DragAndDropService implements OnDestroy {
}
public member(container:HTMLElement):DragMember|undefined {
return _.find(this.members, (el) => el.dragContainer === container);
return this.members.find((el) => el.dragContainer === container);
}
public get initialized() {