From 564d5c8fc42021ed6bb1b81bd65e2d0a41febdb6 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Wed, 27 Mar 2024 15:47:45 +0100 Subject: [PATCH] feat: order list filtering (#1571) * filter orders by date range, order number, sku and state * hide order filter if there are no orders * load more orders button * update order include types * add date range picker formly component * vertical formly wrapper Co-authored-by: Dilara Gueler Co-authored-by: Silke --- docs/guides/formly.md | 30 +-- src/app/core/facades/account.facade.ts | 15 +- .../order-list-query.model.ts | 34 +++ .../core/services/order/order.service.spec.ts | 3 +- src/app/core/services/order/order.service.ts | 19 +- .../store/customer/orders/orders.actions.ts | 11 +- .../customer/orders/orders.effects.spec.ts | 31 ++- .../store/customer/orders/orders.effects.ts | 13 +- .../store/customer/orders/orders.reducer.ts | 16 +- .../customer/orders/orders.selectors.spec.ts | 16 +- .../store/customer/orders/orders.selectors.ts | 4 + .../account-order-filters.component.html | 28 +++ .../account-order-filters.component.spec.ts | 41 ++++ .../account-order-filters.component.ts | 217 ++++++++++++++++++ .../account-order-history-page.component.html | 19 +- ...count-order-history-page.component.spec.ts | 9 +- .../account-order-history-page.component.ts | 27 ++- .../account-order-history-page.module.ts | 3 +- .../order-widget/order-widget.component.ts | 3 +- .../dev/testing/formly-testing.module.ts | 7 + .../translate-placeholder.extension.ts | 2 +- .../date-picker-field.component.ts | 2 +- .../date-range-picker-field.component.html | 84 +++++++ .../date-range-picker-field.component.scss | 24 ++ .../date-range-picker-field.component.spec.ts | 102 ++++++++ .../date-range-picker-field.component.ts | 143 ++++++++++++ src/app/shared/formly/types/types.module.ts | 7 + src/assets/i18n/de_DE.json | 11 + src/assets/i18n/en_US.json | 13 +- src/assets/i18n/fr_FR.json | 11 + 30 files changed, 886 insertions(+), 59 deletions(-) create mode 100644 src/app/core/models/order-list-query/order-list-query.model.ts create mode 100644 src/app/pages/account-order-history/account-order-filters/account-order-filters.component.html create mode 100644 src/app/pages/account-order-history/account-order-filters/account-order-filters.component.spec.ts create mode 100644 src/app/pages/account-order-history/account-order-filters/account-order-filters.component.ts create mode 100644 src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.html create mode 100644 src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.scss create mode 100644 src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.spec.ts create mode 100644 src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.ts diff --git a/docs/guides/formly.md b/docs/guides/formly.md index 8430787029..b2318a0df4 100644 --- a/docs/guides/formly.md +++ b/docs/guides/formly.md @@ -254,20 +254,22 @@ Refer to the tables below for an overview of these parts. - Template option `inputClass`: These CSS class(es) will be added to all input/select/textarea/text tags. - Template option `ariaLabel`: Adds an aria-label to all input/select/textarea tags. -| Name | Description | Relevant props | -| -------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ish-text-input-field | Basic input field, supports all text types | type: 'text' (default),'email','tel','password' | -| ish-select-field | Basic select field | `options`: `{ value: any; label: string}[]` or Observable. `placeholder`: Translation key or string for the default selection | -| ish-textarea-field | Basic textarea field | `cols` & `rows`: Specifies the dimensions of the textarea | -| ish-checkbox-field | Basic checkbox input | `title`: Title for a checkbox | -| ish-email-field | Email input field that automatically adds an e-mail validator and error messages | ---- | -| ish-password-field | Password input field that automatically adds a password validator and error messages | ---- | -| ish-phone-field | Phone number input field that automatically adds a phone number validator and error messages | ---- | -| ish-fieldset-field | Wraps fields in a `
` tag for styling | `fieldsetClass`: Class that will be added to the fieldset tag. `childClass`: Class that will be added to the child div. `legend`: Legend element that will be added to the fieldset, use the value as the legend text. `legendClass`: Class that will be added to the legend tag. | -| ish-captcha-field | Includes the `` component and adds the relevant `formControls` to the form | `topic`: Topic that will be passed to the Captcha component. | -| ish-radio-field | Basic radio input | ---- | -| ish-plain-text-field | Only display the form value | ---- | -| ish-html-text-field | Only display the form value as html | ---- | +| Name | Description | Relevant props | +| --------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ish-text-input-field | Basic input field, supports all text types | type: 'text' (default),'email','tel','password' | +| ish-select-field | Basic select field | `options`: `{ value: any; label: string}[]` or Observable. `placeholder`: Translation key or string for the default selection | +| ish-textarea-field | Basic textarea field | `cols` & `rows`: Specifies the dimensions of the textarea | +| ish-checkbox-field | Basic checkbox input | `title`: Title for a checkbox | +| ish-email-field | Email input field that automatically adds an e-mail validator and error messages | ---- | +| ish-password-field | Password input field that automatically adds a password validator and error messages | ---- | +| ish-phone-field | Phone number input field that automatically adds a phone number validator and error messages | ---- | +| ish-fieldset-field | Wraps fields in a `
` tag for styling | `fieldsetClass`: Class that will be added to the fieldset tag. `childClass`: Class that will be added to the child div. `legend`: Legend element that will be added to the fieldset, use the value as the legend text. `legendClass`: Class that will be added to the legend tag. | +| ish-captcha-field | Includes the `` component and adds the relevant `formControls` to the form | `topic`: Topic that will be passed to the Captcha component. | +| ish-radio-field | Basic radio input | ---- | +| ish-plain-text-field | Only display the form value | ---- | +| ish-html-text-field | Only display the form value as html | ---- | +| ish-date-picker-field | Basic datepicker | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `isSatExcluded`: Specifies if saturdays can be disabled. `isSunExcluded`: Specifies if sundays can be disabled. | +| ish-date-range-picker-field | Datepicker with range | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `startDate`: The start date. `placeholder`: Placeholder that displays the date format in the input field. | ### Wrappers diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts index 3c2481cf36..c77a8c98c2 100644 --- a/src/app/core/facades/account.facade.ts +++ b/src/app/core/facades/account.facade.ts @@ -7,11 +7,11 @@ import { Address } from 'ish-core/models/address/address.model'; import { Credentials } from 'ish-core/models/credentials/credentials.model'; import { Customer, CustomerRegistrationType, SsoRegistrationType } from 'ish-core/models/customer/customer.model'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { PasswordReminderUpdate } from 'ish-core/models/password-reminder-update/password-reminder-update.model'; import { PasswordReminder } from 'ish-core/models/password-reminder/password-reminder.model'; import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; import { User } from 'ish-core/models/user/user.model'; -import { OrderListQuery } from 'ish-core/services/order/order.service'; import { MessagesPayloadType } from 'ish-core/store/core/messages'; import { getServerConfigParameter } from 'ish-core/store/core/server-config'; import { @@ -30,10 +30,12 @@ import { getDataRequestLoading, } from 'ish-core/store/customer/data-requests'; import { + getMoreOrdersAvailable, getOrders, getOrdersError, getOrdersLoading, getSelectedOrder, + loadMoreOrders, loadOrders, } from 'ish-core/store/customer/orders'; import { @@ -173,9 +175,16 @@ export class AccountFacade { // ORDERS - orders$(query?: OrderListQuery) { + orders$ = this.store.pipe(select(getOrders)); + + loadOrders(query?: OrderListQuery) { this.store.dispatch(loadOrders({ query: query || { limit: 30 } })); - return this.store.pipe(select(getOrders)); + } + + moreOrdersAvailable$ = this.store.pipe(select(getMoreOrdersAvailable)); + + loadMoreOrders() { + this.store.dispatch(loadMoreOrders()); } selectedOrder$ = this.store.pipe(select(getSelectedOrder)); diff --git a/src/app/core/models/order-list-query/order-list-query.model.ts b/src/app/core/models/order-list-query/order-list-query.model.ts new file mode 100644 index 0000000000..34e25ba582 --- /dev/null +++ b/src/app/core/models/order-list-query/order-list-query.model.ts @@ -0,0 +1,34 @@ +export type OrderIncludeType = + | 'all' + | 'buckets' + | 'buckets_discounts' + | 'buckets_shipToAddress' + | 'buckets_shippingMethod' + | 'buyingContext' + | 'commonShipToAddress' + | 'commonShippingMethod' + | 'discounts' + | 'discounts_promotion' + | 'invoiceToAddress' + | 'lineItems' + | 'lineItems_discounts' + | 'lineItems_product' + | 'lineItems_shipToAddress' + | 'lineItems_shippingMethod' + | 'lineItems_warranty' + | 'payments' + | 'payments_paymentInstrument' + | 'payments_paymentMethod'; + +export interface OrderListQuery { + limit: number; + include?: OrderIncludeType[]; + offset?: number; + documentNumber?: string[]; + customerOrderID?: string[]; + creationDateFrom?: string; + creationDateTo?: string; + lineItem_product?: string[]; + lineItem_customerProductID?: string[]; + lineItem_partialOrderNo?: string[]; +} diff --git a/src/app/core/services/order/order.service.spec.ts b/src/app/core/services/order/order.service.spec.ts index 97b569cf6f..6e554db910 100644 --- a/src/app/core/services/order/order.service.spec.ts +++ b/src/app/core/services/order/order.service.spec.ts @@ -5,13 +5,14 @@ import { provideMockStore } from '@ngrx/store/testing'; import { of } from 'rxjs'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { OrderBaseData } from 'ish-core/models/order/order.interface'; import { Order } from 'ish-core/models/order/order.model'; import { ApiService, AvailableOptions } from 'ish-core/services/api/api.service'; import { getCurrentLocale } from 'ish-core/store/core/configuration'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; -import { OrderListQuery, OrderService, orderListQueryToHttpParams } from './order.service'; +import { OrderService, orderListQueryToHttpParams } from './order.service'; describe('Order Service', () => { let orderService: OrderService; diff --git a/src/app/core/services/order/order.service.ts b/src/app/core/services/order/order.service.ts index 452781a1e8..8a47b618b3 100644 --- a/src/app/core/services/order/order.service.ts +++ b/src/app/core/services/order/order.service.ts @@ -5,28 +5,13 @@ import { Store, select } from '@ngrx/store'; import { EMPTY, Observable, of, throwError } from 'rxjs'; import { catchError, concatMap, map, withLatestFrom } from 'rxjs/operators'; +import { OrderIncludeType, OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { OrderData } from 'ish-core/models/order/order.interface'; import { OrderMapper } from 'ish-core/models/order/order.mapper'; import { Order } from 'ish-core/models/order/order.model'; import { ApiService } from 'ish-core/services/api/api.service'; import { getCurrentLocale } from 'ish-core/store/core/configuration'; -type OrderIncludeType = - | 'invoiceToAddress' - | 'commonShipToAddress' - | 'commonShippingMethod' - | 'discounts' - | 'lineItems_discounts' - | 'lineItems' - | 'payments' - | 'payments_paymentMethod' - | 'payments_paymentInstrument'; - -export interface OrderListQuery { - limit: number; - include?: OrderIncludeType[]; -} - export function orderListQueryToHttpParams(query: OrderListQuery): HttpParams { return Object.entries(query).reduce( (acc, [key, value]: [keyof OrderListQuery, OrderListQuery[keyof OrderListQuery]]) => { @@ -34,7 +19,7 @@ export function orderListQueryToHttpParams(query: OrderListQuery): HttpParams { if (key === 'include') { return acc.set(key, value.join(',')); } else { - return value.reduce((acc, value) => acc.append(key, value.toString()), acc); + return (value as string[]).reduce((acc, value) => acc.append(key, value?.toString()), acc); } } else if (value !== undefined) { return acc.set(key, value.toString()); diff --git a/src/app/core/store/customer/orders/orders.actions.ts b/src/app/core/store/customer/orders/orders.actions.ts index 1f3ade8162..36380e31e8 100644 --- a/src/app/core/store/customer/orders/orders.actions.ts +++ b/src/app/core/store/customer/orders/orders.actions.ts @@ -1,8 +1,8 @@ import { Params } from '@angular/router'; import { createAction } from '@ngrx/store'; +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { Order } from 'ish-core/models/order/order.model'; -import { OrderListQuery } from 'ish-core/services/order/order.service'; import { httpError, payload } from 'ish-core/utils/ngrx-creators'; export const createOrder = createAction('[Orders] Create Order'); @@ -11,11 +11,16 @@ export const createOrderFail = createAction('[Orders API] Create Order Fail', ht export const createOrderSuccess = createAction('[Orders API] Create Order Success', payload<{ order: Order }>()); -export const loadOrders = createAction('[Orders Internal] Load Orders', payload<{ query: OrderListQuery }>()); +export const loadOrders = createAction('[Orders] Load Orders', payload<{ query: OrderListQuery }>()); + +export const loadMoreOrders = createAction('[Orders] Load More Orders'); export const loadOrdersFail = createAction('[Orders API] Load Orders Fail', httpError()); -export const loadOrdersSuccess = createAction('[Orders API] Load Orders Success', payload<{ orders: Order[] }>()); +export const loadOrdersSuccess = createAction( + '[Orders API] Load Orders Success', + payload<{ orders: Order[]; query: OrderListQuery; allRetrieved?: boolean }>() +); export const loadOrder = createAction('[Orders Internal] Load Order', payload<{ orderId: string }>()); diff --git a/src/app/core/store/customer/orders/orders.effects.spec.ts b/src/app/core/store/customer/orders/orders.effects.spec.ts index 9dde9cdc7d..0e60275b66 100644 --- a/src/app/core/store/customer/orders/orders.effects.spec.ts +++ b/src/app/core/store/customer/orders/orders.effects.spec.ts @@ -27,6 +27,7 @@ import { createOrder, createOrderFail, createOrderSuccess, + loadMoreOrders, loadOrder, loadOrderByAPIToken, loadOrderFail, @@ -208,8 +209,19 @@ describe('Orders Effects', () => { }); it('should load all orders of a user and dispatch a LoadOrdersSuccess action', () => { - const action = loadOrders({ query: { limit: 30 } }); - const completion = loadOrdersSuccess({ orders }); + const query = { limit: 30 }; + const action = loadOrders({ query }); + const completion = loadOrdersSuccess({ orders, query, allRetrieved: true }); + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.loadOrders$).toBeObservable(expected$); + }); + + it('should report more available if limit was reached', () => { + const query = { limit: orders.length }; + const action = loadOrders({ query }); + const completion = loadOrdersSuccess({ orders, query, allRetrieved: false }); actions$ = hot('-a-a-a', { a: action }); const expected$ = cold('-c-c-c', { c: completion }); @@ -228,6 +240,19 @@ describe('Orders Effects', () => { }); }); + describe('loadMoreOrders$', () => { + it('should load more orders', () => { + store.dispatch(loadOrdersSuccess({ orders, query: { limit: 30 } })); + + const action = loadMoreOrders(); + const completion = loadOrders({ query: { limit: 30, offset: 30 } }); + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.loadMoreOrders$).toBeObservable(expected$); + }); + }); + describe('loadOrder$', () => { it('should call the orderService for loadOrder', done => { const action = loadOrder({ orderId: order.id }); @@ -429,7 +454,7 @@ describe('Orders Effects', () => { describe('setOrderBreadcrumb$', () => { beforeEach(fakeAsync(() => { - store.dispatch(loadOrdersSuccess({ orders })); + store.dispatch(loadOrdersSuccess({ orders, query: { limit: 30 } })); router.navigateByUrl(`/account/orders/${orders[0].id}`); tick(500); store.dispatch(selectOrder({ orderId: orders[0].id })); diff --git a/src/app/core/store/customer/orders/orders.effects.ts b/src/app/core/store/customer/orders/orders.effects.ts index 85f5032189..aead236d64 100644 --- a/src/app/core/store/customer/orders/orders.effects.ts +++ b/src/app/core/store/customer/orders/orders.effects.ts @@ -19,6 +19,7 @@ import { createOrder, createOrderFail, createOrderSuccess, + loadMoreOrders, loadOrder, loadOrderByAPIToken, loadOrderFail, @@ -30,7 +31,7 @@ import { selectOrderAfterRedirect, selectOrderAfterRedirectFail, } from './orders.actions'; -import { getOrder, getSelectedOrder } from './orders.selectors'; +import { getOrder, getOrderListQuery, getSelectedOrder } from './orders.selectors'; @Injectable() export class OrdersEffects { @@ -116,13 +117,21 @@ export class OrdersEffects { mapToPayloadProperty('query'), switchMap(query => this.orderService.getOrders(query).pipe( - map(orders => loadOrdersSuccess({ orders })), + map(orders => loadOrdersSuccess({ orders, query, allRetrieved: orders.length < query.limit })), mapErrorToAction(loadOrdersFail) ) ) ) ); + loadMoreOrders$ = createEffect(() => + this.actions$.pipe( + ofType(loadMoreOrders), + concatLatestFrom(() => this.store.pipe(select(getOrderListQuery))), + map(([, query]) => loadOrders({ query: { ...query, offset: (query.offset ?? 0) + query.limit } })) + ) + ); + loadOrder$ = createEffect(() => this.actions$.pipe( ofType(loadOrder), diff --git a/src/app/core/store/customer/orders/orders.reducer.ts b/src/app/core/store/customer/orders/orders.reducer.ts index ba98b11745..8b62c66cac 100644 --- a/src/app/core/store/customer/orders/orders.reducer.ts +++ b/src/app/core/store/customer/orders/orders.reducer.ts @@ -2,6 +2,7 @@ import { EntityState, createEntityAdapter } from '@ngrx/entity'; import { createReducer, on } from '@ngrx/store'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { Order } from 'ish-core/models/order/order.model'; import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn } from 'ish-core/utils/ngrx-creators'; @@ -26,12 +27,16 @@ export const orderAdapter = createEntityAdapter({ export interface OrdersState extends EntityState { loading: boolean; selected: string; + query: OrderListQuery; + moreAvailable: boolean; error: HttpError; } const initialState: OrdersState = orderAdapter.getInitialState({ loading: false, selected: undefined, + query: undefined, + moreAvailable: true, error: undefined, }); @@ -57,10 +62,13 @@ export const ordersReducer = createReducer( }; }), on(loadOrdersSuccess, (state, action) => { - const { orders } = action.payload; - return { - ...orderAdapter.setAll(orders, state), - }; + const { orders, query, allRetrieved } = action.payload; + const newState = { ...state, query, moreAvailable: !allRetrieved }; + if (!query.offset) { + return orderAdapter.setAll(orders, newState); + } else { + return orderAdapter.addMany(orders, newState); + } }), on( diff --git a/src/app/core/store/customer/orders/orders.selectors.spec.ts b/src/app/core/store/customer/orders/orders.selectors.spec.ts index c10d2217ef..f988f88adf 100644 --- a/src/app/core/store/customer/orders/orders.selectors.spec.ts +++ b/src/app/core/store/customer/orders/orders.selectors.spec.ts @@ -19,7 +19,9 @@ import { selectOrder, } from './orders.actions'; import { + getMoreOrdersAvailable, getOrder, + getOrderListQuery, getOrders, getOrdersError, getOrdersLoading, @@ -66,7 +68,7 @@ describe('Orders Selectors', () => { describe('select order', () => { beforeEach(() => { - store$.dispatch(loadOrdersSuccess({ orders })); + store$.dispatch(loadOrdersSuccess({ orders, query: { limit: 30 } })); store$.dispatch(selectOrder({ orderId: orders[1].id })); }); it('should get a certain order if they are loaded orders', () => { @@ -92,19 +94,29 @@ describe('Orders Selectors', () => { describe('and reporting success', () => { beforeEach(() => { - store$.dispatch(loadOrdersSuccess({ orders })); + store$.dispatch(loadOrdersSuccess({ orders, query: { limit: 30 }, allRetrieved: true })); }); it('should set loading to false', () => { expect(getOrdersLoading(store$.state)).toBeFalse(); expect(getOrdersError(store$.state)).toBeUndefined(); + }); + it('should have orders', () => { const loadedOrders = getOrders(store$.state); expect(loadedOrders[1].documentNo).toEqual(orders[1].documentNo); expect(loadedOrders[1].lineItems).toHaveLength(1); expect(loadedOrders[1].lineItems[0].id).toEqual('test2'); expect(loadedOrders[1].lineItems[0].productSKU).toEqual('sku'); }); + + it('should have a query', () => { + expect(getOrderListQuery(store$.state)).toEqual({ limit: 30 }); + }); + + it('should have no more orders available', () => { + expect(getMoreOrdersAvailable(store$.state)).toBeFalse(); + }); }); describe('and reporting failure', () => { diff --git a/src/app/core/store/customer/orders/orders.selectors.ts b/src/app/core/store/customer/orders/orders.selectors.ts index 59d4b972ce..a239bc6e8d 100644 --- a/src/app/core/store/customer/orders/orders.selectors.ts +++ b/src/app/core/store/customer/orders/orders.selectors.ts @@ -18,6 +18,10 @@ export const getSelectedOrder = createSelector( export const getOrders = selectAll; +export const getOrderListQuery = createSelector(getOrdersState, state => state.query); + +export const getMoreOrdersAvailable = createSelector(getOrdersState, state => state.moreAvailable); + export const getOrder = (orderId: string) => createSelector(selectAll, entities => entities.find(e => e.id === orderId)); diff --git a/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.html b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.html new file mode 100644 index 0000000000..65b908dcc7 --- /dev/null +++ b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.html @@ -0,0 +1,28 @@ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
diff --git a/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.spec.ts b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.spec.ts new file mode 100644 index 0000000000..17723378ed --- /dev/null +++ b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { FormlyForm } from '@ngx-formly/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; + +import { AccountOrderFiltersComponent } from './account-order-filters.component'; + +describe('Account Order Filters Component', () => { + let component: AccountOrderFiltersComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MockComponent(FormlyForm), + NgbCollapseModule, + ReactiveFormsModule, + RouterTestingModule, + TranslateModule.forRoot(), + ], + declarations: [AccountOrderFiltersComponent, MockComponent(FaIconComponent)], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AccountOrderFiltersComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.ts b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.ts new file mode 100644 index 0000000000..21f9a7c0d7 --- /dev/null +++ b/src/app/pages/account-order-history/account-order-filters/account-order-filters.component.ts @@ -0,0 +1,217 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + DestroyRef, + EventEmitter, + Injectable, + Input, + OnInit, + Output, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { UntypedFormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import { FormlyFieldConfig } from '@ngx-formly/core'; + +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; + +@Injectable() +export class OrderDateFilterAdapter extends NgbDateAdapter { + fromModel(value: string): NgbDateStruct { + if (value) { + const dateParts = value.split('-'); + return { + year: +dateParts[0], + month: +dateParts[1], + day: +dateParts[2], + }; + } + } + toModel(date: NgbDateStruct): string { + if (date) { + return `${date.year}-${date.month.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')}`; + } + } +} + +interface FormModel extends Record { + date?: { + fromDate: string; + toDate: string; + }; + orderNo?: string; + sku?: string; + state?: string; +} + +type UrlModel = Partial>; + +function selectFirst(val: string | string[]): string { + return Array.isArray(val) ? val[0] : val; +} + +function selectAll(val: string | string[]): string { + return Array.isArray(val) ? val.join(',') : val; +} + +function selectArray(val: string | string[]): string[] { + if (!val) { + return; + } + return Array.isArray(val) ? val : [val]; +} + +function removeEmpty>(obj: T): T { + return Object.keys(obj).reduce>((acc, key) => { + if (Array.isArray(obj[key])) { + if ((obj[key] as unknown[]).length > 0) { + acc[key] = obj[key]; + } + } else if (obj[key]) { + acc[key] = obj[key]; + } + return acc; + }, {}) as T; +} + +function urlToModel(params: UrlModel): FormModel { + return removeEmpty({ + date: { + fromDate: selectFirst(params.from), + toDate: selectFirst(params.to), + }, + orderNo: selectAll(params.orderNo), + sku: selectAll(params.sku), + state: selectFirst(params.state), + }); +} + +function modelToUrl(model: FormModel): UrlModel { + return removeEmpty({ + from: model.date?.fromDate, + to: model.date?.toDate, + orderNo: model.orderNo?.split(',').map(s => s.trim()), + sku: model.sku?.split(',').map(s => s.trim()), + state: model.state + ?.split(',') + .map(s => s.trim()) + .filter(x => !!x), + }); +} + +function urlToQuery(params: UrlModel): Partial { + return removeEmpty>({ + creationDateFrom: selectFirst(params.from), + creationDateTo: selectFirst(params.to), + documentNumber: selectArray(params.orderNo), + lineItem_product: selectArray(params.sku), + }); +} + +@Component({ + selector: 'ish-account-order-filters', + templateUrl: './account-order-filters.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ provide: NgbDateAdapter, useClass: OrderDateFilterAdapter }], +}) +export class AccountOrderFiltersComponent implements OnInit, AfterViewInit { + @Input() fragmentOnRouting: string; + + form = new UntypedFormGroup({}); + + fields: FormlyFieldConfig[]; + + @Output() modelChange = new EventEmitter>(); + + private destroyRef = inject(DestroyRef); + + formIsCollapsed = true; + + constructor(private route: ActivatedRoute, private router: Router) {} + + ngOnInit() { + this.fields = [ + { + key: 'orderNo', + type: 'ish-text-input-field', + props: { + placeholder: 'account.order_history.filter.label.order_no', + fieldClass: 'col-12', + }, + }, + { + fieldGroupClassName: 'row', + fieldGroup: [ + { + className: 'col-12 col-md-6', + key: 'sku', + type: 'ish-text-input-field', + props: { + label: 'account.order_history.filter.label.sku', + placeholder: 'account.order_history.filter.label.sku', + labelClass: 'col-md-6', + fieldClass: 'col-md-12', + }, + }, + { + className: 'col-12 col-md-6', + key: 'date', + type: 'ish-date-range-picker-field', + props: { + label: 'account.order_history.filter.label.date', + placeholder: 'checkout.desired_delivery_date.note', + minDays: -365 * 10, + maxDays: 0, + startDate: -30, + labelClass: 'col-md-6', + fieldClass: 'col-md-12', + }, + }, + ], + }, + ]; + } + + ngAfterViewInit(): void { + this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(params => { + if ( + Object.keys(params).length > 1 || + (Object.keys(params).length === 1 && Object.keys(params)[0] !== 'orderNo') + ) { + this.formIsCollapsed = false; + } + this.form.patchValue(urlToModel(params)); + this.modelChange.emit(urlToQuery(params)); + }); + } + + private navigate(queryParams: UrlModel) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + fragment: this.fragmentOnRouting, + }); + } + + expandForm() { + this.formIsCollapsed = !this.formIsCollapsed; + } + + submitForm() { + this.navigate(modelToUrl(this.form.value)); + } + + resetForm() { + this.navigate(undefined); + } + + showResetButton(): boolean { + const productId = this.form.get('sku')?.value; + const date = this.form.get('date')?.value; + + return !this.formIsCollapsed && (!!productId || !!date); + } +} diff --git a/src/app/pages/account-order-history/account-order-history-page.component.html b/src/app/pages/account-order-history/account-order-history-page.component.html index 2f4639636e..598c7c756b 100644 --- a/src/app/pages/account-order-history/account-order-history-page.component.html +++ b/src/app/pages/account-order-history/account-order-history-page.component.html @@ -1,14 +1,31 @@ +

{{ 'account.order_history.heading' | translate }}

{{ 'account.order.subtitle' | translate }}

+ + +
+ +
+

{{ 'account.order.questions.title' | translate }}

{ let component: AccountOrderHistoryPageComponent; let fixture: ComponentFixture; let element: HTMLElement; + let accountFacade: AccountFacade; beforeEach(async () => { + accountFacade = mock(AccountFacade); + when(accountFacade.orders$).thenReturn(of([])); + await TestBed.configureTestingModule({ declarations: [ AccountOrderHistoryPageComponent, @@ -24,7 +29,7 @@ describe('Account Order History Page Component', () => { MockDirective(ServerHtmlDirective), ], imports: [TranslateModule.forRoot()], - providers: [{ provide: AccountFacade, useFactory: () => instance(mock(AccountFacade)) }], + providers: [{ provide: AccountFacade, useFactory: () => instance(accountFacade) }], }).compileComponents(); }); diff --git a/src/app/pages/account-order-history/account-order-history-page.component.ts b/src/app/pages/account-order-history/account-order-history-page.component.ts index 8d770451a7..8122589ede 100644 --- a/src/app/pages/account-order-history/account-order-history-page.component.ts +++ b/src/app/pages/account-order-history/account-order-history-page.component.ts @@ -1,13 +1,16 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, shareReplay } from 'rxjs'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model'; import { Order } from 'ish-core/models/order/order.model'; /** * The Order History Page Component renders the account history page of a logged in user. * + * If search results have no order, filters should be rendered + * If no order placed yet, filters should not be rendered */ @Component({ templateUrl: './account-order-history-page.component.html', @@ -17,12 +20,32 @@ export class AccountOrderHistoryPageComponent implements OnInit { orders$: Observable; ordersLoading$: Observable; ordersError$: Observable; + moreOrdersAvailable$: Observable; + filtersActive: boolean; constructor(private accountFacade: AccountFacade) {} ngOnInit(): void { - this.orders$ = this.accountFacade.orders$({ limit: 30, include: ['commonShipToAddress'] }); + this.orders$ = this.accountFacade.orders$.pipe(shareReplay(1)); this.ordersLoading$ = this.accountFacade.ordersLoading$; this.ordersError$ = this.accountFacade.ordersError$; + this.moreOrdersAvailable$ = this.accountFacade.moreOrdersAvailable$; + } + + /** + * Load filtered orders + * + */ + loadFilteredOrders(filters: Partial) { + this.filtersActive = Object.keys(filters).length > 0; + this.accountFacade.loadOrders({ + ...filters, + limit: 30, + include: ['commonShipToAddress'], + }); + } + + loadMoreOrders(): void { + this.accountFacade.loadMoreOrders(); } } diff --git a/src/app/pages/account-order-history/account-order-history-page.module.ts b/src/app/pages/account-order-history/account-order-history-page.module.ts index cdc002087c..c424b53db9 100644 --- a/src/app/pages/account-order-history/account-order-history-page.module.ts +++ b/src/app/pages/account-order-history/account-order-history-page.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from 'ish-shared/shared.module'; +import { AccountOrderFiltersComponent } from './account-order-filters/account-order-filters.component'; import { AccountOrderHistoryPageComponent } from './account-order-history-page.component'; const routes: Routes = [ @@ -19,6 +20,6 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forChild(routes), SharedModule], exports: [RouterModule], - declarations: [AccountOrderHistoryPageComponent], + declarations: [AccountOrderFiltersComponent, AccountOrderHistoryPageComponent], }) export class AccountOrderHistoryPageModule {} diff --git a/src/app/shared/components/order/order-widget/order-widget.component.ts b/src/app/shared/components/order/order-widget/order-widget.component.ts index 0f79a6ef90..118d51d8c9 100644 --- a/src/app/shared/components/order/order-widget/order-widget.component.ts +++ b/src/app/shared/components/order/order-widget/order-widget.component.ts @@ -22,7 +22,8 @@ export class OrderWidgetComponent implements OnInit { constructor(private accountfacade: AccountFacade) {} ngOnInit(): void { - this.orders$ = this.accountfacade.orders$({ limit: 5 }); + this.orders$ = this.accountfacade.orders$; this.ordersLoading$ = this.accountfacade.ordersLoading$; + this.accountfacade.loadOrders({ limit: 5 }); } } diff --git a/src/app/shared/formly/dev/testing/formly-testing.module.ts b/src/app/shared/formly/dev/testing/formly-testing.module.ts index 69a702384d..54f139ca17 100644 --- a/src/app/shared/formly/dev/testing/formly-testing.module.ts +++ b/src/app/shared/formly/dev/testing/formly-testing.module.ts @@ -105,6 +105,12 @@ class DummyWrapperComponent extends FieldWrapper {} }) class DatePickerFieldComponent extends FieldType {} +@Component({ + selector: 'ish-date-range-picker-test-field', + template: 'DateRangePickerFieldComponent: {{ field.key }} {{ field.type }} {{ to | json }}', +}) +class DateRangePickerFieldComponent extends FieldType {} + @Component({ selector: 'ish-repeat-test-field', template: 'RepeatFieldComponent: {{ field.key }} {{ field.type }} {{ to | json }}', @@ -180,6 +186,7 @@ class RepeatFieldComponent extends FieldArrayType {} { name: 'ish-radio-field', component: RadioFieldComponent }, { name: 'ish-captcha-field', component: CaptchaFieldComponent }, { name: 'ish-date-picker-field', component: DatePickerFieldComponent }, + { name: 'ish-date-range-picker-field', component: DateRangePickerFieldComponent }, { name: 'repeat', component: RepeatFieldComponent }, ], wrappers: [ diff --git a/src/app/shared/formly/extensions/translate-placeholder.extension.ts b/src/app/shared/formly/extensions/translate-placeholder.extension.ts index 743b624fd6..08a5cd003c 100644 --- a/src/app/shared/formly/extensions/translate-placeholder.extension.ts +++ b/src/app/shared/formly/extensions/translate-placeholder.extension.ts @@ -12,7 +12,7 @@ class TranslatePlaceholderExtension implements FormlyExtension { prePopulate(field: FormlyFieldConfig): void { const props = field.props; - if (!props?.placeholder) { + if (!props?.placeholder || field.type === 'ish-date-range-picker-field') { return; } diff --git a/src/app/shared/formly/types/date-picker-field/date-picker-field.component.ts b/src/app/shared/formly/types/date-picker-field/date-picker-field.component.ts index 6aa8af040a..5ccd613137 100644 --- a/src/app/shared/formly/types/date-picker-field/date-picker-field.component.ts +++ b/src/app/shared/formly/types/date-picker-field/date-picker-field.component.ts @@ -28,7 +28,7 @@ export class DatePickerFieldComponent extends FieldType { super(); } - addDaysToToday$(days$: Observable): Observable { + private addDaysToToday$(days$: Observable): Observable { return days$.pipe( map(daysLoc => typeof daysLoc === 'number' ? this.calendar.getNext(this.calendar.getToday(), 'd', daysLoc) : undefined diff --git a/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.html b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.html new file mode 100644 index 0000000000..4204ca04af --- /dev/null +++ b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.html @@ -0,0 +1,84 @@ +

+
+
+
+ + + + {{ date.day }} + + +
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
diff --git a/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.scss b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.scss new file mode 100644 index 0000000000..bd364d43e7 --- /dev/null +++ b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.scss @@ -0,0 +1,24 @@ +@import 'variables'; + +.custom-day { + display: inline-block; + width: 2rem; + height: 2rem; + padding: 0.185rem 0.25rem; + text-align: center; +} + +.custom-day.range, +.custom-day:hover { + color: $color-tertiary; + background-color: $CORPORATE-PRIMARY; +} + +.custom-day.faded { + background-color: $CORPORATE-LIGHT; +} + +.custom-day.disabled { + color: $color-tertiary; + background-color: transparent; +} diff --git a/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.spec.ts b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.spec.ts new file mode 100644 index 0000000000..47e88c3c3e --- /dev/null +++ b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.spec.ts @@ -0,0 +1,102 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { NgbCalendar, NgbInputDatepicker } from '@ng-bootstrap/ng-bootstrap'; +import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent, MockDirective } from 'ng-mocks'; + +import { FormlyTestingComponentsModule } from 'ish-shared/formly/dev/testing/formly-testing-components.module'; +import { FormlyTestingContainerComponent } from 'ish-shared/formly/dev/testing/formly-testing-container/formly-testing-container.component'; + +import { DateRangePickerFieldComponent } from './date-range-picker-field.component'; + +describe('Date Range Picker Field Component', () => { + let component: FormlyTestingContainerComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let calendar: NgbCalendar; + + const templateOptionsVal = { + minDays: -365 * 10, // 10 years ago + maxDays: 'a', + startDate: -5, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DateRangePickerFieldComponent, MockComponent(FaIconComponent), MockDirective(NgbInputDatepicker)], + imports: [ + FormlyModule.forRoot({ + types: [{ name: 'ish-date-range-picker-field', component: DateRangePickerFieldComponent }], + }), + FormlyTestingComponentsModule, + TranslateModule.forRoot(), + ], + }).compileComponents(); + + calendar = TestBed.inject(NgbCalendar); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FormlyTestingContainerComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + const setTestComponentInputs = (templateOptionsLoc = templateOptionsVal) => { + component.testComponentInputs = { + fields: [ + { + key: 'dateRange', + type: 'ish-date-range-picker-field', + props: templateOptionsLoc, + } as FormlyFieldConfig, + ], + form: new FormGroup({}), + model: { + dateRange: undefined, + }, + }; + }; + + it('should be created', () => { + setTestComponentInputs(); + + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should be rendered after creation', () => { + setTestComponentInputs(); + + fixture.detectChanges(); + expect(element.querySelector('[data-testing-id="date-range-picker"]')).toBeTruthy(); + }); + + it('should properly set start date 5 days from now', () => { + setTestComponentInputs(); + + fixture.detectChanges(); + const datePickerDirective = fixture.debugElement + .query(By.directive(NgbInputDatepicker)) + .injector.get(NgbInputDatepicker) as NgbInputDatepicker; + + const expectedStartDate = calendar.getPrev(calendar.getToday(), 'd', 5); + + expect(datePickerDirective.startDate).toEqual(expectedStartDate); + }); + + it('should not set max date because property is invalid', () => { + setTestComponentInputs(); + + fixture.detectChanges(); + const datePickerDirective = fixture.debugElement + .query(By.directive(NgbInputDatepicker)) + .injector.get(NgbInputDatepicker) as NgbInputDatepicker; + + expect(datePickerDirective.maxDate).toBeUndefined(); + }); +}); diff --git a/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.ts b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.ts new file mode 100644 index 0000000000..f07edc67ce --- /dev/null +++ b/src/app/shared/formly/types/date-range-picker-field/date-range-picker-field.component.ts @@ -0,0 +1,143 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + NgbCalendar, + NgbDate, + NgbDateAdapter, + NgbDateParserFormatter, + NgbDateStruct, +} from '@ng-bootstrap/ng-bootstrap'; +import { FieldType, FieldTypeConfig } from '@ngx-formly/core'; +import { Observable, isObservable, map, of } from 'rxjs'; + +function toObservableNumber(days: number | Observable) { + const days$ = isObservable(days) ? days : of(days); + return days$.pipe(map(daysLoc => (typeof daysLoc === 'number' ? daysLoc : undefined))); +} + +/** + * Form control for picking a date range. + * Uses NgbDatepicker with custom formatting and parsing. + * Refer to `fixed-format-adapter.ts` and `localized-parser-formatter.ts` for more information on date formatting. + * + * @props **minDays** - computes the minDate by adding the minimum allowed days to today. + * @props **maxDays** - computes the maxDate by adding the maximum allowed days to today. + * @props **startDate** - the start date. + * @props **placeholder** - placeholder that displays the date format in the input field. + * @props **inputClass** - class to apply to the input field + * + * @defaultWrappers 'form-field-horizontal', 'validation' + * + * + */ +@Component({ + selector: 'ish-date-range-picker-field', + templateUrl: './date-range-picker-field.component.html', + styleUrls: ['./date-range-picker-field.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DateRangePickerFieldComponent extends FieldType implements AfterViewInit { + hoveredDate: NgbDateStruct; + + private destroyRef = inject(DestroyRef); + + constructor( + private calendar: NgbCalendar, + public formatter: NgbDateParserFormatter, + private adapter: NgbDateAdapter, + private cdRef: ChangeDetectorRef + ) { + super(); + } + + ngAfterViewInit(): void { + this.formControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.cdRef.markForCheck()); + } + + get fromDate() { + return this.adapter.fromModel(this.formControl.getRawValue()?.fromDate); + } + + set fromDate(value: NgbDateStruct) { + const oldValue = this.formControl.getRawValue() || {}; + this.formControl.setValue({ ...oldValue, fromDate: this.adapter.toModel(value) }); + } + + get toDate() { + return this.adapter.fromModel(this.formControl.getRawValue()?.toDate); + } + + set toDate(value: NgbDateStruct) { + const oldValue = this.formControl.getRawValue() || {}; + this.formControl.setValue({ ...oldValue, toDate: this.adapter.toModel(value) }); + } + + onDateSelection(date: NgbDate) { + if (!this.fromDate) { + this.fromDate = date; + } else if (!this.toDate && date && date.equals(this.fromDate)) { + this.toDate = date; + } else if (this.fromDate && !this.toDate && date && date.after(this.fromDate)) { + this.toDate = date; + } else { + this.toDate = undefined; + this.fromDate = date; + } + } + + isHovered(date: NgbDate) { + return ( + this.fromDate && !this.toDate && this.hoveredDate && date.after(this.fromDate) && date.before(this.hoveredDate) + ); + } + + isInside(date: NgbDate) { + return this.toDate && date.after(this.fromDate) && date.before(this.toDate); + } + + isRange(date: NgbDate) { + return ( + date.equals(this.fromDate) || + (this.toDate && date.equals(this.toDate)) || + this.isInside(date) || + this.isHovered(date) + ); + } + + validateInput(currentValue: NgbDateStruct, input: string): NgbDateStruct { + if (input) { + const parsed = this.formatter.parse(input); + return parsed && this.calendar.isValid(NgbDate.from(parsed)) ? NgbDate.from(parsed) : currentValue; + } + } + + private addDaysToToday$(days$: Observable): Observable { + return days$.pipe( + map(daysLoc => + typeof daysLoc === 'number' ? this.calendar.getNext(this.calendar.getToday(), 'd', daysLoc) : undefined + ) + ); + } + + get minDate$(): Observable { + const minDays$ = toObservableNumber(this.props.minDays); + return this.addDaysToToday$(minDays$); + } + + get maxDate$(): Observable { + const maxDays$ = toObservableNumber(this.props.maxDays); + return this.addDaysToToday$(maxDays$); + } + + get startDate$(): Observable { + const startDate$ = toObservableNumber(this.props.startDate); + return this.addDaysToToday$(startDate$); + } +} diff --git a/src/app/shared/formly/types/types.module.ts b/src/app/shared/formly/types/types.module.ts index 6cfcf80e29..1e656ddfbd 100644 --- a/src/app/shared/formly/types/types.module.ts +++ b/src/app/shared/formly/types/types.module.ts @@ -22,6 +22,7 @@ import { CheckboxFieldComponent } from './checkbox-field/checkbox-field.componen import { DatePickerFieldComponent } from './date-picker-field/date-picker-field.component'; import { IshDatepickerI18n } from './date-picker-field/ish-datepicker-i18n'; import { LocalizedParserFormatter } from './date-picker-field/localized-parser-formatter'; +import { DateRangePickerFieldComponent } from './date-range-picker-field/date-range-picker-field.component'; import { FieldsetFieldComponent } from './fieldset-field/fieldset-field.component'; import { HtmlTextFieldComponent } from './html-text-field/html-text-field.component'; import { PlainTextFieldComponent } from './plain-text-field/plain-text-field.component'; @@ -34,6 +35,7 @@ const fieldComponents = [ CaptchaFieldComponent, CheckboxFieldComponent, DatePickerFieldComponent, + DateRangePickerFieldComponent, FieldsetFieldComponent, HtmlTextFieldComponent, PlainTextFieldComponent, @@ -156,6 +158,11 @@ const fieldComponents = [ component: DatePickerFieldComponent, wrappers: ['form-field-horizontal', 'validation'], }, + { + name: 'ish-date-range-picker-field', + component: DateRangePickerFieldComponent, + wrappers: ['form-field-horizontal', 'validation'], + }, ], }), ], diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index 05e40cf2d6..b994c4c942 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -156,6 +156,7 @@ "account.customer.registered.title": "Vielen Dank für Ihre Registrierung!", "account.date.month": "Monat", "account.date.month.error.required": "Bitte wählen Sie einen Monat aus.", + "account.date.title": "Wählen Sie ein Datum aus", "account.date.year": "Jahr", "account.date.year.error.required": "Bitte wählen Sie ein Jahr aus.", "account.default_address.state.label": "Bundesland", @@ -213,11 +214,21 @@ "account.notifications.table.notification": "Benachrichtigung", "account.notifications.table.product": "Produkt", "account.option.select.text": "Bitte auswählen", + "account.order.load_more": "Mehr laden", "account.order.most_recent.heading": "Letzte Bestellungen", "account.order.questions.note": "Besuchen Sie die Hilfe auf unserer Website für umfassende Bestell- und Versandinformationen oder kontaktieren Sie uns rund um die Uhr.", "account.order.questions.title": "Fragen?", "account.order.subtitle": "Die letzte Bestellung erscheint zuerst. Bitte gedulden Sie sich bis zu fünf Minuten, bis alle neueren Bestellungen darunter erscheinen.", "account.order.view_all_order.link": "Alle Bestellungen anzeigen", + "account.order_history.filter.apply": "Bestellung suchen", + "account.order_history.filter.clear": "Alle Filter entfernen", + "account.order_history.filter.hide": "Alle Filter ausblenden", + "account.order_history.filter.label.date": "Zeitraum", + "account.order_history.filter.label.date.aria_label": "Datum", + "account.order_history.filter.label.order_no": "Nach Bestellnr. suchen", + "account.order_history.filter.label.sku": "Produkt-ID", + "account.order_history.filter.no_results_found_message": "Ihre Suche ergab keine passenden Treffer. Bitte überprüfen Sie den Suchbegriff oder ändern Sie Ihre gewählten Suchkriterien.", + "account.order_history.filter.show": "Alle Filter anzeigen", "account.order_history.heading": "Bestellhistorie", "account.order_history.link": "Bestellhistorie", "account.order_template.add_to_template.button.add_to_template.label": "In Bestellvorlage aufnehmen", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 54f0295cd9..9bdd6e407f 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -156,6 +156,7 @@ "account.customer.registered.title": "Thanks for Registering!", "account.date.month": "Month", "account.date.month.error.required": "Please select a month.", + "account.date.title": "Pick a date", "account.date.year": "Year", "account.date.year.error.required": "Please select a year.", "account.default_address.state.label": "State/Province", @@ -213,11 +214,21 @@ "account.notifications.table.notification": "Notification", "account.notifications.table.product": "Product", "account.option.select.text": "Please select", + "account.order.load_more": "Load more", "account.order.most_recent.heading": "Most Recent Orders", "account.order.questions.note": "Please visit the Help area of our website for comprehensive order and shipping information or Contact Us 24 hours a day.", "account.order.questions.title": "Questions?", "account.order.subtitle": "The most recent order appears first. Please allow up to 5 minutes for new orders to appear below.", - "account.order.view_all_order.link": "View All Orders", + "account.order.view_all_order.link": "View all orders", + "account.order_history.filter.apply": "Find order", + "account.order_history.filter.clear": "Clear all filters", + "account.order_history.filter.hide": "Hide all filters", + "account.order_history.filter.label.date": "Time frame", + "account.order_history.filter.label.date.aria_label": "Date", + "account.order_history.filter.label.order_no": "Search for order no.", + "account.order_history.filter.label.sku": "Product ID", + "account.order_history.filter.no_results_found_message": "No matching results could be found for your search. Please check the search term or change your selected search criteria.", + "account.order_history.filter.show": "Show all filters", "account.order_history.heading": "Order History", "account.order_history.link": "Order History", "account.order_template.add_to_template.button.add_to_template.label": "Add to Order Template", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 35907cfd69..c80970e64b 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -156,6 +156,7 @@ "account.customer.registered.title": "Merci de vous inscrire!", "account.date.month": "Mois", "account.date.month.error.required": "Veuillez sélectionner un mois.", + "account.date.title": "Choisissez une date", "account.date.year": "Année", "account.date.year.error.required": "Veuillez sélectionner une année.", "account.default_address.state.label": "Région", @@ -213,11 +214,21 @@ "account.notifications.table.notification": "Notification", "account.notifications.table.product": "Produit", "account.option.select.text": "Veuillez sélectionner", + "account.order.load_more": "Charger plus", "account.order.most_recent.heading": "Les commandes les plus récentes", "account.order.questions.note": "Veuillez consulter la section Aide de notre site Web pour des renseignements détaillés sur votre commande et l’expédition ou Contactez-nous 24 heures sur 24.", "account.order.questions.title": "Avez-vous des questions supplémentaires ?", "account.order.subtitle": "La commande la plus récente apparaît en premier. Veuillez prévoir jusqu’à 5 minutes pour que les nouvelles commandes apparaissent ci-dessous.", "account.order.view_all_order.link": "Afficher toutes les commandes", + "account.order_history.filter.apply": "Trouver une commande", + "account.order_history.filter.clear": "Effacer tous les filtres", + "account.order_history.filter.hide": "Cacher tous les filtres", + "account.order_history.filter.label.date": "Période de temps", + "account.order_history.filter.label.date.aria_label": "Date", + "account.order_history.filter.label.order_no": "Rechercher un n° de commande", + "account.order_history.filter.label.sku": "ID de produit", + "account.order_history.filter.no_results_found_message": "Aucun résultat correspondant n’a pu être trouvé pour votre recherche. Veuillez vérifier le terme de votre recherche ou changer vos critères de recherche.", + "account.order_history.filter.show": "Afficher tous les filtres", "account.order_history.heading": "Historique de commandes", "account.order_history.link": "Historique de commandes", "account.order_template.add_to_template.button.add_to_template.label": "Ajouter au modèle de commande",