Repair filter dialog to accept rich expressions
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
2f5e6d8bcb
commit
c2dbf6846c
@ -0,0 +1,3 @@
|
||||
<div (click)="this.onClick()" [class.selected]="this.selected" class="selectable">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
.selectable.selected {
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
body {
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SelectableComponent } from './selectable.component';
|
||||
|
||||
describe('SelectableComponent', () => {
|
||||
let component: SelectableComponent;
|
||||
let fixture: ComponentFixture<SelectableComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SelectableComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SelectableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import {Component, EventEmitter, Output} from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-selectable",
|
||||
templateUrl: "./selectable.component.html",
|
||||
styleUrls: ["./selectable.component.scss"]
|
||||
})
|
||||
export class SelectableComponent {
|
||||
public selected = false;
|
||||
|
||||
@Output() appSelect = new EventEmitter<this>();
|
||||
@Output() appUnselect = new EventEmitter<this>();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public onClick(): void {
|
||||
this.selected = !this.selected;
|
||||
if (this.selected) {
|
||||
this.appSelect.emit(this);
|
||||
} else {
|
||||
this.appUnselect.emit(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,177 +1,143 @@
|
||||
import {Component, HostListener, Inject, ViewChildren} from "@angular/core";
|
||||
import {Component, Inject, OnChanges, SimpleChanges} from "@angular/core";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
|
||||
import {
|
||||
GenericFilter,
|
||||
OrFilterExpression,
|
||||
SingleFilterExpression
|
||||
} from "../../../../../models/GenericFilter";
|
||||
import {TagQuery} from "../../../../../models/TagQuery";
|
||||
import {Tag} from "../../../../../../api/models/Tag";
|
||||
import {
|
||||
TagFilterListItemComponent
|
||||
} from "./tag-filter-list-item/tag-filter-list-item.component";
|
||||
import {Selectable} from "../../../../../models/Selectable";
|
||||
import {SearchFilters} from "../../../../../../api/models/SearchFilters";
|
||||
import {FilterExpression, FilterQuery} from "../../../../../../api/api-types/files";
|
||||
import {enumerate, removeByValue} from "../../../../../utils/list-utils";
|
||||
|
||||
type IndexableSelection<T> = {
|
||||
[key: number]: T
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-filter-dialog",
|
||||
templateUrl: "./filter-dialog.component.html",
|
||||
styleUrls: ["./filter-dialog.component.scss"]
|
||||
})
|
||||
export class FilterDialogComponent {
|
||||
|
||||
public filters: Selectable<GenericFilter>[];
|
||||
export class FilterDialogComponent implements OnChanges {
|
||||
public availableTags: Tag[] = [];
|
||||
public mode: "AND" | "OR" = "AND";
|
||||
|
||||
@ViewChildren(
|
||||
TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[];
|
||||
|
||||
private selectedQueries: TagQuery[] = [];
|
||||
public filters = new SearchFilters([]);
|
||||
public renderedFilterEntries: [number, FilterExpression][] = [];
|
||||
private selectedIndices: IndexableSelection<number[]> = {};
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
|
||||
MAT_DIALOG_DATA) data: any) {
|
||||
this.filters = data.filterEntries.map(
|
||||
(f: GenericFilter) => new Selectable<GenericFilter>(f,
|
||||
false)) ?? [];
|
||||
this.availableTags = data.availableTags ?? [];
|
||||
this.filters = data.filters;
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
|
||||
private static checkFiltersEqual(l: GenericFilter, r: GenericFilter): boolean {
|
||||
const lTags = l.queryList().map(q => q.getNormalizedTag()).sort();
|
||||
const rTags = r.queryList().map(q => q.getNormalizedTag()).sort();
|
||||
let match = false;
|
||||
|
||||
if (lTags.length == rTags.length) {
|
||||
match = true;
|
||||
|
||||
for (const tag of lTags) {
|
||||
match = rTags.includes(tag);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["filters"]) {
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
public cancelFilter(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public confirmFilter(): void {
|
||||
this.dialogRef.close(this.filters.map(f => f.data));
|
||||
this.dialogRef.close(this.filters);
|
||||
}
|
||||
|
||||
public removeFilter(event: TagFilterListItemComponent): void {
|
||||
const filter = event.expression;
|
||||
const index = this.filters.findIndex(f => f === filter);
|
||||
if (index >= 0) {
|
||||
this.filters.splice(index, 1);
|
||||
public entrySelect(index: number, subindex: number = -1): void {
|
||||
this.selectedIndices[index] = this.selectedIndices[index] ?? [];
|
||||
this.selectedIndices[index].push(subindex);
|
||||
}
|
||||
this.unselectAll();
|
||||
|
||||
public entryUnselect(index: number, subindex: number = -1): void {
|
||||
this.selectedIndices[index] = this.selectedIndices[index] ?? [];
|
||||
removeByValue(this.selectedIndices[index], subindex);
|
||||
}
|
||||
|
||||
public addFilter(tag: string) {
|
||||
const query = TagQuery.fromString(tag);
|
||||
public addFilter(expression: FilterExpression): void {
|
||||
this.filters.addFilterExpression(expression);
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
|
||||
if (this.mode === "AND" || this.filters.length === 0) {
|
||||
this.filters.push(
|
||||
new Selectable<GenericFilter>(
|
||||
new SingleFilterExpression(query),
|
||||
false));
|
||||
tag = tag.replace(/^-/g, "");
|
||||
public removeSelectedFilters(): void {
|
||||
const orderedIndices = Object.keys(this.selectedIndices).map(k => Number(k)).sort().reverse();
|
||||
|
||||
if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) {
|
||||
const index = this.filters.findIndex(
|
||||
t => t.data.partiallyEq(tag));
|
||||
this.filters.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
let queryList = this.filters.pop()?.data.queryList() ?? [];
|
||||
for (const indexStr of orderedIndices) {
|
||||
const index = indexStr;
|
||||
const subIndices: number[] = this.selectedIndices[index];
|
||||
|
||||
queryList.push(query);
|
||||
const filterExpression = new OrFilterExpression(queryList);
|
||||
filterExpression.removeDuplicates();
|
||||
this.filters.push(
|
||||
new Selectable<GenericFilter>(filterExpression,
|
||||
false));
|
||||
if (subIndices.length === 1 && subIndices[0] === -1) {
|
||||
this.filters.removeFilterAtIndex(index);
|
||||
} else if (subIndices.length > 0) {
|
||||
for (const subIndex of subIndices.sort().reverse()) { // need to remove from the top down to avoid index shifting
|
||||
this.filters.removeSubfilterAtIndex(index, subIndex);
|
||||
}
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
public addToSelection(query: TagQuery): void {
|
||||
this.selectedQueries.push(query);
|
||||
}
|
||||
this.selectedIndices = {};
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
|
||||
public removeFromSelection(query: TagQuery): void {
|
||||
const index = this.selectedQueries.indexOf(query);
|
||||
if (index > 0) {
|
||||
this.selectedQueries.splice(index, 1);
|
||||
public createAndFromSelection(deleteOriginal: boolean): void {
|
||||
const expressions: FilterExpression[] = [];
|
||||
|
||||
for (const indexStr in this.selectedIndices) {
|
||||
const index = Number(indexStr);
|
||||
const subindices = this.selectedIndices[index];
|
||||
|
||||
if (subindices.length === 1 && subindices[0] === -1) {
|
||||
expressions.push(this.filters.getFilters()[index]);
|
||||
} else {
|
||||
for (const subIndex of subindices) {
|
||||
const query = this.filters.getSubfilterAtIndex(index, subIndex);
|
||||
if (query) {
|
||||
expressions.push({ Query: query });
|
||||
}
|
||||
}
|
||||
|
||||
public unselectAll() {
|
||||
this.filters.forEach(filter => filter.selected = false);
|
||||
this.selectedQueries = [];
|
||||
this.filterListItems.forEach(i => i.selectedIndices = []);
|
||||
}
|
||||
|
||||
public convertSelectionToAndExpression(): void {
|
||||
for (const query of this.selectedQueries) {
|
||||
this.filters.push(
|
||||
new Selectable<GenericFilter>(
|
||||
new SingleFilterExpression(query),
|
||||
false));
|
||||
}
|
||||
this.removeFilterDuplicates();
|
||||
this.unselectAll();
|
||||
if (deleteOriginal) {
|
||||
this.removeSelectedFilters();
|
||||
} else {
|
||||
this.selectedIndices = {};
|
||||
}
|
||||
|
||||
public convertSelectionToOrExpression(): void {
|
||||
const queries = this.selectedQueries;
|
||||
const expression = new OrFilterExpression(queries);
|
||||
this.filters.push(new Selectable<GenericFilter>(expression, false));
|
||||
this.removeFilterDuplicates();
|
||||
this.unselectAll();
|
||||
expressions.forEach(e => this.filters.addFilterExpression(e));
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
|
||||
public invertSelection(): void {
|
||||
this.selectedQueries.forEach(query => query.negate = !query.negate);
|
||||
}
|
||||
public createOrFromSelection(deleteOriginal: boolean): void {
|
||||
const queries: FilterQuery[] = [];
|
||||
|
||||
private removeFilterDuplicates() {
|
||||
const filters = this.filters;
|
||||
let newFilters: Selectable<GenericFilter>[] = [];
|
||||
for (const indexStr in this.selectedIndices) {
|
||||
const index = Number(indexStr);
|
||||
const subindices = this.selectedIndices[index];
|
||||
|
||||
for (const filterItem of filters) {
|
||||
if (filterItem.data.filter_type == "OrExpression") {
|
||||
(filterItem.data as OrFilterExpression).removeDuplicates();
|
||||
if (subindices.length === 1 && subindices[0] === -1) {
|
||||
const filterEntry = this.filters.getFilters()[index];
|
||||
if ("Query" in filterEntry) {
|
||||
queries.push(filterEntry.Query);
|
||||
}
|
||||
if (newFilters.findIndex(
|
||||
f => FilterDialogComponent.checkFiltersEqual(f.data,
|
||||
filterItem.data)) < 0) {
|
||||
if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) {
|
||||
filterItem.data = new SingleFilterExpression(
|
||||
filterItem.data.queryList()[0]);
|
||||
} else {
|
||||
for (const subIndex of subindices) {
|
||||
const query = this.filters.getSubfilterAtIndex(index, subIndex);
|
||||
if (query) {
|
||||
queries.push(query);
|
||||
}
|
||||
newFilters.push(filterItem);
|
||||
}
|
||||
}
|
||||
this.filters = newFilters;
|
||||
}
|
||||
|
||||
@HostListener("window:keydown", ["$event"])
|
||||
private async handleKeydownEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Shift") {
|
||||
this.mode = "OR";
|
||||
if (deleteOriginal) {
|
||||
this.removeSelectedFilters();
|
||||
} else {
|
||||
this.selectedIndices = {};
|
||||
}
|
||||
if (queries.length > 1) {
|
||||
this.filters.addFilterExpression({ OrExpression: queries });
|
||||
} else if (queries.length === 1) {
|
||||
this.filters.addFilterExpression({ Query: queries[0] });
|
||||
}
|
||||
|
||||
@HostListener("window:keyup", ["$event"])
|
||||
private async handleKeyupEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Shift") {
|
||||
this.mode = "AND";
|
||||
this.buildRenderedEntries();
|
||||
}
|
||||
|
||||
private buildRenderedEntries() {
|
||||
this.renderedFilterEntries = enumerate(this.filters.getFilters());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
<span *ngIf="this.orExpression" class="or-expression">
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let entry of this.orExpression"
|
||||
[class.selected]="componentSelectable.selected"
|
||||
class="or-filter-list-item">
|
||||
<app-selectable #componentSelectable
|
||||
(appSelect)="this.entrySelect.emit(entry)"
|
||||
(appUnselect)="this.entryUnselect.emit(entry)">
|
||||
<app-property-query-item *ngIf="this.queryIs(entry[1], 'Property')"
|
||||
[propertyQuery]="this.propertyQuery(entry[1]).Property"></app-property-query-item>
|
||||
<app-tag-query-item *ngIf="this.queryIs(entry[1], 'Tag')"
|
||||
[tagQuery]="this.tagQuery(entry[1]).Tag"></app-tag-query-item>
|
||||
</app-selectable>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</span>
|
||||
<span *ngIf="this.query" [class.selected]="singleSelectable.selected" class="query">
|
||||
<app-selectable #singleSelectable
|
||||
(appSelect)="this.appSelect.emit(this.query)"
|
||||
(appUnselect)="this.appUnselect.emit(this.query)">
|
||||
<app-property-query-item *ngIf="this.queryIs(this.query, 'Property')"
|
||||
[propertyQuery]="this.propertyQuery(this.query).Property"></app-property-query-item>
|
||||
<app-tag-query-item *ngIf="this.queryIs(this.query, 'Tag')"
|
||||
[tagQuery]="this.tagQuery(this.query).Tag"></app-tag-query-item>
|
||||
</app-selectable>
|
||||
</span>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterExpressionListItemComponent } from './filter-expression-list-item.component';
|
||||
|
||||
describe('FilterExpressionListItemComponent', () => {
|
||||
let component: FilterExpressionListItemComponent;
|
||||
let fixture: ComponentFixture<FilterExpressionListItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterExpressionListItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterExpressionListItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "@angular/core";
|
||||
import {
|
||||
FilterExpression,
|
||||
FilterQuery,
|
||||
FilterQueryProperty,
|
||||
FilterQueryTag
|
||||
} from "../../../../../../../api/api-types/files";
|
||||
import {enumerate} from "../../../../../../utils/list-utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-filter-expression-list-item",
|
||||
templateUrl: "./filter-expression-list-item.component.html",
|
||||
styleUrls: ["./filter-expression-list-item.component.scss"]
|
||||
})
|
||||
export class FilterExpressionListItemComponent implements OnChanges {
|
||||
|
||||
|
||||
@Input() filter!: FilterExpression;
|
||||
@Output() entrySelect = new EventEmitter<[number, FilterQuery]>();
|
||||
@Output() entryUnselect = new EventEmitter<[number, FilterQuery]>();
|
||||
|
||||
@Output() appSelect = new EventEmitter<FilterQuery>();
|
||||
@Output() appUnselect = new EventEmitter<FilterQuery>();
|
||||
|
||||
public orExpression: undefined | [number, FilterQuery][] = undefined;
|
||||
public query: undefined | FilterQuery = undefined;
|
||||
|
||||
constructor() {
|
||||
this.parseFilter();
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["filter"]) {
|
||||
this.parseFilter();
|
||||
}
|
||||
}
|
||||
|
||||
public queryIs(query: FilterQuery, key: "Property" | "Tag"): boolean {
|
||||
return key in query;
|
||||
}
|
||||
|
||||
public propertyQuery(query: FilterQuery): FilterQueryProperty {
|
||||
return query as FilterQueryProperty;
|
||||
}
|
||||
|
||||
public tagQuery(query: FilterQuery): FilterQueryTag {
|
||||
return query as FilterQueryTag;
|
||||
}
|
||||
|
||||
private parseFilter() {
|
||||
if (this.filter && "OrExpression" in this.filter) {
|
||||
this.orExpression = enumerate(this.filter.OrExpression);
|
||||
} else if (this.filter) {
|
||||
this.query = this.filter.Query;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
<div (click)="onSelect()" *ngIf="expression.data.filter_type === 'Query'">
|
||||
{{expression.data.getDisplayName()}}
|
||||
<button (click)="this.removeClicked.emit(this)" class="remove-button" mat-button>
|
||||
<ng-icon name="mat-remove"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="expression.data.filter_type === 'OrExpression'">
|
||||
<mat-list>
|
||||
<mat-list-item (mousedown)="$event.button === 0 && this.selectInnerIndex(entry[0])"
|
||||
*ngFor="let entry of enumerate(this.expression.data.queryList())"
|
||||
[class.selected]="this.selectedIndices.includes(entry[0])"
|
||||
class="or-filter-list-item">
|
||||
<span *ngIf="entry[0] > 0" class="or-span">OR</span>
|
||||
{{entry[1].getNormalizedTag()}}
|
||||
<button (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])"
|
||||
class="remove-button-inner-list"
|
||||
mat-button>
|
||||
<ng-icon name="mat-remove"></ng-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
@ -1,25 +0,0 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {TagFilterListItemComponent} from "./tag-filter-list-item.component";
|
||||
|
||||
describe("TagFilterListItemComponent", () => {
|
||||
let component: TagFilterListItemComponent;
|
||||
let fixture: ComponentFixture<TagFilterListItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [TagFilterListItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagFilterListItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,77 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges
|
||||
} from "@angular/core";
|
||||
import {
|
||||
GenericFilter,
|
||||
OrFilterExpression,
|
||||
SingleFilterExpression
|
||||
} from "../../../../../../models/GenericFilter";
|
||||
import {TagQuery} from "../../../../../../models/TagQuery";
|
||||
import {Selectable} from "../../../../../../models/Selectable";
|
||||
|
||||
@Component({
|
||||
selector: "app-tag-filter-list-item",
|
||||
templateUrl: "./tag-filter-list-item.component.html",
|
||||
styleUrls: ["./tag-filter-list-item.component.scss"]
|
||||
})
|
||||
export class TagFilterListItemComponent implements OnChanges {
|
||||
|
||||
@Input() expression!: Selectable<GenericFilter>;
|
||||
@Output() removeClicked = new EventEmitter<TagFilterListItemComponent>();
|
||||
@Output() querySelect = new EventEmitter<TagQuery>();
|
||||
@Output() queryUnselect = new EventEmitter<TagQuery>();
|
||||
|
||||
public selectedIndices: number[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["expression"]) {
|
||||
this.selectedIndices = [];
|
||||
}
|
||||
}
|
||||
|
||||
public enumerate<T>(items: T[]): [number, T][] {
|
||||
return items.map((value, index) => [index, value]);
|
||||
}
|
||||
|
||||
public removeOrExpression(index: number) {
|
||||
const expression = this.expression.data as OrFilterExpression;
|
||||
expression.removeQueryEntry(index);
|
||||
|
||||
if (expression.filter.length == 0) {
|
||||
this.removeClicked.emit(this);
|
||||
} else if (expression.filter.length == 1) {
|
||||
this.expression.data = new SingleFilterExpression(
|
||||
expression.filter[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public selectInnerIndex(index: number): void {
|
||||
const expression = this.expression.data as OrFilterExpression;
|
||||
|
||||
if (this.selectedIndices.includes(index)) {
|
||||
const elementIndex = this.selectedIndices.indexOf(index);
|
||||
this.selectedIndices.splice(elementIndex, 1);
|
||||
this.queryUnselect.emit(expression.filter[index]);
|
||||
} else {
|
||||
this.selectedIndices.push(index);
|
||||
this.querySelect.emit(expression.filter[index]);
|
||||
}
|
||||
}
|
||||
|
||||
public onSelect(): void {
|
||||
this.expression.selected = !this.expression.selected;
|
||||
if (this.expression.selected) {
|
||||
this.querySelect.emit(this.expression.data.filter as TagQuery);
|
||||
} else {
|
||||
this.queryUnselect.emit(this.expression.data.filter as TagQuery);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
export function enumerate<T>(list: T[]): [number, T][] {
|
||||
const enumeratedEntries = [];
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
enumeratedEntries.push([i, list[i]] as [number, T]);
|
||||
}
|
||||
return enumeratedEntries;
|
||||
}
|
||||
|
||||
export function removeByValue<T>(list: T[], entry: T) {
|
||||
const index = list.indexOf(entry);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue