feat: add Master Admin application functionality
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<app-data-table [columns]="columns()" [rows]="languages()" [actions]="actions()" [totalRecords]="totalRecords()"
|
||||
[pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()" tableTitle="Languages" buttonTitle="Add"
|
||||
[showSearch]="true" [showAddButton]="true" searchPlaceholder="Search languages..." [searchDebounceTime]="300"
|
||||
toolTip="Add Language" (addClicked)="onAddLanguage()" (searchChanged)="onSearch($event)"
|
||||
(pageChanged)="onPageChange($event)" (sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)">
|
||||
<ng-template appDataTableCell="name" let-value="value">
|
||||
<span class="font-semibold">{{ value }}</span>
|
||||
</ng-template>
|
||||
<ng-template appDataTableCell="code" let-value="value">
|
||||
<span class="badge bg-primary/10 text-primary">{{ value }}</span>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-confirm-dialog title="Delete Language" text="Do you really want to delete this language?"
|
||||
confirmButtonText="Delete" cancelButtonText="Cancel" (confirmed)="onDeleteConfirmed()"
|
||||
(cancelled)="onDeleteCancelled()" />
|
||||
|
||||
<modal [open]="showModal()" [title]="modalTitle()" size="md" [submitAction]="submitAction()"
|
||||
[submitLabel]="submitLabel()" [loadingLabel]="loadingLabel()" [loading]="saving() || modalLoading()"
|
||||
(closed)="closeModal()" (submitted)="saveLanguage()">
|
||||
@if (modalLoading()) {
|
||||
<div class="flex min-h-32 items-center justify-center" role="status" aria-live="polite">
|
||||
<span class="ti ti-loader-2 animate-spin text-2xl text-primary" aria-hidden="true"></span>
|
||||
<span class="ms-2">Loading language...</span>
|
||||
</div>
|
||||
} @else {
|
||||
<form [formGroup]="languageForm" (ngSubmit)="saveLanguage()" autocomplete="off">
|
||||
<div class="grid grid-cols-12 gap-x-5 gap-y-5">
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<app-form-input formControlName="name" inputId="language-name" variant="floating" label="Language Name"
|
||||
placeholder="e.g.: English" autocomplete="off" [required]="true" [maxLength]="100"
|
||||
[submitAttempted]="submitAttempted()" [validationMessages]="{
|
||||
required: 'Language Name is required.',
|
||||
maxlength: 'Language Name cannot exceed 100 characters.',
|
||||
pattern: 'Language Name cannot contain only whitespace.'
|
||||
}" />
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<app-form-input formControlName="code" inputId="language-code" variant="floating" label="Language Code"
|
||||
placeholder="e.g.: en-US" autocomplete="off" [required]="true" [maxLength]="35"
|
||||
[submitAttempted]="submitAttempted()" [validationMessages]="{
|
||||
required: 'Language Code is required.',
|
||||
maxlength: 'Language Code cannot exceed 35 characters.',
|
||||
pattern: 'Use a valid language code, for example en-US or hi-IN.'
|
||||
}" />
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<app-form-input formControlName="nativeName" inputId="language-native-name" variant="floating"
|
||||
label="Native Name" placeholder="e.g.: English" autocomplete="off" [required]="true" [maxLength]="100"
|
||||
[submitAttempted]="submitAttempted()" [validationMessages]="{
|
||||
required: 'Native Name is required.',
|
||||
maxlength: 'Native Name cannot exceed 100 characters.',
|
||||
pattern: 'Native Name cannot contain only whitespace.'
|
||||
}" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-span-12 md:col-span-6 flex items-center pt-3">
|
||||
<label for="language-rtl" class="inline-flex cursor-pointer items-center gap-2">
|
||||
<input id="language-rtl" type="checkbox" formControlName="isRightToLeft" class="form-check-input" />
|
||||
<span>Right To Left Language</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</modal>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, DestroyRef, ElementRef, computed, inject, signal, viewChild } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, catchError, finalize, of, switchMap } from 'rxjs';
|
||||
|
||||
import {
|
||||
CreateLanguageRequest,
|
||||
LanguageDto,
|
||||
LanguageModalMode,
|
||||
UpdateLanguageRequest
|
||||
} from '../../models/language.model';
|
||||
import { LanguageService } from '../../data-access/language.service';
|
||||
import { DataTable } from '../../../../../shared/components/data-table/data-table';
|
||||
import { DataTableQueryState } from '../../../../../shared/components/data-table/data-table-query.state';
|
||||
import {
|
||||
DataTableAction,
|
||||
DataTableActionEvent,
|
||||
DataTableColumn,
|
||||
DataTablePageEvent,
|
||||
DataTableQuery,
|
||||
DataTableRecord,
|
||||
DataTableSortEvent
|
||||
} from '../../../../../shared/components/data-table/data-table.types';
|
||||
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
|
||||
import { Modal } from '../../../../../shared/components/modal/modal';
|
||||
import { DataTableCellDirective } from '../../../../../shared/directives/data-table-cell.directive';
|
||||
import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
|
||||
|
||||
interface LanguageTableRow extends DataTableRecord {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
isRightToLeft: boolean;
|
||||
isActive: boolean;
|
||||
serialNumber: number;
|
||||
createdOn: string;
|
||||
modifiedOn: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'language-list',
|
||||
standalone: true,
|
||||
imports: [DataTable, DataTableCellDirective, Modal, ReactiveFormsModule, FormInput, ConfirmDialog],
|
||||
templateUrl: './language-list.html',
|
||||
styleUrl: './language-list.scss'
|
||||
})
|
||||
export class LanguageList {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly languageApi = inject(LanguageService);
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly queryRequests$ = new Subject<DataTableQuery>();
|
||||
|
||||
readonly queryState = new DataTableQueryState();
|
||||
readonly languages = signal<LanguageTableRow[]>([]);
|
||||
readonly totalRecords = signal(0);
|
||||
readonly modalLoading = signal(false);
|
||||
readonly saving = signal(false);
|
||||
readonly statusChangingId = signal<string | null>(null);
|
||||
readonly showModal = signal(false);
|
||||
readonly modalMode = signal<LanguageModalMode>('create');
|
||||
readonly selectedLanguageId = signal<string | null>(null);
|
||||
readonly selectedLanguage = signal<LanguageDto | null>(null);
|
||||
readonly submitAttempted = signal(false);
|
||||
readonly pendingDeleteLanguageId = signal<string | null>(null);
|
||||
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
|
||||
|
||||
readonly languageForm = this.formBuilder.nonNullable.group({
|
||||
name: ['', [Validators.required, Validators.maxLength(100), Validators.pattern(/.*\S.*/)]],
|
||||
code: ['', [
|
||||
Validators.required,
|
||||
Validators.maxLength(35),
|
||||
Validators.pattern(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/)
|
||||
]],
|
||||
nativeName: ['', [Validators.required, Validators.maxLength(100), Validators.pattern(/.*\S.*/)]],
|
||||
isRightToLeft: [false]
|
||||
});
|
||||
|
||||
readonly modalTitle = computed(() =>
|
||||
this.modalMode() === 'create' ? 'Add Language' : 'Edit Language'
|
||||
);
|
||||
readonly submitLabel = computed(() =>
|
||||
this.modalMode() === 'create' ? 'Save' : 'Update'
|
||||
);
|
||||
readonly loadingLabel = computed(() =>
|
||||
this.modalMode() === 'create' ? 'Saving...' : 'Updating...'
|
||||
);
|
||||
readonly submitAction = computed<'save' | 'update'>(() =>
|
||||
this.modalMode() === 'create' ? 'save' : 'update'
|
||||
);
|
||||
|
||||
readonly columns = signal<DataTableColumn<LanguageTableRow>[]>([
|
||||
{ key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '100px' },
|
||||
{ key: 'name', label: 'Language Name', header: 'Language Name', sortable: true, align: 'left' },
|
||||
{ key: 'code', label: 'Language Code', header: 'Language Code', sortable: true },
|
||||
{ key: 'nativeName', label: 'Native Name', header: 'Native Name', sortable: true },
|
||||
{
|
||||
key: 'isRightToLeft', label: 'Direction', header: 'Direction', sortable: true,
|
||||
formatter: value => value ? 'RTL' : 'LTR'
|
||||
},
|
||||
{
|
||||
key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true,
|
||||
badgeClass: value => value === true
|
||||
? 'badge bg-success/10 text-success'
|
||||
: 'badge bg-danger/10 text-danger',
|
||||
formatter: value => value ? 'Active' : 'Inactive'
|
||||
}
|
||||
]);
|
||||
|
||||
readonly actions = signal<DataTableAction<LanguageTableRow>[]>([
|
||||
{
|
||||
type: 'edit',
|
||||
label: 'Edit',
|
||||
icon: 'ti ti-edit',
|
||||
className: 'text-primary'
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
label: 'Delete',
|
||||
icon: 'ti ti-trash',
|
||||
className: 'text-danger',
|
||||
visible: row => row.isActive,
|
||||
disabled: row => this.statusChangingId() === row.id
|
||||
},
|
||||
{
|
||||
type: 'activate',
|
||||
label: 'Activate',
|
||||
icon: 'ti ti-check',
|
||||
className: 'text-success',
|
||||
visible: row => !row.isActive,
|
||||
disabled: row => this.statusChangingId() === row.id
|
||||
}
|
||||
]);
|
||||
|
||||
constructor() {
|
||||
this.queryRequests$.pipe(
|
||||
switchMap(query => this.languageApi.getDataTable(query).pipe(
|
||||
catchError(() => {
|
||||
this.toastr.error('Unable to load languages.');
|
||||
this.languages.set([]);
|
||||
this.totalRecords.set(0);
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(response => {
|
||||
if (!response || response.draw !== this.queryState.getQuery().draw) {
|
||||
return;
|
||||
}
|
||||
const query = this.queryState.getQuery();
|
||||
this.languages.set(response.rows.map((language, index) => ({
|
||||
...language,
|
||||
serialNumber: (query.page - 1) * query.pageSize + index + 1
|
||||
})));
|
||||
this.totalRecords.set(response.total);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadLanguages(this.queryState.getQuery());
|
||||
}
|
||||
|
||||
loadLanguages(query: DataTableQuery): void { this.queryRequests$.next(query); }
|
||||
onSearch(value: string): void { this.loadLanguages(this.queryState.setSearch(value.trim())); }
|
||||
onPageChange(event: DataTablePageEvent): void { this.loadLanguages(this.queryState.setPage(event)); }
|
||||
onSortChange(event: DataTableSortEvent): void { this.loadLanguages(this.queryState.setSort(event)); }
|
||||
|
||||
onDeleteConfirmed(): void {
|
||||
const languageId = this.pendingDeleteLanguageId();
|
||||
|
||||
if (!languageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingDeleteLanguageId.set(null);
|
||||
this.changeLanguageStatus(languageId, false);
|
||||
}
|
||||
|
||||
onDeleteCancelled(): void {
|
||||
this.pendingDeleteLanguageId.set(null);
|
||||
}
|
||||
|
||||
onActionClick(event: DataTableActionEvent<LanguageTableRow>): void {
|
||||
if (event.action.type === 'edit') {
|
||||
this.openEditLanguage(event.row.id);
|
||||
} else if (event.action.type === 'delete') {
|
||||
this.requestDeleteLanguage(event.row.id);
|
||||
} else if (event.action.type === 'activate') {
|
||||
this.changeLanguageStatus(event.row.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
private requestDeleteLanguage(id: string): void {
|
||||
this.pendingDeleteLanguageId.set(id);
|
||||
this.deleteConfirmDialog()?.open();
|
||||
}
|
||||
|
||||
onAddLanguage(): void {
|
||||
this.modalMode.set('create');
|
||||
this.selectedLanguageId.set(null);
|
||||
this.selectedLanguage.set(null);
|
||||
this.submitAttempted.set(false);
|
||||
this.languageForm.reset({ name: '', code: '', nativeName: '', isRightToLeft: false });
|
||||
this.resetFormState();
|
||||
this.showModal.set(true);
|
||||
}
|
||||
|
||||
openEditLanguage(id: string): void {
|
||||
this.modalMode.set('edit');
|
||||
this.selectedLanguageId.set(id);
|
||||
this.selectedLanguage.set(null);
|
||||
this.submitAttempted.set(false);
|
||||
this.languageForm.reset({ name: '', code: '', nativeName: '', isRightToLeft: false });
|
||||
this.resetFormState();
|
||||
this.modalLoading.set(true);
|
||||
this.showModal.set(true);
|
||||
|
||||
this.languageApi.getById(id).pipe(
|
||||
finalize(() => this.modalLoading.set(false)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: language => {
|
||||
if (this.selectedLanguageId() !== language.id || !this.showModal()) {
|
||||
return;
|
||||
}
|
||||
this.selectedLanguage.set(language);
|
||||
this.languageForm.reset({
|
||||
name: language.name,
|
||||
code: language.code,
|
||||
nativeName: language.nativeName,
|
||||
isRightToLeft: language.isRightToLeft
|
||||
});
|
||||
this.resetFormState();
|
||||
},
|
||||
error: () => this.showModal.set(false)
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
if (this.saving()) return;
|
||||
this.showModal.set(false);
|
||||
this.selectedLanguageId.set(null);
|
||||
this.selectedLanguage.set(null);
|
||||
this.submitAttempted.set(false);
|
||||
}
|
||||
|
||||
saveLanguage(): void {
|
||||
if (this.languageForm.invalid) {
|
||||
this.submitAttempted.set(true);
|
||||
this.languageForm.markAllAsTouched();
|
||||
this.focusFirstInvalidControl();
|
||||
return;
|
||||
}
|
||||
if (this.saving() || this.modalLoading()) return;
|
||||
|
||||
this.saving.set(true);
|
||||
const request = this.buildCreateRequest();
|
||||
const operation = this.modalMode() === 'create'
|
||||
? this.languageApi.create(request)
|
||||
: this.languageApi.update(
|
||||
this.selectedLanguageId() ?? '',
|
||||
{ ...request, isActive: this.selectedLanguage()?.isActive ?? true }
|
||||
);
|
||||
|
||||
operation.pipe(
|
||||
finalize(() => this.saving.set(false)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success(
|
||||
this.modalMode() === 'create'
|
||||
? 'Language saved successfully.'
|
||||
: 'Language updated successfully.'
|
||||
);
|
||||
this.finishSave();
|
||||
},
|
||||
error: (error: HttpErrorResponse) => this.handleSaveError(error)
|
||||
});
|
||||
}
|
||||
|
||||
private buildCreateRequest(): CreateLanguageRequest {
|
||||
const value = this.languageForm.getRawValue();
|
||||
return {
|
||||
code: this.normalizeCode(value.code),
|
||||
name: value.name.trim(),
|
||||
nativeName: value.nativeName.trim(),
|
||||
isRightToLeft: value.isRightToLeft
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeCode(code: string): string {
|
||||
return code.trim().split('-').map((part, index) =>
|
||||
index === 0 ? part.toLowerCase() : part.toUpperCase()
|
||||
).join('-');
|
||||
}
|
||||
|
||||
private changeLanguageStatus(id: string, isActive: boolean): void {
|
||||
if (this.statusChangingId()) return;
|
||||
this.statusChangingId.set(id);
|
||||
this.languageApi.getById(id).pipe(
|
||||
switchMap(language => this.languageApi.update(id, {
|
||||
code: language.code,
|
||||
name: language.name,
|
||||
nativeName: language.nativeName,
|
||||
isRightToLeft: language.isRightToLeft,
|
||||
isActive
|
||||
})),
|
||||
finalize(() => this.statusChangingId.set(null)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success(isActive
|
||||
? 'Language activated successfully.'
|
||||
: 'Language deleted successfully.');
|
||||
this.loadLanguages(this.queryState.getQuery());
|
||||
},
|
||||
error: (error: HttpErrorResponse) => {
|
||||
if (error.status === 404) this.toastr.error('The language is no longer available.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleSaveError(error: HttpErrorResponse): void {
|
||||
if (error.status === 409) {
|
||||
this.toastr.error('A language with this code already exists.', 'Duplicate language code');
|
||||
}
|
||||
}
|
||||
|
||||
private finishSave(): void {
|
||||
this.showModal.set(false);
|
||||
this.selectedLanguageId.set(null);
|
||||
this.selectedLanguage.set(null);
|
||||
this.submitAttempted.set(false);
|
||||
this.loadLanguages(this.queryState.getQuery());
|
||||
}
|
||||
|
||||
private resetFormState(): void {
|
||||
this.languageForm.markAsPristine();
|
||||
this.languageForm.markAsUntouched();
|
||||
this.languageForm.updateValueAndValidity();
|
||||
}
|
||||
|
||||
private focusFirstInvalidControl(): void {
|
||||
queueMicrotask(() => {
|
||||
const control = this.elementRef.nativeElement.querySelector<HTMLElement>(
|
||||
'modal [data-form-control][aria-invalid="true"]'
|
||||
);
|
||||
control?.focus();
|
||||
control?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user