mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[OP-19544] Replace lodash set/dedup helpers
Set, dedup and difference helpers move from the global `_` to native operations: `flatten` -> `flat()`, `uniq` -> `Array.from(new Set(...))`, `without`/`difference` -> `filter` + `includes`, `uniqBy` -> `filter` + `findIndex`, `differenceBy` -> `filter` + `some`. The global `_` stays until the remaining buckets land. `uniqBy` keeps the first occurrence of each key (via `findIndex`), as lodash does — not the last, which a `Map`-based dedup would yield. https://community.openproject.org/wp/OP-19544
This commit is contained in:
@@ -56,7 +56,7 @@ export class ApiV3RelationsPaths extends ApiV3ResourceCollection<RelationResourc
|
||||
const chunks = _.chunk(workPackageIds, MAGIC_RELATION_SIZE);
|
||||
return forkJoin(chunks.map((chunk) => this.loadInvolved(chunk)))
|
||||
.pipe(
|
||||
map((results) => _.flatten(results)),
|
||||
map((results) => results.flat()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export class ApiV3WorkPackagesPaths extends ApiV3Collection<WorkPackageResource,
|
||||
|
||||
return new Promise<undefined>((resolve, reject) => {
|
||||
this
|
||||
.loadCollectionsFor(_.uniq(ids))
|
||||
.loadCollectionsFor(Array.from(new Set(ids)))
|
||||
.then((pagedResults:WorkPackageCollectionResource[]) => {
|
||||
_.each(pagedResults, (results) => {
|
||||
if (results.schemas) {
|
||||
|
||||
@@ -94,20 +94,20 @@ export class ErrorResource extends HalResource {
|
||||
}
|
||||
|
||||
public getInvolvedAttributes():string[] {
|
||||
let columns = [];
|
||||
let columns:ErrorResource[] = [];
|
||||
|
||||
if (this.details) {
|
||||
columns = [{ details: this.details }];
|
||||
columns = [{ details: this.details } as ErrorResource];
|
||||
} else if (this.errors) {
|
||||
columns = this.errors;
|
||||
columns = this.errors as ErrorResource[];
|
||||
}
|
||||
|
||||
return _.flatten(columns.map((resource:ErrorResource) => {
|
||||
return columns.map((resource:ErrorResource):string => {
|
||||
if (resource.errorIdentifier === v3ErrorIdentifierMultipleErrors) {
|
||||
return this.extractMultiError(resource)[0];
|
||||
}
|
||||
return resource.details.attribute;
|
||||
}));
|
||||
return resource.details.attribute as string;
|
||||
}).flat();
|
||||
}
|
||||
|
||||
public getMessagesPerAttribute():Record<string, string[]> {
|
||||
|
||||
@@ -286,7 +286,7 @@ export class HalResource {
|
||||
*/
|
||||
public $embeddableKeys():string[] {
|
||||
const properties = Object.keys(this.$source);
|
||||
return _.without(properties, '_links', '_embedded', 'id');
|
||||
return properties.filter((property) => !['_links', '_embedded', 'id'].includes(property));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,6 +295,6 @@ export class HalResource {
|
||||
*/
|
||||
public $linkableKeys():string[] {
|
||||
const properties = Object.keys(this.$links);
|
||||
return _.without(properties, 'self');
|
||||
return properties.filter((property) => property !== 'self');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,6 @@ export class ProjectResource extends HalResource {
|
||||
* Exclude the schema _link from the linkable Resources.
|
||||
*/
|
||||
public $linkableKeys():string[] {
|
||||
return _.without(super.$linkableKeys(), 'schema');
|
||||
return super.$linkableKeys().filter((key) => key !== 'schema');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class TimeEntryResource extends HalResource {
|
||||
* Exclude the schema _link from the linkable Resources.
|
||||
*/
|
||||
public $linkableKeys():string[] {
|
||||
return _.without(super.$linkableKeys(), 'schema');
|
||||
return super.$linkableKeys().filter((key) => key !== 'schema');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ export class WorkPackageBaseResource extends HalResource {
|
||||
* Exclude the schema _link from the linkable Resources.
|
||||
*/
|
||||
public $linkableKeys():string[] {
|
||||
return _.without(super.$linkableKeys(), 'schema');
|
||||
return super.$linkableKeys().filter((key) => key !== 'schema');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -69,6 +69,6 @@ export class InAppNotificationActorsLineComponent implements OnInit {
|
||||
})
|
||||
.filter((actor) => actor !== null) as PrincipalLike[];
|
||||
|
||||
this.actors = _.uniqBy(actors, (item) => item.href);
|
||||
this.actors = actors.filter((item, index, self) => index === self.findIndex((other) => other.href === item.href));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ export class WorkPackageCardDragAndDropService {
|
||||
* Update current order
|
||||
*/
|
||||
private updateOrder(newOrder:string[]) {
|
||||
newOrder = _.uniq(newOrder);
|
||||
newOrder = Array.from(new Set(newOrder));
|
||||
|
||||
Promise
|
||||
.all(newOrder.map((id) => this
|
||||
|
||||
+6
-1
@@ -132,7 +132,12 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
|
||||
// Append all new elements
|
||||
elements = elements.concat(newElements);
|
||||
// Remove duplicates (Regression #29652)
|
||||
this.deferred[parent.id!] = _.uniqBy(elements, (el) => el.id!);
|
||||
const seen = new Set<string>();
|
||||
this.deferred[parent.id!] = elements.filter((el) => {
|
||||
if (seen.has(el.id!)) { return false; }
|
||||
seen.add(el.id!);
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Otherwise, continue the chain upwards
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ export class ChildRelationsRenderPass extends RelationsRenderPass {
|
||||
}
|
||||
|
||||
private loadMissingTargets(ids:string[]) {
|
||||
const uniqueIds = _.uniq(ids);
|
||||
const uniqueIds = Array.from(new Set(ids));
|
||||
|
||||
if (uniqueIds.length === 0 || this.loadingMissingTargets) {
|
||||
return;
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ export class DragAndDropTransformer {
|
||||
* Update current rendered order
|
||||
*/
|
||||
private async updateRenderedOrder(order:string[]) {
|
||||
order = _.uniq(order);
|
||||
order = Array.from(new Set(order));
|
||||
|
||||
const mappedOrder = await Promise.all(
|
||||
order.map(
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ export class WorkPackagesActivityService extends WorkPackageLinkedResourceCache<
|
||||
}
|
||||
|
||||
protected sortedActivityList(activities:HalResource[], attr = 'createdAt'):HalResource[] {
|
||||
const sorted = _.sortBy(_.flatten(activities), attr);
|
||||
const sorted = _.sortBy(activities.flat(), attr);
|
||||
|
||||
if (this.isReversed) {
|
||||
return sorted.reverse();
|
||||
|
||||
+2
-2
@@ -90,7 +90,7 @@ export class WpTableConfigurationSortByTabComponent implements TabComponent, OnI
|
||||
|
||||
// For whatever reason, even though the UI doesn't implement it,
|
||||
// QuerySortByResources are doubled for each column (one for asc/desc direction)
|
||||
this.allColumns = _.uniqBy(allColumns, 'href');
|
||||
this.allColumns = allColumns.filter((col, index, self) => index === self.findIndex((other) => other.href === col.href));
|
||||
|
||||
this.getManualSortingOption();
|
||||
|
||||
@@ -122,7 +122,7 @@ export class WpTableConfigurationSortByTabComponent implements TabComponent, OnI
|
||||
.filter((o) => o.column !== null)
|
||||
.map((object:SortModalObject) => object.column);
|
||||
|
||||
this.availableColumns = _.sortBy(_.differenceBy(this.allColumns, usedColumns, 'href'), 'name');
|
||||
this.availableColumns = _.sortBy(this.allColumns.filter((col) => !usedColumns.some((used) => used.href === col.href)), 'name');
|
||||
}
|
||||
|
||||
public updateSortingMode(mode:SortingMode) {
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ export class WorkPackageTimelineCellsRenderer {
|
||||
newCells.push(identifier);
|
||||
});
|
||||
|
||||
_.difference(currentlyActive, newCells).forEach((identifier:string) => {
|
||||
currentlyActive.filter((identifier) => !newCells.includes(identifier)).forEach((identifier:string) => {
|
||||
this.cells[identifier].clear();
|
||||
delete this.cells[identifier];
|
||||
});
|
||||
|
||||
+5
-7
@@ -73,7 +73,7 @@ export class WorkPackageViewAdditionalElementsService {
|
||||
this.requireWorkPackageShares(workPackageIds),
|
||||
this.requireSumsSchema(results),
|
||||
]).then((wpResults:string[][]) => {
|
||||
this.loadAdditional(_.flatten(wpResults));
|
||||
this.loadAdditional(wpResults.flat());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export class WorkPackageViewAdditionalElementsService {
|
||||
.requireAll(rows)
|
||||
.then(() => {
|
||||
const ids = this.getInvolvedWorkPackages(rows.map((id) => this.wpRelations.state(id).value!));
|
||||
return _.flatten(ids);
|
||||
return ids.flat();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,9 +116,7 @@ export class WorkPackageViewAdditionalElementsService {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const ids = _.flatten(
|
||||
rows.map((el) => el.children?.map((child) => child.id!) || []),
|
||||
);
|
||||
const ids = rows.map((el) => el.children?.map((child) => child.id!) || []).flat();
|
||||
|
||||
return Promise.resolve(ids);
|
||||
}
|
||||
@@ -134,7 +132,7 @@ export class WorkPackageViewAdditionalElementsService {
|
||||
}
|
||||
|
||||
const resultIds = rows.map((el:WorkPackageResource) => (el.id as string | number).toString());
|
||||
const ids = _.flatten(rows.map((el) => el.ancestorIds))
|
||||
const ids = rows.map((el) => el.ancestorIds).flat()
|
||||
.filter((id) => !resultIds.includes(id));
|
||||
|
||||
return Promise.resolve(ids);
|
||||
@@ -184,7 +182,7 @@ export class WorkPackageViewAdditionalElementsService {
|
||||
map((elements) => {
|
||||
const shares = elements as ShareResource[];
|
||||
|
||||
const sharedWpIds = _.uniq(shares.map((share) => share.entity.id!));
|
||||
const sharedWpIds = Array.from(new Set(shares.map((share) => share.entity.id!)));
|
||||
|
||||
sharedWpIds.forEach((wpId) => {
|
||||
this
|
||||
|
||||
+3
-2
@@ -65,7 +65,7 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
|
||||
query.columns = cloneHalResourceCollection<QueryColumn>(toApply);
|
||||
|
||||
// We can avoid reloading even with relation columns if we only removed columns
|
||||
const onlyRemoved = _.difference(newColumns, oldColumns).length === 0;
|
||||
const onlyRemoved = newColumns.filter((column) => !oldColumns.includes(column)).length === 0;
|
||||
|
||||
// Reload the table visibly if adding relation or share columns.
|
||||
return !onlyRemoved && (this.hasRelationColumns() || this.hasShareColumn());
|
||||
@@ -283,7 +283,8 @@ export class WorkPackageViewColumnsService extends WorkPackageQueryStateService<
|
||||
* Get columns not yet selected
|
||||
*/
|
||||
public get unused():QueryColumn[] {
|
||||
return _.differenceBy(this.all, this.getColumns(), '$href');
|
||||
const columns = this.getColumns();
|
||||
return this.all.filter((column) => !columns.some((other) => other.$href === column.$href));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -313,7 +313,7 @@ export class WorkPackageViewFiltersService extends WorkPackageQueryStateService<
|
||||
* Get all filters that are not in the current active set
|
||||
*/
|
||||
private remainingFilters(filters = this.rawFilters) {
|
||||
return _.differenceBy(this.availableFilters, filters, (filter) => filter.id);
|
||||
return this.availableFilters.filter((available) => !filters.some((filter) => filter.id === available.id));
|
||||
}
|
||||
|
||||
isAvailable(el:QueryFilterInstanceResource):boolean {
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ export class WorkPackageViewHierarchyIdentationService {
|
||||
// get the first element of the ancestor chain that workPackage is not in
|
||||
const predecessor = await firstValueFrom(this.apiV3Service.work_packages.id(predecessorId).get());
|
||||
|
||||
const difference = _.difference(predecessor.ancestorIds, workPackage.ancestorIds);
|
||||
const difference = predecessor.ancestorIds.filter((id) => !workPackage.ancestorIds.includes(id));
|
||||
if (difference && difference.length > 0) {
|
||||
newParentId = difference[0];
|
||||
}
|
||||
|
||||
+9
-1
@@ -127,7 +127,15 @@ export class UserAutocompleterComponent extends OpAutocompleterComponent<IUserAu
|
||||
.http
|
||||
.get<IHALCollection<IUser>>(filteredURL.toString())
|
||||
.pipe(
|
||||
map((res) => _.uniqBy(res._embedded.elements, (el) => el._links.self?.href || el.id)),
|
||||
map((res) => {
|
||||
const seen = new Set<string|number>();
|
||||
return res._embedded.elements.filter((el) => {
|
||||
const key = el._links.self?.href || el.id;
|
||||
if (seen.has(key)) { return false; }
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}),
|
||||
map((users) => {
|
||||
const mapped:IUserAutocompleteItem[] = users.map((user) => {
|
||||
return { id: user.id, name: user.name, href: user._links.self?.href, email: user.email };
|
||||
|
||||
+2
-5
@@ -307,11 +307,8 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl
|
||||
filter((resource) => !!resource),
|
||||
)
|
||||
.subscribe((resource:HalResource&{ attachments:AttachmentCollectionResource }) => {
|
||||
const missingAttachments = _.differenceBy(
|
||||
this.attachments,
|
||||
resource.attachments.elements,
|
||||
(attachment:HalResource) => attachment.id,
|
||||
);
|
||||
const presentIds = new Set<string|null>(resource.attachments.elements.map((other:HalResource) => other.id));
|
||||
const missingAttachments = this.attachments.filter((attachment:HalResource) => !presentIds.has(attachment.id));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return
|
||||
const removedUrls = missingAttachments.map((attachment) => attachment.downloadLocation.href);
|
||||
|
||||
+8
-1
@@ -327,7 +327,14 @@ export class SearchableProjectListService {
|
||||
return forkJoin(extraFetches).pipe(
|
||||
map((collections) => collections.map((collection) => collection._embedded.elements)),
|
||||
map((collections) => projects.concat(...collections)),
|
||||
map((allProjects) => _.uniqBy(allProjects, (p) => p.id)),
|
||||
map((allProjects) => {
|
||||
const seen = new Set<ID>();
|
||||
return allProjects.filter((p) => {
|
||||
if (seen.has(p.id)) { return false; }
|
||||
seen.add(p.id);
|
||||
return true;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -75,10 +75,10 @@ export class WorkPackageEmbeddedGraphComponent implements OnChanges {
|
||||
}
|
||||
|
||||
private updateChartData() {
|
||||
let uniqLabels = _.uniq(this.datasets.reduce((array, dataset) => {
|
||||
let uniqLabels = Array.from(new Set(this.datasets.reduce((array, dataset) => {
|
||||
const groups = (dataset.groups || []).map((group) => group.value) as any;
|
||||
return array.concat(groups);
|
||||
}, [])) as string[];
|
||||
}, []))) as string[];
|
||||
|
||||
const labelCountMaps = this.datasets.map((dataset) => {
|
||||
const countMap = (dataset.groups || []).reduce<any>((hash, group) => ({
|
||||
|
||||
@@ -563,11 +563,11 @@ export default class FiltersFormController extends Controller {
|
||||
if (operator && this.daysOperators.includes(operator)) {
|
||||
const dateValue = this.findTargetByName(filterName, this.daysTargets)?.value;
|
||||
|
||||
value = _.without([dateValue], '');
|
||||
value = [dateValue].filter((v) => v !== '');
|
||||
} else if (operator === this.onDateOperator) {
|
||||
const dateValue = this.findTargetById(filterName, this.singleDayTargets)?.value;
|
||||
|
||||
value = _.without([dateValue], '');
|
||||
value = [dateValue].filter((v) => v !== '');
|
||||
} else if (operator === this.betweenDatesOperator) {
|
||||
const rangeValue = this.findTargetById(filterName, this.dateRangeTargets)?.value;
|
||||
const [fromValue, toValue] = rangeValue?.split(' - ') ?? [];
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ export default class PreviewController extends DialogPreviewController {
|
||||
|
||||
const fieldDates = _.compact([this.currentStartDate, this.currentDueDate])
|
||||
.map((date) => this.timezoneService.utcDateToISODateString(date));
|
||||
const diff = _.difference(flatPickrDates, fieldDates);
|
||||
const diff = flatPickrDates.filter((date) => !fieldDates.includes(date));
|
||||
return this.toDate(diff[0]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user