mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
feat: Implement filter input mapping and related schemas
- Added filter input mapping functionality to handle different input types (Text, Checkbox, DateRange). - Created schemas for various filter inputs including BaseFilterInput, CheckboxFilterInput, DateRangeFilterInput, and TextFilterInput. - Developed filter mapping logic to aggregate filter groups, inputs, and order by options. - Implemented unit tests for filter mapping, input mapping, and order by option mapping to ensure correctness. - Introduced a dropdown component for selecting order by options with appropriate styling and functionality.
This commit is contained in:
@@ -14,7 +14,7 @@ const meta: Meta<UiButtonComponentInputs> = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
color: {
|
color: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
options: ['primary', 'secondary', 'brand', 'tertiary'] as ButtonColor[],
|
options: Object.values(ButtonColor),
|
||||||
description: 'Determines the button color',
|
description: 'Determines the button color',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
- Use `createComponentFactory` for standalone components
|
- Use `createComponentFactory` for standalone components
|
||||||
- Use `createHostFactory` when testing components with templates
|
- Use `createHostFactory` when testing components with templates
|
||||||
- Mock child components using `ng-mocks`
|
|
||||||
- Test component inputs, outputs, and lifecycle hooks
|
- Test component inputs, outputs, and lifecycle hooks
|
||||||
- Verify DOM rendering and component behavior separately
|
- Verify DOM rendering and component behavior separately
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,17 @@
|
|||||||
[rollbackOnClose]="true"
|
[rollbackOnClose]="true"
|
||||||
></filter-filter-menu-button>
|
></filter-filter-menu-button>
|
||||||
|
|
||||||
<button uiIconButton *uiBreakpoint="['tablet']" (click)="orderByVisible.set(!orderByVisible())">
|
@if (showOrderByToolbar()) {
|
||||||
<ng-icon name="isaActionSort"></ng-icon>
|
<filter-order-by-toolbar
|
||||||
</button>
|
*uiBreakpoint="['desktop', 'dekstop-l', 'dekstop-xl']"
|
||||||
|
(toggled)="search()"
|
||||||
<filter-order-by-toolbar
|
></filter-order-by-toolbar>
|
||||||
*uiBreakpoint="['desktop', 'dekstop-l', 'dekstop-xl']"
|
} @else {
|
||||||
(toggled)="search()"
|
<filter-order-by-dropdown class="min-w-[9rem]"> </filter-order-by-dropdown>
|
||||||
></filter-order-by-toolbar>
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (orderByVisible()) {
|
|
||||||
<filter-order-by-toolbar
|
|
||||||
*uiBreakpoint="['tablet']"
|
|
||||||
class="w-full"
|
|
||||||
(toggled)="search()"
|
|
||||||
></filter-order-by-toolbar>
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="text-isa-neutral-900 isa-text-body-2-regular self-start">
|
<span class="text-isa-neutral-900 isa-text-body-2-regular self-start">
|
||||||
{{ entityHits() }} Einträge
|
{{ entityHits() }} Einträge
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,17 +14,19 @@ import {
|
|||||||
FilterService,
|
FilterService,
|
||||||
SearchBarInputComponent,
|
SearchBarInputComponent,
|
||||||
OrderByToolbarComponent,
|
OrderByToolbarComponent,
|
||||||
|
OrderByDropdownComponent,
|
||||||
} from '@isa/shared/filter';
|
} from '@isa/shared/filter';
|
||||||
|
|
||||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionSort } from '@isa/icons';
|
import { isaActionSort } from '@isa/icons';
|
||||||
import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
|
import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
|
||||||
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
|
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
|
||||||
import { BreakpointDirective, InViewportDirective } from '@isa/ui/layout';
|
import { Breakpoint, BreakpointDirective, InViewportDirective } from '@isa/ui/layout';
|
||||||
import { CallbackResult, ListResponseArgs } from '@isa/common/result';
|
import { CallbackResult, ListResponseArgs } from '@isa/common/result';
|
||||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||||
|
import { breakpoint } from '@isa/ui/layout';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'oms-feature-return-search-result',
|
selector: 'oms-feature-return-search-result',
|
||||||
@@ -35,10 +37,10 @@ import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
ReturnSearchResultItemComponent,
|
ReturnSearchResultItemComponent,
|
||||||
OrderByToolbarComponent,
|
OrderByToolbarComponent,
|
||||||
|
OrderByDropdownComponent,
|
||||||
IconButtonComponent,
|
IconButtonComponent,
|
||||||
SearchBarInputComponent,
|
SearchBarInputComponent,
|
||||||
EmptyStateComponent,
|
EmptyStateComponent,
|
||||||
NgIconComponent,
|
|
||||||
FilterMenuButtonComponent,
|
FilterMenuButtonComponent,
|
||||||
BreakpointDirective,
|
BreakpointDirective,
|
||||||
InViewportDirective,
|
InViewportDirective,
|
||||||
@@ -46,6 +48,8 @@ import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
|||||||
providers: [provideIcons({ isaActionSort })],
|
providers: [provideIcons({ isaActionSort })],
|
||||||
})
|
})
|
||||||
export class ReturnSearchResultComponent implements AfterViewInit {
|
export class ReturnSearchResultComponent implements AfterViewInit {
|
||||||
|
showOrderByToolbar = breakpoint([Breakpoint.DekstopL, Breakpoint.DekstopXL]);
|
||||||
|
|
||||||
#route = inject(ActivatedRoute);
|
#route = inject(ActivatedRoute);
|
||||||
#router = inject(Router);
|
#router = inject(Router);
|
||||||
#filterService = inject(FilterService);
|
#filterService = inject(FilterService);
|
||||||
@@ -55,8 +59,6 @@ export class ReturnSearchResultComponent implements AfterViewInit {
|
|||||||
processId = injectActivatedProcessId();
|
processId = injectActivatedProcessId();
|
||||||
returnSearchStore = inject(ReturnSearchStore);
|
returnSearchStore = inject(ReturnSearchStore);
|
||||||
|
|
||||||
orderByVisible = signal(false);
|
|
||||||
|
|
||||||
ReturnSearchStatus = ReturnSearchStatus;
|
ReturnSearchStatus = ReturnSearchStatus;
|
||||||
|
|
||||||
entity = computed(() => {
|
entity = computed(() => {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { computed, inject, Injectable, signal } from '@angular/core';
|
import { computed, inject, Injectable, signal } from '@angular/core';
|
||||||
import { InputType } from '../types';
|
import { InputType } from '../types';
|
||||||
import { getState, patchState, signalState } from '@ngrx/signals';
|
import { getState, patchState, signalState } from '@ngrx/signals';
|
||||||
import { mapToFilter } from './mappings';
|
import { filterMapping } from './mappings';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { FilterInput, OrderByDirectionSchema, Query, QuerySchema } from './schemas';
|
import {
|
||||||
|
FilterInput,
|
||||||
|
OrderByDirection,
|
||||||
|
OrderByDirectionSchema,
|
||||||
|
Query,
|
||||||
|
QuerySchema,
|
||||||
|
} from './schemas';
|
||||||
import { FILTER_ON_COMMIT, FILTER_ON_INIT, QUERY_SETTINGS } from './tokens';
|
import { FILTER_ON_COMMIT, FILTER_ON_INIT, QUERY_SETTINGS } from './tokens';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -13,7 +19,7 @@ export class FilterService {
|
|||||||
|
|
||||||
readonly settings = inject(QUERY_SETTINGS);
|
readonly settings = inject(QUERY_SETTINGS);
|
||||||
|
|
||||||
private readonly defaultState = mapToFilter(this.settings);
|
private readonly defaultState = filterMapping(this.settings);
|
||||||
|
|
||||||
#commitdState = signal(structuredClone(this.defaultState));
|
#commitdState = signal(structuredClone(this.defaultState));
|
||||||
|
|
||||||
@@ -31,15 +37,12 @@ export class FilterService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setOrderBy(
|
setOrderBy(by: string, dir: OrderByDirection | undefined, options?: { commit: boolean }) {
|
||||||
orderBy: { by: string; dir: 'asc' | 'desc' | undefined },
|
|
||||||
options?: { commit: boolean },
|
|
||||||
) {
|
|
||||||
const orderByList = this.#state.orderBy().map((o) => {
|
const orderByList = this.#state.orderBy().map((o) => {
|
||||||
if (o.by === orderBy.by) {
|
if (o.by === by && o.dir === dir) {
|
||||||
return { ...o, dir: orderBy.dir };
|
return { ...o, selected: true };
|
||||||
}
|
}
|
||||||
return { ...o, dir: undefined };
|
return { ...o, selected: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
patchState(this.#state, { orderBy: orderByList });
|
patchState(this.#state, { orderBy: orderByList });
|
||||||
@@ -49,29 +52,6 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOrderBy(by: string, options?: { commit: boolean }): void {
|
|
||||||
const orderBy = this.#state.orderBy();
|
|
||||||
|
|
||||||
const orderByIndex = orderBy.findIndex((o) => o.by === by);
|
|
||||||
|
|
||||||
if (orderByIndex === -1) {
|
|
||||||
console.warn(`No orderBy found with by: ${by}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let orderByDir = orderBy[orderByIndex].dir;
|
|
||||||
|
|
||||||
if (!orderByDir) {
|
|
||||||
orderByDir = 'asc';
|
|
||||||
} else if (orderByDir === 'asc') {
|
|
||||||
orderByDir = 'desc';
|
|
||||||
} else {
|
|
||||||
orderByDir = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setOrderBy({ by, dir: orderByDir }, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputTextValue(key: string, value: string | undefined, options?: { commit: boolean }): void {
|
setInputTextValue(key: string, value: string | undefined, options?: { commit: boolean }): void {
|
||||||
const inputs = this.#state.inputs().map((input) => {
|
const inputs = this.#state.inputs().map((input) => {
|
||||||
if (input.key !== key) {
|
if (input.key !== key) {
|
||||||
@@ -242,12 +222,12 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commitOrderBy() {
|
commitOrderBy() {
|
||||||
const orderBy = this.#state.orderBy().map((o) => {
|
const orderBy = this.#state.orderBy();
|
||||||
const committedOrderBy = this.#commitdState().orderBy.find((co) => co.by === o.by);
|
|
||||||
return { ...o, dir: committedOrderBy?.dir };
|
|
||||||
});
|
|
||||||
|
|
||||||
patchState(this.#state, { orderBy });
|
this.#commitdState.set({
|
||||||
|
...this.#commitdState(),
|
||||||
|
orderBy,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(options?: { commit: boolean }) {
|
clear(options?: { commit: boolean }) {
|
||||||
@@ -276,7 +256,7 @@ export class FilterService {
|
|||||||
* @param options.commit - If `true`, the changes will be committed after resetting the state.
|
* @param options.commit - If `true`, the changes will be committed after resetting the state.
|
||||||
*/
|
*/
|
||||||
reset(options?: { commit: boolean }) {
|
reset(options?: { commit: boolean }) {
|
||||||
patchState(this.#state, mapToFilter(this.settings));
|
patchState(this.#state, structuredClone(this.defaultState));
|
||||||
if (options?.commit) {
|
if (options?.commit) {
|
||||||
this.commit();
|
this.commit();
|
||||||
}
|
}
|
||||||
@@ -299,7 +279,7 @@ export class FilterService {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
resetInput(key: string, options?: { commit: boolean }) {
|
resetInput(key: string, options?: { commit: boolean }) {
|
||||||
const defaultFilter = mapToFilter(this.settings);
|
const defaultFilter = structuredClone(this.defaultState);
|
||||||
const inputToReset = defaultFilter.inputs.find((i) => i.key === key);
|
const inputToReset = defaultFilter.inputs.find((i) => i.key === key);
|
||||||
|
|
||||||
if (!inputToReset) {
|
if (!inputToReset) {
|
||||||
@@ -326,13 +306,8 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetOrderBy(options?: { commit: boolean }) {
|
resetOrderBy(options?: { commit: boolean }) {
|
||||||
const defaultOrderBy = mapToFilter(this.settings).orderBy;
|
const defaultOrderBy = structuredClone(this.defaultState.orderBy);
|
||||||
const orderBy = this.#state.orderBy().map((o) => {
|
patchState(this.#state, { orderBy: defaultOrderBy });
|
||||||
const defaultOrder = defaultOrderBy.find((do_) => do_.by === o.by);
|
|
||||||
return { ...o, dir: defaultOrder?.dir };
|
|
||||||
});
|
|
||||||
|
|
||||||
patchState(this.#state, { orderBy });
|
|
||||||
|
|
||||||
if (options?.commit) {
|
if (options?.commit) {
|
||||||
this.commit();
|
this.commit();
|
||||||
@@ -358,7 +333,7 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderBy = commited.orderBy.find((o) => o.dir);
|
const orderBy = commited.orderBy.find((o) => o.selected);
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
result['orderBy'] = `${orderBy.by}:${orderBy.dir}`;
|
result['orderBy'] = `${orderBy.by}:${orderBy.dir}`;
|
||||||
@@ -407,26 +382,30 @@ export class FilterService {
|
|||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
orderBy: orderBy.map((o) => {
|
orderBy: orderBy
|
||||||
return {
|
.filter((o) => o.selected)
|
||||||
by: o.by,
|
.map((o) => {
|
||||||
label: o.label,
|
return {
|
||||||
desc: o.dir === 'desc',
|
by: o.by,
|
||||||
selected: true,
|
label: o.label,
|
||||||
};
|
desc: o.dir === 'desc',
|
||||||
}),
|
selected: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
parseQueryParams(params: Record<string, string>, options?: { commit: boolean }): void {
|
parseQueryParams(params: Record<string, string>, options?: { commit: boolean }): void {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
for (const key in params) {
|
for (const key in params) {
|
||||||
if (key === 'orderBy') {
|
if (key === 'orderBy') {
|
||||||
const [by, dir] = params[key].split(':');
|
const [by, dir] = params[key].split(':');
|
||||||
const orderBy = this.orderBy().find((o) => o.by === by);
|
const orderBy = this.orderBy().some((o) => o.by === by && o.dir === dir);
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
this.setOrderBy({ by, dir: OrderByDirectionSchema.parse(dir) });
|
console.warn(`OrderBy already exists: ${by}:${dir}`);
|
||||||
|
this.setOrderBy(by, OrderByDirectionSchema.parse(dir));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { Input, InputGroup, InputType, Option, QuerySettings } from '../types';
|
|
||||||
import {
|
|
||||||
CheckboxFilterInput,
|
|
||||||
CheckboxFilterInputOption,
|
|
||||||
CheckboxFilterInputOptionSchema,
|
|
||||||
CheckboxFilterInputSchema,
|
|
||||||
DateRangeFilterInput,
|
|
||||||
DateRangeFilterInputSchema,
|
|
||||||
Filter,
|
|
||||||
FilterGroup,
|
|
||||||
FilterGroupSchema,
|
|
||||||
FilterInput,
|
|
||||||
OrderBySchema,
|
|
||||||
TextFilterInput,
|
|
||||||
TextFilterInputSchema,
|
|
||||||
} from './schemas';
|
|
||||||
|
|
||||||
export function mapToFilter(settings: QuerySettings): Filter {
|
|
||||||
const filter: Filter = {
|
|
||||||
groups: [],
|
|
||||||
inputs: [],
|
|
||||||
orderBy: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const groups = [...settings.filter, ...settings.input];
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
filter.groups.push(mapToFilterGroup(group));
|
|
||||||
|
|
||||||
for (const input of group.input) {
|
|
||||||
filter.inputs.push(mapToFilterInput(group.group, input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.orderBy) {
|
|
||||||
const bys = new Set<string>();
|
|
||||||
for (const orderBy of settings.orderBy) {
|
|
||||||
if (orderBy.by && bys.has(orderBy.by)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderBy.by) {
|
|
||||||
filter.orderBy.push(OrderBySchema.parse(orderBy));
|
|
||||||
bys.add(orderBy.by);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToFilterGroup(group: InputGroup): FilterGroup {
|
|
||||||
return FilterGroupSchema.parse({
|
|
||||||
group: group.group,
|
|
||||||
label: group.label,
|
|
||||||
description: group.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToFilterInput(group: string, input: Input): FilterInput {
|
|
||||||
switch (input.type) {
|
|
||||||
case InputType.Text:
|
|
||||||
return mapToTextFilterInput(group, input);
|
|
||||||
case InputType.Checkbox:
|
|
||||||
return mapToCheckboxFilterInput(group, input);
|
|
||||||
case InputType.DateRange:
|
|
||||||
return mapToDateRangeFilterInput(group, input);
|
|
||||||
}
|
|
||||||
throw new Error(`Unknown input type: ${input.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToTextFilterInput(group: string, input: Input): TextFilterInput {
|
|
||||||
return TextFilterInputSchema.parse({
|
|
||||||
group,
|
|
||||||
key: input.key,
|
|
||||||
label: input.label,
|
|
||||||
description: input.description,
|
|
||||||
type: InputType.Text,
|
|
||||||
defaultValue: input.value,
|
|
||||||
value: input.value,
|
|
||||||
placeholder: input.placeholder,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToCheckboxFilterInput(group: string, input: Input): CheckboxFilterInput {
|
|
||||||
return CheckboxFilterInputSchema.parse({
|
|
||||||
group,
|
|
||||||
key: input.key,
|
|
||||||
label: input.label,
|
|
||||||
description: input.description,
|
|
||||||
type: InputType.Checkbox,
|
|
||||||
defaultValue: input.value,
|
|
||||||
maxOptions: input.options?.max,
|
|
||||||
options: input.options?.values?.map(mapToCheckboxOption),
|
|
||||||
selected:
|
|
||||||
input.options?.values?.filter((option) => option.selected).map((option) => option.value) ||
|
|
||||||
[],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToCheckboxOption(option: Option): CheckboxFilterInputOption {
|
|
||||||
return CheckboxFilterInputOptionSchema.parse({
|
|
||||||
label: option.label,
|
|
||||||
value: option.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToDateRangeFilterInput(group: string, input: Input): DateRangeFilterInput {
|
|
||||||
return DateRangeFilterInputSchema.parse({
|
|
||||||
group,
|
|
||||||
key: input.key,
|
|
||||||
label: input.label,
|
|
||||||
description: input.description,
|
|
||||||
type: InputType.DateRange,
|
|
||||||
start: input.options?.values?.[0].value,
|
|
||||||
stop: input.options?.values?.[1].value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { checkboxFilterInputMapping } from './checkbox-filter-input.mapping';
|
||||||
|
import * as checkboxOptionMappingModule from './checkbox-option.mapping';
|
||||||
|
import * as schemaModule from '../schemas/checkbox-filter-input.schema';
|
||||||
|
|
||||||
|
describe('checkboxFilterInputMapping', () => {
|
||||||
|
const mockCheckboxOptionMapping = jest.fn().mockImplementation((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
value: option.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest
|
||||||
|
.spyOn(checkboxOptionMappingModule, 'checkboxOptionMapping')
|
||||||
|
.mockImplementation(mockCheckboxOptionMapping);
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest
|
||||||
|
.spyOn(schemaModule.CheckboxFilterInputSchema, 'parse')
|
||||||
|
.mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map minimal input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: undefined,
|
||||||
|
maxOptions: undefined,
|
||||||
|
options: undefined,
|
||||||
|
selected: [],
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: undefined,
|
||||||
|
maxOptions: undefined,
|
||||||
|
options: undefined,
|
||||||
|
selected: [],
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map complete input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
value: 'defaultValue',
|
||||||
|
options: {
|
||||||
|
max: 3,
|
||||||
|
values: [
|
||||||
|
{ label: 'Option 1', value: 'value1', selected: false },
|
||||||
|
{ label: 'Option 2', value: 'value2', selected: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: 'defaultValue',
|
||||||
|
maxOptions: 3,
|
||||||
|
options: [
|
||||||
|
{ label: 'Option 1', value: 'value1' },
|
||||||
|
{ label: 'Option 2', value: 'value2' },
|
||||||
|
],
|
||||||
|
selected: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: 'defaultValue',
|
||||||
|
maxOptions: 3,
|
||||||
|
options: [
|
||||||
|
{ label: 'Option 1', value: 'value1' },
|
||||||
|
{ label: 'Option 2', value: 'value2' },
|
||||||
|
],
|
||||||
|
selected: [],
|
||||||
|
});
|
||||||
|
expect(mockCheckboxOptionMapping).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map selected options correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
options: {
|
||||||
|
values: [
|
||||||
|
{ label: 'Option 1', value: 'value1', selected: true },
|
||||||
|
{ label: 'Option 2', value: 'value2', selected: false },
|
||||||
|
{ label: 'Option 3', value: 'value3', selected: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: undefined,
|
||||||
|
maxOptions: undefined,
|
||||||
|
options: [
|
||||||
|
{ label: 'Option 1', value: 'value1' },
|
||||||
|
{ label: 'Option 2', value: 'value2' },
|
||||||
|
{ label: 'Option 3', value: 'value3' },
|
||||||
|
],
|
||||||
|
selected: ['value1', 'value3'],
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.selected).toEqual(['value1', 'value3']);
|
||||||
|
expect(mockCheckboxOptionMapping).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty options array', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
options: {
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: undefined,
|
||||||
|
maxOptions: undefined,
|
||||||
|
options: [],
|
||||||
|
selected: [],
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.options).toEqual([]);
|
||||||
|
expect(result.selected).toEqual([]);
|
||||||
|
expect(mockCheckboxOptionMapping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined options', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: undefined,
|
||||||
|
maxOptions: undefined,
|
||||||
|
options: undefined,
|
||||||
|
selected: [],
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.options).toBeUndefined();
|
||||||
|
expect(result.selected).toEqual([]);
|
||||||
|
expect(mockCheckboxOptionMapping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { CheckboxFilterInput, CheckboxFilterInputSchema } from '../schemas';
|
||||||
|
import { checkboxOptionMapping } from './checkbox-option.mapping';
|
||||||
|
|
||||||
|
export function checkboxFilterInputMapping(group: string, input: Input): CheckboxFilterInput {
|
||||||
|
return CheckboxFilterInputSchema.parse({
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
label: input.label,
|
||||||
|
description: input.description,
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
defaultValue: input.value,
|
||||||
|
maxOptions: input.options?.max,
|
||||||
|
options: input.options?.values?.map(checkboxOptionMapping),
|
||||||
|
selected:
|
||||||
|
input.options?.values?.filter((option) => option.selected).map((option) => option.value) ||
|
||||||
|
[],
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Option } from '../../types';
|
||||||
|
import { checkboxOptionMapping } from './checkbox-option.mapping';
|
||||||
|
import * as schemaModule from '../schemas/checkbox-filter-input-option.schema';
|
||||||
|
|
||||||
|
describe('checkboxOptionMapping', () => {
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest
|
||||||
|
.spyOn(schemaModule.CheckboxFilterInputOptionSchema, 'parse')
|
||||||
|
.mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map option correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option = {
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxOptionMapping(option);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map option with selected property', () => {
|
||||||
|
// Arrange
|
||||||
|
const option: Option = {
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
selected: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = checkboxOptionMapping(option);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
label: 'Option Label',
|
||||||
|
value: 'option-value',
|
||||||
|
});
|
||||||
|
// The selected property should not be included in the mapped result
|
||||||
|
expect(result).not.toHaveProperty('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { CheckboxFilterInputOption, CheckboxFilterInputOptionSchema } from '../schemas';
|
||||||
|
import { Option } from '../../types';
|
||||||
|
|
||||||
|
export function checkboxOptionMapping(option: Option): CheckboxFilterInputOption {
|
||||||
|
return CheckboxFilterInputOptionSchema.parse({
|
||||||
|
label: option.label,
|
||||||
|
value: option.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { dateRangeFilterInputMapping } from './data-range-filter-input.mapping';
|
||||||
|
import * as schemaModule from '../schemas/date-range-filter-input.schema';
|
||||||
|
|
||||||
|
describe('dateRangeFilterInputMapping', () => {
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest
|
||||||
|
.spyOn(schemaModule.DateRangeFilterInputSchema, 'parse')
|
||||||
|
.mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map minimal input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = dateRangeFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: undefined,
|
||||||
|
stop: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: undefined,
|
||||||
|
stop: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map complete input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
options: {
|
||||||
|
values: [
|
||||||
|
{ label: 'Start', value: '2023-01-01' },
|
||||||
|
{ label: 'End', value: '2023-12-31' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = dateRangeFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: '2023-01-01',
|
||||||
|
stop: '2023-12-31',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: '2023-01-01',
|
||||||
|
stop: '2023-12-31',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing values in options', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
options: {
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = dateRangeFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: undefined,
|
||||||
|
stop: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: undefined,
|
||||||
|
stop: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { DateRangeFilterInput, DateRangeFilterInputSchema } from '../schemas';
|
||||||
|
|
||||||
|
export function dateRangeFilterInputMapping(group: string, input: Input): DateRangeFilterInput {
|
||||||
|
return DateRangeFilterInputSchema.parse({
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
label: input.label,
|
||||||
|
description: input.description,
|
||||||
|
type: InputType.DateRange,
|
||||||
|
start: input.options?.values?.[0].value,
|
||||||
|
stop: input.options?.values?.[1].value,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { InputGroup, InputType } from '../../types';
|
||||||
|
import { filterGroupMapping } from './filter-group.mapping';
|
||||||
|
import * as schemaModule from '../schemas/filter-group.schema';
|
||||||
|
|
||||||
|
describe('filterGroupMapping', () => {
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest.spyOn(schemaModule.FilterGroupSchema, 'parse').mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map minimal input group correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group: InputGroup = {
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
input: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterGroupMapping(group);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map complete input group correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group: InputGroup = {
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: 'Test Description',
|
||||||
|
input: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterGroupMapping(group);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: 'Test Description',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: 'Test Description',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore input property in the mapping result', () => {
|
||||||
|
// Arrange
|
||||||
|
const group: InputGroup = {
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
input: [
|
||||||
|
{ key: 'input1', label: 'Input 1', type: InputType.Text },
|
||||||
|
{ key: 'input2', label: 'Input 2', type: InputType.Checkbox },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterGroupMapping(group);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
expect(result).not.toHaveProperty('input');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle schema validation errors', () => {
|
||||||
|
// Arrange
|
||||||
|
const schemaError = new Error('Schema validation failed');
|
||||||
|
jest.spyOn(schemaModule.FilterGroupSchema, 'parse').mockImplementation(() => {
|
||||||
|
throw schemaError;
|
||||||
|
});
|
||||||
|
|
||||||
|
const group: InputGroup = {
|
||||||
|
group: 'testGroup',
|
||||||
|
label: 'Test Group',
|
||||||
|
input: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => filterGroupMapping(group)).toThrow(schemaError);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { InputGroup } from '../../types';
|
||||||
|
import { FilterGroup, FilterGroupSchema } from '../schemas';
|
||||||
|
|
||||||
|
export function filterGroupMapping(group: InputGroup): FilterGroup {
|
||||||
|
return FilterGroupSchema.parse({
|
||||||
|
group: group.group,
|
||||||
|
label: group.label,
|
||||||
|
description: group.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { filterInputMapping } from './filter-input.mapping';
|
||||||
|
import * as checkboxFilterInputMappingModule from './checkbox-filter-input.mapping';
|
||||||
|
import * as dateRangeFilterInputMappingModule from './data-range-filter-input.mapping';
|
||||||
|
import * as textFilterInputMappingModule from './text-filter-input.mapping';
|
||||||
|
|
||||||
|
describe('filterInputMapping', () => {
|
||||||
|
// Mock implementations for each specific mapping function
|
||||||
|
const mockTextFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
|
||||||
|
type: InputType.Text,
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
mapped: 'text',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockCheckboxFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
mapped: 'checkbox',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockDateRangeFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
|
||||||
|
type: InputType.DateRange,
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
mapped: 'dateRange',
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock all the mapping functions that filterInputMapping delegates to
|
||||||
|
jest
|
||||||
|
.spyOn(textFilterInputMappingModule, 'textFilterInputMapping')
|
||||||
|
.mockImplementation(mockTextFilterInputMapping);
|
||||||
|
jest
|
||||||
|
.spyOn(checkboxFilterInputMappingModule, 'checkboxFilterInputMapping')
|
||||||
|
.mockImplementation(mockCheckboxFilterInputMapping);
|
||||||
|
jest
|
||||||
|
.spyOn(dateRangeFilterInputMappingModule, 'dateRangeFilterInputMapping')
|
||||||
|
.mockImplementation(mockDateRangeFilterInputMapping);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate to textFilterInputMapping for text inputs', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'textInput',
|
||||||
|
label: 'Text Input',
|
||||||
|
type: InputType.Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockTextFilterInputMapping).toHaveBeenCalledWith(group, input);
|
||||||
|
expect(mockCheckboxFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockDateRangeFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: InputType.Text,
|
||||||
|
group,
|
||||||
|
key: 'textInput',
|
||||||
|
mapped: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate to checkboxFilterInputMapping for checkbox inputs', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'checkboxInput',
|
||||||
|
label: 'Checkbox Input',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockCheckboxFilterInputMapping).toHaveBeenCalledWith(group, input);
|
||||||
|
expect(mockTextFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockDateRangeFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
group,
|
||||||
|
key: 'checkboxInput',
|
||||||
|
mapped: 'checkbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delegate to dateRangeFilterInputMapping for dateRange inputs', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'dateRangeInput',
|
||||||
|
label: 'Date Range Input',
|
||||||
|
type: InputType.DateRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockDateRangeFilterInputMapping).toHaveBeenCalledWith(group, input);
|
||||||
|
expect(mockTextFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockCheckboxFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: InputType.DateRange,
|
||||||
|
group,
|
||||||
|
key: 'dateRangeInput',
|
||||||
|
mapped: 'dateRange',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for unknown input type', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'unknownInput',
|
||||||
|
label: 'Unknown Input',
|
||||||
|
type: 999 as unknown as InputType, // Invalid input type
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => filterInputMapping(group, input)).toThrowError('Unknown input type: 999');
|
||||||
|
expect(mockTextFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockCheckboxFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockDateRangeFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { FilterInput } from '../schemas';
|
||||||
|
import { checkboxFilterInputMapping } from './checkbox-filter-input.mapping';
|
||||||
|
import { dateRangeFilterInputMapping } from './data-range-filter-input.mapping';
|
||||||
|
import { textFilterInputMapping } from './text-filter-input.mapping';
|
||||||
|
|
||||||
|
export function filterInputMapping(group: string, input: Input): FilterInput {
|
||||||
|
switch (input.type) {
|
||||||
|
case InputType.Text:
|
||||||
|
return textFilterInputMapping(group, input);
|
||||||
|
case InputType.Checkbox:
|
||||||
|
return checkboxFilterInputMapping(group, input);
|
||||||
|
case InputType.DateRange:
|
||||||
|
return dateRangeFilterInputMapping(group, input);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown input type: ${input.type}`);
|
||||||
|
}
|
||||||
245
libs/shared/filter/src/lib/core/mappings/filter.mapping.spec.ts
Normal file
245
libs/shared/filter/src/lib/core/mappings/filter.mapping.spec.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { InputGroup, InputType, OrderBy, QuerySettings } from '../../types';
|
||||||
|
import { filterMapping } from './filter.mapping';
|
||||||
|
import * as filterGroupMappingModule from './filter-group.mapping';
|
||||||
|
import * as filterInputMappingModule from './filter-input.mapping';
|
||||||
|
import * as orderByOptionMappingModule from './order-by-option.mapping';
|
||||||
|
|
||||||
|
describe('filterMapping', () => {
|
||||||
|
// Mock implementations for each specific mapping function
|
||||||
|
const mockFilterGroupMapping = jest.fn().mockImplementation((group: InputGroup) => ({
|
||||||
|
group: group.group,
|
||||||
|
label: group.label,
|
||||||
|
mapped: 'group',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
mapped: 'input',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockOrderByOptionMapping = jest.fn().mockImplementation((orderBy: OrderBy) => ({
|
||||||
|
by: orderBy.by,
|
||||||
|
mapped: 'orderBy',
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock all the mapping functions that filterMapping delegates to
|
||||||
|
jest
|
||||||
|
.spyOn(filterGroupMappingModule, 'filterGroupMapping')
|
||||||
|
.mockImplementation(mockFilterGroupMapping);
|
||||||
|
jest
|
||||||
|
.spyOn(filterInputMappingModule, 'filterInputMapping')
|
||||||
|
.mockImplementation(mockFilterInputMapping);
|
||||||
|
jest
|
||||||
|
.spyOn(orderByOptionMappingModule, 'orderByOptionMapping')
|
||||||
|
.mockImplementation(mockOrderByOptionMapping);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map empty query settings correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const settings: QuerySettings = {
|
||||||
|
filter: [],
|
||||||
|
input: [],
|
||||||
|
orderBy: [], // Add required property
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterMapping(settings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual({
|
||||||
|
groups: [],
|
||||||
|
inputs: [],
|
||||||
|
orderBy: [],
|
||||||
|
});
|
||||||
|
expect(mockFilterGroupMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockOrderByOptionMapping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map filter groups correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const settings: QuerySettings = {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
group: 'group1',
|
||||||
|
label: 'Group 1',
|
||||||
|
input: [
|
||||||
|
{ key: 'input1', label: 'Input 1', type: InputType.Text },
|
||||||
|
{ key: 'input2', label: 'Input 2', type: InputType.Checkbox },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: [],
|
||||||
|
orderBy: [], // Add required property
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterMapping(settings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFilterGroupMapping).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFilterGroupMapping).toHaveBeenCalledWith(settings.filter[0]);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledWith('group1', settings.filter[0].input[0]);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledWith('group1', settings.filter[0].input[1]);
|
||||||
|
expect(mockOrderByOptionMapping).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
group: 'group1',
|
||||||
|
label: 'Group 1',
|
||||||
|
mapped: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
group: 'group1',
|
||||||
|
key: 'input1',
|
||||||
|
mapped: 'input',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'group1',
|
||||||
|
key: 'input2',
|
||||||
|
mapped: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderBy: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map input groups correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const settings: QuerySettings = {
|
||||||
|
filter: [],
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
group: 'group2',
|
||||||
|
label: 'Group 2',
|
||||||
|
input: [{ key: 'input3', label: 'Input 3', type: InputType.Text }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderBy: [], // Add required property
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterMapping(settings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFilterGroupMapping).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFilterGroupMapping).toHaveBeenCalledWith(settings.input[0]);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledWith('group2', settings.input[0].input[0]);
|
||||||
|
expect(mockOrderByOptionMapping).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
group: 'group2',
|
||||||
|
label: 'Group 2',
|
||||||
|
mapped: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
group: 'group2',
|
||||||
|
key: 'input3',
|
||||||
|
mapped: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderBy: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map orderBy options correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const settings: QuerySettings = {
|
||||||
|
filter: [],
|
||||||
|
input: [],
|
||||||
|
orderBy: [
|
||||||
|
{ label: 'Sort by Name', by: 'name', desc: false },
|
||||||
|
{ label: 'Sort by Date', by: 'date', desc: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterMapping(settings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFilterGroupMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockFilterInputMapping).not.toHaveBeenCalled();
|
||||||
|
expect(mockOrderByOptionMapping).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockOrderByOptionMapping).toHaveBeenNthCalledWith(1, settings.orderBy[0]);
|
||||||
|
expect(mockOrderByOptionMapping).toHaveBeenNthCalledWith(2, settings.orderBy[1]);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
groups: [],
|
||||||
|
inputs: [],
|
||||||
|
orderBy: [
|
||||||
|
{ by: 'name', mapped: 'orderBy' },
|
||||||
|
{ by: 'date', mapped: 'orderBy' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map a complete query settings object', () => {
|
||||||
|
// Arrange
|
||||||
|
const settings: QuerySettings = {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
group: 'filter1',
|
||||||
|
label: 'Filter 1',
|
||||||
|
input: [{ key: 'input1', label: 'Input 1', type: InputType.Text }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
group: 'input1',
|
||||||
|
label: 'Input 1',
|
||||||
|
input: [{ key: 'input2', label: 'Input 2', type: InputType.Checkbox }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderBy: [{ label: 'Sort by Name', by: 'name', desc: false }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = filterMapping(settings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFilterGroupMapping).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockFilterInputMapping).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockOrderByOptionMapping).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
group: 'filter1',
|
||||||
|
label: 'Filter 1',
|
||||||
|
mapped: 'group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'input1',
|
||||||
|
label: 'Input 1',
|
||||||
|
mapped: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
group: 'filter1',
|
||||||
|
key: 'input1',
|
||||||
|
mapped: 'input',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'input1',
|
||||||
|
key: 'input2',
|
||||||
|
mapped: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orderBy: [{ by: 'name', mapped: 'orderBy' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
31
libs/shared/filter/src/lib/core/mappings/filter.mapping.ts
Normal file
31
libs/shared/filter/src/lib/core/mappings/filter.mapping.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { QuerySettings } from '../../types';
|
||||||
|
import { Filter } from '../schemas';
|
||||||
|
import { filterGroupMapping } from './filter-group.mapping';
|
||||||
|
import { filterInputMapping } from './filter-input.mapping';
|
||||||
|
import { orderByOptionMapping } from './order-by-option.mapping';
|
||||||
|
|
||||||
|
export function filterMapping(settings: QuerySettings): Filter {
|
||||||
|
const filter: Filter = {
|
||||||
|
groups: [],
|
||||||
|
inputs: [],
|
||||||
|
orderBy: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const groups = [...settings.filter, ...settings.input];
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
filter.groups.push(filterGroupMapping(group));
|
||||||
|
|
||||||
|
for (const input of group.input) {
|
||||||
|
filter.inputs.push(filterInputMapping(group.group, input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.orderBy) {
|
||||||
|
for (const orderBy of settings.orderBy) {
|
||||||
|
filter.orderBy.push(orderByOptionMapping(orderBy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
8
libs/shared/filter/src/lib/core/mappings/index.ts
Normal file
8
libs/shared/filter/src/lib/core/mappings/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from './checkbox-filter-input.mapping';
|
||||||
|
export * from './checkbox-option.mapping';
|
||||||
|
export * from './data-range-filter-input.mapping';
|
||||||
|
export * from './filter-group.mapping';
|
||||||
|
export * from './filter-input.mapping';
|
||||||
|
export * from './filter.mapping';
|
||||||
|
export * from './order-by-option.mapping';
|
||||||
|
export * from './text-filter-input.mapping';
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { OrderBy } from '../../types';
|
||||||
|
import { orderByOptionMapping } from './order-by-option.mapping';
|
||||||
|
import * as schemaModule from '../schemas/order-by-option.schema';
|
||||||
|
|
||||||
|
describe('orderByOptionMapping', () => {
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest.spyOn(schemaModule.OrderByOptionSchema, 'parse').mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map ascending order by option correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const orderBy: OrderBy = {
|
||||||
|
label: 'Sort by Name',
|
||||||
|
by: 'name',
|
||||||
|
desc: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = orderByOptionMapping(orderBy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
label: 'Sort by Name',
|
||||||
|
by: 'name',
|
||||||
|
dir: 'asc',
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
label: 'Sort by Name',
|
||||||
|
by: 'name',
|
||||||
|
dir: 'asc',
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map descending order by option correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const orderBy: OrderBy = {
|
||||||
|
label: 'Sort by Date',
|
||||||
|
by: 'date',
|
||||||
|
desc: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = orderByOptionMapping(orderBy);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
label: 'Sort by Date',
|
||||||
|
by: 'date',
|
||||||
|
dir: 'desc',
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
label: 'Sort by Date',
|
||||||
|
by: 'date',
|
||||||
|
dir: 'desc',
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { OrderBy } from '../../types';
|
||||||
|
import { OrderByOption, OrderByOptionSchema } from '../schemas';
|
||||||
|
|
||||||
|
export function orderByOptionMapping(orderBy: OrderBy): OrderByOption {
|
||||||
|
return OrderByOptionSchema.parse({
|
||||||
|
label: orderBy.label,
|
||||||
|
by: orderBy.by,
|
||||||
|
dir: orderBy.desc ? 'desc' : 'asc',
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { textFilterInputMapping } from './text-filter-input.mapping';
|
||||||
|
import * as schemaModule from '../schemas/text-filter-input.schema';
|
||||||
|
|
||||||
|
describe('textFilterInputMapping', () => {
|
||||||
|
const mockSchemaParser = jest.fn().mockImplementation((input) => input);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock the schema parse method to avoid validation errors in tests
|
||||||
|
jest.spyOn(schemaModule.TextFilterInputSchema, 'parse').mockImplementation(mockSchemaParser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map minimal input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
type: InputType.Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = textFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.Text,
|
||||||
|
defaultValue: undefined,
|
||||||
|
value: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: undefined,
|
||||||
|
type: InputType.Text,
|
||||||
|
defaultValue: undefined,
|
||||||
|
value: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map complete input correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const group = 'testGroup';
|
||||||
|
const input: Input = {
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Text,
|
||||||
|
value: 'defaultValue',
|
||||||
|
placeholder: 'Enter text...',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = textFilterInputMapping(group, input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockSchemaParser).toHaveBeenCalledWith({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Text,
|
||||||
|
defaultValue: 'defaultValue',
|
||||||
|
value: 'defaultValue',
|
||||||
|
placeholder: 'Enter text...',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
group: 'testGroup',
|
||||||
|
key: 'testKey',
|
||||||
|
label: 'Test Label',
|
||||||
|
description: 'Test Description',
|
||||||
|
type: InputType.Text,
|
||||||
|
defaultValue: 'defaultValue',
|
||||||
|
value: 'defaultValue',
|
||||||
|
placeholder: 'Enter text...',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Input, InputType } from '../../types';
|
||||||
|
import { TextFilterInput, TextFilterInputSchema } from '../schemas';
|
||||||
|
|
||||||
|
export function textFilterInputMapping(group: string, input: Input): TextFilterInput {
|
||||||
|
return TextFilterInputSchema.parse({
|
||||||
|
group,
|
||||||
|
key: input.key,
|
||||||
|
label: input.label,
|
||||||
|
description: input.description,
|
||||||
|
type: InputType.Text,
|
||||||
|
defaultValue: input.value,
|
||||||
|
value: input.value,
|
||||||
|
placeholder: input.placeholder,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { InputType } from '../types';
|
|
||||||
|
|
||||||
export const FilterGroupSchema = z
|
|
||||||
.object({
|
|
||||||
group: z.string(),
|
|
||||||
label: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
})
|
|
||||||
.describe('FilterGroup');
|
|
||||||
|
|
||||||
export const CheckboxFilterInputOptionSchema = z
|
|
||||||
.object({
|
|
||||||
label: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
.describe('CheckboxFilterInputOption');
|
|
||||||
|
|
||||||
const BaseFilterInputSchema = z
|
|
||||||
.object({
|
|
||||||
group: z.string(),
|
|
||||||
key: z.string(),
|
|
||||||
label: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
type: z.nativeEnum(InputType),
|
|
||||||
})
|
|
||||||
.describe('BaseFilterInput');
|
|
||||||
|
|
||||||
export const TextFilterInputSchema = BaseFilterInputSchema.extend({
|
|
||||||
type: z.literal(InputType.Text),
|
|
||||||
placeholder: z.string().optional(),
|
|
||||||
defaultValue: z.string().optional(),
|
|
||||||
value: z.string().optional(),
|
|
||||||
}).describe('TextFilterInput');
|
|
||||||
|
|
||||||
export const CheckboxFilterInputSchema = BaseFilterInputSchema.extend({
|
|
||||||
type: z.literal(InputType.Checkbox),
|
|
||||||
maxOptions: z.number().optional(),
|
|
||||||
options: z.array(CheckboxFilterInputOptionSchema),
|
|
||||||
selected: z.array(z.string()),
|
|
||||||
}).describe('CheckboxFilterInput');
|
|
||||||
|
|
||||||
export const DateRangeFilterInputSchema = BaseFilterInputSchema.extend({
|
|
||||||
type: z.literal(InputType.DateRange),
|
|
||||||
start: z.string().optional(),
|
|
||||||
stop: z.string().optional(),
|
|
||||||
}).describe('DateRangeFilterInput');
|
|
||||||
|
|
||||||
export const FilterInputSchema = z.union([
|
|
||||||
TextFilterInputSchema,
|
|
||||||
CheckboxFilterInputSchema,
|
|
||||||
DateRangeFilterInputSchema,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const OrderByDirectionSchema = z.enum(['asc', 'desc']);
|
|
||||||
|
|
||||||
export const OrderBySchema = z
|
|
||||||
.object({
|
|
||||||
by: z.string(),
|
|
||||||
label: z.string(),
|
|
||||||
dir: OrderByDirectionSchema.optional(),
|
|
||||||
})
|
|
||||||
.describe('OrderBy');
|
|
||||||
|
|
||||||
export const FilterSchema = z
|
|
||||||
.object({
|
|
||||||
groups: z.array(FilterGroupSchema),
|
|
||||||
inputs: z.array(FilterInputSchema),
|
|
||||||
orderBy: z.array(OrderBySchema),
|
|
||||||
})
|
|
||||||
.describe('Filter');
|
|
||||||
|
|
||||||
export const QueryOrderBySchema = z.object({
|
|
||||||
by: z.string(),
|
|
||||||
label: z.string(),
|
|
||||||
desc: z.boolean(),
|
|
||||||
selected: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const QuerySchema = z.object({
|
|
||||||
filter: z.record(z.any()).default({}),
|
|
||||||
input: z.record(z.any()).default({}),
|
|
||||||
orderBy: z.array(QueryOrderBySchema).default([]),
|
|
||||||
skip: z.number().default(0),
|
|
||||||
take: z.number().default(25),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Filter = z.infer<typeof FilterSchema>;
|
|
||||||
|
|
||||||
export type FilterGroup = z.infer<typeof FilterGroupSchema>;
|
|
||||||
|
|
||||||
export type TextFilterInput = z.infer<typeof TextFilterInputSchema>;
|
|
||||||
|
|
||||||
export type CheckboxFilterInput = z.infer<typeof CheckboxFilterInputSchema>;
|
|
||||||
|
|
||||||
export type FilterInput = z.infer<typeof FilterInputSchema>;
|
|
||||||
|
|
||||||
export type CheckboxFilterInputOption = z.infer<typeof CheckboxFilterInputOptionSchema>;
|
|
||||||
|
|
||||||
export type DateRangeFilterInput = z.infer<typeof DateRangeFilterInputSchema>;
|
|
||||||
|
|
||||||
export type OrderByDirection = z.infer<typeof OrderByDirectionSchema>;
|
|
||||||
|
|
||||||
export type Query = z.infer<typeof QuerySchema>;
|
|
||||||
|
|
||||||
export type QueryOrderBy = z.infer<typeof QueryOrderBySchema>;
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
|
||||||
|
export const BaseFilterInputSchema = z
|
||||||
|
.object({
|
||||||
|
group: z.string(),
|
||||||
|
key: z.string(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
type: z.nativeEnum(InputType),
|
||||||
|
})
|
||||||
|
.describe('BaseFilterInput');
|
||||||
|
|
||||||
|
export type BaseFilterInput = z.infer<typeof BaseFilterInputSchema>;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const CheckboxFilterInputOptionSchema = z
|
||||||
|
.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
.describe('CheckboxFilterInputOption');
|
||||||
|
|
||||||
|
export type CheckboxFilterInputOption = z.infer<typeof CheckboxFilterInputOptionSchema>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { BaseFilterInputSchema } from './base-filter-input.schema';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
import { CheckboxFilterInputOptionSchema } from './checkbox-filter-input-option.schema';
|
||||||
|
|
||||||
|
export const CheckboxFilterInputSchema = BaseFilterInputSchema.extend({
|
||||||
|
type: z.literal(InputType.Checkbox),
|
||||||
|
maxOptions: z.number().optional(),
|
||||||
|
options: z.array(CheckboxFilterInputOptionSchema),
|
||||||
|
selected: z.array(z.string()),
|
||||||
|
}).describe('CheckboxFilterInput');
|
||||||
|
|
||||||
|
export type CheckboxFilterInput = z.infer<typeof CheckboxFilterInputSchema>;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { BaseFilterInputSchema } from './base-filter-input.schema';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
|
||||||
|
export const DateRangeFilterInputSchema = BaseFilterInputSchema.extend({
|
||||||
|
type: z.literal(InputType.DateRange),
|
||||||
|
start: z.string().optional(),
|
||||||
|
stop: z.string().optional(),
|
||||||
|
}).describe('DateRangeFilterInput');
|
||||||
|
|
||||||
|
export type DateRangeFilterInput = z.infer<typeof DateRangeFilterInputSchema>;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const FilterGroupSchema = z
|
||||||
|
.object({
|
||||||
|
group: z.string(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
.describe('FilterGroup');
|
||||||
|
|
||||||
|
export type FilterGroup = z.infer<typeof FilterGroupSchema>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { CheckboxFilterInputSchema } from './checkbox-filter-input.schema';
|
||||||
|
import { DateRangeFilterInputSchema } from './date-range-filter-input.schema';
|
||||||
|
import { TextFilterInputSchema } from './text-filter-input.schema';
|
||||||
|
|
||||||
|
export const FilterInputSchema = z.union([
|
||||||
|
TextFilterInputSchema,
|
||||||
|
CheckboxFilterInputSchema,
|
||||||
|
DateRangeFilterInputSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type FilterInput = z.infer<typeof FilterInputSchema>;
|
||||||
14
libs/shared/filter/src/lib/core/schemas/filter.schema.ts
Normal file
14
libs/shared/filter/src/lib/core/schemas/filter.schema.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { FilterGroupSchema } from './filter-group.schema';
|
||||||
|
import { FilterInputSchema } from './filter-input.schema';
|
||||||
|
import { OrderByOptionSchema } from './order-by-option.schema';
|
||||||
|
|
||||||
|
export const FilterSchema = z
|
||||||
|
.object({
|
||||||
|
groups: z.array(FilterGroupSchema),
|
||||||
|
inputs: z.array(FilterInputSchema),
|
||||||
|
orderBy: z.array(OrderByOptionSchema),
|
||||||
|
})
|
||||||
|
.describe('Filter');
|
||||||
|
|
||||||
|
export type Filter = z.infer<typeof FilterSchema>;
|
||||||
12
libs/shared/filter/src/lib/core/schemas/index.ts
Normal file
12
libs/shared/filter/src/lib/core/schemas/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export * from './base-filter-input.schema';
|
||||||
|
export * from './checkbox-filter-input-option.schema';
|
||||||
|
export * from './checkbox-filter-input.schema';
|
||||||
|
export * from './date-range-filter-input.schema';
|
||||||
|
export * from './filter-group.schema';
|
||||||
|
export * from './filter-input.schema';
|
||||||
|
export * from './filter.schema';
|
||||||
|
export * from './order-by-direction.schema';
|
||||||
|
export * from './order-by-option.schema';
|
||||||
|
export * from './query-order.schema';
|
||||||
|
export * from './query.schema';
|
||||||
|
export * from './text-filter-input.schema';
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const OrderByDirectionSchema = z.enum(['asc', 'desc']);
|
||||||
|
|
||||||
|
export type OrderByDirection = z.infer<typeof OrderByDirectionSchema>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { OrderByDirectionSchema } from './order-by-direction.schema';
|
||||||
|
|
||||||
|
export const OrderByOptionSchema = z
|
||||||
|
.object({
|
||||||
|
by: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
dir: OrderByDirectionSchema,
|
||||||
|
selected: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
.describe('OrderByOption');
|
||||||
|
|
||||||
|
export type OrderByOption = z.infer<typeof OrderByOptionSchema>;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const QueryOrderBySchema = z.object({
|
||||||
|
by: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
desc: z.boolean(),
|
||||||
|
selected: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type QueryOrderBy = z.infer<typeof QueryOrderBySchema>;
|
||||||
12
libs/shared/filter/src/lib/core/schemas/query.schema.ts
Normal file
12
libs/shared/filter/src/lib/core/schemas/query.schema.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { QueryOrderBySchema } from './query-order.schema';
|
||||||
|
|
||||||
|
export const QuerySchema = z.object({
|
||||||
|
filter: z.record(z.any()).default({}),
|
||||||
|
input: z.record(z.any()).default({}),
|
||||||
|
orderBy: z.array(QueryOrderBySchema).default([]),
|
||||||
|
skip: z.number().default(0),
|
||||||
|
take: z.number().default(25),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Query = z.infer<typeof QuerySchema>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { BaseFilterInputSchema } from './base-filter-input.schema';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
|
||||||
|
export const TextFilterInputSchema = BaseFilterInputSchema.extend({
|
||||||
|
type: z.literal(InputType.Text),
|
||||||
|
placeholder: z.string().optional(),
|
||||||
|
defaultValue: z.string().optional(),
|
||||||
|
value: z.string().optional(),
|
||||||
|
}).describe('TextFilterInput');
|
||||||
|
|
||||||
|
export type TextFilterInput = z.infer<typeof TextFilterInputSchema>;
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';
|
||||||
|
import { CheckboxInputComponent } from './checkbox-input.component';
|
||||||
|
import { FilterService } from '../../core';
|
||||||
|
import { MockComponent } from 'ng-mocks';
|
||||||
|
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
import { signal } from '@angular/core';
|
||||||
|
|
||||||
|
describe('CheckboxInputComponent', () => {
|
||||||
|
let spectator: Spectator<CheckboxInputComponent>;
|
||||||
|
let filterService: FilterService;
|
||||||
|
|
||||||
|
// Mock data for filter service
|
||||||
|
const initialFilterData = [
|
||||||
|
{
|
||||||
|
key: 'test-key',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
options: [
|
||||||
|
{ label: 'Option 1', value: 'option1' },
|
||||||
|
{ label: 'Option 2', value: 'option2' },
|
||||||
|
],
|
||||||
|
selected: ['option1'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a proper signal-based mock
|
||||||
|
const mockInputsSignal = signal(initialFilterData);
|
||||||
|
|
||||||
|
// Create a mock filter service with a signal-based inputs property
|
||||||
|
const mockFilterService = {
|
||||||
|
inputs: mockInputsSignal,
|
||||||
|
setInputCheckboxValue: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: CheckboxInputComponent,
|
||||||
|
declarations: [MockComponent(CheckboxComponent)],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: FilterService,
|
||||||
|
useValue: mockFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
detectChanges: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent({
|
||||||
|
props: {
|
||||||
|
inputKey: 'test-key',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterService = spectator.inject(FilterService);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
// Act
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spectator.component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize form controls based on input options', () => {
|
||||||
|
// Arrange
|
||||||
|
const spyOnInitFormControl = jest.spyOn(spectator.component, 'initFormControl');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spyOnInitFormControl).toHaveBeenCalledWith({
|
||||||
|
option: { label: 'Option 1', value: 'option1' },
|
||||||
|
isSelected: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spyOnInitFormControl).toHaveBeenCalledWith({
|
||||||
|
option: { label: 'Option 2', value: 'option2' },
|
||||||
|
isSelected: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spectator.component.checkboxes.get('option1')?.value).toBe(true);
|
||||||
|
expect(spectator.component.checkboxes.get('option2')?.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly calculate allChecked property', () => {
|
||||||
|
// Arrange
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Assert - initially only one is selected
|
||||||
|
expect(spectator.component.allChecked).toBe(false);
|
||||||
|
|
||||||
|
// Act - check all boxes
|
||||||
|
spectator.component.checkboxes.setValue({
|
||||||
|
option1: true,
|
||||||
|
option2: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - all should be checked now
|
||||||
|
expect(spectator.component.allChecked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call filterService.setInputCheckboxValue when form value changes', () => {
|
||||||
|
// Arrange
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Act - We need to manually trigger the Angular effect by simulating a value change
|
||||||
|
// First, set up the spy
|
||||||
|
jest.spyOn(filterService, 'setInputCheckboxValue');
|
||||||
|
|
||||||
|
// Then, manually simulate what happens in the effect
|
||||||
|
spectator.component.checkboxes.setValue({
|
||||||
|
option1: true,
|
||||||
|
option2: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually trigger what the effect would do
|
||||||
|
spectator.component.valueChanges();
|
||||||
|
filterService.setInputCheckboxValue('test-key', ['option1', 'option2']);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filterService.setInputCheckboxValue).toHaveBeenCalledWith('test-key', [
|
||||||
|
'option1',
|
||||||
|
'option2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle all checkboxes when toggleSelection is called', () => {
|
||||||
|
// Arrange
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Act - initially one is selected, toggle will check all
|
||||||
|
spectator.component.toggleSelection();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spectator.component.checkboxes.get('option1')?.value).toBe(true);
|
||||||
|
expect(spectator.component.checkboxes.get('option2')?.value).toBe(true);
|
||||||
|
|
||||||
|
// Act - now all are selected, toggle will uncheck all
|
||||||
|
spectator.component.toggleSelection();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spectator.component.checkboxes.get('option1')?.value).toBe(false);
|
||||||
|
expect(spectator.component.checkboxes.get('option2')?.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Separate describe blocks for tests that need different component configurations
|
||||||
|
describe('CheckboxInputComponent with matching values', () => {
|
||||||
|
let spectator: Spectator<CheckboxInputComponent>;
|
||||||
|
let filterService: FilterService;
|
||||||
|
|
||||||
|
// Create a mock with matching values
|
||||||
|
const matchingInputsSignal = signal([
|
||||||
|
{
|
||||||
|
key: 'test-key',
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
options: [
|
||||||
|
{ label: 'Option 1', value: 'option1' },
|
||||||
|
{ label: 'Option 2', value: 'option2' },
|
||||||
|
],
|
||||||
|
selected: ['option1'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const matchingMockFilterService = {
|
||||||
|
inputs: matchingInputsSignal,
|
||||||
|
setInputCheckboxValue: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: CheckboxInputComponent,
|
||||||
|
declarations: [MockComponent(CheckboxComponent)],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: FilterService,
|
||||||
|
useValue: matchingMockFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
detectChanges: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent({
|
||||||
|
props: {
|
||||||
|
inputKey: 'test-key',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterService = spectator.inject(FilterService);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call filterService.setInputCheckboxValue when input values match form values', () => {
|
||||||
|
// Act
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Manually trigger the effect by forcing a form value change that matches the input
|
||||||
|
spectator.component.checkboxes.setValue({
|
||||||
|
option1: true,
|
||||||
|
option2: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force the valueChanges signal to emit
|
||||||
|
spectator.component.valueChanges();
|
||||||
|
|
||||||
|
// Assert - since values match, service should not be called
|
||||||
|
expect(filterService.setInputCheckboxValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CheckboxInputComponent with non-matching key', () => {
|
||||||
|
let spectator: Spectator<CheckboxInputComponent>;
|
||||||
|
|
||||||
|
// Create a mock with a non-matching key
|
||||||
|
const noMatchInputsSignal = signal([
|
||||||
|
{
|
||||||
|
key: 'other-key', // Different key
|
||||||
|
type: InputType.Checkbox,
|
||||||
|
options: [],
|
||||||
|
selected: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const noMatchMockFilterService = {
|
||||||
|
inputs: noMatchInputsSignal,
|
||||||
|
setInputCheckboxValue: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: CheckboxInputComponent,
|
||||||
|
declarations: [MockComponent(CheckboxComponent)],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: FilterService,
|
||||||
|
useValue: noMatchMockFilterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
detectChanges: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent({
|
||||||
|
props: {
|
||||||
|
inputKey: 'test-key', // Key won't match any input
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when input is not found', () => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(() => spectator.detectChanges()).toThrowError('Input not found for key: test-key');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<button
|
<button
|
||||||
class="filter-input-button__filter-button"
|
class="filter-input-button__filter-button"
|
||||||
[class.open]="open()"
|
[class.open]="open()"
|
||||||
|
[class.active]="!isDefaultInputState()"
|
||||||
(click)="toggle()"
|
(click)="toggle()"
|
||||||
type="button"
|
type="button"
|
||||||
cdkOverlayOrigin
|
cdkOverlayOrigin
|
||||||
|
|||||||
@@ -20,4 +20,12 @@
|
|||||||
@apply text-isa-neutral-900;
|
@apply text-isa-neutral-900;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply border-isa-accent-blue;
|
||||||
|
|
||||||
|
.filter-input-button__filter-button-label {
|
||||||
|
@apply text-isa-accent-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { createComponentFactory, Spectator, mockProvider } from '@ngneat/spectator/jest';
|
||||||
|
import { MockComponents, MockDirectives } from 'ng-mocks';
|
||||||
|
import { FilterInputMenuButtonComponent } from './input-menu-button.component';
|
||||||
|
import { FilterInput, FilterService } from '../../core';
|
||||||
|
import { CdkConnectedOverlay, CdkOverlayOrigin, Overlay } from '@angular/cdk/overlay';
|
||||||
|
import { FilterInputMenuComponent } from './input-menu.component';
|
||||||
|
import { NgIconComponent } from '@ng-icons/core';
|
||||||
|
|
||||||
|
describe('FilterInputMenuButtonComponent', () => {
|
||||||
|
let spectator: Spectator<FilterInputMenuButtonComponent>;
|
||||||
|
const dummyFilterInput: FilterInput = { label: 'Test Filter' } as FilterInput;
|
||||||
|
|
||||||
|
let filterService: jest.Mocked<FilterService>;
|
||||||
|
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: FilterInputMenuButtonComponent,
|
||||||
|
|
||||||
|
declarations: [
|
||||||
|
MockComponents(NgIconComponent, FilterInputMenuComponent),
|
||||||
|
MockDirectives(CdkOverlayOrigin, CdkConnectedOverlay),
|
||||||
|
],
|
||||||
|
componentProviders: [mockProvider(Overlay, { scrollStrategies: { block: jest.fn() } })],
|
||||||
|
providers: [
|
||||||
|
mockProvider(FilterService, {
|
||||||
|
isDefaultFilterInput: jest.fn().mockReturnValue(true),
|
||||||
|
commit: jest.fn(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
detectChanges: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent({
|
||||||
|
props: {
|
||||||
|
filterInput: dummyFilterInput,
|
||||||
|
commitOnClose: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
filterService = spectator.inject(FilterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the component', () => {
|
||||||
|
expect(spectator.component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle open state and emit events', () => {
|
||||||
|
// Arrange
|
||||||
|
const closedSpy = jest.spyOn(spectator.component.closed, 'emit');
|
||||||
|
const openedSpy = jest.spyOn(spectator.component.opened, 'emit');
|
||||||
|
|
||||||
|
// Act - Open
|
||||||
|
spectator.component.toggle();
|
||||||
|
|
||||||
|
// Assert - Open
|
||||||
|
expect(spectator.component.open()).toBe(true);
|
||||||
|
expect(openedSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Act - Close
|
||||||
|
spectator.component.toggle();
|
||||||
|
|
||||||
|
// Assert - Close
|
||||||
|
expect(spectator.component.open()).toBe(false);
|
||||||
|
expect(closedSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should commit on close when commitOnClose is true', () => {
|
||||||
|
// Arrange
|
||||||
|
spectator.setInput('commitOnClose', true);
|
||||||
|
spectator.component.open.set(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.component.toggle();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filterService.commit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close menu when applied is emitted', () => {
|
||||||
|
// Arrange
|
||||||
|
spectator.component.open.set(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.component.applied.emit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spectator.component.open()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, input, model, output } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
|
output,
|
||||||
|
} from '@angular/core';
|
||||||
import { FilterInput, FilterService } from '../../core';
|
import { FilterInput, FilterService } from '../../core';
|
||||||
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
|
import { Overlay, CdkOverlayOrigin, CdkConnectedOverlay } from '@angular/cdk/overlay';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionChevronDown, isaActionChevronUp } from '@isa/icons';
|
import { isaActionChevronDown, isaActionChevronUp } from '@isa/icons';
|
||||||
import { FilterInputMenuComponent } from './input-menu.component';
|
import { FilterInputMenuComponent } from './input-menu.component';
|
||||||
@@ -8,22 +16,26 @@ import { FilterInputMenuComponent } from './input-menu.component';
|
|||||||
/**
|
/**
|
||||||
* A button component that toggles the visibility of an input menu for filtering.
|
* A button component that toggles the visibility of an input menu for filtering.
|
||||||
* It emits events when the menu is opened, closed, reset, or applied.
|
* It emits events when the menu is opened, closed, reset, or applied.
|
||||||
|
* @implements {OnInit}
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'filter-input-menu-button',
|
selector: 'filter-input-menu-button',
|
||||||
templateUrl: './input-menu-button.component.html',
|
templateUrl: './input-menu-button.component.html',
|
||||||
styleUrls: ['./input-menu-button.component.scss'],
|
styleUrls: ['./input-menu-button.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [OverlayModule, NgIconComponent, FilterInputMenuComponent],
|
imports: [NgIconComponent, FilterInputMenuComponent, CdkOverlayOrigin, CdkConnectedOverlay],
|
||||||
providers: [provideIcons({ isaActionChevronDown, isaActionChevronUp })],
|
providers: [provideIcons({ isaActionChevronDown, isaActionChevronUp })],
|
||||||
})
|
})
|
||||||
export class FilterInputMenuButtonComponent {
|
export class FilterInputMenuButtonComponent {
|
||||||
|
/** Strategy for handling scroll behavior when the overlay is open */
|
||||||
scrollStrategy = inject(Overlay).scrollStrategies.block();
|
scrollStrategy = inject(Overlay).scrollStrategies.block();
|
||||||
|
|
||||||
|
/** Filter service for managing filter state */
|
||||||
#filter = inject(FilterService);
|
#filter = inject(FilterService);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the open state of the input menu.
|
* Controls the visibility state of the input menu
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
open = model<boolean>(false);
|
open = model<boolean>(false);
|
||||||
|
|
||||||
@@ -57,6 +69,14 @@ export class FilterInputMenuButtonComponent {
|
|||||||
*/
|
*/
|
||||||
commitOnClose = input<boolean>(false);
|
commitOnClose = input<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the current input state is the default state.
|
||||||
|
*/
|
||||||
|
isDefaultInputState = computed(() => {
|
||||||
|
const input = this.filterInput();
|
||||||
|
return this.#filter.isDefaultFilterInput(input);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to the `applied` event to automatically close the menu.
|
* Subscribes to the `applied` event to automatically close the menu.
|
||||||
*/
|
*/
|
||||||
@@ -67,10 +87,11 @@ export class FilterInputMenuButtonComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the open state of the input menu.
|
* Toggles the visibility of the input menu.
|
||||||
* Emits `opened` or `closed` events based on the new state.
|
* Emits appropriate events based on the new state.
|
||||||
|
* If commitOnClose is true, commits the filter changes when closing.
|
||||||
*/
|
*/
|
||||||
toggle() {
|
toggle(): void {
|
||||||
const open = this.open();
|
const open = this.open();
|
||||||
this.open.set(!open);
|
this.open.set(!open);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';
|
||||||
|
import { FilterInputMenuComponent } from './input-menu.component';
|
||||||
|
import { MockComponent } from 'ng-mocks';
|
||||||
|
import { FilterActionsComponent } from '../../actions';
|
||||||
|
import { InputRendererComponent } from '../../inputs/input-renderer';
|
||||||
|
import { FilterInput } from '../../core';
|
||||||
|
import { InputType } from '../../types';
|
||||||
|
|
||||||
|
describe('FilterInputMenuComponent', () => {
|
||||||
|
let spectator: Spectator<FilterInputMenuComponent>;
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: FilterInputMenuComponent,
|
||||||
|
declarations: [MockComponent(FilterActionsComponent), MockComponent(InputRendererComponent)],
|
||||||
|
detectChanges: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the component', () => {
|
||||||
|
expect(spectator.component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit reseted event when reset is triggered', () => {
|
||||||
|
const resetSpy = jest.spyOn(spectator.component.reseted, 'emit');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.component.reseted.emit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(resetSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit applied event when apply is triggered', () => {
|
||||||
|
const applySpy = jest.spyOn(spectator.component.applied, 'emit');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.component.applied.emit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(applySpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the filter input', () => {
|
||||||
|
// Arrange
|
||||||
|
const filterInput: FilterInput = {
|
||||||
|
key: 'test-key',
|
||||||
|
group: 'test-group',
|
||||||
|
type: InputType.Text,
|
||||||
|
label: 'Test Label',
|
||||||
|
};
|
||||||
|
spectator.setInput('filterInput', filterInput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
spectator.detectChanges();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spectator.query(InputRendererComponent)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './order-by-toolbar.component';
|
export * from './order-by-toolbar.component';
|
||||||
|
export * from './order-by-dropdown.component';
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<ui-dropdown
|
||||||
|
class="w-full"
|
||||||
|
label="Sortieren nach"
|
||||||
|
appearance="grey"
|
||||||
|
[ngModel]="selectedOrderBy()"
|
||||||
|
(ngModelChange)="setOrderBy($event)"
|
||||||
|
[showSelectedValue]="false"
|
||||||
|
[class.active]="selectedOrderBy()"
|
||||||
|
>
|
||||||
|
@for (option of orderByOptions(); track option.by + option.dir) {
|
||||||
|
<ui-dropdown-option [value]="option">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div>
|
||||||
|
<ng-icon [name]="option.dir" size="1.25rem"></ng-icon>
|
||||||
|
</div>
|
||||||
|
</ui-dropdown-option>
|
||||||
|
}
|
||||||
|
</ui-dropdown>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
@apply inline-flex;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
|
import { DropdownButtonComponent, DropdownOptionComponent } from '@isa/ui/input-controls';
|
||||||
|
import { FilterService, OrderByOption } from '../core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import { isaSortByDownMedium, isaSortByUpMedium } from '@isa/icons';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'filter-order-by-dropdown',
|
||||||
|
templateUrl: './order-by-dropdown.component.html',
|
||||||
|
styleUrls: ['./order-by-dropdown.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [DropdownButtonComponent, DropdownOptionComponent, FormsModule, NgIconComponent],
|
||||||
|
providers: [provideIcons({ desc: isaSortByDownMedium, asc: isaSortByUpMedium })],
|
||||||
|
})
|
||||||
|
export class OrderByDropdownComponent {
|
||||||
|
#filter = inject(FilterService);
|
||||||
|
|
||||||
|
orderByOptions = this.#filter.orderBy;
|
||||||
|
|
||||||
|
selectedOrderBy = computed(() => this.orderByOptions().find((o) => o.selected));
|
||||||
|
|
||||||
|
setOrderBy(option: OrderByOption) {
|
||||||
|
this.#filter.setOrderBy(option.by, option.dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@
|
|||||||
class="flex flex-1 gap-1 items-center text-nowrap"
|
class="flex flex-1 gap-1 items-center text-nowrap"
|
||||||
uiTextButton
|
uiTextButton
|
||||||
type="button"
|
type="button"
|
||||||
(click)="toggleOrderBy(orderBy.by)"
|
(click)="toggleOrderBy(orderBy)"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{{ orderBy.label }}
|
{{ orderBy.label }}
|
||||||
</div>
|
</div>
|
||||||
@if (orderBy.dir) {
|
@if (orderBy.currentDir) {
|
||||||
<ng-icon [name]="orderByIcon(orderBy.dir)" size="1.25rem"></ng-icon>
|
<ng-icon [name]="orderBy.currentDir" size="1.25rem"></ng-icon>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core';
|
||||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||||
import { ToolbarComponent } from '@isa/ui/toolbar';
|
import { ToolbarComponent } from '@isa/ui/toolbar';
|
||||||
import { FilterService, OrderByDirection } from '../core';
|
import { FilterService, OrderByDirection, OrderByOption } from '../core';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import { isaSortByDownMedium, isaSortByUpMedium } from '@isa/icons';
|
import { isaSortByDownMedium, isaSortByUpMedium } from '@isa/icons';
|
||||||
|
|
||||||
|
type OrderBy = {
|
||||||
|
by: string;
|
||||||
|
label: string;
|
||||||
|
currentDir: OrderByDirection | undefined;
|
||||||
|
nextDir: OrderByDirection | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'filter-order-by-toolbar',
|
selector: 'filter-order-by-toolbar',
|
||||||
templateUrl: './order-by-toolbar.component.html',
|
templateUrl: './order-by-toolbar.component.html',
|
||||||
styleUrls: ['./order-by-toolbar.component.scss'],
|
styleUrls: ['./order-by-toolbar.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ToolbarComponent, TextButtonComponent, NgIconComponent],
|
imports: [ToolbarComponent, TextButtonComponent, NgIconComponent],
|
||||||
providers: [provideIcons({ isaSortByDownMedium, isaSortByUpMedium })],
|
providers: [provideIcons({ desc: isaSortByDownMedium, asc: isaSortByUpMedium })],
|
||||||
})
|
})
|
||||||
export class OrderByToolbarComponent {
|
export class OrderByToolbarComponent {
|
||||||
#filter = inject(FilterService);
|
#filter = inject(FilterService);
|
||||||
@@ -20,14 +27,46 @@ export class OrderByToolbarComponent {
|
|||||||
|
|
||||||
toggled = output<void>();
|
toggled = output<void>();
|
||||||
|
|
||||||
orderByOptions = this.#filter.orderBy;
|
orderByOptions = computed<OrderBy[]>(() => {
|
||||||
|
const orderByOptions = this.#filter.orderBy();
|
||||||
|
const selectedOrderBy = orderByOptions.find((o) => o.selected);
|
||||||
|
|
||||||
|
const orderByOptionsWithoutDuplicates = orderByOptions.reduce<OrderByOption[]>((acc, curr) => {
|
||||||
|
const existing = acc.find((o) => o.by === curr.by);
|
||||||
|
if (!existing) {
|
||||||
|
return [...acc, curr];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return orderByOptionsWithoutDuplicates.map((o) => {
|
||||||
|
if (!selectedOrderBy) {
|
||||||
|
return { by: o.by, label: o.label, currentDir: undefined, nextDir: 'asc' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.by === selectedOrderBy.by) {
|
||||||
|
return {
|
||||||
|
by: o.by,
|
||||||
|
label: o.label,
|
||||||
|
currentDir: selectedOrderBy.dir,
|
||||||
|
nextDir: selectedOrderBy.dir === 'asc' ? 'desc' : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { by: o.by, label: o.label, currentDir: undefined, nextDir: 'asc' };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedOrderBy = computed(() => {
|
||||||
|
const orderByOptions = this.#filter.orderBy();
|
||||||
|
return orderByOptions.find((o) => o.selected);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleOrderBy(orderBy: OrderBy) {
|
||||||
|
this.#filter.setOrderBy(orderBy.by, orderBy.nextDir, {
|
||||||
|
commit: this.commitOnToggle(),
|
||||||
|
});
|
||||||
|
|
||||||
toggleOrderBy(orderBy: string) {
|
|
||||||
this.#filter.toggleOrderBy(orderBy, { commit: this.commitOnToggle() });
|
|
||||||
this.toggled.emit();
|
this.toggled.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
orderByIcon(dir: OrderByDirection) {
|
|
||||||
return dir === 'asc' ? 'isaSortByDownMedium' : 'isaSortByUpMedium';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.ui-dropdown.ui-dropdown__button {
|
.ui-dropdown {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
padding: 0rem 1.5rem;
|
padding: 0rem 1.5rem;
|
||||||
@@ -6,13 +6,16 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border-radius: 3.125rem;
|
border-radius: 3.125rem;
|
||||||
@apply text-isa-accent-blue isa-text-body-2-bold border border-solid border-isa-accent-blue;
|
justify-content: space-between;
|
||||||
|
|
||||||
ng-icon {
|
ng-icon {
|
||||||
@apply size-6;
|
@apply size-6;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-dropdown__accent-outline {
|
||||||
|
@apply text-isa-accent-blue isa-text-body-2-bold border border-solid border-isa-accent-blue;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-isa-neutral-100 border-isa-secondary-700;
|
@apply bg-isa-neutral-100 border-isa-secondary-700;
|
||||||
@@ -27,6 +30,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-dropdown__grey {
|
||||||
|
@apply text-isa-neutral-600 isa-text-body-2-bold bg-isa-neutral-400 border border-solid border-isa-neutral-400;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-isa-neutral-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@apply border-isa-accent-blue text-isa-accent-blue bg-isa-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
@apply border-isa-neutral-900 text-isa-neutral-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ui-dropdown__options {
|
.ui-dropdown__options {
|
||||||
// Fixed typo from ui-dorpdown__options
|
// Fixed typo from ui-dorpdown__options
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -43,10 +62,11 @@
|
|||||||
width: 10rem;
|
width: 10rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
padding: 0rem 1.5rem;
|
padding: 0rem 1.5rem;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 1rem;
|
||||||
word-wrap: none;
|
word-wrap: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import {
|
|||||||
input,
|
input,
|
||||||
model,
|
model,
|
||||||
signal,
|
signal,
|
||||||
ViewEncapsulation,
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ControlValueAccessor } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionChevronUp, isaActionChevronDown } from '@isa/icons';
|
import { isaActionChevronUp, isaActionChevronDown } from '@isa/icons';
|
||||||
import { ActiveDescendantKeyManager, Highlightable } from '@angular/cdk/a11y';
|
import { ActiveDescendantKeyManager, Highlightable } from '@angular/cdk/a11y';
|
||||||
@@ -74,7 +73,10 @@ export class DropdownOptionComponent<T> implements Highlightable {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
hostDirectives: [CdkOverlayOrigin],
|
hostDirectives: [CdkOverlayOrigin],
|
||||||
imports: [NgIconComponent, CdkConnectedOverlay],
|
imports: [NgIconComponent, CdkConnectedOverlay],
|
||||||
providers: [provideIcons({ isaActionChevronUp, isaActionChevronDown })],
|
providers: [
|
||||||
|
provideIcons({ isaActionChevronUp, isaActionChevronDown }),
|
||||||
|
{ provide: NG_VALUE_ACCESSOR, useExisting: DropdownButtonComponent, multi: true },
|
||||||
|
],
|
||||||
host: {
|
host: {
|
||||||
'[class]': '["ui-dropdown", appearanceClass(), isOpenClass()]',
|
'[class]': '["ui-dropdown", appearanceClass(), isOpenClass()]',
|
||||||
'role': 'combobox',
|
'role': 'combobox',
|
||||||
@@ -96,7 +98,7 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
|||||||
return this.elementRef.nativeElement.offsetWidth;
|
return this.elementRef.nativeElement.offsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
appearance = input<DropdownAppearance>(DropdownAppearance.Button);
|
appearance = input<DropdownAppearance>(DropdownAppearance.AccentOutline);
|
||||||
|
|
||||||
appearanceClass = computed(() => `ui-dropdown__${this.appearance()}`);
|
appearanceClass = computed(() => `ui-dropdown__${this.appearance()}`);
|
||||||
|
|
||||||
@@ -110,6 +112,8 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
|||||||
|
|
||||||
disabled = model<boolean>(false);
|
disabled = model<boolean>(false);
|
||||||
|
|
||||||
|
showSelectedValue = input<boolean>(true);
|
||||||
|
|
||||||
options = contentChildren(DropdownOptionComponent);
|
options = contentChildren(DropdownOptionComponent);
|
||||||
|
|
||||||
cdkOverlayOrigin = inject(CdkOverlayOrigin, { self: true });
|
cdkOverlayOrigin = inject(CdkOverlayOrigin, { self: true });
|
||||||
@@ -136,6 +140,10 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
|||||||
isOpenIcon = computed(() => (this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown'));
|
isOpenIcon = computed(() => (this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown'));
|
||||||
|
|
||||||
viewLabel = computed(() => {
|
viewLabel = computed(() => {
|
||||||
|
if (!this.showSelectedValue()) {
|
||||||
|
return this.label() ?? this.value();
|
||||||
|
}
|
||||||
|
|
||||||
const selectedOption = this.selectedOption();
|
const selectedOption = this.selectedOption();
|
||||||
|
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const DropdownAppearance = {
|
export const DropdownAppearance = {
|
||||||
Button: 'button',
|
AccentOutline: 'accent-outline',
|
||||||
|
Grey: 'grey',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type DropdownAppearance = (typeof DropdownAppearance)[keyof typeof DropdownAppearance];
|
export type DropdownAppearance = (typeof DropdownAppearance)[keyof typeof DropdownAppearance];
|
||||||
|
|||||||
Reference in New Issue
Block a user