Browse Source

Merge pull request #20804 from abpframework/issue-datatableLoadingIndicator

Angular - Datatable Loading Indicator Updates
pull/20976/head
oykuermann 2 years ago
committed by GitHub
parent
commit
96584981f7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  2. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  3. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  4. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  5. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  6. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json
  7. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  8. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  9. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  10. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  11. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  12. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  13. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  14. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  15. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  16. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  17. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  18. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  19. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  20. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json
  21. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json
  22. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json
  23. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json
  24. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json
  25. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json
  26. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  27. 29
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  28. 36
      npm/ng-packs/packages/core/src/lib/services/list.service.ts
  29. 32
      npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts
  30. 3
      npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
  31. 14
      npm/ng-packs/packages/theme-shared/src/lib/components/spinner/spinner.component.ts
  32. 10
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts
  33. 95
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts
  34. 2
      npm/ng-packs/packages/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts
  35. 407
      templates/app/angular/src/app/home/home.component.html

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "عرض 0 الي 0 من 0 إدخالات",
"PagerInfoFiltered": "(تمت تصفيته من _MAX_ مجموع الإدخالات)",
"NoDataAvailableInDatatable": "لا توجد بيانات متاحة في الجدول",
"ErrorLoadingDatatable": "حدث خطأ أثناء الطلب. تحقق من الرسالة للتفاصيل.",
"Total": "مجموع",
"Selected": "مختارة",
"PagerShowMenuEntries": "عرض _MENU_ إدخالات",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json

@ -39,6 +39,7 @@
"PagerInfoFiltered": "(filtrováno ze všech _MAX_ záznamů)",
"NoDataAvailableInDatatable": "Nejsou žádná data",
"Total": "celkem",
"ErrorLoadingDatatable": "Došlo k chybě během požadavku. Zkontrolujte zprávu pro podrobnosti.",
"Selected": "vybráno",
"PagerShowMenuEntries": "Zobrazit _MENU_ záznamů",
"DatatableActionDropdownDefaultText": "Akce",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "0 bis 0 von 0 Einträgen anzeigen",
"PagerInfoFiltered": "(gefiltert aus _MAX_ Einträgen)",
"NoDataAvailableInDatatable": "Keine Daten verfügbar",
"ErrorLoadingDatatable": "Ein Fehler ist während der Anfrage aufgetreten. Überprüfen Sie die Nachricht auf Details.",
"Total": "gesamt",
"Selected": "ausgewählt",
"PagerShowMenuEntries": "_MENU_ Einträge anzeigen",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Εμφάνιση 0 έως 0 από 0 εγγραφές",
"PagerInfoFiltered": "(φιλτραρισμένο από _MAX_ συνολικές εγγραφές)",
"NoDataAvailableInDatatable": "Δεν υπάρχουν διαθέσιμα δεδομένα",
"ErrorLoadingDatatable": "Παρουσιάστηκε σφάλμα κατά τη διάρκεια του αιτήματος. Ελέγξτε το μήνυμα για λεπτομέρειες.",
"Total": "Σύνολο",
"Selected": "Έχει γίνει επιλογή",
"PagerShowMenuEntries": "Εμφάνιση στοιχείων _MENU_",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Showing 0 to 0 of 0 entries",
"PagerInfoFiltered": "(filtered from _MAX_ total entries)",
"NoDataAvailableInDatatable": "No data available",
"ErrorLoadingDatatable": "An error occurred during the request. Check the message for details.",
"Total": "total",
"Selected": "selected",
"PagerShowMenuEntries": "Show _MENU_ entries",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Showing 0 to 0 of 0 entries",
"PagerInfoFiltered": "(filtered from _MAX_ total entries)",
"NoDataAvailableInDatatable": "No data available",
"ErrorLoadingDatatable": "An error occurred during the request. Check the message for details.",
"Total": "total",
"Selected": "selected",
"PagerShowMenuEntries": "Show _MENU_ entries",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Mostrando de 0 a 0 de 0 entradas",
"PagerInfoFiltered": "(filtrado desde _MAX_ entradas totales)",
"NoDataAvailableInDatatable": "No hay datos disponibles",
"ErrorLoadingDatatable": "Se produjo un error durante la solicitud. Consulte el mensaje para más detalles.",
"Total": "Total",
"Selected": "Seleccionado",
"PagerShowMenuEntries": "Mostrar _MENU_ registros",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "نمایش از 0 تا 0 از مجموع 0 مورد",
"PagerInfoFiltered": "(فیلتر شده از مجموع _MAX_ مورد)",
"NoDataAvailableInDatatable": "داده ای وجود ندارد",
"ErrorLoadingDatatable": "در طول درخواست خطایی رخ داد. برای جزئیات پیام را بررسی کنید.",
"Total": "مجموع",
"Selected": "انتخاب شده",
"PagerShowMenuEntries": "نمایش _MENU_ مورد",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Näytetään 0 - 0/0 tietueesta",
"PagerInfoFiltered": "(suodatettu _MAX_ tietueen kokonaismäärästä)",
"NoDataAvailableInDatatable": "Tietoja ei löydy",
"ErrorLoadingDatatable": "Pyynnön aikana tapahtui virhe. Tarkista viesti yksityiskohtia varten.",
"Total": "kaikki yhteensä",
"Selected": "valittu",
"PagerShowMenuEntries": "Näytä _MENU_ tietueet",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Affichage de 0 à 0 sur 0 entrées",
"PagerInfoFiltered": "(filtré à partir des entrées _MAX_ totale)",
"NoDataAvailableInDatatable": "Aucune donnée disponible",
"ErrorLoadingDatatable": "Une erreur est survenue lors de la requête. Vérifiez le message pour plus de détails.",
"Total": "Total",
"Selected": "Sélectionné",
"PagerShowMenuEntries": "Afficher les entrées _MENU_",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "0 से 0 प्रविष्टियों को दिखा रहा है",
"PagerInfoFiltered": "(_MAX_ कुल प्रविष्टियों से फ़िल्टर किया गया)",
"NoDataAvailableInDatatable": "कोई डेटा उपलब्ध नहीं है",
"ErrorLoadingDatatable": "अनुरोध के दौरान एक त्रुटि हुई। विवरण के लिए संदेश देखें।",
"Total": "संपूर्ण",
"Selected": "चयनित",
"PagerShowMenuEntries": "_MENU_ प्रविष्टियाँ दिखाएँ",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Prikazano 0 do 0 od 0 zapisa",
"PagerInfoFiltered": "(filtrirano od _MAX_ ukupnih zapisa)",
"NoDataAvailableInDatatable": "Nema dostupnih podataka",
"ErrorLoadingDatatable": "Došlo je do pogreške tijekom zahtjeva. Provjerite poruku za detalje.",
"Total": "ukupno",
"Selected": "odabrano",
"PagerShowMenuEntries": "Pokaži _MENU_ zapise",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Látható 0-tól 0-ig 0 elemből",
"PagerInfoFiltered": "(szűrve _MAX_ elemből)",
"NoDataAvailableInDatatable": "Nem elérhető adat",
"ErrorLoadingDatatable": "Hiba történt a kérés során. Ellenőrizze az üzenetet a részletekért.",
"Total": "összesen",
"Selected": "kiválasztott",
"PagerShowMenuEntries": "Megjelenítve _MENU_ elem",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Sýnir 0 til 0 af 0 færslum",
"PagerInfoFiltered": "(síað úr _MAX_ heildar færslum)",
"NoDataAvailableInDatatable": "Engar upplýsingar fáanlegar",
"ErrorLoadingDatatable": "Villa kom upp við beiðni. Athugaðu skilaboðin til að fá frekari upplýsingar.",
"Total": "Samtals",
"Selected": "valið",
"PagerShowMenuEntries": "Sýna færslur _MENU_",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Risultati da 0 a 0 di 0 elementi",
"PagerInfoFiltered": "(filtrati da _MAX_ elementi totali)",
"NoDataAvailableInDatatable": "Nessun dato disponibile",
"ErrorLoadingDatatable": "Si è verificato un errore durante la richiesta. Controlla il messaggio per i dettagli.",
"Total": "Totale",
"Selected": "Selezionato",
"PagerShowMenuEntries": "Righe per pagina _MENU_",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Toont 0 tot 0 van 0 vermeldingen",
"PagerInfoFiltered": "(gefilterd uit in totaal _MAX_ vermeldingen)",
"NoDataAvailableInDatatable": "Geen gegevens beschikbaar",
"ErrorLoadingDatatable": "Er is een fout opgetreden tijdens het verzoek. Controleer het bericht voor details.",
"Total": "totaal",
"Selected": "geselecteed",
"PagerShowMenuEntries": "Toon _MENU_-vermeldingen",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Pokaż od 0 do 0 z 0 rekordów",
"PagerInfoFiltered": "(filtruj od _MAX_ wszystkich rekordów)",
"NoDataAvailableInDatatable": "Nie znaleziono danych w tabeli",
"ErrorLoadingDatatable": "Wystąpił błąd podczas żądania. Sprawdź wiadomość, aby uzyskać szczegóły.",
"Total": "całkowity",
"Selected": "wybrany",
"PagerShowMenuEntries": "Pokaż _MENU_ rekordów",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Mostrando 0 até 0 de 0 registros",
"PagerInfoFiltered": "(filtrado de um total de _MAX_ registros)",
"NoDataAvailableInDatatable": "Nenhum dado disponível",
"ErrorLoadingDatatable": "Ocorreu um erro durante a solicitação. Verifique a mensagem para mais detalhes.",
"Total": "total",
"Selected": "selecionado",
"PagerShowMenuEntries": "Mostrando _MENU_ registros",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Afişate 0 din 0 înregistrări",
"PagerInfoFiltered": "(filtrate de la _MAX_ înregistrări totale)",
"NoDataAvailableInDatatable": "Nicio informaţie disponibilă",
"ErrorLoadingDatatable": "A apărut o eroare în timpul solicitării. Verificați mesajul pentru detalii.",
"Total": "total",
"Selected": "selectate",
"PagerShowMenuEntries": "Afişează _MENU_ intrări",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Записи с 0 до 0 из 0 записей.",
"PagerInfoFiltered": "(отфильтровано из _MAX_ записей)",
"NoDataAvailableInDatatable": "Данные в таблице отсутствуют",
"ErrorLoadingDatatable": "Произошла ошибка во время запроса. Проверьте сообщение для получения деталей.",
"Total": "общий",
"Selected": "выбранный",
"PagerShowMenuEntries": "Показать _MENU_ записей",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Zobrazených 0 až 0 z 0 záznamov",
"PagerInfoFiltered": "(filtrované z celkom _MAX_ záznamov)",
"NoDataAvailableInDatatable": "Nie sú k dispozícii žiadne údaje",
"ErrorLoadingDatatable": "Počas žiadosti došlo k chybe. Skontrolujte správu pre podrobnosti.",
"Total": "celkom",
"Selected": "vybrané",
"PagerShowMenuEntries": "Zobraziť _MENU_ záznamov",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Prikazanih 0 do 0 od 0 zapisov",
"PagerInfoFiltered": "(filtrirano od vseh _MAX_ zapisov)",
"NoDataAvailableInDatatable": "V tabeli ni na voljo podatkov",
"ErrorLoadingDatatable": "Pri zahtevi je prišlo do napake. Preverite sporočilo za podrobnosti.",
"Total": "skupaj",
"Selected": "izbrani",
"PagerShowMenuEntries": "Prikaži _MENU_ zapise",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "0 kayıttan 0 ile 0 arası gösteriliyor.",
"PagerInfoFiltered": "(_MAX_ kayıt arasından filtrelendi)",
"NoDataAvailableInDatatable": "Tabloda kayıt mevcut değil.",
"ErrorLoadingDatatable": "İstek sırasında bir hata oluştu. Detaylar için mesajı kontrol edin.",
"Total": "toplam",
"Selected": "seçilen",
"PagerShowMenuEntries": "Sayfada _MENU_ kayıt göster.",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "Hiển thị từ 0 đến 0 trong 0 mục",
"PagerInfoFiltered": "(được lọc từ tổng số _MAX_ mục)",
"NoDataAvailableInDatatable": "Không có dữ liệu trong bảng",
"ErrorLoadingDatatable": "Đã xảy ra lỗi trong quá trình yêu cầu. Kiểm tra tin nhắn để biết chi tiết.",
"Total": "toàn bộ",
"Selected": "đã chọn",
"PagerShowMenuEntries": "Hiển thị _MENU_ mục",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "显示0个条目中的0到0",
"PagerInfoFiltered": "(从 _MAX_ 总条目中过滤掉)",
"NoDataAvailableInDatatable": "表中没有数据",
"ErrorLoadingDatatable": " 请求过程中发生错误。检查消息以获取详细信息",
"Total": "总计",
"Selected": "已选",
"PagerShowMenuEntries": "显示 _MENU_ 条数据",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json

@ -38,6 +38,7 @@
"PagerInfoEmpty": "顯示0個紀錄中的0到0",
"PagerInfoFiltered": "(從 _MAX_ 所有紀錄中過濾掉)",
"NoDataAvailableInDatatable": "資料表中沒有資料",
"ErrorLoadingDatatable": "請求過程中發生錯誤。檢查消息以獲取詳細信息。",
"Total": "總共",
"Selected": "已選擇",
"PagerShowMenuEntries": "顯示 _MENU_ 實體",

29
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -1,5 +1,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
inject,
@ -14,8 +16,7 @@ import {
} from '@angular/core';
import { AsyncPipe, formatDate, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable, filter, map } from 'rxjs';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
@ -71,7 +72,15 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
templateUrl: './extensible-table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExtensibleTableComponent<R = any> implements OnChanges {
export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewInit {
readonly #injector = inject(Injector);
readonly getInjected = this.#injector.get.bind(this.#injector);
protected readonly cdr = inject(ChangeDetectorRef);
protected readonly locale = inject(LOCALE_ID);
protected readonly config = inject(ConfigStateService);
protected readonly entityPropTypeClasses = inject(ENTITY_PROP_TYPE_CLASSES);
protected readonly permissionService = inject(PermissionService);
protected _actionsText!: string;
@Input()
set actionsText(value: string) {
@ -104,13 +113,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges {
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name;
locale = inject(LOCALE_ID);
private config = inject(ConfigStateService);
entityPropTypeClasses = inject(ENTITY_PROP_TYPE_CLASSES);
#injector = inject(Injector);
getInjected = this.#injector.get.bind(this.#injector);
permissionService = this.#injector.get(PermissionService);
constructor() {
const extensions = this.#injector.get(ExtensionsService);
const name = this.#injector.get(EXTENSIONS_IDENTIFIER);
@ -227,4 +229,11 @@ export class ExtensibleTableComponent<R = any> implements OnChanges {
return visibleActions.length > 0;
}
ngAfterViewInit(): void {
this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => {
this.data = [];
this.cdr.markForCheck();
});
}
}

36
npm/ng-packs/packages/core/src/lib/services/list.service.ts

@ -1,9 +1,9 @@
import { Injectable, Injector, OnDestroy } from '@angular/core';
import {
EMPTY,
BehaviorSubject,
MonoTypeOperatorFunction,
Observable,
of,
ReplaySubject,
Subject,
} from 'rxjs';
@ -11,6 +11,7 @@ import {
catchError,
debounceTime,
filter,
finalize,
shareReplay,
switchMap,
takeUntil,
@ -20,6 +21,8 @@ import { ABP } from '../models/common';
import { PagedResultDto } from '../models/dtos';
import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens/list.token';
export type RequestStatus = 'idle' | 'loading' | 'success' | 'error';
@Injectable()
export class ListService<QueryParamsType = ABP.PageQueryParams | any> implements OnDestroy {
private _filter = '';
@ -40,7 +43,6 @@ export class ListService<QueryParamsType = ABP.PageQueryParams | any> implements
return this._maxResultCount;
}
private _skipCount = 0;
private _page = 0;
set page(value: number) {
if (value === this._page) return;
@ -91,12 +93,21 @@ export class ListService<QueryParamsType = ABP.PageQueryParams | any> implements
private _isLoading$ = new BehaviorSubject(false);
private _requestStatus = new BehaviorSubject<RequestStatus>('idle');
private destroy$ = new Subject<void>();
private delay: MonoTypeOperatorFunction<QueryParamsType>;
/**
* @deprecated Use `requestStatus$` instead.
*/
get isLoading$(): Observable<boolean> {
return this._isLoading$.asObservable();
return this._isLoading$.asObservable().pipe(takeUntil(this.destroy$));
}
get requestStatus$(): Observable<RequestStatus> {
return this._requestStatus.asObservable().pipe(takeUntil(this.destroy$));
}
get = () => {
@ -119,9 +130,21 @@ export class ListService<QueryParamsType = ABP.PageQueryParams | any> implements
): Observable<PagedResultDto<T>> {
return this.query$.pipe(
tap(() => this._isLoading$.next(true)),
switchMap(query => streamCreatorCallback(query).pipe(catchError(() => of(null)))),
tap(() => this._requestStatus.next('loading')),
switchMap(query =>
streamCreatorCallback(query).pipe(
catchError(() => {
this._requestStatus.next('error');
return EMPTY;
}),
tap(() => this._requestStatus.next('success')),
finalize(() => {
this._isLoading$.next(false);
this._requestStatus.next('idle');
}),
),
),
filter(Boolean),
tap(() => this._isLoading$.next(false)),
shareReplay<any>({ bufferSize: 1, refCount: true }),
takeUntil(this.destroy$),
);
@ -130,18 +153,15 @@ export class ListService<QueryParamsType = ABP.PageQueryParams | any> implements
ngOnDestroy() {
this.destroy$.next();
}
private resetPageWhenUnchanged() {
const maxPage = Number(Number(this.totalCount / this._maxResultCount).toFixed());
const skipCount = this._page * this._maxResultCount;
if (skipCount !== this._totalCount) {
this._skipCount = skipCount;
return;
}
if (this.page === maxPage && this.page > 0) {
this._skipCount = skipCount - this._maxResultCount;
this.page = this.page - 1;
}
}

32
npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts

@ -149,5 +149,37 @@ describe('ListService', () => {
service.hookToQuery(callback).subscribe();
});
it('should emit requestStatus as side effect', done => {
const callback: QueryStreamCreatorCallback<ABP.PageQueryParams> = query =>
of({ items: [query], totalCount: 1 });
service.requestStatus$.pipe(bufferCount(3)).subscribe(([idle, init, end]) => {
expect(idle).toBe('idle');
expect(init).toBe('loading');
expect(end).toBe('success');
done();
});
service.hookToQuery(callback).subscribe();
});
it('should emit error requestStatus as side effect and stop processing', done => {
const errCallback: QueryStreamCreatorCallback<ABP.PageQueryParams> = query => {
throw Error('A server error occurred');
};
service.requestStatus$.pipe(bufferCount(3)).subscribe(([idle, loading, error]) => {
expect(idle).toBe('idle');
expect(loading).toBe('loading');
expect(error).toBe('error');
done();
});
service.hookToQuery(errCallback).subscribe({
error: () => done(),
});
});
});
});

3
npm/ng-packs/packages/theme-shared/src/lib/components/index.ts

@ -14,4 +14,5 @@ export * from './password/password.component';
export * from './card/index';
export * from './checkbox/checkbox.component';
export * from './form-input/form-input.component';
export * from './internet-connection-status/internet-connection-status.component'
export * from './internet-connection-status/internet-connection-status.component';
export * from './spinner/spinner.component';

14
npm/ng-packs/packages/theme-shared/src/lib/components/spinner/spinner.component.ts

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
@Component({
selector: 'abp-spinner',
standalone: true,
template: `
<div class="d-flex justify-content-center align-items-center border-top" style="height: 62px">
<div class="spinner-border" role="status" id="loading">
<span class="visually-hidden">Loading...</span>
</div>
</div>
`,
})
export class SpinnerComponent {}

10
npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts

@ -7,12 +7,11 @@ import { debounceTime } from 'rxjs/operators';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'ngx-datatable[default]',
standalone:true,
standalone: true,
exportAs: 'ngxDatatableDefault',
})
export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private subscription = new Subscription();
private resizeDiff = 0;
@Input() class = 'material bordered';
@ -22,7 +21,10 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
return `ngx-datatable ${this.class}`;
}
constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: MockDocument) {
constructor(
private table: DatatableComponent,
@Inject(DOCUMENT) private document: MockDocument,
) {
this.table.columnMode = ColumnMode.force;
this.table.footerHeight = 50;
this.table.headerHeight = 50;

95
npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts

@ -7,15 +7,19 @@ import {
DoCheck,
SimpleChanges,
inject,
DestroyRef
DestroyRef,
ViewContainerRef,
Renderer2,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { distinctUntilChanged } from 'rxjs';
import { DatatableComponent } from '@swimlane/ngx-datatable';
import { ListService, LocalizationService } from '@abp/ng.core';
import {
defaultNgxDatatableMessages,
NGX_DATATABLE_MESSAGES,
} from '../tokens/ngx-datatable-messages.token';
import { SpinnerComponent } from '../components';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
@ -24,13 +28,15 @@ import {
exportAs: 'ngxDatatableList',
})
export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
@Input() list!: ListService;
protected readonly table = inject(DatatableComponent);
protected readonly cdRef = inject(ChangeDetectorRef);
protected readonly destroyRef = inject(DestroyRef);
protected readonly localizationService = inject(LocalizationService);
protected readonly ngxDatatableMessages = inject(NGX_DATATABLE_MESSAGES, { optional: true });
protected readonly viewContainerRef = inject(ViewContainerRef);
protected readonly renderer = inject(Renderer2);
@Input() list!: ListService;
constructor() {
this.setInitialValues();
@ -43,6 +49,7 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
ngOnInit() {
this.subscribeToPage();
this.subscribeToSort();
this.subscribeToRequestStatus();
}
ngOnChanges({ list }: SimpleChanges) {
@ -55,32 +62,88 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
this.table.offset = page;
}
protected subscribeToRequestStatus() {
const requestStatus$ = this.list.requestStatus$.pipe(distinctUntilChanged());
const { emptyMessage, errorMessage } = this.ngxDatatableMessages || defaultNgxDatatableMessages;
requestStatus$.subscribe(status => {
this.table.loadingIndicator = false;
if (status === 'idle') {
return;
}
if (status === 'loading') {
this.table.messages.emptyMessage = undefined;
this.table.loadingIndicator = true;
this.cdRef.detectChanges();
this.updateLoadingIndicator();
return;
}
if (status === 'error') {
this.table.messages.emptyMessage = this.localizationService.instant(errorMessage);
this.viewContainerRef.clear();
this.cdRef.markForCheck();
}
if (status === 'success') {
this.table.messages.emptyMessage = this.localizationService.instant(emptyMessage);
this.viewContainerRef.clear();
}
});
}
protected updateLoadingIndicator() {
const body = this.table.element.querySelector('datatable-body');
const progress = this.table.element.querySelector('datatable-progress');
if (!body) {
return;
}
if (progress) {
this.replaceLoadingIndicator(body, progress);
}
}
protected replaceLoadingIndicator(parent: Element, placeholder: Element) {
this.viewContainerRef.clear();
const spinnerRef = this.viewContainerRef.createComponent(SpinnerComponent);
const spinnerElement = spinnerRef.location.nativeElement;
this.renderer.insertBefore(parent, spinnerElement, placeholder);
this.renderer.removeChild(parent, placeholder);
}
protected setInitialValues() {
this.table.externalPaging = true;
this.table.externalSorting = true;
const { emptyMessage, selectedMessage, totalMessage } =
const { selectedMessage, totalMessage } =
this.ngxDatatableMessages || defaultNgxDatatableMessages;
this.table.messages = {
emptyMessage: this.localizationService.instant(emptyMessage),
totalMessage: this.localizationService.instant(totalMessage),
selectedMessage: this.localizationService.instant(selectedMessage),
};
}
protected subscribeToSort() {
this.table.sort.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(({ sorts: [{ prop, dir }] }) => {
if (prop === this.list.sortKey && this.list.sortOrder === 'desc') {
this.list.sortKey = '';
this.list.sortOrder = '';
this.table.sorts = [];
this.cdRef.detectChanges();
} else {
this.list.sortKey = prop;
this.list.sortOrder = dir;
}
});
this.table.sort
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ sorts: [{ prop, dir }] }) => {
if (prop === this.list.sortKey && this.list.sortOrder === 'desc') {
this.list.sortKey = '';
this.list.sortOrder = '';
this.table.sorts = [];
this.cdRef.detectChanges();
} else {
this.list.sortKey = prop;
this.list.sortOrder = dir;
}
});
}
protected subscribeToPage() {

2
npm/ng-packs/packages/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts

@ -2,12 +2,14 @@ import { InjectionToken } from '@angular/core';
export interface NgxDatatableMessages {
emptyMessage: string;
errorMessage: string;
totalMessage: string;
selectedMessage: string;
}
export const defaultNgxDatatableMessages = {
emptyMessage: 'AbpUi::NoDataAvailableInDatatable',
errorMessage: 'AbpUi::ErrorLoadingDatatable',
totalMessage: 'AbpUi::Total',
selectedMessage: 'AbpUi::Selected',
};

407
templates/app/angular/src/app/home/home.component.html

@ -1,120 +1,309 @@
<div class="my-3 text-center">
<h3>Let's improve your application!</h3>
<p>Here are some links to help you get started:</p>
</div>
<div class="card mt-4 mb-5">
<div class="card-body">
<div class="row text-center justify-content-md-center">
<ng-container
*ngTemplateOutlet="
starterLinkTemplate;
context: {
$implicit: {
title: 'Learn the ABP Framework',
description:
'Explore the comprehensive documentation to learn how to build a modern web application.',
links: [
{
href: 'https://abp.io/docs/latest?ref=tmpl',
label: 'See Documents'
}
]
}
}
"
></ng-container>
<div class="row mb-3">
<div class="col-xl-6 col-12 d-flex">
<div class="card h-lg-100 w-100 overflow-hidden">
<div class="card-body">
<div class="starting-content">
@if(!hasLoggedIn){
<h3>Welcome</h3>
<p class="lead text-muted mb-2">
{{ '::LongWelcomeMessage' | abpLocalization }}
</p>
<a (click)="login()" class="px-4 btn btn-primary mb-4" role="button"
><i class="fa fa-sign-in"></i> {{ 'AbpAccount::Login' | abpLocalization }}</a
>
}
<ng-container
*ngTemplateOutlet="
starterLinkTemplate;
context: {
$implicit: {
title: 'Samples',
description: 'See the example projects built with the ABP Framework.',
links: [
{
href: 'https://abp.io/docs/latest/samples?ref=tmpl',
label: 'All samples'
}
]
<h3>Getting Started</h3>
<ng-container
*ngTemplateOutlet="
startingContentTemplate;
context: {
$implicit: {
description:
'Learn how to create and run a new web application using the application startup template.',
href: 'https://docs.abp.io/en/commercial/latest/getting-started',
label: 'Getting Started'
}
}
}
"
></ng-container>
"
></ng-container>
<ng-container
*ngTemplateOutlet="
starterLinkTemplate;
context: {
$implicit: {
title: 'ABP Community',
description: 'Get involved with a vibrant community and become a contributor.',
links: [
{
href: 'https://abp.io/community/',
label: 'Community'
},
{
href: 'https://abp.io/docs/latest/contribution?ref=tmpl',
label: 'Contribute'
}
]
<h4>Web Application Development Tutorial</h4>
<ng-container
*ngTemplateOutlet="
startingContentTemplate;
context: {
$implicit: {
description:
'Learn how to build an ABP based web application named Acme.BookStore.',
href: 'https://docs.abp.io/en/commercial/latest/tutorials/book-store/part-1?UI=MVC&DB=EF',
label: 'Explore Tutorial'
}
}
"
></ng-container>
<h4>Customize Lepton Theme</h4>
<p class="text-muted mb-2">Learn how to customize LeptonX Theme as you wish.</p>
<a
href="https://docs.abp.io/en/commercial/latest/themes/lepton/customizing-lepton-theme"
class="btn btn-primary soft mb-5 mb-xl-0"
target="_blank"
>Customize Lepton</a
>
</div>
<img class="card-bg-image" src="assets/images/getting-started/bg-01.png" />
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 d-flex">
<div class="row">
<ng-container
*ngTemplateOutlet="
visitLinkTemplate;
context: {
$implicit: {
label: 'Visit Support',
description:
'You can check for similar problems and solutions, or open a new topic to discuss your specific issue.',
href: 'https://support.abp.io/QA/Questions',
card: 'card overflow-hidden',
imgClass: '',
imgSrc: 'assets/images/getting-started/img-support.png',
logo: 'abp-support abp-logo mb-2'
}
}
"
></ng-container>
<ng-container
*ngTemplateOutlet="
visitLinkTemplate;
context: {
$implicit: {
label: 'Visit Blog',
description:
'You can find content on .NET development, cross-platform, ASP.NET application templates, ABP-related news, and more.',
href: 'https://blog.abp.io/abp',
card: 'card h-md-100 overflow-hidden',
imgClass: 'margin-bottom: -24px',
imgSrc: 'assets/images/getting-started/img-blog.png',
logo: 'abp-blog abp-logo mb-2'
}
}
"
></ng-container>
</div>
</div>
<div class="col-xl-3 col-md-6 d-flex">
<div class="card h-100 overflow-hidden">
<div class="card-body">
<div class="abp-community abp-logo mb-2"></div>
<p class="lead text-muted">
A unique community platform for
<span class="fw-bold">ABP Lovers!</span>
</p>
<p class="text-muted mb-2">
Explore all ABP users' experiences with the ABP Framework, discover articles and videos on
how to use ABP, and join raffles for a chance to win surprise gifts!
</p>
<a class="btn btn-brand soft mb-3" href="https://community.abp.io/" target="_blank"
>Join ABP Community</a
>
</div>
<img class="mt-3" src="assets/images/getting-started/img-community.png" />
</div>
</div>
</div>
<div class="row">
<ng-container
*ngTemplateOutlet="
tableTemplate;
context: {
$implicit: {
title: 'More from ABP.IO',
items: [
{
iconType: 'icon',
style: true,
icon: 'bi bi-check fs-4',
iconClass: 'rounded-circle me-2 flex-center bg-brand soft',
title: 'Latest Release Logs',
link: 'https://github.com/abpframework/abp/releases',
},
{
iconType: 'icon',
style: true,
icon: 'bi bi-check fs-4',
iconClass: 'rounded-circle me-2 flex-center bg-brand soft',
title: 'Video Courses',
link: 'https://abp.io/video-courses/essentials',
},
{
iconType: 'icon',
style: true,
icon: 'bi bi-check fs-4',
iconClass: 'rounded-circle me-2 flex-center bg-brand soft',
title: 'Samples',
link: 'https://docs.abp.io/en/abp/latest/Samples/Index',
},
{
iconType: 'icon',
style: true,
icon: 'bi bi-check fs-4',
iconClass: 'rounded-circle me-2 flex-center bg-brand soft',
title: 'Books',
link: 'https://abp.io/books',
},
{
iconType: 'icon',
style: true,
icon: 'bi bi-check fs-4',
iconClass: 'rounded-circle me-2 flex-center bg-brand soft',
title: 'FAQ',
link: 'https://commercial.abp.io/faq',
},
]
}
}
"
></ng-container>
<div class="col-xl-6 col-lg-8 d-flex">
<div class="card">
<div class="card-body d-flex">
<div class="row align-items-center">
<div class="col-md-8">
<p class="text-muted mb-0">THE OFFICIAL GUIDE</p>
<h3>Mastering ABP Framework</h3>
<p class="text-muted">
Written by the creator of the ABP Framework, this book will help you gain a complete
understanding of the framework and modern web application development techniques.
</p>
<div class="d-md-flex mb-2 mb-md-0">
<a
href="https://www.amazon.com/gp/product/B097Z2DM8Q"
target="_blank"
class="btn btn-primary soft me-md-2 mb-2 mb-md-0"
>
Buy on Amazon US
</a>
<a
href="https://www.packtpub.com/product/mastering-abp-framework/9781801079242"
target="_blank"
class="btn btn-primary soft mb-2 mb-md-0"
>
Buy on PACKT
</a>
</div>
</div>
<div class="col">
<img src="assets/images/getting-started/book.png" />
</div>
</div>
</div>
</div>
</div>
<ng-container
*ngTemplateOutlet="
tableTemplate;
context: {
$implicit: {
title: 'Follow us on Social Media',
items: [
{
title: 'X.com',
icon: 'logo-x me-2',
link: 'https://twitter.com/abpframework'
},
{
iconType: 'image',
icon: 'assets/images/getting-started/discord.svg',
title: 'Discord',
link: 'https://community.abp.io/discord'
},
{
iconType: 'image',
icon: 'assets/images/getting-started/stack-overflow.svg',
title: 'Stack Overflow',
link: 'https://stackoverflow.com/questions/tagged/abp'
},
{
iconType: 'image',
icon: 'assets/images/getting-started/youtube.svg',
title: 'YouTube',
link: 'https://www.youtube.com/@Volosoft'
},
{
iconType: 'image',
icon: 'assets/images/getting-started/instagram.svg',
title: 'Instagram',
link: 'https://www.instagram.com/abpframework/'
}
"
></ng-container>
]
}
}
"
></ng-container>
</div>
<ng-template #startingContentTemplate let-context>
<p class="lead text-muted mb-2" [innerHTML]="context.description"></p>
<a [href]="context.href" class="btn btn-brand mb-4" target="_blank">{{ context.label }}</a>
</ng-template>
<ng-template #visitLinkTemplate let-context>
<div class="col-12 d-flex">
<div [class]="context.card">
<div class="card-body pb-0">
<div [class]="context.logo"></div>
<p class="text-muted mb-2" [innerHTML]="context.description"></p>
<a [href]="context.href" class="btn btn-brand soft" target="_blank">{{ context.label }}</a>
<img [class]="context.imgClass" [src]="context.imgSrc" />
</div>
</div>
</div>
</ng-template>
<ng-template #tableTemplate let-context>
<div class="col-xl-3 col-lg-4">
<div class="card">
<div class="card-header">
<div class="card-title text-body">{{ context.title }}</div>
</div>
<div class="row text-center mt-lg-3 justify-content-md-center">
<ng-container
*ngTemplateOutlet="
starterLinkTemplate;
context: {
$implicit: {
title: 'ABP Blog',
description: 'Take a look at our recently published articles.',
links: [
{
href: 'https://abp.io/blog?ref=tmpl',
label: 'See Blog'
<div class="card-body pt-0">
<div class="table-responsive">
<table class="table mb-0">
<tbody>
@for(item of context.items; track $index; let last = $last){
<tr>
<td class="ps-0 d-flex" [class.border-bottom-0]="last">
@if(item.icon){
<div
[ngClass]="item.iconClass"
[style]="item.style ? 'width: 20px; height: 20px' : ''"
>
@if(item.iconType === 'image'){
<img [src]="item.icon" class="me-2" />
}@else if(item.iconType === 'icon'){
<i [class]="item.icon"></i>
}@else{
<div [class]="item.icon"></div>
}
</div>
}
]
<div>{{ item.title }}</div>
</td>
<td class="text-end pe-0 opacity-25" [class.border-bottom-0]="last">
<a [href]="item.link" target="_blank">
<i class="bi bi-box-arrow-up-right text-dark"></i>
</a>
</td>
</tr>
}
}
"
></ng-container>
<ng-template #githubButtonsTemplate>
<p class="mb-1">
<iframe
scrolling="no"
src="https://buttons.github.io/buttons.html#href=https%3A%2F%2Fgithub.com%2Fabpframework%2Fabp&amp;title=&amp;aria-label=Star%20tabalinas%2Fjsgrid%20on%20GitHub&amp;data-icon=octicon-star&amp;data-text=Star&amp;data-size=large&amp;data-show-count=true"
style="
width: 122px;
height: 28px;
border: none;
display: inline-block;
margin-right: 4px;
"
></iframe>
<iframe
scrolling="no"
src="https://buttons.github.io/buttons.html#href=https%3A%2F%2Fgithub.com%2Fabpframework%2Fabp%2Fissues&amp;title=&amp;aria-label=Issue%20tabalinas%2Fjsgrid%20on%20GitHub&amp;data-icon=octicon-issue-opened&amp;data-text=Issue&amp;data-size=large"
style="
width: 72px;
height: 28px;
border: none;
display: inline-block;
margin-right: 4px;
"
></iframe>
<iframe
scrolling="no"
src="https://buttons.github.io/buttons.html#href=https%3A%2F%2Fgithub.com%2Fabpframework%2Fabp%2Ffork&amp;title=&amp;aria-label=Fork%20tabalinas%2Fjsgrid%20on%20GitHub&amp;data-icon=octicon-repo-forked&amp;data-text=Fork&amp;data-size=large&amp;"
style="width: 72px; height: 28px; border: none; display: inline-block"
></iframe>
</p>
</ng-template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</ng-template>

Loading…
Cancel
Save