@sneat/ui 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/index.js +4 -0
- package/esm2022/index.js.map +1 -0
- package/esm2022/lib/components/index.js +3 -0
- package/esm2022/lib/components/index.js.map +1 -0
- package/esm2022/lib/components/sneat-base-modal.component.js +24 -0
- package/esm2022/lib/components/sneat-base-modal.component.js.map +1 -0
- package/esm2022/lib/components/sneat-base.component.js +58 -0
- package/esm2022/lib/components/sneat-base.component.js.map +1 -0
- package/esm2022/lib/focus.js +17 -0
- package/esm2022/lib/focus.js.map +1 -0
- package/esm2022/lib/selector/index.js +7 -0
- package/esm2022/lib/selector/index.js.map +1 -0
- package/esm2022/lib/selector/multi-selector/index.js +3 -0
- package/esm2022/lib/selector/multi-selector/index.js.map +1 -0
- package/esm2022/lib/selector/multi-selector/multi-selector.component.js +70 -0
- package/esm2022/lib/selector/multi-selector/multi-selector.component.js.map +1 -0
- package/esm2022/lib/selector/multi-selector/multi-selector.service.js +49 -0
- package/esm2022/lib/selector/multi-selector/multi-selector.service.js.map +1 -0
- package/esm2022/lib/selector/select-from-list/index.js +2 -0
- package/esm2022/lib/selector/select-from-list/index.js.map +1 -0
- package/esm2022/lib/selector/select-from-list/select-from-list.component.js +234 -0
- package/esm2022/lib/selector/select-from-list/select-from-list.component.js.map +1 -0
- package/esm2022/lib/selector/selector-base.component.js +36 -0
- package/esm2022/lib/selector/selector-base.component.js.map +1 -0
- package/esm2022/lib/selector/selector-base.service.js +49 -0
- package/esm2022/lib/selector/selector-base.service.js.map +1 -0
- package/esm2022/lib/selector/selector-interfaces.js +2 -0
- package/esm2022/lib/selector/selector-interfaces.js.map +1 -0
- package/esm2022/lib/selector/selector-options.js +2 -0
- package/esm2022/lib/selector/selector-options.js.map +1 -0
- package/esm2022/sneat-ui.js +5 -0
- package/esm2022/sneat-ui.js.map +1 -0
- package/lib/components/sneat-base-modal.component.d.ts +9 -0
- package/lib/components/sneat-base.component.d.ts +28 -0
- package/lib/focus.d.ts +3 -0
- package/lib/selector/multi-selector/multi-selector.component.d.ts +17 -0
- package/lib/selector/multi-selector/multi-selector.service.d.ts +9 -0
- package/lib/selector/select-from-list/select-from-list.component.d.ts +60 -0
- package/lib/selector/selector-base.component.d.ts +18 -0
- package/lib/selector/selector-base.service.d.ts +11 -0
- package/lib/selector/selector-interfaces.d.ts +17 -0
- package/lib/selector/selector-options.d.ts +15 -0
- package/package.json +14 -2
- package/sneat-ui.d.ts +5 -0
- package/eslint.config.js +0 -7
- package/ng-package.json +0 -7
- package/project.json +0 -38
- package/src/lib/components/sneat-base-modal.component.ts +0 -22
- package/src/lib/components/sneat-base.component.spec.ts +0 -71
- package/src/lib/components/sneat-base.component.ts +0 -78
- package/src/lib/focus.ts +0 -19
- package/src/lib/selector/multi-selector/multi-selector.component.html +0 -26
- package/src/lib/selector/multi-selector/multi-selector.component.spec.ts +0 -147
- package/src/lib/selector/multi-selector/multi-selector.component.ts +0 -79
- package/src/lib/selector/multi-selector/multi-selector.service.spec.ts +0 -91
- package/src/lib/selector/multi-selector/multi-selector.service.ts +0 -49
- package/src/lib/selector/select-from-list/select-from-list.component.html +0 -210
- package/src/lib/selector/select-from-list/select-from-list.component.spec.ts +0 -297
- package/src/lib/selector/select-from-list/select-from-list.component.ts +0 -283
- package/src/lib/selector/selector-base.component.ts +0 -43
- package/src/lib/selector/selector-base.service.ts +0 -62
- package/src/lib/selector/selector-interfaces.ts +0 -28
- package/src/lib/selector/selector-options.ts +0 -18
- package/src/test-setup.ts +0 -3
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -19
- package/tsconfig.lib.prod.json +0 -7
- package/tsconfig.spec.json +0 -31
- package/vite.config.mts +0 -10
- /package/{src/index.ts → index.d.ts} +0 -0
- /package/{src/lib/components/index.ts → lib/components/index.d.ts} +0 -0
- /package/{src/lib/selector/index.ts → lib/selector/index.d.ts} +0 -0
- /package/{src/lib/selector/multi-selector/index.ts → lib/selector/multi-selector/index.d.ts} +0 -0
- /package/{src/lib/selector/select-from-list/index.ts → lib/selector/select-from-list/index.d.ts} +0 -0
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { inject, Injectable, InjectionToken, OnDestroy } from '@angular/core';
|
|
2
|
-
import { createSetFocusToInput } from '../focus';
|
|
3
|
-
import { ErrorLogger } from '@sneat/core';
|
|
4
|
-
import { MonoTypeOperatorFunction, Subject, Subscription } from 'rxjs';
|
|
5
|
-
import { takeUntil } from 'rxjs/operators';
|
|
6
|
-
|
|
7
|
-
export interface IConsole {
|
|
8
|
-
log(...data: unknown[]): void;
|
|
9
|
-
|
|
10
|
-
warn(...data: unknown[]): void;
|
|
11
|
-
|
|
12
|
-
trace(...data: unknown[]): void;
|
|
13
|
-
|
|
14
|
-
error(...data: unknown[]): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const ClassName = new InjectionToken<string>('className');
|
|
18
|
-
|
|
19
|
-
@Injectable()
|
|
20
|
-
export abstract class SneatBaseComponent implements OnDestroy {
|
|
21
|
-
// protected $isInitialized = signal(false);
|
|
22
|
-
//
|
|
23
|
-
// // eslint-disable-next-line @angular-eslint/contextual-lifecycle
|
|
24
|
-
// public ngOnInit() {
|
|
25
|
-
// console.info(`${this.className}.SneatBaseComponent.ngOnInit()`);
|
|
26
|
-
// // $isInitialized is for workaround for https://angular.dev/errors/NG0950
|
|
27
|
-
// // Required input is accessed before a value is set.
|
|
28
|
-
// // Might be excessive and should be removed if we can find a better way.
|
|
29
|
-
// this.$isInitialized.set(true);
|
|
30
|
-
// }
|
|
31
|
-
|
|
32
|
-
private readonly destroyed = new Subject<void>();
|
|
33
|
-
// Signals that the component is destroyed and should not be used anymore
|
|
34
|
-
protected readonly destroyed$ = this.destroyed.asObservable();
|
|
35
|
-
|
|
36
|
-
// All active subscriptions of a component. Will be unsubscribed on destroy
|
|
37
|
-
protected readonly subs = new Subscription();
|
|
38
|
-
|
|
39
|
-
protected readonly console: IConsole = console;
|
|
40
|
-
|
|
41
|
-
protected readonly errorLogger = inject(ErrorLogger);
|
|
42
|
-
|
|
43
|
-
protected readonly logError = this.errorLogger.logError;
|
|
44
|
-
protected readonly logErrorHandler = this.errorLogger.logErrorHandler;
|
|
45
|
-
|
|
46
|
-
// Passes focus to the input element
|
|
47
|
-
protected readonly setFocusToInput = createSetFocusToInput(this.errorLogger);
|
|
48
|
-
|
|
49
|
-
protected readonly className = inject(ClassName);
|
|
50
|
-
|
|
51
|
-
public constructor() {
|
|
52
|
-
this.log(`${this.className}.SneatBaseComponent.constructor()`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
protected log(msg: string, ...data: unknown[]): void {
|
|
56
|
-
this.console.log(msg, ...data);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// protected readonly takeUntilDestroyed = <T>() =>
|
|
60
|
-
// takeUntil<T>(this.destroyed$);
|
|
61
|
-
protected takeUntilDestroyed<T>(): MonoTypeOperatorFunction<T> {
|
|
62
|
-
return takeUntil(this.destroyed$);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public ngOnDestroy(): void {
|
|
66
|
-
this.log(`${this.className}.SneatBaseComponent.ngOnDestroy()`);
|
|
67
|
-
this.unsubscribe(`${this.className}.SneatBaseComponent.ngOnDestroy()`);
|
|
68
|
-
this.destroyed?.next();
|
|
69
|
-
this.destroyed?.complete();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
protected unsubscribe(reason?: string): void {
|
|
73
|
-
this.log(
|
|
74
|
-
`${this.className}.SneatBaseComponent.unsubscribe(reason: ${reason})`,
|
|
75
|
-
);
|
|
76
|
-
this.subs.unsubscribe();
|
|
77
|
-
}
|
|
78
|
-
}
|
package/src/lib/focus.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { IonInput, IonTextarea } from '@ionic/angular/standalone';
|
|
2
|
-
import { IErrorLogger } from '@sneat/core';
|
|
3
|
-
|
|
4
|
-
export function createSetFocusToInput(errorLogger: IErrorLogger) {
|
|
5
|
-
return (input?: IonInput | IonTextarea, delay = 100): void => {
|
|
6
|
-
if (!input) {
|
|
7
|
-
console.error('can not set focus to undefined input');
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
setTimeout(() => {
|
|
11
|
-
requestAnimationFrame(() => {
|
|
12
|
-
// input.getInputElement().then(el => el.focus()).catch(errorLogger.logErrorHandler('failed to set focus to input'));
|
|
13
|
-
input
|
|
14
|
-
.setFocus()
|
|
15
|
-
.catch(errorLogger.logErrorHandler('failed to set focus to input'));
|
|
16
|
-
});
|
|
17
|
-
}, delay);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<ion-card>
|
|
2
|
-
<ion-item-divider color="light">
|
|
3
|
-
<ion-label color="medium">{{ title }}</ion-label>
|
|
4
|
-
<ion-buttons slot="end">
|
|
5
|
-
<ion-button color="medium">
|
|
6
|
-
<ion-icon name="add-outline" />
|
|
7
|
-
<ion-label>Add</ion-label>
|
|
8
|
-
</ion-button>
|
|
9
|
-
</ion-buttons>
|
|
10
|
-
</ion-item-divider>
|
|
11
|
-
|
|
12
|
-
<ion-list>
|
|
13
|
-
@for (item of selectedItems; track item.id) {
|
|
14
|
-
<ion-item>
|
|
15
|
-
<ion-label>{{ item.title }}</ion-label>
|
|
16
|
-
@if (canRemove) {
|
|
17
|
-
<ion-buttons slot="end">
|
|
18
|
-
<ion-button title="Remove" (click)="removeItem($event, item)">
|
|
19
|
-
<ion-icon name="close-outline" />
|
|
20
|
-
</ion-button>
|
|
21
|
-
</ion-buttons>
|
|
22
|
-
}
|
|
23
|
-
</ion-item>
|
|
24
|
-
}
|
|
25
|
-
</ion-list>
|
|
26
|
-
</ion-card>
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
|
|
3
|
-
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
4
|
-
import { ErrorLogger } from '@sneat/core';
|
|
5
|
-
import { ClassName } from '../../components';
|
|
6
|
-
import { OverlayController } from '../selector-base.component';
|
|
7
|
-
import { MultiSelectorComponent } from './multi-selector.component';
|
|
8
|
-
import { ISelectItem } from '../selector-interfaces';
|
|
9
|
-
|
|
10
|
-
// Suppress Ionic icon warnings - mock console methods to filter Ionic-specific errors
|
|
11
|
-
const originalWarn = console.warn;
|
|
12
|
-
const originalError = console.error;
|
|
13
|
-
|
|
14
|
-
vi.spyOn(console, 'error').mockImplementation((...args: any[]) => {
|
|
15
|
-
const message = args[0]?.toString() || '';
|
|
16
|
-
// Only suppress Ionic/Stencil icon loading errors
|
|
17
|
-
if (
|
|
18
|
-
message.includes('Ionicons') ||
|
|
19
|
-
message.includes('@ionic/core/components/icon') ||
|
|
20
|
-
message.includes('getAssetPath')
|
|
21
|
-
) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
// Let other errors through
|
|
25
|
-
originalError(...args);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
vi.spyOn(console, 'warn').mockImplementation((...args: any[]) => {
|
|
29
|
-
const message = args[0]?.toString() || '';
|
|
30
|
-
// Only suppress Ionic icon warnings
|
|
31
|
-
if (message.includes('Ionicons') || message.includes('Could not load icon')) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
// Let other warnings through
|
|
35
|
-
originalWarn(...args);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('MultiSelectorComponent', () => {
|
|
39
|
-
let component: MultiSelectorComponent;
|
|
40
|
-
let fixture: ComponentFixture<MultiSelectorComponent>;
|
|
41
|
-
|
|
42
|
-
const mockItems: ISelectItem[] = [
|
|
43
|
-
{ id: '1', title: 'Item 1' },
|
|
44
|
-
{ id: '2', title: 'Item 2' },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
beforeEach(waitForAsync(async () => {
|
|
48
|
-
await TestBed.configureTestingModule({
|
|
49
|
-
imports: [MultiSelectorComponent],
|
|
50
|
-
providers: [
|
|
51
|
-
{ provide: ClassName, useValue: 'TestComponent' },
|
|
52
|
-
{
|
|
53
|
-
provide: ErrorLogger,
|
|
54
|
-
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
provide: OverlayController,
|
|
58
|
-
useValue: { dismiss: vi.fn().mockResolvedValue(undefined) },
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
62
|
-
})
|
|
63
|
-
.overrideComponent(MultiSelectorComponent, {
|
|
64
|
-
set: {
|
|
65
|
-
imports: [],
|
|
66
|
-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
67
|
-
providers: [
|
|
68
|
-
{ provide: ClassName, useValue: 'TestComponent' },
|
|
69
|
-
{
|
|
70
|
-
provide: OverlayController,
|
|
71
|
-
useValue: { dismiss: vi.fn().mockResolvedValue(undefined) },
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
.compileComponents();
|
|
77
|
-
fixture = TestBed.createComponent(MultiSelectorComponent);
|
|
78
|
-
component = fixture.componentInstance;
|
|
79
|
-
fixture.detectChanges();
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
it('should create', () => {
|
|
83
|
-
expect(component).toBeTruthy();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('ngOnChanges', () => {
|
|
87
|
-
it('should set selectedItems when allItems changes', () => {
|
|
88
|
-
component.allItems = mockItems;
|
|
89
|
-
component.selectedIDs = ['1'];
|
|
90
|
-
component.ngOnChanges({
|
|
91
|
-
allItems: new SimpleChange(undefined, mockItems, true),
|
|
92
|
-
});
|
|
93
|
-
// @ts-expect-error accessing protected member
|
|
94
|
-
expect(component.selectedItems).toEqual([mockItems[0]]);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('removeItem', () => {
|
|
99
|
-
it('should remove item and emit event', () => {
|
|
100
|
-
component.allItems = mockItems;
|
|
101
|
-
component.selectedIDs = ['1', '2'];
|
|
102
|
-
component.ngOnChanges({
|
|
103
|
-
allItems: new SimpleChange(undefined, mockItems, true),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const removeSpy = vi.spyOn(component.removeItems, 'emit');
|
|
107
|
-
const event = { stopPropagation: vi.fn() };
|
|
108
|
-
// @ts-expect-error accessing protected member
|
|
109
|
-
component.removeItem(event as any, mockItems[0]);
|
|
110
|
-
|
|
111
|
-
expect(event.stopPropagation).toHaveBeenCalled();
|
|
112
|
-
// @ts-expect-error accessing protected member
|
|
113
|
-
expect(component.selectedItems).toEqual([mockItems[1]]);
|
|
114
|
-
expect(removeSpy).toHaveBeenCalledWith([{ event, item: mockItems[0] }]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('SelectorBaseComponent coverage', () => {
|
|
118
|
-
it('should close and dismiss overlay', () => {
|
|
119
|
-
// @ts-expect-error accessing protected member
|
|
120
|
-
const dismissSpy = vi.spyOn(component.overlayController, 'dismiss');
|
|
121
|
-
const event = { stopPropagation: vi.fn(), preventDefault: vi.fn() };
|
|
122
|
-
// @ts-expect-error accessing protected member
|
|
123
|
-
component.close(event as any);
|
|
124
|
-
expect(event.stopPropagation).toHaveBeenCalled();
|
|
125
|
-
expect(event.preventDefault).toHaveBeenCalled();
|
|
126
|
-
expect(dismissSpy).toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should close without event and dismiss overlay', () => {
|
|
130
|
-
// @ts-expect-error accessing protected member
|
|
131
|
-
const dismissSpy = vi.spyOn(component.overlayController, 'dismiss');
|
|
132
|
-
// @ts-expect-error accessing protected member
|
|
133
|
-
component.close();
|
|
134
|
-
expect(dismissSpy).toHaveBeenCalled();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should ignore ngOnChanges if allItems not changed', () => {
|
|
138
|
-
// @ts-expect-error accessing protected member
|
|
139
|
-
component.selectedItems = undefined;
|
|
140
|
-
component.ngOnChanges({
|
|
141
|
-
other: new SimpleChange(undefined, 'value', true),
|
|
142
|
-
});
|
|
143
|
-
// @ts-expect-error accessing protected member
|
|
144
|
-
expect(component.selectedItems).toBeUndefined();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Component,
|
|
3
|
-
EventEmitter,
|
|
4
|
-
Input,
|
|
5
|
-
OnChanges,
|
|
6
|
-
Output,
|
|
7
|
-
SimpleChanges,
|
|
8
|
-
} from '@angular/core';
|
|
9
|
-
import {
|
|
10
|
-
IonButton,
|
|
11
|
-
IonButtons,
|
|
12
|
-
IonCard,
|
|
13
|
-
IonIcon,
|
|
14
|
-
IonItem,
|
|
15
|
-
IonItemDivider,
|
|
16
|
-
IonLabel,
|
|
17
|
-
IonList,
|
|
18
|
-
ModalController,
|
|
19
|
-
} from '@ionic/angular/standalone';
|
|
20
|
-
import { ClassName } from '../../components';
|
|
21
|
-
import { ISelectItem, ISelectItemEvent } from '../selector-interfaces';
|
|
22
|
-
import {
|
|
23
|
-
OverlayController,
|
|
24
|
-
SelectorBaseComponent,
|
|
25
|
-
} from '../selector-base.component';
|
|
26
|
-
|
|
27
|
-
@Component({
|
|
28
|
-
selector: 'sneat-multi-selector',
|
|
29
|
-
templateUrl: './multi-selector.component.html',
|
|
30
|
-
imports: [
|
|
31
|
-
IonCard,
|
|
32
|
-
IonItemDivider,
|
|
33
|
-
IonLabel,
|
|
34
|
-
IonButtons,
|
|
35
|
-
IonButton,
|
|
36
|
-
IonItem,
|
|
37
|
-
IonIcon,
|
|
38
|
-
IonList,
|
|
39
|
-
],
|
|
40
|
-
providers: [
|
|
41
|
-
{
|
|
42
|
-
provide: ClassName,
|
|
43
|
-
useValue: 'MultiSelectorComponent',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
provide: OverlayController,
|
|
47
|
-
useClass: ModalController,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
})
|
|
51
|
-
export class MultiSelectorComponent<T = ISelectItem>
|
|
52
|
-
extends SelectorBaseComponent<T>
|
|
53
|
-
implements OnChanges
|
|
54
|
-
{
|
|
55
|
-
@Input() title = 'Select';
|
|
56
|
-
|
|
57
|
-
@Input() canRemove = false;
|
|
58
|
-
@Input() public allItems?: ISelectItem[];
|
|
59
|
-
@Input() public selectedIDs?: readonly string[];
|
|
60
|
-
|
|
61
|
-
@Output() readonly removeItems = new EventEmitter<ISelectItemEvent[]>();
|
|
62
|
-
@Output() readonly addItems = new EventEmitter<ISelectItemEvent[]>();
|
|
63
|
-
|
|
64
|
-
protected selectedItems?: ISelectItem[];
|
|
65
|
-
|
|
66
|
-
ngOnChanges(changes: SimpleChanges): void {
|
|
67
|
-
if (changes['allItems']) {
|
|
68
|
-
this.selectedItems = this.allItems?.filter((item) =>
|
|
69
|
-
this.selectedIDs?.includes(item.id),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected removeItem(event: Event, item: ISelectItem): void {
|
|
75
|
-
event.stopPropagation();
|
|
76
|
-
this.selectedItems = this.selectedItems?.filter((i) => i.id !== item.id);
|
|
77
|
-
this.removeItems.emit([{ event, item }]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { MultiSelectorService } from './multi-selector.service';
|
|
2
|
-
import { ISelectItem } from '../selector-interfaces';
|
|
3
|
-
import { ErrorLogger } from '@sneat/core';
|
|
4
|
-
import { ModalController } from '@ionic/angular/standalone';
|
|
5
|
-
|
|
6
|
-
describe('MultiSelectorService', () => {
|
|
7
|
-
let service: MultiSelectorService;
|
|
8
|
-
let errorLoggerMock: ErrorLogger;
|
|
9
|
-
let modalControllerMock: ModalController;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
errorLoggerMock = {
|
|
13
|
-
logError: vi.fn(),
|
|
14
|
-
logErrorHandler: vi.fn(() => vi.fn()),
|
|
15
|
-
};
|
|
16
|
-
modalControllerMock = {
|
|
17
|
-
create: vi.fn(),
|
|
18
|
-
};
|
|
19
|
-
service = new MultiSelectorService(errorLoggerMock, modalControllerMock);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should create', () => {
|
|
23
|
-
expect(service).toBeTruthy();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('selectMultiple', () => {
|
|
27
|
-
const items: ISelectItem[] = [{ id: '1', title: 'test' }];
|
|
28
|
-
|
|
29
|
-
it('should resolve with selected items on success', async () => {
|
|
30
|
-
const modalMock = {
|
|
31
|
-
present: vi.fn().mockReturnValue(Promise.resolve()),
|
|
32
|
-
onDidDismiss: vi.fn().mockReturnValue(
|
|
33
|
-
Promise.resolve({ data: { selectedItems: items } }),
|
|
34
|
-
),
|
|
35
|
-
};
|
|
36
|
-
modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
|
|
37
|
-
|
|
38
|
-
const result = await service.selectMultiple(items, []);
|
|
39
|
-
expect(result).toEqual(items);
|
|
40
|
-
expect(modalControllerMock.create).toHaveBeenCalled();
|
|
41
|
-
expect(modalMock.present).toHaveBeenCalled();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should reject and log error if modal creation fails', async () => {
|
|
45
|
-
modalControllerMock.create.mockReturnValue(Promise.reject('fail'));
|
|
46
|
-
await expect(service.selectMultiple([], [])).rejects.toBe('fail');
|
|
47
|
-
expect(errorLoggerMock.logError).toHaveBeenCalledWith(
|
|
48
|
-
'fail',
|
|
49
|
-
'Failed to create modal',
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should reject and log error if modal presentation fails', async () => {
|
|
54
|
-
const modalMock = {
|
|
55
|
-
present: vi.fn().mockReturnValue(Promise.reject('present-fail')),
|
|
56
|
-
onDidDismiss: vi.fn().mockReturnValue(new Promise(() => { /* never resolves */ })),
|
|
57
|
-
};
|
|
58
|
-
modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
|
|
59
|
-
await expect(service.selectMultiple([], [])).rejects.toBe('present-fail');
|
|
60
|
-
expect(errorLoggerMock.logError).toHaveBeenCalledWith(
|
|
61
|
-
'Failed to present modal',
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should reject and log error if modal dismiss fails', async () => {
|
|
66
|
-
const modalMock = {
|
|
67
|
-
present: vi.fn().mockReturnValue(Promise.resolve()),
|
|
68
|
-
onDidDismiss: vi.fn().mockReturnValue(Promise.reject('dismiss-fail')),
|
|
69
|
-
};
|
|
70
|
-
modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
|
|
71
|
-
await expect(service.selectMultiple([], [])).rejects.toBe('dismiss-fail');
|
|
72
|
-
expect(errorLoggerMock.logError).toHaveBeenCalledWith(
|
|
73
|
-
'dismiss-fail',
|
|
74
|
-
'Failed to handle modal dismiss',
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should resolve with empty array if selectedItems is missing in response', async () => {
|
|
79
|
-
const modalMock = {
|
|
80
|
-
present: vi.fn().mockReturnValue(Promise.resolve()),
|
|
81
|
-
onDidDismiss: vi.fn().mockReturnValue(
|
|
82
|
-
Promise.resolve({ data: {} }),
|
|
83
|
-
),
|
|
84
|
-
};
|
|
85
|
-
modalControllerMock.create.mockReturnValue(Promise.resolve(modalMock));
|
|
86
|
-
|
|
87
|
-
const result = await service.selectMultiple([], []);
|
|
88
|
-
expect(result).toEqual([]);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { Inject } from '@angular/core';
|
|
2
|
-
import { ModalController, ModalOptions } from '@ionic/angular/standalone';
|
|
3
|
-
import { ErrorLogger, IErrorLogger } from '@sneat/core';
|
|
4
|
-
import { ISelectItem } from '../selector-interfaces';
|
|
5
|
-
import { MultiSelectorComponent } from './multi-selector.component';
|
|
6
|
-
|
|
7
|
-
export class MultiSelectorService {
|
|
8
|
-
constructor(
|
|
9
|
-
@Inject(ErrorLogger) private readonly errorLogger: IErrorLogger,
|
|
10
|
-
private readonly modalController: ModalController,
|
|
11
|
-
) {}
|
|
12
|
-
|
|
13
|
-
selectMultiple(
|
|
14
|
-
items: ISelectItem[],
|
|
15
|
-
selectedItems: ISelectItem[],
|
|
16
|
-
): Promise<ISelectItem[]> {
|
|
17
|
-
const result = new Promise<ISelectItem[]>((resolve, reject) => {
|
|
18
|
-
const modalOptions: ModalOptions = {
|
|
19
|
-
component: MultiSelectorComponent,
|
|
20
|
-
componentProps: {
|
|
21
|
-
items,
|
|
22
|
-
selectedItems,
|
|
23
|
-
},
|
|
24
|
-
keyboardClose: true,
|
|
25
|
-
};
|
|
26
|
-
this.modalController
|
|
27
|
-
.create(modalOptions)
|
|
28
|
-
.then((modal) => {
|
|
29
|
-
modal
|
|
30
|
-
.onDidDismiss()
|
|
31
|
-
.then((res) => resolve(res.data?.selectedItems || []))
|
|
32
|
-
.catch((err) => {
|
|
33
|
-
this.errorLogger.logError(err, 'Failed to handle modal dismiss');
|
|
34
|
-
reject(err);
|
|
35
|
-
});
|
|
36
|
-
modal.present().catch((err) => {
|
|
37
|
-
reject(err);
|
|
38
|
-
this.errorLogger.logError('Failed to present modal');
|
|
39
|
-
});
|
|
40
|
-
})
|
|
41
|
-
.catch((err) => {
|
|
42
|
-
this.errorLogger.logError(err, 'Failed to create modal');
|
|
43
|
-
reject(err);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
}
|